From fe8fb22def4aa5b2ce96b2e5959a12a6c9b61b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rochel?= Date: Thu, 17 Sep 2026 12:40:30 +0200 Subject: [PATCH 01/10] feat(skill-authoring): adopt Python plugin mechanics --- .github/workflows/python-quality.yml | 35 +- AGENTS.md | 20 +- ...table-bash-facades-for-plugin-mechanics.md | 4 +- ...and-uv-for-substantial-plugin-mechanics.md | 59 +++ docs/decisions/README.md | 7 +- docs/design.md | 40 +- docs/installing-plugins.md | 5 + docs/specs/marketplace-installation.md | 5 +- docs/specs/python-quality.md | 18 +- docs/specs/skill-authoring.md | 36 +- .../.claude-plugin/plugin.json | 2 +- .../.codex-plugin/plugin.json | 2 +- .../darrow-skill-authoring/README.md | 32 +- .../skills/author-agent-skill/SKILL.md | 46 +- .../author-agent-skill/backend/pyproject.toml | 58 +++ .../src/darrow_skill_authoring/__init__.py | 1 + .../src/darrow_skill_authoring/inspector.py | 302 ++++++++++++ .../src/darrow_skill_authoring/shell_tests.py | 246 ++++++++++ .../backend/tests/test_inspector.py | 347 +++++++++++++ .../backend/tests/test_properties.py | 33 ++ .../backend/tests/test_shell_tests.py | 277 +++++++++++ .../skills/author-agent-skill/backend/uv.lock | 462 ++++++++++++++++++ .../skills/author-agent-skill/evals/README.md | 1 + .../evals/create-python-mechanic.yaml | 75 +++ .../references/plugin-mechanics.md | 61 +++ .../references/portable-shell.md | 4 +- .../author-agent-skill/scripts/inspect-skill | 207 +------- .../scripts/verify-shell-tests | 181 +------ .../tests/fresh-install.test.ps1 | 38 ++ .../tests/fresh-install.test.sh | 47 ++ python-packages.txt | 1 + 31 files changed, 2182 insertions(+), 470 deletions(-) create mode 100644 docs/decisions/ADR-0009-adopt-python-and-uv-for-substantial-plugin-mechanics.md create mode 100644 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/pyproject.toml create mode 100644 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/__init__.py create mode 100644 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/inspector.py create mode 100644 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/shell_tests.py create mode 100644 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_inspector.py create mode 100644 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_properties.py create mode 100644 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_shell_tests.py create mode 100644 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/uv.lock create mode 100644 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/create-python-mechanic.yaml create mode 100644 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/plugin-mechanics.md create mode 100644 plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 create mode 100755 plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.sh diff --git a/.github/workflows/python-quality.yml b/.github/workflows/python-quality.yml index 6aa2e0e9..4244422b 100644 --- a/.github/workflows/python-quality.yml +++ b/.github/workflows/python-quality.yml @@ -15,9 +15,12 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash env: UV_PYTHON: ${{ matrix.python-version }} steps: @@ -38,15 +41,39 @@ jobs: - run: scripts/test-python-inventory fresh-install: - name: Python fresh plugin install - runs-on: ubuntu-latest + name: Fresh install ${{ matrix.plugin }} on ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + plugin: observability-langfuse + shell: bash + test: plugins/capability/darrow-observability-langfuse/tests/fresh-install.test.sh + - os: ubuntu-latest + plugin: skill-authoring + shell: bash + test: plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.sh + - os: macos-latest + plugin: skill-authoring + shell: bash + test: plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.sh + - os: windows-latest + plugin: skill-authoring + shell: pwsh + test: plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.13" - uses: astral-sh/setup-uv@v6 - - run: bash plugins/capability/darrow-observability-langfuse/tests/fresh-install.test.sh + - if: matrix.shell == 'bash' + run: bash "${{ matrix.test }}" + - if: matrix.shell == 'pwsh' + shell: pwsh + run: "& '${{ matrix.test }}'" performance: name: Python performance invariants diff --git a/AGENTS.md b/AGENTS.md index 803efe9b..20068d27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,18 +37,22 @@ intent-level inputs instead of reproducing call details. Review agents must not run Git or GitHub commands against this repository. -## Shell portability - -- Write plugin-shipped executable mechanics in portable Bash, using baseline - Unix utilities and the host CLIs the capability wraps. Do not add Python, - JavaScript/TypeScript, Ruby, JVM, or compiled runtime dependencies. Shared - repository development and eval infrastructure such as `evals/runner/` is - outside this plugin-runtime boundary. +## Plugin mechanics + +- Use a contained Python package managed by UV for substantial deterministic + plugin mechanics when Python materially improves clarity or native Windows + support. Commit `pyproject.toml` and `uv.lock`, keep runtime and development + dependencies separate, invoke runtime entrypoints with frozen resolution, and + register the package in `python-packages.txt`. +- Keep genuinely small host-specific launchers and command glue in portable + Bash, using baseline Unix utilities and the host CLIs the capability wraps. + Do not introduce JavaScript/TypeScript, Ruby, JVM, compiled, or shared Darrow + runtime dependencies inside a plugin. - Avoid early-exit pipelines under `pipefail`; use here-strings for bounded matching. - Avoid `${var//pat/}` on unbounded input and `awk -v` for backslash-bearing values. -- Support old BSD awk and Bash 3.2 in plugin scripts. +- Support old BSD awk and Bash 3.2 in plugin shell scripts. - Refuse unreadable configuration rather than silently skipping it. - Use absolute paths in model-facing output. - Resolve the primary repository via the first `git worktree list --porcelain` diff --git a/docs/decisions/ADR-0005-use-portable-bash-facades-for-plugin-mechanics.md b/docs/decisions/ADR-0005-use-portable-bash-facades-for-plugin-mechanics.md index 1aa2b5ac..4121fc1a 100644 --- a/docs/decisions/ADR-0005-use-portable-bash-facades-for-plugin-mechanics.md +++ b/docs/decisions/ADR-0005-use-portable-bash-facades-for-plugin-mechanics.md @@ -1,9 +1,9 @@ # ADR-0005: Use portable Bash facades for plugin mechanics -Status: Accepted +Status: Superseded Date: 2026-08-13 Summary: Put deterministic plugin mechanics behind narrow portable Bash facades while keeping authority and contextual judgment in skills. -Revisit when: Every supported Claude Code and Codex host provides a common, independently installable runtime that is more portable than Bash 3.2 plus baseline Unix utilities, or the supported macOS boundary no longer includes Bash 3.2. +Superseded by: ADR-0009 ## Context diff --git a/docs/decisions/ADR-0009-adopt-python-and-uv-for-substantial-plugin-mechanics.md b/docs/decisions/ADR-0009-adopt-python-and-uv-for-substantial-plugin-mechanics.md new file mode 100644 index 00000000..24304903 --- /dev/null +++ b/docs/decisions/ADR-0009-adopt-python-and-uv-for-substantial-plugin-mechanics.md @@ -0,0 +1,59 @@ +# ADR-0009: Adopt Python and UV for substantial plugin mechanics + +Status: Accepted +Date: 2026-09-17 +Summary: Adopt contained Python packages managed by UV incrementally for substantial cross-platform plugin mechanics while retaining portable Bash for small host-specific glue. +Supersedes: ADR-0005 +Revisit when: UV and supported Python cannot provide independently installable helpers across every supported native host, or a lighter common runtime offers materially better portability and containment. + +## Context + +ADR-0005 standardized portable Bash because it was available on Darrow's +original macOS and Linux hosts without another runtime. That kept plugins +independent, but substantial parsers and filesystem validators accumulated +conservative shell code and remained unavailable on native Windows. + +The Langfuse plugin established the repository's contained Python and UV +pattern: a plugin-local package and lock, frozen execution, separate runtime and +development dependencies, fresh-artifact validation, and repository-wide +Python quality gates. Issue #168 selects incremental migration so each plugin +can adopt that established pattern without creating a shared Darrow runtime. + +## Decision + +Use a contained Python package managed by UV for substantial deterministic +plugin mechanics when Python materially improves clarity, testing, or native +Windows support. + +- Keep the package, `pyproject.toml`, `uv.lock`, entrypoints, dependencies, + environments, and state inside the owning plugin. +- Invoke public runtime entrypoints with frozen resolution and without + development dependencies. Keep development tools in the locked development + group and register each package in the repository Python inventory. +- Validate cross-platform entrypoints from a fresh copied plugin artifact on + Linux, macOS, and native Windows. Apply the repository Python quality gates + across the declared Python support range. +- Retain portable Bash for genuinely small host-specific launchers and command + glue. Such scripts continue to support Bash 5 and macOS `/bin/bash` 3.2. +- Adopt Python incrementally. A plugin without substantial cross-platform + mechanics does not need a Python package, and no plugin may depend on a + sibling package or shared Darrow runtime. +- Keep contextual judgment, authority, and material choices in `SKILL.md`; + language choice does not expand a helper's responsibility. + +ADR-0008 remains the authoritative record for the Langfuse plugin's hook and +failure semantics. This decision generalizes the established packaging and +quality model to later plugin migrations. + +## Consequences + +- Migrated helpers can run through the same locked console entrypoints on + native Windows, macOS, and Linux. +- Substantial mechanics gain strict typing, focused unit and integration tests, + property tests where useful, and separate line and branch coverage gates. +- Adopters of a Python-backed plugin need UV and a supported UV-managed Python; + plugins that have not migrated keep their existing prerequisites. +- Small POSIX launchers remain inspectable and compatible with legacy hosts, + while their substantial logic no longer carries Bash and BSD utility limits. +- Every migration must preserve the plugin's independent installation boundary + and prove behavior at its existing public entrypoints. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 0adb6d4b..aff58cae 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -20,19 +20,19 @@ Rebuild it with `decision catalog rebuild` and verify it with `decision catalog | [ADR-0002: Separate capabilities from orchestration](ADR-0002-separate-capabilities-from-orchestration.md) | Accepted | 2026-08-13 | Keep capabilities intent-matched and independently selectable while starting continuation-owning orchestration only through explicit user invocation. | None | | [ADR-0003: Treat plugins as optionality boundaries](ADR-0003-treat-plugins-as-optionality-boundaries.md) | Accepted | 2026-08-13 | Treat each plugin as a self-contained unit of adoption, compatibility, and ownership that composes through host-visible skill intent rather than sibling dependencies or a separate capability registry. | None | | [ADR-0004: Use native goal ownership for core orchestration](ADR-0004-use-native-goal-ownership-for-core-orchestration.md) | Accepted | 2026-08-13 | Use Darrow to compile and launch one host-native goal owner instead of operating a second execution controller or general workflow runtime. | Revisit when: Matched multi-trial evidence on supported hosts shows that a Darrow-owned execution controller materially improves task outcomes over native goal ownership after accounting for wall time, model usage, child invocations, and human interruptions. | -| [ADR-0005: Use portable Bash facades for plugin mechanics](ADR-0005-use-portable-bash-facades-for-plugin-mechanics.md) | Accepted | 2026-08-13 | Put deterministic plugin mechanics behind narrow portable Bash facades while keeping authority and contextual judgment in skills. | Revisit when: Every supported Claude Code and Codex host provides a common, independently installable runtime that is more portable than Bash 3.2 plus baseline Unix utilities, or the supported macOS boundary no longer includes Bash 3.2. | | [ADR-0006: Keep decisions with their authoritative owners](ADR-0006-keep-decisions-with-their-authoritative-owners.md) | Accepted | 2026-08-13 | Keep each decision at the narrowest durable authoritative owner its consumers obey, with one canonical sink per effect and honest gaps for inaccessible owners. | None | | [ADR-0007: Separate skill evaluation evidence dimensions](ADR-0007-separate-skill-evaluation-evidence-dimensions.md) | Accepted | 2026-08-13 | Represent invariant coverage, task outcomes, matched skill ablation, and skill activation as separate evaluation evidence dimensions. | None | | [ADR-0008: Allow Python and UV for Langfuse observability](ADR-0008-allow-python-and-uv-for-langfuse-observability.md) | Accepted | 2026-08-24 | Permit the independently installable Langfuse observability plugin to use a locked Python backend managed by UV while retaining a portable Bash hook launcher and keeping the exception scoped to that plugin. | Revisit when: Codex exposes equivalent native Langfuse export, the Langfuse SDK no longer requires Python, or the plugin can meet its rollout-reconstruction and export contract with the portable Bash baseline alone. | +| [ADR-0009: Adopt Python and UV for substantial plugin mechanics](ADR-0009-adopt-python-and-uv-for-substantial-plugin-mechanics.md) | Accepted | 2026-09-17 | Adopt contained Python packages managed by UV incrementally for substantial cross-platform plugin mechanics while retaining portable Bash for small host-specific glue. | Supersedes: ADR-0005; Revisit when: UV and supported Python cannot provide independently installable helpers across every supported native host, or a lighter common runtime offers materially better portability and containment. | - + ## Rejected @@ -51,3 +51,6 @@ Rebuild it with `decision catalog rebuild` and verify it with `decision catalog | Decision | Status | Date | Summary | Relationships | | --- | --- | --- | --- | --- | +| [ADR-0005: Use portable Bash facades for plugin mechanics](ADR-0005-use-portable-bash-facades-for-plugin-mechanics.md) | Superseded | 2026-08-13 | Put deterministic plugin mechanics behind narrow portable Bash facades while keeping authority and contextual judgment in skills. | Superseded by: ADR-0009 | + + diff --git a/docs/design.md b/docs/design.md index 5d899e06..0c8564e8 100644 --- a/docs/design.md +++ b/docs/design.md @@ -122,23 +122,25 @@ consequences. Its interface and result still identify the operation, target, material side effects, inspected evidence, failure, and any required next decision. -## Plugin mechanics use portable Bash - -Executable mechanics shipped inside a plugin use portable Bash, baseline Unix -utilities, and the host CLIs that the capability explicitly wraps. A plugin -does not introduce Python, JavaScript or TypeScript, Ruby, a JVM, a compiled -binary, or another full language runtime for its internal helpers. - -This keeps each plugin inspectable, independently installable, and usable in -the default macOS and Linux environments of both Claude Code and Codex. Darrow -targets Bash 5 and the macOS `/bin/bash` 3.2 boundary, so scripts also avoid -GNU-only assumptions and newer Bash-only syntax. - -The restriction applies to plugin-shipped runtime mechanics. Repository-level -development infrastructure may use a different implementation when an accepted -decision establishes it. In particular, the shared evaluation runner uses -TypeScript on Bun; plugins consume its declarative eval contract but do not ship -it as a runtime dependency. +## Plugin mechanics use contained Python or portable Bash + +Substantial deterministic mechanics may use a contained Python package managed +by UV when that materially improves clarity, testability, or native Windows +support. The package, lock, entrypoints, dependencies, and state stay inside the +owning plugin. Runtime and development dependencies remain separate, and frozen +entrypoints never rely on a sibling plugin or shared Darrow runtime. + +Genuinely small host-specific launchers and command glue use portable Bash, +baseline Unix utilities, and the host CLIs that the capability explicitly +wraps. Darrow targets Bash 5 and the macOS `/bin/bash` 3.2 boundary for those +scripts, so they avoid GNU-only assumptions and newer Bash-only syntax. + +Each plugin declares its own runtime prerequisites and remains independently +installable. Python adoption is incremental rather than a requirement that all +plugins carry a Python package. Plugins do not introduce JavaScript or +TypeScript, Ruby, a JVM, compiled binaries, or shared language-runtime +infrastructure for their helpers. Repository-level development infrastructure +remains outside this plugin-runtime boundary. ## Evals make development evidence-based @@ -226,7 +228,7 @@ are specified in [Capability: Ticket Pipeline](specs/ticket-pipeline.md). runtime around the marketplace. - Hide repeatable tool mechanics behind narrow, testable scripts without hiding their authority or effects. -- Ship plugin mechanics as portable Bash and keep heavier languages in - repository-level development infrastructure. +- Use contained Python plus UV for substantial cross-platform plugin mechanics, + and portable Bash for small host-specific launchers and command glue. - Give every skill functional evals and compare behavior-changing variants with matched controls before claiming an improvement. diff --git a/docs/installing-plugins.md b/docs/installing-plugins.md index 845bb4d1..61501763 100644 --- a/docs/installing-plugins.md +++ b/docs/installing-plugins.md @@ -156,6 +156,11 @@ The shortcut deliberately excludes two marketplace entries: it does not enable tracing: configure credentials and explicitly opt in before it exports anything. Read its local README before enabling it. +`darrow-skill-authoring` remains part of the shortcut, but its deterministic +authoring helpers require UV and a UV-managed Python `>=3.10,<3.14` when used. +Its plugin-local lock and package do not create a dependency on another Darrow +plugin. + ## Verify the installation First check the installed listing names your selected plugin. In a fresh diff --git a/docs/specs/marketplace-installation.md b/docs/specs/marketplace-installation.md index c81e5220..8905ccb0 100644 --- a/docs/specs/marketplace-installation.md +++ b/docs/specs/marketplace-installation.md @@ -20,4 +20,7 @@ installation scope is passed to every per-plugin command. The installation documentation must identify both excluded entries: the ticket-pipeline entry is deprecated, and the Langfuse plugin requires UV/Python, -hook trust, and separate opt-in configuration. +hook trust, and separate opt-in configuration. It also names UV and the +supported Python range as runtime prerequisites for the included skill-authoring +plugin without presenting that self-contained package as a cross-plugin +dependency. diff --git a/docs/specs/python-quality.md b/docs/specs/python-quality.md index 6123438a..af059228 100644 --- a/docs/specs/python-quality.md +++ b/docs/specs/python-quality.md @@ -56,10 +56,10 @@ including property tests, and enforces both coverage measures. The pre-commit hook runs the fast formatting, lint, and type portion when staged Python or Python project files change. -CI runs the canonical gate on Linux and macOS for every declared Python minor -version. A stable aggregate `Python quality` status is the branch-protection -check. The inventory guard itself is tested by introducing a temporary -unregistered package and proving that the gate refuses it. +CI runs the canonical gate on Linux, macOS, and native Windows for every +declared Python minor version. A stable aggregate `Python quality` status is the +branch-protection check. The inventory guard itself is tested by introducing a +temporary unregistered package and proving that the gate refuses it. ## Performance and release evidence @@ -76,7 +76,9 @@ live inside the plugin that owns the behavior. Repository commands and CI may invoke those entrypoints, but do not reproduce the plugin's internal path resolution or benchmark logic. -Release validation installs the plugin from a fresh copied artifact using only -locked runtime dependencies before exercising its launcher and backend. The -isolated live Langfuse ingestion procedure remains release evidence for the -external service boundary; it is not replaced by mocked CI tests. +Release validation installs each Python-backed plugin from a fresh copied +artifact using only locked runtime dependencies before exercising its public +entrypoints. Cross-platform helpers run this validation on Linux, macOS, and +native Windows. The isolated live Langfuse ingestion procedure remains release +evidence for that plugin's external service boundary; it is not replaced by +mocked CI tests. diff --git a/docs/specs/skill-authoring.md b/docs/specs/skill-authoring.md index ef7a4ec8..464af29b 100644 --- a/docs/specs/skill-authoring.md +++ b/docs/specs/skill-authoring.md @@ -76,23 +76,26 @@ checkable, and recorded evidence for trigger behavior and workflow quality. - **SA-C5 — Mechanics are executable.** Put deterministic, repeatedly needed, or error-prone checks in a bundled script with narrow inputs and stable machine-readable output. Keep contextual decisions and trade-offs in - `SKILL.md`. Do not add a script when existing tools and instructions are - already reliable. + `SKILL.md`. Use a contained, locked Python package when substantial mechanics + benefit from structured code or native Windows support; use portable Bash for + genuinely small host-specific launchers and command glue. Do not add a script + when existing tools and instructions are already reliable. - **SA-C6 — Fail closed and identify evidence.** A required unreadable skill, manifest, configuration, instruction, or referenced local resource blocks the dependent result. Model-facing script output identifies inspected inputs and local references with absolute paths. Missing evidence is never treated as a successful empty state. -- **SA-C7 — Portable self-containment.** Bundled shell scripts work in Bash 5 - and `/bin/bash` 3.2, avoid GNU-only assumptions, and never require files from a - sibling plugin. Normalize paths before emitting or comparing them, and handle - ordinary environment spelling variants such as `TMPDIR` with or without a - trailing separator. Label shell evidence by the interpreter's observed - version rather than its command name, deduplicate equivalent interpreters, - and leave an unavailable required version explicitly unverified. Shell tests - must preserve their executing interpreter in nested implementation calls; - a different `bash` on `PATH` must not substitute for the selected interpreter. - A packaged skill remains useful with no MCP server or other Darrow plugin +- **SA-C7 — Portable self-containment.** A Python-backed helper keeps its + package, lock, entrypoints, dependencies, and state inside the owning plugin; + separates runtime from development dependencies; uses frozen UV execution; + and verifies its public entrypoints from a fresh artifact on every claimed + native platform. Bundled shell scripts work in Bash 5 and `/bin/bash` 3.2, + avoid GNU-only assumptions, and never require files from a sibling plugin. + Normalize paths before emitting or comparing them. Label shell evidence by + the interpreter's observed version, deduplicate equivalent interpreters, and + leave an unavailable required version explicitly unverified. Shell tests + preserve their executing interpreter in nested implementation calls. A + packaged skill remains useful with no MCP server or other Darrow plugin installed unless its own manifest declares and supplies that dependency. - **SA-C8 — Evaluation separates prompt from criteria.** Participant-visible eval prompts contain the task and repository evidence but not their pass @@ -108,10 +111,11 @@ checkable, and recorded evidence for trigger behavior and workflow quality. self-containment, safety, and loophole findings, then rerun affected script tests and evals. - **SA-C10 — Verified delivery.** Validate skill metadata, plugin manifests, - marketplace/discovery entries, local-reference containment, both-shell script - behavior, and relevant repository gates. Report exact commands, outcomes, - evaluation limitations, and residual risks. Authoring does not imply commit, - push, pull request, publication, release, or deployment authority. + marketplace/discovery entries, local-reference containment, relevant native + platform or both-shell behavior, and repository gates. Report exact commands, + outcomes, evaluation limitations, and residual risks. Authoring does not + imply commit, push, pull request, publication, release, or deployment + authority. ## Non-goals diff --git a/plugins/foundation/darrow-skill-authoring/.claude-plugin/plugin.json b/plugins/foundation/darrow-skill-authoring/.claude-plugin/plugin.json index a525a59f..768901c0 100644 --- a/plugins/foundation/darrow-skill-authoring/.claude-plugin/plugin.json +++ b/plugins/foundation/darrow-skill-authoring/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "darrow-skill-authoring", "description": "Create and improve reliable cross-runtime agent skills", - "version": "0.1.7", + "version": "0.2.0", "license": "BUSL-1.1", "author": { "name": "Björn Rochel", diff --git a/plugins/foundation/darrow-skill-authoring/.codex-plugin/plugin.json b/plugins/foundation/darrow-skill-authoring/.codex-plugin/plugin.json index a1a56e33..bd7f9bb7 100644 --- a/plugins/foundation/darrow-skill-authoring/.codex-plugin/plugin.json +++ b/plugins/foundation/darrow-skill-authoring/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "darrow-skill-authoring", - "version": "0.1.7", + "version": "0.2.0", "description": "Create and improve reliable cross-runtime agent skills", "author": { "name": "Björn Rochel", diff --git a/plugins/foundation/darrow-skill-authoring/README.md b/plugins/foundation/darrow-skill-authoring/README.md index 23b805e6..327f73de 100644 --- a/plugins/foundation/darrow-skill-authoring/README.md +++ b/plugins/foundation/darrow-skill-authoring/README.md @@ -11,21 +11,31 @@ Codex. verifies the resulting package in fresh context. The plugin is self-contained. It does not require another Darrow plugin, an MCP -server, or a workflow runtime. Its bundled inspector validates portable -metadata and containment for inline Markdown-linked local resources; the -workflow inspects other path forms, and native runtime validators remain -authoritative for their complete formats. +server, or a workflow runtime. Its locked Python inspector validates portable +metadata and containment for inline Markdown-linked local resources on Linux, +macOS, and native Windows; the workflow inspects other path forms, and native +runtime validators remain authoritative for their complete formats. ## Design boundaries -Validation is read-only. Creation and revision require a supplied goal, -destination, supported runtimes, and clear authority. The skill does not -invent a capability or publish it without a separate explicit request. +Validation keeps the target repository read-only. On first use, UV may create +`skills/author-agent-skill/backend/.venv` inside the installed plugin and fetch +the locked Python environment. Creation and revision require a supplied goal, +destination, supported runtimes, and clear authority. The skill does not invent +a capability or publish it without a separate explicit request. ## Development -From this plugin directory, run all deterministic tests through the -version-aware matrix: +From this plugin directory, prepare the locked helper environment without +development dependencies: + +```text +uv sync --quiet --frozen --no-dev --project skills/author-agent-skill/backend +``` + +From the repository root, run the Python quality checks with +`bun run check:python`. From this plugin directory, run the compatibility shell +suites through the version-aware matrix: ```sh bash skills/author-agent-skill/scripts/verify-shell-tests -- \ @@ -53,7 +63,9 @@ Create, revise, or validate one reusable skill. Do not use it for generic instru ## Hosts and prerequisites -Codex and Claude Code; Bash and baseline Unix tools. Live evaluations need the target's supported host harness. +Codex and Claude Code; UV and a UV-managed Python `>=3.10,<3.14`. The small +POSIX compatibility launchers use Bash and baseline Unix tools. Live +evaluations need the target's supported host harness. ## Installation diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/SKILL.md b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/SKILL.md index 53829afe..09bc25ae 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/SKILL.md +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/SKILL.md @@ -9,12 +9,12 @@ Turn one recognizable user goal into a discoverable, bounded, and evaluated skill. Treat discovery metadata, workflow behavior, resources, and verification as one public interface. -Run the bundled inspector with Bash. In commands below, `` is the -directory containing this file, `` is the target skill directory, -and `` is its owning plugin root: +Run the bundled inspector through its locked UV entrypoint. In commands below, +`` is the directory containing this file, `` is the +target skill directory, and `` is its owning plugin root: -```sh -bash /scripts/inspect-skill inspect +```text +uv run --quiet --frozen --no-dev --project "/backend" inspect-skill inspect "" "" ``` The inspector checks portable metadata and containment for inline @@ -22,10 +22,19 @@ Markdown-linked local resources. Inspect command literals, reference-style links, and other path forms separately. It does not replace native runtime validators or decide whether the workflow is well designed. -When verifying target shell tests, read +Before adding executable mechanics, read +[`references/plugin-mechanics.md`](references/plugin-mechanics.md) and choose +existing tools, small shell glue, or contained Python from the target's actual +complexity and supported hosts. When verifying target shell tests, also read [`references/portable-shell.md`](references/portable-shell.md) and run the -version-aware [`scripts/verify-shell-tests`](scripts/verify-shell-tests) helper. -Base every shell-version claim on its emitted evidence. +version-aware helper through the same backend: + +```text +uv run --quiet --frozen --no-dev --project "/backend" verify-shell-tests -- ""... +``` + +Base every shell-version claim on its emitted evidence. The small scripts with +the same command names preserve compatibility for existing POSIX callers. ## Workflow @@ -149,7 +158,8 @@ work without summarizing the procedure. Add or update evidence at the most stable public seam before implementation: -- deterministic script tests for mechanical contracts and failure modes; +- deterministic script or package tests for mechanical contracts and failure + modes; - behavior evals for judgment, sequencing, output, and safety boundaries; and - activation cases for direct, indirect, incomplete, negative, and edge input. @@ -172,9 +182,11 @@ arguments, produces stable output, refuses unreadable inputs, and emits absolute paths when its output is model-facing. Keep policy choices, trade-offs, and contextual judgment in `SKILL.md`. -When the target adds or changes a shell script or shell test, read and apply -[`references/portable-shell.md`](references/portable-shell.md) to both the -implementation and its tests. Do not load that reference for prose-only skills. +When the target adds or changes executable mechanics, read and apply +[`references/plugin-mechanics.md`](references/plugin-mechanics.md). For a shell +script or shell test, also apply +[`references/portable-shell.md`](references/portable-shell.md) to the +implementation and tests. Do not load either reference for prose-only skills. Keep the skill independently installable. All required files live inside its plugin; a reference must not escape the plugin or require a sibling plugin. @@ -188,14 +200,16 @@ resources, and packaging changes, with no unrelated capability added. Run, in order: -1. each bundled shell test through the version-aware helper, preserving any - unavailable required version as unverified; +1. each bundled package test through its locked quality command and each shell + test through the version-aware helper, preserving unavailable required + versions as unverified; 2. the bundled inspector on the finished skill and plugin root; 3. the repository's skill and native plugin-manifest validators; 4. scoped eval dry validation and live trials on the supported harnesses chosen by the repository; -5. affected documentation and marketplace/discovery checks; and -6. relevant repository lint, type, or test gates. +5. fresh copied-artifact entrypoints on each supported native platform; +6. affected documentation and marketplace/discovery checks; and +7. relevant repository lint, type, or test gates. Then give a fresh-context reviewer the finished artifact and task-local evidence, not the intended answer or prior conclusions. Ask it to challenge diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/pyproject.toml b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/pyproject.toml new file mode 100644 index 00000000..6e12d27b --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/pyproject.toml @@ -0,0 +1,58 @@ +[project] +name = "darrow-skill-authoring" +version = "0.1.0" +description = "Portable deterministic helpers for Darrow skill authoring" +requires-python = ">=3.10,<3.14" +dependencies = [] + +[project.scripts] +inspect-skill = "darrow_skill_authoring.inspector:entrypoint" +verify-shell-tests = "darrow_skill_authoring.shell_tests:entrypoint" + +[dependency-groups] +dev = [ + "coverage[toml]>=7.10,<8", + "hypothesis>=6.138,<7", + "mypy>=1.18,<2", + "pytest>=8.4,<9", + "ruff>=0.12,<1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv] +default-groups = [] + +[tool.hatch.build.targets.wheel] +packages = ["src/darrow_skill_authoring"] + +[tool.ruff] +target-version = "py310" +line-length = 88 +src = ["src", "tests"] + +[tool.ruff.lint] +select = ["B", "C4", "C90", "E4", "E7", "E9", "F", "I", "N", "RUF", "SIM", "UP"] +mccabe.max-complexity = 5 + +[tool.ruff.format] +docstring-code-format = true + +[tool.mypy] +python_version = "3.10" +strict = true +files = ["src", "tests"] + +[tool.pytest.ini_options] +addopts = ["--strict-config", "--strict-markers"] +testpaths = ["tests"] + +[tool.coverage.run] +branch = true +source = ["darrow_skill_authoring"] + +[tool.coverage.report] +show_missing = true +skip_covered = false diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/__init__.py b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/__init__.py new file mode 100644 index 00000000..81f54f6e --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/__init__.py @@ -0,0 +1 @@ +"""Deterministic helpers for the Darrow skill-authoring plugin.""" diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/inspector.py b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/inspector.py new file mode 100644 index 00000000..ec537875 --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/inspector.py @@ -0,0 +1,302 @@ +from __future__ import annotations + +import os +import re +import sys +from dataclasses import dataclass +from pathlib import Path, PureWindowsPath +from typing import TextIO + +PROGRAM = "inspect-skill" +USAGE = f"""Usage: {PROGRAM} inspect SKILL_DIRECTORY PLUGIN_ROOT + +Validate portable skill metadata and inline Markdown-linked resource containment. +Output paths are absolute. The command does not modify the inspected plugin. +""" +LINK_PATTERN = re.compile(r"\]\(([^)]*)\)") +METADATA_PATTERN = re.compile( + r"^(name|description|disable-model-invocation)[ \t]*:[ \t]*(.*)$" +) +NAME_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") + + +class InspectionError(ValueError): + """A user-facing validation failure.""" + + +class MetadataFormatError(ValueError): + """A malformed portable frontmatter block.""" + + +@dataclass(frozen=True) +class Metadata: + name: str + description: str + + +def _attempted_path(candidate: str) -> Path: + path = Path(candidate) + return path if path.is_absolute() else Path.cwd() / path + + +def _readable_directory(candidate: str, label: str) -> Path: + attempted = _attempted_path(candidate) + if not attempted.is_dir() or not os.access(attempted, os.R_OK | os.X_OK): + raise InspectionError(f"{label} is not a readable directory: {attempted}") + try: + return attempted.resolve(strict=True) + except OSError as error: + raise InspectionError(f"cannot resolve {label}: {attempted}") from error + + +def _record_metadata(values: dict[str, str], line: str) -> None: + match = METADATA_PATTERN.fullmatch(line) + if match is None: + raise MetadataFormatError + key, value = match.groups() + if key in values: + raise MetadataFormatError + if key == "disable-model-invocation" and value not in {"true", "false"}: + raise MetadataFormatError + values[key] = value + + +def _frontmatter_lines(text: str) -> list[str]: + lines = text.splitlines() + if not lines or lines[0] != "---": + raise MetadataFormatError + try: + end = lines.index("---", 1) + except ValueError as error: + raise MetadataFormatError from error + return lines[1:end] + + +def _parse_frontmatter(text: str) -> Metadata: + values: dict[str, str] = {} + for line in _frontmatter_lines(text): + if not line.strip() or line.lstrip().startswith("#"): + continue + _record_metadata(values, line) + if "name" not in values or "description" not in values: + raise MetadataFormatError + return Metadata(values["name"].strip(), values["description"].strip()) + + +def _unquote(value: str, label: str, skill_file: Path) -> str: + for quote, name in (('"', "double"), ("'", "single")): + if value.startswith(quote): + if not value.endswith(quote) or len(value) == 1: + raise InspectionError( + f"skill {label} has an unmatched {name} quote: {skill_file}" + ) + return value[1:-1] + return value + + +def _validate_metadata(metadata: Metadata, skill_directory: Path) -> Metadata: + skill_file = skill_directory / "SKILL.md" + name = _unquote(metadata.name, "name", skill_file) + description = _unquote(metadata.description, "description", skill_file) + if NAME_PATTERN.fullmatch(name) is None: + raise InspectionError( + "skill name must use lower-case letters, digits, and internal hyphens: " + f"{skill_file}" + ) + if name != skill_directory.name: + raise InspectionError( + f"skill name '{name}' does not match directory " + f"'{skill_directory.name}': {skill_file}" + ) + if not description: + raise InspectionError(f"skill description is empty: {skill_file}") + return Metadata(name, description) + + +def _is_within(path: Path, root: Path) -> bool: + try: + path.relative_to(root) + except ValueError: + return False + return True + + +def _skill_directory(skill_input: str, plugin_root: Path) -> Path: + attempted = _attempted_path(skill_input) + if not attempted.is_dir(): + raise InspectionError( + f"skill file is not a readable regular file: {attempted / 'SKILL.md'}" + ) + skill_directory = _readable_directory(skill_input, "skill directory") + if not _is_within(skill_directory, plugin_root): + raise InspectionError(f"skill directory escapes plugin root: {skill_directory}") + return skill_directory + + +def _read_skill(skill_directory: Path) -> str: + skill_file = skill_directory / "SKILL.md" + invalid = not skill_file.is_file() or not os.access(skill_file, os.R_OK) + if invalid or skill_file.is_symlink(): + detail = ( + "skill file must not be a symbolic link" + if skill_file.is_symlink() + else "skill file is not a readable regular file" + ) + raise InspectionError(f"{detail}: {skill_file}") + try: + return skill_file.read_text(encoding="utf-8") + except (OSError, UnicodeError) as error: + raise InspectionError( + f"skill file is not a readable regular file: {skill_file}" + ) from error + + +def _portable_target(raw_target: str) -> str | None: + target = raw_target + if target.startswith("<") and target.endswith(">"): + target = target[1:-1] + target = target.split("#", 1)[0] + if not target or target.startswith(("http://", "https://", "mailto:")): + return None + windows_target = PureWindowsPath(target) + if ( + "\\" in target + or Path(target).is_absolute() + or windows_target.is_absolute() + or bool(windows_target.drive) + ): + raise InspectionError( + f"local references must use portable relative paths: {target}" + ) + return target + + +def _local_reference(target: str, skill_directory: Path, plugin_root: Path) -> Path: + relative = Path(target) + parent_input = skill_directory / relative.parent + try: + parent = parent_input.resolve(strict=True) + except OSError as error: + raise InspectionError( + f"local reference parent is not readable: {parent_input}" + ) from error + reference = parent / relative.name + if not _is_within(reference, plugin_root): + raise InspectionError(f"local reference escapes plugin root: {reference}") + if not reference.is_file() or not os.access(reference, os.R_OK): + raise InspectionError( + f"local reference is not a readable regular file: {reference}" + ) + if reference.is_symlink(): + raise InspectionError( + f"local reference must not be a symbolic link: {reference}" + ) + return reference + + +def _references(text: str, skill_directory: Path, plugin_root: Path) -> list[Path]: + references: list[Path] = [] + for match in LINK_PATTERN.finditer(text): + target = _portable_target(match.group(1)) + if target is not None: + references.append(_local_reference(target, skill_directory, plugin_root)) + return references + + +def _walk_error(error: OSError) -> None: + raise error + + +def _visible_directories(parent: Path, names: list[str]) -> list[str]: + return [name for name in names if not (parent / name).is_symlink()] + + +def _regular_files(parent: Path, names: list[str]) -> list[Path]: + return [ + path + for name in names + if (path := parent / name).is_file() and not path.is_symlink() + ] + + +def _walk_scripts(scripts_directory: Path) -> list[Path]: + scripts: list[Path] = [] + for parent, directories, files in os.walk( + scripts_directory, followlinks=False, onerror=_walk_error + ): + parent_path = Path(parent) + if not os.access(parent_path, os.R_OK | os.X_OK): + raise OSError + directories[:] = _visible_directories(parent_path, directories) + scripts.extend(_regular_files(parent_path, files)) + return scripts + + +def _bundled_scripts(skill_directory: Path) -> list[Path]: + scripts_directory = skill_directory / "scripts" + if not scripts_directory.exists(): + return [] + if not scripts_directory.is_dir() or not os.access( + scripts_directory, os.R_OK | os.X_OK + ): + raise InspectionError( + f"scripts path is not a readable directory: {scripts_directory}" + ) + try: + scripts = _walk_scripts(scripts_directory) + except OSError as error: + raise InspectionError( + f"cannot enumerate bundled scripts: {scripts_directory}" + ) from error + return sorted(scripts, key=lambda path: os.fsencode(path)) + + +def _inspect(skill_input: str, plugin_input: str, stdout: TextIO) -> None: + plugin_root = _readable_directory(plugin_input, "plugin root") + skill_directory = _skill_directory(skill_input, plugin_root) + skill_file = skill_directory / "SKILL.md" + text = _read_skill(skill_directory) + try: + metadata = _parse_frontmatter(text) + except MetadataFormatError as error: + raise InspectionError( + "frontmatter must contain one single-line name and description plus an " + f"optional boolean disable-model-invocation: {skill_file}" + ) from error + metadata = _validate_metadata(metadata, skill_directory) + stdout.write("format\tdarrow-skill-inspection-v1\n") + stdout.write(f"plugin_root\t{plugin_root}\n") + stdout.write(f"skill_directory\t{skill_directory}\n") + stdout.write(f"skill_file\t{skill_file}\n") + stdout.write(f"name\t{metadata.name}\n") + stdout.write(f"description_length\t{len(metadata.description)}\n") + for reference in _references(text, skill_directory, plugin_root): + stdout.write(f"local_reference\t{reference}\n") + for script in _bundled_scripts(skill_directory): + if not os.access(script, os.R_OK): + raise InspectionError(f"bundled script is unreadable: {script}") + stdout.write(f"bundled_script\t{script}\n") + stdout.write("status\tvalid\n") + + +def run(arguments: list[str], *, stdout: TextIO, stderr: TextIO) -> int: + if not arguments or arguments[0] != "inspect": + stderr.write(USAGE) + return 2 + if len(arguments) != 3: + stderr.write(f"{PROGRAM}: inspect requires SKILL_DIRECTORY and PLUGIN_ROOT\n") + return 2 + try: + _inspect(arguments[1], arguments[2], stdout) + except InspectionError as error: + stderr.write(f"{PROGRAM}: {error}\n") + return 2 + return 0 + + +def entrypoint() -> None: + raise SystemExit(run(sys.argv[1:], stdout=sys.stdout, stderr=sys.stderr)) + + +if __name__ == "__main__": + entrypoint() diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/shell_tests.py b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/shell_tests.py new file mode 100644 index 00000000..24873705 --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/shell_tests.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import TextIO + +PROGRAM = "verify-shell-tests" +USAGE = "usage: verify-shell-tests [--shell ]... -- ...\n" +DEFAULT_CANDIDATES = ( + "/bin/bash", + "bash", + "bash5", + "/opt/homebrew/bin/bash", + "/usr/local/bin/bash", +) +VERSION_COMMAND = ( + "printf '%s\\t%s\\t%s\\n' " + '"${BASH_VERSINFO[0]}" "${BASH_VERSINFO[1]}" "$BASH_VERSION"' +) + + +@dataclass(frozen=True) +class BashInterpreter: + label: str + path: Path + version: str + + +def _consume_shell_options( + arguments: list[str], candidates: list[str], position: int +) -> tuple[list[str], int]: + if position >= len(arguments): + return candidates, position + argument = arguments[position] + if argument == "--shell": + return _consume_shell_argument(arguments, candidates, position) + if argument == "--": + return candidates, position + 1 + if argument.startswith("-"): + raise ValueError + return candidates, position + + +def _consume_shell_argument( + arguments: list[str], candidates: list[str], position: int +) -> tuple[list[str], int]: + if position + 1 >= len(arguments): + raise ValueError + candidates.append(arguments[position + 1]) + return _consume_shell_options(arguments, candidates, position + 2) + + +def _parse_arguments(arguments: list[str]) -> tuple[list[str], list[str], bool]: + candidates, position = _consume_shell_options(arguments, [], 0) + tests = arguments[position:] + if not tests: + raise ValueError + return candidates or list(DEFAULT_CANDIDATES), tests, bool(candidates) + + +def _canonical_file(value: str) -> Path: + path = Path(value) + parent = path.parent if path.parent != Path("") else Path.cwd() + return parent.resolve(strict=True) / path.name + + +def _resolve_interpreter(requested: str) -> Path | None: + has_separator = any(separator in requested for separator in ("/", "\\")) + located = requested if has_separator else shutil.which(requested) + if located is None: + return None + path = Path(located) + if not path.is_file() or not os.access(path, os.X_OK): + return None + try: + return _canonical_file(str(path)) + except OSError: + return None + + +def _version(interpreter: Path) -> tuple[int, int, str] | None: + try: + completed = subprocess.run( + [str(interpreter), "-c", VERSION_COMMAND], + check=False, + capture_output=True, + text=True, + ) + except OSError: + return None + if completed.returncode != 0 or not completed.stdout: + return None + fields = completed.stdout.splitlines()[0].split("\t") + if len(fields) != 3: + return None + try: + return int(fields[0]), int(fields[1]), fields[2] + except ValueError: + return None + + +def _classify_version( + path: Path, version: tuple[int, int, str], explicit: bool +) -> BashInterpreter | None: + major, minor, display = version + if (major, minor) == (3, 2): + return BashInterpreter("bash-3.2", path, display) + if major == 5: + return BashInterpreter("bash-5", path, display) + if explicit: + raise ValueError(f"unsupported Bash version {display}: {path}") + return None + + +def _inspect_candidate(requested: str, explicit: bool) -> BashInterpreter | None: + path = _resolve_interpreter(requested) + if path is None: + if explicit: + raise ValueError(f"shell is not executable: {requested}") + return None + version = _version(path) + if version is None: + if explicit: + raise ValueError(f"cannot read Bash version: {path}") + return None + return _classify_version(path, version, explicit) + + +def discover_interpreters( + candidates: list[str], *, explicit: bool +) -> list[BashInterpreter]: + found: dict[str, BashInterpreter] = {} + for candidate in candidates: + interpreter = _inspect_candidate(candidate, explicit) + if interpreter is not None and interpreter.label not in found: + found[interpreter.label] = interpreter + return [found[label] for label in ("bash-3.2", "bash-5") if label in found] + + +def _test_files(inputs: list[str]) -> list[Path]: + tests: list[Path] = [] + for value in inputs: + path = Path(value) + if not path.is_file() or not os.access(path, os.R_OK): + raise ValueError(f"test script is not a readable regular file: {value}") + try: + tests.append(_canonical_file(value)) + except OSError as error: + raise ValueError(f"cannot resolve test script: {value}") from error + return tests + + +def run_test(interpreter: BashInterpreter, test: Path) -> int: + try: + returncode = subprocess.run( + [str(interpreter.path), str(test)], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ).returncode + except OSError: + return 127 + return returncode if returncode >= 0 else 128 - returncode + + +def _report_interpreter( + interpreter: BashInterpreter, tests: list[Path], stdout: TextIO +) -> int: + stdout.write( + f"interpreter\t{interpreter.label}\tavailable\t" + f"{interpreter.path}\t{interpreter.version}\n" + ) + failures = 0 + for test in tests: + status = run_test(interpreter, test) + if status == 0: + stdout.write(f"test_result\t{interpreter.label}\t{test}\tpassed\n") + else: + stdout.write( + f"test_result\t{interpreter.label}\t{test}\tfailed\texit={status}\n" + ) + failures += 1 + return failures + + +def _report_matrix( + interpreters: list[BashInterpreter], tests: list[Path], stdout: TextIO +) -> int: + stdout.write("format\tdarrow-shell-test-matrix-v1\n") + for test in tests: + stdout.write(f"test_script\t{test}\n") + by_label = {interpreter.label: interpreter for interpreter in interpreters} + results = [ + _report_target(label, by_label, tests, stdout) + for label in ("bash-3.2", "bash-5") + ] + failures = sum(result[0] for result in results) + missing = sum(result[1] for result in results) + if failures: + stdout.write("matrix_status\tfailed\n") + return 1 + if missing: + stdout.write("matrix_status\tunverified\n") + return 3 + stdout.write("matrix_status\tcomplete\n") + return 0 + + +def _report_target( + label: str, + by_label: dict[str, BashInterpreter], + tests: list[Path], + stdout: TextIO, +) -> tuple[int, int]: + interpreter = by_label.get(label) + if interpreter is None: + stdout.write(f"interpreter\t{label}\tunavailable\n") + return 0, 1 + return _report_interpreter(interpreter, tests, stdout), 0 + + +def run(arguments: list[str], *, stdout: TextIO, stderr: TextIO) -> int: + try: + candidates, test_inputs, explicit = _parse_arguments(arguments) + except ValueError: + stderr.write(USAGE) + return 2 + try: + tests = _test_files(test_inputs) + interpreters = discover_interpreters(candidates, explicit=explicit) + except ValueError as error: + stderr.write(f"{PROGRAM}: {error}\n") + return 2 + return _report_matrix(interpreters, tests, stdout) + + +def entrypoint() -> None: + raise SystemExit(run(sys.argv[1:], stdout=sys.stdout, stderr=sys.stderr)) + + +if __name__ == "__main__": + entrypoint() diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_inspector.py b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_inspector.py new file mode 100644 index 00000000..ea8f3e23 --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_inspector.py @@ -0,0 +1,347 @@ +from __future__ import annotations + +import io +import os +import sys +from pathlib import Path + +import pytest + +from darrow_skill_authoring import inspector +from darrow_skill_authoring.inspector import InspectionError, MetadataFormatError, run + + +def make_skill(root: Path, name: str = "author-release-skill") -> tuple[Path, Path]: + skill = root / "skills" / name + (skill / "references").mkdir(parents=True) + (skill / "scripts").mkdir() + (skill / "SKILL.md").write_text( + "---\n" + f"name: {name}\n" + "description: Create release workflow skills for repeatable publishing.\n" + "---\n\n" + "Read [policy](references/policy.md) before changing safeguards.\n", + encoding="utf-8", + ) + (skill / "references" / "policy.md").write_text("# Policy\n", encoding="utf-8") + (skill / "scripts" / "check-release").write_text("exit 0\n", encoding="utf-8") + return root, skill + + +def invoke(*arguments: str) -> tuple[int, str, str]: + stdout = io.StringIO() + stderr = io.StringIO() + status = run(list(arguments), stdout=stdout, stderr=stderr) + return status, stdout.getvalue(), stderr.getvalue() + + +def test_valid_skill_reports_absolute_stable_evidence(tmp_path: Path) -> None: + plugin, skill = make_skill(tmp_path) + + status, output, error = invoke("inspect", str(skill), str(plugin)) + + assert status == 0 + assert error == "" + assert "format\tdarrow-skill-inspection-v1\n" in output + assert f"plugin_root\t{plugin.resolve()}\n" in output + assert f"skill_directory\t{skill.resolve()}\n" in output + assert ( + f"local_reference\t{skill.resolve() / 'references' / 'policy.md'}\n" in output + ) + assert ( + f"bundled_script\t{skill.resolve() / 'scripts' / 'check-release'}\n" in output + ) + assert output.endswith("status\tvalid\n") + + +@pytest.mark.parametrize("value", ["true", "false"]) +def test_optional_invocation_boolean_is_accepted(tmp_path: Path, value: str) -> None: + plugin, skill = make_skill(tmp_path) + path = skill / "SKILL.md" + path.write_text( + path.read_text(encoding="utf-8").replace( + "description:", f"disable-model-invocation: {value}\ndescription:" + ), + encoding="utf-8", + ) + + assert invoke("inspect", str(skill), str(plugin))[0] == 0 + + +@pytest.mark.parametrize( + ("frontmatter", "message"), + [ + ( + "name: author-release-skill\ndescription: valid\nunknown: true", + "frontmatter must contain", + ), + ( + "name: author-release-skill\ndescription: valid\n" + "disable-model-invocation: sometimes", + "frontmatter must contain", + ), + ( + "name: author-release-skill\ndescription: valid\n" + "disable-model-invocation: true\ndisable-model-invocation: false", + "frontmatter must contain", + ), + ( + 'name: "author-release-skill\ndescription: valid', + "unmatched double quote", + ), + ("name: different-name\ndescription: valid", "does not match directory"), + ], +) +def test_malformed_metadata_fails_closed( + tmp_path: Path, frontmatter: str, message: str +) -> None: + plugin, skill = make_skill(tmp_path) + (skill / "SKILL.md").write_text(f"---\n{frontmatter}\n---\n", encoding="utf-8") + + status, _, error = invoke("inspect", str(skill), str(plugin)) + + assert status == 2 + assert message in error + assert str(skill.resolve() / "SKILL.md") in error + + +def test_missing_inputs_report_absolute_paths( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.chdir(tmp_path) + status, _, skill_error = invoke("inspect", "missing", ".") + assert status == 2 + assert str(tmp_path / "missing" / "SKILL.md") in skill_error + + status, _, plugin_error = invoke("inspect", ".", "missing-plugin") + assert status == 2 + assert str(tmp_path / "missing-plugin") in plugin_error + + +def test_skill_and_references_cannot_escape_plugin(tmp_path: Path) -> None: + plugin = tmp_path / "plugin" + external, skill = make_skill(tmp_path / "external") + plugin.mkdir() + + status, _, error = invoke("inspect", str(skill), str(plugin)) + assert status == 2 + assert "skill directory escapes plugin root" in error + + plugin, skill = make_skill(tmp_path / "contained") + (skill / "SKILL.md").write_text( + (skill / "SKILL.md").read_text(encoding="utf-8") + + "Read [outside](../../../outside.md).\n", + encoding="utf-8", + ) + (tmp_path / "outside.md").write_text("outside\n", encoding="utf-8") + status, _, error = invoke("inspect", str(skill), str(plugin)) + assert status == 2 + assert "local reference escapes plugin root" in error + assert external.exists() + + +def test_symlinked_skill_and_reference_are_rejected(tmp_path: Path) -> None: + if not hasattr(os, "symlink"): + pytest.skip("symbolic links are unavailable") + plugin, skill = make_skill(tmp_path / "plugin") + external = tmp_path / "external.md" + external.write_text("outside\n", encoding="utf-8") + reference = skill / "references" / "policy.md" + reference.unlink() + try: + reference.symlink_to(external) + except OSError: + pytest.skip("symbolic links are unavailable") + + status, _, error = invoke("inspect", str(skill), str(plugin)) + assert status == 2 + assert "local reference must not be a symbolic link" in error + + linked_skill = plugin / "skills" / "linked-skill" + linked_skill.mkdir() + (linked_skill / "SKILL.md").symlink_to(skill / "SKILL.md") + status, _, error = invoke("inspect", str(linked_skill), str(plugin)) + assert status == 2 + assert "skill file must not be a symbolic link" in error + + +def test_reported_symlinks_are_rejected_on_hosts_without_symlink_creation( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + plugin, skill = make_skill(tmp_path) + skill_file = skill / "SKILL.md" + reference = skill / "references" / "policy.md" + original = Path.is_symlink + + def report_skill(path: Path) -> bool: + return path == skill_file or original(path) + + monkeypatch.setattr(Path, "is_symlink", report_skill) + with pytest.raises(InspectionError, match="skill file must not be a symbolic link"): + inspector._read_skill(skill) + + def report_reference(path: Path) -> bool: + return path == reference or original(path) + + monkeypatch.setattr(Path, "is_symlink", report_reference) + with pytest.raises( + InspectionError, match="local reference must not be a symbolic link" + ): + inspector._local_reference("references/policy.md", skill, plugin) + + +def test_space_bearing_relative_paths_work_outside_skill( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + plugin, skill = make_skill(tmp_path / "plugin with spaces") + monkeypatch.chdir(tmp_path) + + status, output, _ = invoke( + "inspect", str(skill.relative_to(tmp_path)), str(plugin.relative_to(tmp_path)) + ) + + assert status == 0 + assert f"skill_directory\t{skill.resolve()}" in output + + +def test_usage_and_argument_errors_keep_exit_two(tmp_path: Path) -> None: + assert invoke()[0:2] == (2, "") + assert "Usage: inspect-skill" in invoke()[2] + status, _, error = invoke("inspect", str(tmp_path)) + assert status == 2 + assert error == "inspect-skill: inspect requires SKILL_DIRECTORY and PLUGIN_ROOT\n" + + +@pytest.mark.parametrize( + "text", + [ + "", + "name: missing-delimiters", + "---\nname: missing-end", + "---\nname: missing-description\n---\n", + "---\nname: duplicate\nname: duplicate\ndescription: valid\n---\n", + ], +) +def test_frontmatter_parser_rejects_incomplete_shapes(text: str) -> None: + with pytest.raises(MetadataFormatError): + inspector._parse_frontmatter(text) + + +def test_frontmatter_comments_and_blank_lines_are_ignored() -> None: + metadata = inspector._parse_frontmatter( + "---\n# host-neutral fields\n\nname: valid-name\ndescription: valid\n---\n" + ) + assert metadata == inspector.Metadata("valid-name", "valid") + + +@pytest.mark.parametrize("name", ["Upper", "-leading", "trailing-", "two--hyphens"]) +def test_invalid_portable_names_are_rejected(tmp_path: Path, name: str) -> None: + skill = tmp_path / "valid-directory" + skill.mkdir() + with pytest.raises(InspectionError, match="lower-case letters"): + inspector._validate_metadata(inspector.Metadata(name, "valid"), skill) + + +def test_empty_description_and_unmatched_single_quote_are_rejected( + tmp_path: Path, +) -> None: + skill = tmp_path / "valid-directory" + skill.mkdir() + with pytest.raises(InspectionError, match="description is empty"): + inspector._validate_metadata(inspector.Metadata("valid-directory", ""), skill) + with pytest.raises(InspectionError, match="unmatched single quote"): + inspector._validate_metadata( + inspector.Metadata("valid-directory", "'unfinished"), skill + ) + + +@pytest.mark.parametrize( + "target", ["", "#section", "https://example.test", "mailto:a@b"] +) +def test_nonlocal_links_are_ignored(target: str) -> None: + assert inspector._portable_target(target) is None + + +@pytest.mark.parametrize( + "target", ["/absolute/path", r"C:\absolute\path", r"references\policy.md", "C:file"] +) +def test_nonportable_local_links_are_rejected(target: str) -> None: + with pytest.raises(InspectionError, match="portable relative paths"): + inspector._portable_target(target) + + +def test_angle_wrapped_link_loses_anchor() -> None: + assert inspector._portable_target("") == ( + "references/policy.md" + ) + + +def test_missing_reference_parent_and_file_are_distinct(tmp_path: Path) -> None: + plugin, skill = make_skill(tmp_path) + with pytest.raises(InspectionError, match="parent is not readable"): + inspector._local_reference("missing/policy.md", skill, plugin) + with pytest.raises(InspectionError, match="not a readable regular file"): + inspector._local_reference("references/missing.md", skill, plugin) + + +def test_skill_without_scripts_has_empty_inventory(tmp_path: Path) -> None: + plugin, skill = make_skill(tmp_path) + for path in (skill / "scripts").iterdir(): + path.unlink() + (skill / "scripts").rmdir() + assert inspector._bundled_scripts(skill) == [] + assert plugin.exists() + + +def test_non_directory_scripts_path_is_rejected(tmp_path: Path) -> None: + _, skill = make_skill(tmp_path) + for path in (skill / "scripts").iterdir(): + path.unlink() + (skill / "scripts").rmdir() + (skill / "scripts").write_text("invalid\n", encoding="utf-8") + with pytest.raises( + InspectionError, match="scripts path is not a readable directory" + ): + inspector._bundled_scripts(skill) + + +def test_walk_failure_is_translated( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _, skill = make_skill(tmp_path) + + def fail_walk(_: Path) -> list[Path]: + raise OSError + + monkeypatch.setattr(inspector, "_walk_scripts", fail_walk) + with pytest.raises(InspectionError, match="cannot enumerate bundled scripts"): + inspector._bundled_scripts(skill) + + +def test_invalid_utf8_skill_is_rejected(tmp_path: Path) -> None: + _, skill = make_skill(tmp_path) + (skill / "SKILL.md").write_bytes(b"\xff") + with pytest.raises(InspectionError, match="not a readable regular file"): + inspector._read_skill(skill) + + +def test_directory_resolution_failure_is_translated( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + original = Path.resolve + + def fail_selected(path: Path, strict: bool = False) -> Path: + if path == tmp_path: + raise OSError + return original(path, strict=strict) + + monkeypatch.setattr(Path, "resolve", fail_selected) + with pytest.raises(InspectionError, match="cannot resolve plugin root"): + inspector._readable_directory(str(tmp_path), "plugin root") + + +def test_entrypoint_returns_run_status(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(sys, "argv", ["inspect-skill"]) + with pytest.raises(SystemExit) as raised: + inspector.entrypoint() + assert raised.value.code == 2 diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_properties.py b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_properties.py new file mode 100644 index 00000000..7976da17 --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_properties.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import io +import tempfile +from pathlib import Path + +from hypothesis import given, settings +from hypothesis import strategies as st + +from darrow_skill_authoring.inspector import run + + +@settings(max_examples=30, deadline=None) +@given(st.from_regex(r"[a-z][a-z0-9]{0,5}(?:-[a-z0-9]{1,6}){0,3}", fullmatch=True)) +def test_valid_portable_names_round_trip_through_inspection( + name: str, +) -> None: + with tempfile.TemporaryDirectory() as directory: + plugin = Path(directory) / "plugin" + skill = plugin / "skills" / name + skill.mkdir(parents=True) + (skill / "SKILL.md").write_text( + f"---\nname: '{name}'\ndescription: \"A generated valid description.\"\n---\n", + encoding="utf-8", + ) + stdout = io.StringIO() + stderr = io.StringIO() + + status = run(["inspect", str(skill), str(plugin)], stdout=stdout, stderr=stderr) + + assert status == 0 + assert stderr.getvalue() == "" + assert f"name\t{name}\n" in stdout.getvalue() diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_shell_tests.py b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_shell_tests.py new file mode 100644 index 00000000..abd77cf4 --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_shell_tests.py @@ -0,0 +1,277 @@ +from __future__ import annotations + +import io +import os +import subprocess +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest + +from darrow_skill_authoring import shell_tests +from darrow_skill_authoring.shell_tests import BashInterpreter, run + + +def invoke(*arguments: str) -> tuple[int, str, str]: + stdout = io.StringIO() + stderr = io.StringIO() + status = run(list(arguments), stdout=stdout, stderr=stderr) + return status, stdout.getvalue(), stderr.getvalue() + + +def test_complete_matrix_runs_each_test_under_both_versions(tmp_path: Path) -> None: + test = tmp_path / "passing test.sh" + test.write_text("exit 0\n", encoding="utf-8") + interpreters = [ + BashInterpreter("bash-3.2", Path("/shell/three"), "3.2.57-test"), + BashInterpreter("bash-5", Path("/shell/five"), "5.2.0-test"), + ] + with ( + patch( + "darrow_skill_authoring.shell_tests.discover_interpreters", + return_value=interpreters, + ), + patch("darrow_skill_authoring.shell_tests.run_test", return_value=0) as execute, + ): + status, output, error = invoke(str(test)) + + assert status == 0 + assert error == "" + assert "format\tdarrow-shell-test-matrix-v1\n" in output + assert ( + f"interpreter\tbash-3.2\tavailable\t{interpreters[0].path}\t3.2.57-test\n" + ) in output + assert ( + f"interpreter\tbash-5\tavailable\t{interpreters[1].path}\t5.2.0-test\n" + ) in output + assert output.endswith("matrix_status\tcomplete\n") + assert execute.call_count == 2 + + +def test_missing_version_is_unverified(tmp_path: Path) -> None: + test = tmp_path / "passing.sh" + test.write_text("exit 0\n", encoding="utf-8") + found = [BashInterpreter("bash-3.2", Path("/shell/three"), "3.2.57-test")] + with ( + patch( + "darrow_skill_authoring.shell_tests.discover_interpreters", + return_value=found, + ), + patch("darrow_skill_authoring.shell_tests.run_test", return_value=0), + ): + status, output, _ = invoke(str(test)) + + assert status == 3 + assert "interpreter\tbash-5\tunavailable\n" in output + assert output.endswith("matrix_status\tunverified\n") + + +def test_test_failure_wins_over_missing_version(tmp_path: Path) -> None: + test = tmp_path / "failing.sh" + test.write_text("exit 9\n", encoding="utf-8") + found = [BashInterpreter("bash-5", Path("/shell/five"), "5.2.0-test")] + with ( + patch( + "darrow_skill_authoring.shell_tests.discover_interpreters", + return_value=found, + ), + patch("darrow_skill_authoring.shell_tests.run_test", return_value=9), + ): + status, output, _ = invoke(str(test)) + + assert status == 1 + assert f"test_result\tbash-5\t{test.resolve()}\tfailed\texit=9\n" in output + assert output.endswith("matrix_status\tfailed\n") + + +@pytest.mark.parametrize("arguments", [(), ("--shell",), ("--unknown",)]) +def test_invalid_invocation_prints_usage(arguments: tuple[str, ...]) -> None: + status, _, error = invoke(*arguments) + assert status == 2 + assert error == ( + "usage: verify-shell-tests [--shell ]... -- ...\n" + ) + + +def test_missing_test_is_identified_with_exit_two(tmp_path: Path) -> None: + missing = tmp_path / "missing.sh" + status, _, error = invoke(str(missing)) + assert status == 2 + assert error == ( + f"verify-shell-tests: test script is not a readable regular file: {missing}\n" + ) + + +def test_explicit_shell_discovery_error_is_reported(tmp_path: Path) -> None: + test = tmp_path / "passing.sh" + test.write_text("exit 0\n", encoding="utf-8") + with patch( + "darrow_skill_authoring.shell_tests.discover_interpreters", + side_effect=ValueError("shell is not executable: missing-bash"), + ): + status, _, error = invoke("--shell", "missing-bash", "--", str(test)) + assert status == 2 + assert error == "verify-shell-tests: shell is not executable: missing-bash\n" + + +def test_duplicate_interpreter_versions_are_run_once(tmp_path: Path) -> None: + test = tmp_path / "passing.sh" + test.write_text("exit 0\n", encoding="utf-8") + found = [ + BashInterpreter("bash-3.2", Path("/shell/first"), "3.2.57"), + BashInterpreter("bash-5", Path("/shell/five"), "5.2.0"), + ] + with ( + patch( + "darrow_skill_authoring.shell_tests.discover_interpreters", + return_value=found, + ), + patch("darrow_skill_authoring.shell_tests.run_test", return_value=0) as execute, + ): + assert invoke(str(test))[0] == 0 + assert execute.call_count == 2 + + +def test_real_interpreter_resolution_accepts_path_and_command(tmp_path: Path) -> None: + executable = tmp_path / "shell" + executable.write_text("placeholder\n", encoding="utf-8") + executable.chmod(0o755) + assert shell_tests._resolve_interpreter(str(executable)) == executable.resolve() + with patch( + "darrow_skill_authoring.shell_tests.shutil.which", return_value=str(executable) + ): + assert shell_tests._resolve_interpreter("shell") == executable.resolve() + + +def test_interpreter_resolution_rejects_missing_and_nonexecutable( + tmp_path: Path, +) -> None: + assert shell_tests._resolve_interpreter(str(tmp_path / "missing")) is None + candidate = tmp_path / "shell" + candidate.write_text("placeholder\n", encoding="utf-8") + candidate.chmod(0o600) + if os.name != "nt": + assert shell_tests._resolve_interpreter(str(candidate)) is None + with patch("darrow_skill_authoring.shell_tests.shutil.which", return_value=None): + assert shell_tests._resolve_interpreter("missing") is None + + +@pytest.mark.parametrize( + ("completed", "expected"), + [ + (subprocess.CompletedProcess([], 1, "", ""), None), + (subprocess.CompletedProcess([], 0, "", ""), None), + (subprocess.CompletedProcess([], 0, "3\t2\n", ""), None), + (subprocess.CompletedProcess([], 0, "three\t2\tbad\n", ""), None), + (subprocess.CompletedProcess([], 0, "3\t2\t3.2.57\n", ""), (3, 2, "3.2.57")), + ], +) +def test_version_output_is_validated( + completed: subprocess.CompletedProcess[str], + expected: tuple[int, int, str] | None, +) -> None: + with patch( + "darrow_skill_authoring.shell_tests.subprocess.run", return_value=completed + ): + assert shell_tests._version(Path("shell")) == expected + + +def test_version_spawn_failure_is_unavailable() -> None: + with patch( + "darrow_skill_authoring.shell_tests.subprocess.run", side_effect=OSError + ): + assert shell_tests._version(Path("shell")) is None + + +def test_version_classification_rejects_explicit_unsupported() -> None: + path = Path("shell") + bash_three = shell_tests._classify_version(path, (3, 2, "3.2"), False) + bash_five = shell_tests._classify_version(path, (5, 3, "5.3"), False) + assert bash_three is not None + assert bash_five is not None + assert bash_three.label == "bash-3.2" + assert bash_five.label == "bash-5" + assert shell_tests._classify_version(path, (4, 4, "4.4"), False) is None + with pytest.raises(ValueError, match="unsupported Bash version"): + shell_tests._classify_version(path, (4, 4, "4.4"), True) + + +@pytest.mark.parametrize("explicit", [False, True]) +def test_candidate_handles_missing_interpreter(explicit: bool) -> None: + with patch( + "darrow_skill_authoring.shell_tests._resolve_interpreter", return_value=None + ): + if explicit: + with pytest.raises(ValueError, match="shell is not executable"): + shell_tests._inspect_candidate("missing", explicit) + else: + assert shell_tests._inspect_candidate("missing", explicit) is None + + +@pytest.mark.parametrize("explicit", [False, True]) +def test_candidate_handles_unreadable_version(explicit: bool) -> None: + with ( + patch( + "darrow_skill_authoring.shell_tests._resolve_interpreter", + return_value=Path("shell"), + ), + patch("darrow_skill_authoring.shell_tests._version", return_value=None), + ): + if explicit: + with pytest.raises(ValueError, match="cannot read Bash version"): + shell_tests._inspect_candidate("shell", explicit) + else: + assert shell_tests._inspect_candidate("shell", explicit) is None + + +def test_discovery_deduplicates_labels_and_skips_unknown() -> None: + values = [ + BashInterpreter("bash-3.2", Path("first"), "3.2"), + BashInterpreter("bash-3.2", Path("second"), "3.2"), + None, + BashInterpreter("bash-5", Path("five"), "5.2"), + ] + with patch( + "darrow_skill_authoring.shell_tests._inspect_candidate", side_effect=values + ): + found = shell_tests.discover_interpreters(["a", "b", "c", "d"], explicit=False) + assert [item.label for item in found] == ["bash-3.2", "bash-5"] + assert found[0].path == Path("first") + + +def test_test_file_resolution_failure_is_translated( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + test = tmp_path / "test.sh" + test.write_text("exit 0\n", encoding="utf-8") + + def fail(_: str) -> Path: + raise OSError + + monkeypatch.setattr(shell_tests, "_canonical_file", fail) + with pytest.raises(ValueError, match="cannot resolve test script"): + shell_tests._test_files([str(test)]) + + +def test_run_test_reports_process_status_and_spawn_failure(tmp_path: Path) -> None: + test = tmp_path / "test.py" + test.write_text("raise SystemExit(7)\n", encoding="utf-8") + interpreter = BashInterpreter("bash-5", Path(sys.executable), "test") + assert shell_tests.run_test(interpreter, test) == 7 + with patch( + "darrow_skill_authoring.shell_tests.subprocess.run", side_effect=OSError + ): + assert shell_tests.run_test(interpreter, test) == 127 + with patch( + "darrow_skill_authoring.shell_tests.subprocess.run", + return_value=subprocess.CompletedProcess([], -9), + ): + assert shell_tests.run_test(interpreter, test) == 137 + + +def test_entrypoint_returns_run_status(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(sys, "argv", ["verify-shell-tests"]) + with pytest.raises(SystemExit) as raised: + shell_tests.entrypoint() + assert raised.value.code == 2 diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/uv.lock b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/uv.lock new file mode 100644 index 00000000..8bc57b4f --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/uv.lock @@ -0,0 +1,462 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.14" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/2d/c738872f477f5687152acae68635790387425d407ae37dd3d3a8a6692307/coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210", size = 969651, upload-time = "2026-09-13T19:12:21.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/62/a8b4dd53308a4b1571e6bdd77ee562c9b1c1742db9886c0aef2f69463067/coverage-7.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f12a9e27ca7b65e40a8475d27899b2d45064d9020e6a89148939e01987b5853", size = 223178, upload-time = "2026-09-13T19:08:21.805Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7a/b9325b8d486a5c05fd4a9eb1cd9c9f697c361ddeda0e73567d5c2d8959f6/coverage-7.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f590d46c30d9e4c1fda3efefe5443f4c2f6a4192c5ca2ba403653e9ebadf097", size = 223701, upload-time = "2026-09-13T19:08:23.391Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ee/3ab3ac6e9e7165a623695b73a83d1bfbdf5069681a68a012cb5abb096060/coverage-7.16.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3df82f0a3cef4e1bcfc799436056f0b978dda319d0bfd4460c6e479b2802d98a", size = 250434, upload-time = "2026-09-13T19:08:24.948Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/31ffc231e9879876f611cf1ac7622cf092d76762c07ca64dce47b29a200b/coverage-7.16.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb462d59146656e278d1e8ed913ce374d0ba68a4e081acde1867f6d3377fc881", size = 252264, upload-time = "2026-09-13T19:08:26.734Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f4/4fb4f6b16de07dbae3d3a111ae28d32ad657ce6ff4d364c49c10962daad5/coverage-7.16.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7db888dd0a1df1a653cae7f99d4047430d2187a3626eda51bc847b0fd6b9b43", size = 254127, upload-time = "2026-09-13T19:08:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/048e88eb610f44b7542dac10fa45bb2ffad5fbfcdab0f03289a843da934b/coverage-7.16.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:38a7e16f061504ac2b45370bf5bf97e8250d8d3f25e37385bae884978166554b", size = 256043, upload-time = "2026-09-13T19:08:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/97/59/d27b113c80b04c7fc64c69457132648e1d287b83a1de41189a504b50b52b/coverage-7.16.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b26b55b18e1a53e1a159dd743728c4ddbbef28ba19000a91aaff5ce023197ec", size = 251092, upload-time = "2026-09-13T19:08:31.57Z" }, + { url = "https://files.pythonhosted.org/packages/45/42/b7eafaceff08dc1dcbfc924fa9233031fd00c8b8287cc4a1a0dcdedcb0f9/coverage-7.16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb2f528b50953e29d22033b3256c396a700193c6e45b2490222ef9c333cbbf9", size = 252170, upload-time = "2026-09-13T19:08:33.191Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8f/57dcc5360aa6d8d24370c12fe855f6ebf5fbf2718c154fb9fdec7161de07/coverage-7.16.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3284754371dc78592aa3ae4d661d30ee20e02b2b6b0de3590a181a936d0d3b38", size = 250173, upload-time = "2026-09-13T19:08:34.75Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/379a8c7ac0f3063769dc43fd2475e9fc9b2722cb88eabe71f4d862660c93/coverage-7.16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:802d1246c540e07486d4ee1adfa19a797e4b33529ffc371dc140644e8f27da0a", size = 253986, upload-time = "2026-09-13T19:08:36.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/25/70218d2a6077730843b75ee2525fa64a750283d71cc4c063dc0b75ba6979/coverage-7.16.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6fc735d6fe6d57f803e7ba021be4dde48e43e6aff94e6954350555d5332b0594", size = 250444, upload-time = "2026-09-13T19:08:38.434Z" }, + { url = "https://files.pythonhosted.org/packages/58/ac/6c41c441baa6c8a1fed5615e4e1e7d0ca649b33b472ee28591a92d70a1ff/coverage-7.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed5ade1bb18f62edace1bd198c66f9d4c75a8385d5fd24e87d917ea1a5958773", size = 251046, upload-time = "2026-09-13T19:08:40.133Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/01166d6f8aa42acd148b4c2c88173d60eefdd0270ea2bcbba79be35bc193/coverage-7.16.1-cp310-cp310-win32.whl", hash = "sha256:0c309096926b119543dc16438a11ef4c80783d2f4e59ff94f7f462651a944cdc", size = 225242, upload-time = "2026-09-13T19:08:41.672Z" }, + { url = "https://files.pythonhosted.org/packages/d2/28/a76060478919becf0fbea2df2053a6e1a7195f11edf8dfd51c58c1636b43/coverage-7.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:7562f8067ed9360e8b9739e5703403a7686dd1b36bf0f89fc538047c54cdea90", size = 225868, upload-time = "2026-09-13T19:08:43.22Z" }, + { url = "https://files.pythonhosted.org/packages/af/3a/d09495dfd5191b5593852bbe2f037afae768157443378d066d9ecea69a49/coverage-7.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72e013665e25cf9d44779f01f340af26319756f9a76822b7c94ce6b1d93813da", size = 223307, upload-time = "2026-09-13T19:08:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/310196a80258e28ebf6e5aa814a5a6ffbb18c03cf3c0c0c04191ed893e88/coverage-7.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb05c0ff98b56ba6969adf35556bc43bcb8d094df8bc9cb403acff53460c4e07", size = 223817, upload-time = "2026-09-13T19:08:46.738Z" }, + { url = "https://files.pythonhosted.org/packages/73/29/ed0fda1fcd440cdfc07e72a07f3c9c3b43f65a6b3d53bff2cba9b9de50e8/coverage-7.16.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0ececb32090e3fbb03e0d352b973a0485879b4de6c58daf47227b9988b99e5", size = 254222, upload-time = "2026-09-13T19:08:48.358Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4e/5e2507fbd71ec1047620978f39568b0d2ed6c4468d08495a0c24f5677f65/coverage-7.16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f0ba3892d81aacf36996c52f16bca04e39af31a6c5de930b7688ab617f4a6475", size = 256134, upload-time = "2026-09-13T19:08:49.926Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b1/a9f97846554bc240473656f9c3874591105f29c08eccf4282daadc6bc281/coverage-7.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6410b75fe07d5271eaa95fc24bd0a9177ed588d9d1c10c0cf67829adb8f0567", size = 258240, upload-time = "2026-09-13T19:08:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/2b1ab208d06719394851cdd7dc322157870a93b178410b1bda6cac7755f9/coverage-7.16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1039cb2de093225d597109342ce1675626bd127565e80b3044f4eca07c15b2e", size = 260202, upload-time = "2026-09-13T19:08:53.204Z" }, + { url = "https://files.pythonhosted.org/packages/af/14/ff4db31d6e3126d5b72aab1e868d35ec94e1b71dee46f5c3b54cb1b2438e/coverage-7.16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf047bc39fde5425be2628666d0f435ed8817859111c3aacc84b32d858069f5d", size = 254305, upload-time = "2026-09-13T19:08:54.833Z" }, + { url = "https://files.pythonhosted.org/packages/ce/27/c28faa4818f61c49ca3bfb4a77e1c191e79a40447504b89af0bd859cd6fc/coverage-7.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c05913d0d5badf7ac83200f35dcf9514cce5df16a7cc89e7d1d7fff0461813b", size = 255935, upload-time = "2026-09-13T19:08:56.453Z" }, + { url = "https://files.pythonhosted.org/packages/46/5a/42e64e5b716048cae33e2e15f8c6fe04a927467056c533b8e88900da895e/coverage-7.16.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4027bf6d7bc0a16df058ce913b69f10c5687f8e1ca668f08caa659ce101744bf", size = 253995, upload-time = "2026-09-13T19:08:58.159Z" }, + { url = "https://files.pythonhosted.org/packages/84/e5/860287acd5a1e29acd337f302b476b54148203590091b1a1555b2e914b50/coverage-7.16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4f48b345f831eaf4402ab6333c2dc3e2e2b5bc7b9c1b8fe12680dee3f0538f01", size = 257767, upload-time = "2026-09-13T19:08:59.958Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c5/4b68006da567b4b413352f891776c35776b3557a0479a28187ab57c21659/coverage-7.16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8643baeb590726c558b2faed6cd59b0917480f9367fcc692026f1a86d824fd08", size = 253715, upload-time = "2026-09-13T19:09:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0e/b2d0c47cfbf11770e197f1d0f11a92864eb29fd7eaca45bc9915573d9725/coverage-7.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d06dcc420b570bf683cdb647cc8fe62b672d9e429ef711c3cbbb7a6880ca1572", size = 254624, upload-time = "2026-09-13T19:09:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/2e/14/3bab8821b2b942155578fc34e88e21b0670d922f039ab23651f6035ba7f7/coverage-7.16.1-cp311-cp311-win32.whl", hash = "sha256:946f58aa59b08bcd6afcc6a7bd0ff54ed5eee844f32ad69fe6814836d15856a2", size = 225404, upload-time = "2026-09-13T19:09:05.174Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ce/4f4ce667a97d1c538b46a79b89161395ad2beffc248b6c782afc9f521927/coverage-7.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:684c7ee9b4c04358fe6ac8b517ab51ec35fcd79d08ff0f105dd8bcd96885bbb7", size = 225879, upload-time = "2026-09-13T19:09:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/26/17/f3dce5e47351ab34522dd037fd64b111742958e24861bdd2820971bc1a26/coverage-7.16.1-cp311-cp311-win_arm64.whl", hash = "sha256:1b24f79e25bcf6c73931aeca7a3dfc7595c0cb5e9364aba3fdf387a3de4b1c22", size = 225428, upload-time = "2026-09-13T19:09:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f7/7cd4c9f2a3b7574414222ec425d5eee21cc690d867a013315e3be8ba185c/coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af", size = 223475, upload-time = "2026-09-13T19:09:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f2/9ac65f9cedd43f91e2d3657c11f13aef82aebae89b2243dc9e44fe58a740/coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c", size = 223845, upload-time = "2026-09-13T19:09:11.988Z" }, + { url = "https://files.pythonhosted.org/packages/62/4d/f13db452d4367fe1122abfd98ae330596f65a3b40498f8e1a5811f68f123/coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809", size = 255341, upload-time = "2026-09-13T19:09:13.708Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6b/85ed86e82835a96ddfbe7705c387c28ce1cbbecd1b6d606f5ddfeeb712df/coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf", size = 258078, upload-time = "2026-09-13T19:09:15.368Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/f66945853d850b9c05f4e012e37396f1f31d0cd40d062394b229c22e7b56/coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f", size = 259191, upload-time = "2026-09-13T19:09:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/77/a1/7b907abe62461f289035c7ac60ab3e6334efb8c425151aac4abfa0c97820/coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360", size = 261452, upload-time = "2026-09-13T19:09:18.756Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/e26f4d6b1069a2a2fee3238a132f85c5b4c1dc25aebd88e015451ff0bd68/coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08", size = 255698, upload-time = "2026-09-13T19:09:20.786Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ec/99db7450e813050ebce300e31bbd4f997c76a8c6e3a6d168c5dffc104109/coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941", size = 257111, upload-time = "2026-09-13T19:09:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9d/500df9d3cd8c541ac84b5e0bcab00646be75654e05aa34e8410ec851282d/coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489", size = 255258, upload-time = "2026-09-13T19:09:24.429Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/53318d96da332bb4946f8ac646fa19fd37fecbd0c49144735c716ac69bf0/coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776", size = 259326, upload-time = "2026-09-13T19:09:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ce/abc0462b2e6ae96ae22197d2bc30fe33b6b4e52937e782745436ceb2a761/coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6", size = 254827, upload-time = "2026-09-13T19:09:28.191Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e1/0a04eedaaf19196b51f0180968134228c7646e469ed29914e48690a7cf3e/coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0", size = 256698, upload-time = "2026-09-13T19:09:29.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/1d/d441a55cf22ce9d8e9e34814806c47441ab844bd534e0f4b64e1c6ae8bd1/coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d", size = 225541, upload-time = "2026-09-13T19:09:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/ec3d032375735dd331477caa051419678079ff90fcb53d0284a6c2bfb757/coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e", size = 226075, upload-time = "2026-09-13T19:09:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/94/00/90e9f5c4434878494306b9c0ee8068ebbd829483737d8cefccc58884d728/coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8", size = 225461, upload-time = "2026-09-13T19:09:35.48Z" }, + { url = "https://files.pythonhosted.org/packages/aa/74/c08c0c4dc9fa6bcd1d90728a63660aa1b17b488a806948598456c48f75d1/coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9", size = 223501, upload-time = "2026-09-13T19:09:37.276Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c8/784986d326663285258a8e39835c46fb730cc85284f0dbcd82078586dd22/coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245", size = 223876, upload-time = "2026-09-13T19:09:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/15/76/73fb792928872bbb07e553f920ff55c65ee962c469265feb5d1d4ae5f97a/coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5", size = 254863, upload-time = "2026-09-13T19:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/ca/8d/1fd78899513244b065ccecdd1cfc6aa8b8f1bdee796d8c4f2aa8e8e5397d/coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5", size = 257460, upload-time = "2026-09-13T19:09:43.086Z" }, + { url = "https://files.pythonhosted.org/packages/47/1c/b23ddfcb7ef9bd7b3fdb7be9a5dccf9925ae88e556df7aa5b655283c78f2/coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f", size = 258697, upload-time = "2026-09-13T19:09:45.161Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/19d0c6ca35778a7e9415c30c46a0c64c9d4374219d004a35563132296896/coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41", size = 260827, upload-time = "2026-09-13T19:09:47.061Z" }, + { url = "https://files.pythonhosted.org/packages/97/fc/5862f7344382c62b85df43d483e579168cc062e007f54d895dfa51fe3e7d/coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878", size = 255038, upload-time = "2026-09-13T19:09:48.945Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9f/5c26583199a68df8d15124b4590520903f8eb7cef17db293fea712bb0783/coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2", size = 256827, upload-time = "2026-09-13T19:09:50.738Z" }, + { url = "https://files.pythonhosted.org/packages/95/13/605198bff079b107336710f28a797312ab132587168600d70a290e7ecd2e/coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080", size = 254794, upload-time = "2026-09-13T19:09:52.577Z" }, + { url = "https://files.pythonhosted.org/packages/43/b7/0bbb32dc5ccdac766a13763fdfbffd7d73fc80349a69230c46c6b71e7508/coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037", size = 258947, upload-time = "2026-09-13T19:09:54.372Z" }, + { url = "https://files.pythonhosted.org/packages/13/bd/65c31ddd43ff4e61b63721b3dad17cca412dba6e2ce89f92abdf75734339/coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b", size = 254613, upload-time = "2026-09-13T19:09:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/25/20/d278115f2ba522b3e3128c6852be265f4e0df7202b2effca4db96cf0217d/coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0", size = 256388, upload-time = "2026-09-13T19:09:58.095Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ca/d43aa396fb3a2f71c9411b99d926475f5acaf5c124f605c592865c80c8d5/coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a", size = 225550, upload-time = "2026-09-13T19:09:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/df2f6114f6a17cffe94721bd1d298f77f86aad493717fa5bc03cb291a1a6/coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa", size = 226091, upload-time = "2026-09-13T19:10:02.331Z" }, + { url = "https://files.pythonhosted.org/packages/ee/31/6f90d8aab72a112492bd50e3b5485b53b772b1f5fa70da0a620e723caae6/coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d", size = 225481, upload-time = "2026-09-13T19:10:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/96/1a/d6d16babd0a5fe4c3fae40702158c570351694e74516d8d81b86c5637448/coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4", size = 215264, upload-time = "2026-09-13T19:12:18.895Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "darrow-skill-authoring" +version = "0.1.0" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "coverage", extra = ["toml"] }, + { name = "hypothesis" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", extras = ["toml"], specifier = ">=7.10,<8" }, + { name = "hypothesis", specifier = ">=6.138,<7" }, + { name = "mypy", specifier = ">=1.18,<2" }, + { name = "pytest", specifier = ">=8.4,<9" }, + { name = "ruff", specifier = ">=0.12,<1" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "hypothesis" +version = "6.168.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/ce/c0946bebffb99b62426a6a7643d4272cc6c5cf777a488b3b4d0ee724e960/hypothesis-6.168.0.tar.gz", hash = "sha256:72af51087b7b5ab21c49f0d502f803c20897678652835596bd2a8b169a39135e", size = 510805, upload-time = "2026-09-08T18:48:36.072Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/f8/8b2cc9ae7b439538f6f2d32a92892340b6343a6b04d171c516666901dedc/hypothesis-6.168.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:47b89491ff02e3ae9b302c440457938e87b47a45b9a1d98ff5575b6910d779e2", size = 791358, upload-time = "2026-09-08T18:47:37.076Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f4/4d7d897310cde5085779fb96feadb8529d98cb8e51ed7b24f7da9b6c6bdc/hypothesis-6.168.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1f4cd0ff11bd470a1a846296ed5fe55e84214194850370994fd1370fe73d3099", size = 787081, upload-time = "2026-09-08T18:47:16.227Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/9d52066d363faba7f3ac20ee60a3c696a475feb2c477d235d0d649d41cc1/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732ae5d47482f99d8028cca096729625f05690a83f5e7ce31466e266155792f4", size = 1123850, upload-time = "2026-09-08T18:48:11.504Z" }, + { url = "https://files.pythonhosted.org/packages/50/cf/aa46d76fa7df43caf2c372e394fda84ce1dc08421814f8674a6b9295e2ea/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2085ee74ac3ab6b70e2f7ffae9b4cb74c246da2f574b2de81a0818a8a30f659f", size = 1147685, upload-time = "2026-09-08T18:46:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/84/c2/78ed8c8d5aa37e4baae2a4b3e29687ee3d9b7f1a5e56ea5ea5ec7ec71ecb/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1894782fae5d9a7bb44e6dcf848ccb09ccb5babab48d8b5c31a0a7fc025b82a1", size = 1149294, upload-time = "2026-09-08T18:47:04.757Z" }, + { url = "https://files.pythonhosted.org/packages/78/7f/d57440f19e9de70e85359cf179ce786f309ee17609bd3c5a0113272875c0/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecf0ab13cef899efb816ffdd7963e0679f372520884ce06756c7642f3df94213", size = 1169729, upload-time = "2026-09-08T18:47:44.056Z" }, + { url = "https://files.pythonhosted.org/packages/a1/86/dc74410a186990bb22c2a3eea0e77804f2eb0f300860b46d7d8948073674/hypothesis-6.168.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3f6dcf66270278d078bed01b401f47db4e26456cd909d8e23c6b9366a6c0b131", size = 1129182, upload-time = "2026-09-08T18:46:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5e/be048fc4f6dac831625e155bdf11e4233caf46bb54030391d6fba8e19449/hypothesis-6.168.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bfef4d46dbf1704a7b8fa3a78778651a2cb18870ca0a70da19c381646822b149", size = 1160180, upload-time = "2026-09-08T18:47:26.459Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4a/15a34498a5f08720fbbdbbe4668a8050fe4e17c16c9eeb6f56a017f2fa6b/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1d1aa5b3484e329295d88488a5ba06243909e65c2ab616513c2d36721de4ed1d", size = 1299711, upload-time = "2026-09-08T18:47:28.34Z" }, + { url = "https://files.pythonhosted.org/packages/77/09/5354e0dae302ab98c4f0046b7e8699c2186ae4349397bda5d5c852bda68b/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3bc00fd8cda04b58e37a1163e8a65389b247b4f5ee547ae37d244a4960995517", size = 1425341, upload-time = "2026-09-08T18:46:56.785Z" }, + { url = "https://files.pythonhosted.org/packages/59/7c/3c0e1f59043ff128c70a51d298d3d6b5973525c357a1e1d0542dbc05ac90/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:990026952d5b2eca290c88f639ac639233f47e13dae338c6dfb6e4774bcab349", size = 1281063, upload-time = "2026-09-08T18:46:55.401Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/db884db9a7d42ae6b72a00c13c725940b638dfb263e618b22af59d5dfa2f/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:a74b0945acbbd552c7c2d0a99a3b5232962b8848c8eed1829451800a9bfcf00b", size = 1300247, upload-time = "2026-09-08T18:47:02.949Z" }, + { url = "https://files.pythonhosted.org/packages/99/8a/4ee9769e1d48676efb6a78a130f82e0d52d3f87b2055294102272a08615c/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a380b521b5a76a9e8917d64adcf7f861a45a4360a34b1579af14c5df8eb0377", size = 1336084, upload-time = "2026-09-08T18:48:05.675Z" }, + { url = "https://files.pythonhosted.org/packages/61/17/d4ed11bc99d205d6d2651a0f1a4874f377150f836b5a0849bd511d68a2eb/hypothesis-6.168.0-cp310-abi3-win32.whl", hash = "sha256:2264f15a1c80329e3ad48e39c44bd5c9429b7b04c9ee62cdd72f4b10aaac9f29", size = 677989, upload-time = "2026-09-08T18:47:24.722Z" }, + { url = "https://files.pythonhosted.org/packages/77/51/abf1fde7b8afab87db30afb73b3847e62440551d146472111cabeba2fe00/hypothesis-6.168.0-cp310-abi3-win_amd64.whl", hash = "sha256:5b54769033b84477931d2072e7133a7555e0de5c53fd5ca3bbde960762d7d31b", size = 684692, upload-time = "2026-09-08T18:46:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/12/e2/64d79aed47a95186ce9edcef60eb4684870c72542da3b7027c2483d8ee8c/hypothesis-6.168.0-cp310-abi3-win_arm64.whl", hash = "sha256:112b0900059bf9d7d6528ed729770629ab146e0d133c4143b9bd4a01dc002bcc", size = 682709, upload-time = "2026-09-08T18:48:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/95/67/bfacadcc6cd4bd179d1cd2f6c85f580136019b38af7db71e3159ebb3ce33/hypothesis-6.168.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cb10aa59b0af45badca76911f5323f40d24fdbe00d01b7b67fef8648c99411b5", size = 792193, upload-time = "2026-09-08T18:47:42.392Z" }, + { url = "https://files.pythonhosted.org/packages/fc/29/4abab4114035f54438e3b07bdab3f68fd8769fe0fb84f8ffd6656eaa58fd/hypothesis-6.168.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e21e30b76b6d3adb87c550576132a3204f4c257ec43353f6c09b9d59bb762abc", size = 788121, upload-time = "2026-09-08T18:47:09.398Z" }, + { url = "https://files.pythonhosted.org/packages/85/d3/c54242439dcb59c7e3274a2952188bebbc605be397c3a6ff027fc81e1726/hypothesis-6.168.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:527452b43e79e6dfbf9cb69145a940547a3cd177c556698a3fc939ed2354c4b3", size = 1124222, upload-time = "2026-09-08T18:46:28.345Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8b/1c9f54be7040fe7d94e4cd5d9343da5a36240bee2fa856f44109f9622251/hypothesis-6.168.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:489d5c060f49f495b64215cae627c71730cffd5ef59dc4d7f431932e6e6d2e67", size = 1170346, upload-time = "2026-09-08T18:46:52.784Z" }, + { url = "https://files.pythonhosted.org/packages/af/d4/f6b1557bd3cf2976d56465fcbc483214bc6e7f1aa7e4db9f8c5f776d547a/hypothesis-6.168.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c3af200b322f710c76c2189866246cdcff2039165dd77edff1a7bf1157162fb0", size = 1300267, upload-time = "2026-09-08T18:46:45.644Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ad/a345588acc038deb09a43610328004d31fe5a53289627e03b0199978dc45/hypothesis-6.168.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73084b76e4a79cd0f7883ce80fc60c9f374ce7dcad8f520b39db40470ce1852f", size = 1336724, upload-time = "2026-09-08T18:46:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/97/e9/c5e380fd29fd552ff442bcfb1090926287e2911d40212ae0602a0736347b/hypothesis-6.168.0-cp310-cp310-win_amd64.whl", hash = "sha256:8067e6b4b48e5cfdc849a1a20c9d4972b3f532b3e3edb5e2b5dfd106045a5236", size = 684644, upload-time = "2026-09-08T18:47:45.814Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7a/7a28d9afd52c9a89c4d8e3170c92fe1ffbd4a4ce3f905ca15fa7ddaaa581/hypothesis-6.168.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4d7d29dd63ad9fdc4aa1d65fa272449e14aaf6c6bb8451091818c2945533a43a", size = 792045, upload-time = "2026-09-08T18:46:29.612Z" }, + { url = "https://files.pythonhosted.org/packages/0a/40/e2d6fe12b54abbc8b802b234ec4974d0a420dde4098f57a324e9c791ac27/hypothesis-6.168.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a72ed7afa1f7e30488b8a5754fca0ad9755518bdb77d6f0b003cadf7437a5f9", size = 787932, upload-time = "2026-09-08T18:47:11.145Z" }, + { url = "https://files.pythonhosted.org/packages/ff/89/cc78e05f3248949c6d9ecb489d0e422247d11b08533ab02e177a10a01610/hypothesis-6.168.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcc5bad4300a751804ce41f0e10d77f85272668160708ce39ec579bca8984843", size = 1123994, upload-time = "2026-09-08T18:47:12.647Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e3/7d483a79e2ed9a6c868cac8783b3c6c2bb82303f7db364c9da9a406df10d/hypothesis-6.168.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53469a1a7c4861b12c9a8622f762d7d1fd7bcf171884e1018ed5a8f063a5c063", size = 1170271, upload-time = "2026-09-08T18:47:54.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/bffaf5b5e6b4566aa40d8a4ae25d8d84b47dfe0d9e771f454e4974cdb428/hypothesis-6.168.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a9650c4882fdbdd8e90bdae602a8bfa8c6f09dc5d06afec5b9b23982e8f60a04", size = 1300162, upload-time = "2026-09-08T18:46:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/6c30a00fc2f5c58546e858259cf2261c6a3ef6a060c373e00d00311267be/hypothesis-6.168.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:348d9b93fd4129f67f9bab94f3d70709a9372bbe0e0d22731325ce85d5eb409f", size = 1336294, upload-time = "2026-09-08T18:46:37.324Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/501910e5a6a9c245f2394fda016376f6c7e209c5dca8c056eaece64ad10e/hypothesis-6.168.0-cp311-cp311-win_amd64.whl", hash = "sha256:719b45b0512e3535a6a0077c2f7c6053b02ac0e72d60693f66f98790a33855b2", size = 684464, upload-time = "2026-09-08T18:47:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/0035896c101f0484c364353f8ee30175eef8936b49171618677287fdd85d/hypothesis-6.168.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b750390dac4429da0cb70ab3fe758457f0cea3d9c843d48c59d0690d1189fda", size = 793115, upload-time = "2026-09-08T18:48:21.352Z" }, + { url = "https://files.pythonhosted.org/packages/11/5c/938173e27df771cc6e92bc47f117a1b1be4a88fc6dc214f7fc65f9c7ad93/hypothesis-6.168.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e4b2d434e0dd134f3d31ac1efc1825bf99730dfe70fec005ff66d7211836d79", size = 784634, upload-time = "2026-09-08T18:47:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/0b2c6ac07d519131f97712f2533750ffe9b2490eec8f518ef3a5ed2dd514/hypothesis-6.168.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d4d36ed2fd62de11382f1d608169c1ffa9a49d3b9351146d8ff87cb81a66f7", size = 1122855, upload-time = "2026-09-08T18:46:21.967Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/dde930fe43171cab37572bd993d70c2a271f240f428f8ccd64ec4c2d661b/hypothesis-6.168.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5920d267f7d8cfd376672f2bde5905cdf284d47519582e41ce7c142d48ee46c4", size = 1168932, upload-time = "2026-09-08T18:46:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/4c/5d/92b83c3d06194ec626e92723d0b0f70221ebf42d7cb355ed36929df6d735/hypothesis-6.168.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fb8cdf45361e259df86e19f8cd042ce2d6c7e6ad88fa631b78a4e3a83c2e572d", size = 1298566, upload-time = "2026-09-08T18:48:19.485Z" }, + { url = "https://files.pythonhosted.org/packages/9c/67/52de8bf3446e3d2d555b812d96673b31bb213b5b9d5804a666e5d0bba76e/hypothesis-6.168.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b3ce1cce70b25a37ed1a38a53ce7204785726c675c0f41a0f83c338a7e47b3d", size = 1335074, upload-time = "2026-09-08T18:48:01.561Z" }, + { url = "https://files.pythonhosted.org/packages/14/fd/e592773c1c0ce55e35d26ec55f75546bf1fd72ef5a5c520ed685969b40cb/hypothesis-6.168.0-cp312-cp312-win_amd64.whl", hash = "sha256:f62bdabf278db9ff61df5f3203d608949f0d893d0e30cdac3f2330e67e41ae68", size = 682026, upload-time = "2026-09-08T18:47:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e9/39bb8fcccfbafd10fcc777d583c6ebad5d2148e7d743cb350562f28e974f/hypothesis-6.168.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d55562bf8d41cfa18559c33f30cadf44ceac8e517509d7a022a9feace621f28", size = 793050, upload-time = "2026-09-08T18:47:56.045Z" }, + { url = "https://files.pythonhosted.org/packages/f7/dd/00fd32e8ec470535e0065cb8d6e175f9fc54b4d3a6f1269f6c487f8bd79d/hypothesis-6.168.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92cff497b92e2285ff6a94193fdee04aba483a4115d501c1f9a570bd103fcd20", size = 784558, upload-time = "2026-09-08T18:46:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/d2/4d/553c47093f68bdbac0438e16c024ce97649b5804dc6972ef86b9bd2db8a1/hypothesis-6.168.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff259260015f9be3756dcd4bc11c08e007314dec6b43d9a89084c4f34f94475", size = 1122841, upload-time = "2026-09-08T18:48:09.374Z" }, + { url = "https://files.pythonhosted.org/packages/43/d6/0b5940aa75e617c8fd12200bae24d1b71347362514e8210735c581d4d3d1/hypothesis-6.168.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35f1262831b5acc74ded15f629965daffcd657f6016ee04fc9605f6eb2b334c0", size = 1168825, upload-time = "2026-09-08T18:47:33.702Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/800de1231b2869b51409bbf85799d6f1bf49a00e0afff0aabc097aa8f1b7/hypothesis-6.168.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:046fe4bcfce2a2fa186ba9d96bbb62c25c2f6c2e4071f0783ed6b5cc481d0669", size = 1298433, upload-time = "2026-09-08T18:46:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/be/35/9907667a30c1dbabbc44a09b4c25a0f575570937fcbbada924ae3a1dbf2a/hypothesis-6.168.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:24b52a2b1c8db6e1e516f9295c8e4ef7ef63303ff24fbbc5b35f4ff71dcd732c", size = 1334988, upload-time = "2026-09-08T18:48:29.395Z" }, + { url = "https://files.pythonhosted.org/packages/98/8a/7bf214e703fff532ed47cb52ab93ce4b7ea41e4e7084593fa02b740828b7/hypothesis-6.168.0-cp313-cp313-win_amd64.whl", hash = "sha256:ec0886fe0be9091669937989f9a662beca42ae14a4a6dab25491c2c63365f88d", size = 681990, upload-time = "2026-09-08T18:47:49.173Z" }, + { url = "https://files.pythonhosted.org/packages/16/f7/5adae1bf1d4877aca2e8c8e007e57077237e9ac3765e430ffda490b17e19/hypothesis-6.168.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:754016594fe78cef91790e0922f60d183c52f531255fbfa30dac495b813e2128", size = 791056, upload-time = "2026-09-08T18:48:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/f2/93/b1b2770b87591db5cf564b9aa0265cf21e9207d28645e0abdeb8df63225b/hypothesis-6.168.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:6f0dd437ec01140676192422b61f2f833b3ce6a3213da9b7e196ad6b3777e795", size = 782980, upload-time = "2026-09-08T18:48:34.087Z" }, + { url = "https://files.pythonhosted.org/packages/de/bd/673171c1d2423379a7d4a0f9f009cca735a422d0c4a4ac6422d1d1736cab/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f77af7721ff35a58fa8797decd14c932c350a2548686c6e9b844db710a3a2441", size = 1120964, upload-time = "2026-09-08T18:46:59.92Z" }, + { url = "https://files.pythonhosted.org/packages/7d/00/33a9bd941b22a4fd8a8c805b1563e0db17efc020422f5bd15bdd0fdf258f/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a0d28418c104d7268fdebcc09bc49f7b6569b5eb942430c6859f53ec8d4edf63", size = 1143869, upload-time = "2026-09-08T18:46:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c1/963460976f41721eff8f67f30d31059ea407cc1b41c737c8859aabf37197/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:812a84c4cc7f7ae4fcb39a5647cc2698e6c18254f8423126425578f1dcdac782", size = 1146453, upload-time = "2026-09-08T18:46:25.757Z" }, + { url = "https://files.pythonhosted.org/packages/ce/53/09db238098ad66f4e6d2fe883f26c270c2595b90e21c8f969d4cb21cad7a/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6de30e559eb151de14a5f74bceb4d97792a9315ada2a1816b5da825cd7d28edc", size = 1166918, upload-time = "2026-09-08T18:48:23.436Z" }, + { url = "https://files.pythonhosted.org/packages/b9/31/e1b7b452c8a6166e445ba2ad80a864f6a9eee0fe4c8cecdb9af5c1ee0aa5/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:9018b20acdb061b2ef4b2fa7f558ca5db97ffea316e0a528bc003a24b2ac996e", size = 1126637, upload-time = "2026-09-08T18:47:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/2b/4eceed248afb46fb6b2df21cf2239362de5b25d295c2dc67a82ec8657d5e/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc935a5d5f86fd8f5af951b8fbe00307f6f7c596f82a9a27c17d974f6ab0a26c", size = 1155682, upload-time = "2026-09-08T18:47:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3e/f3414cda4f325004d5774485e8983b7d1b99e4b91013f013dd088fd778cf/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:45fcfa05f746e253350f55f216bcef59754f5f2b85745f1fc2bb8ba81dd517a9", size = 1296464, upload-time = "2026-09-08T18:46:34.833Z" }, + { url = "https://files.pythonhosted.org/packages/aa/40/ca79cf96545e1f172b36b8df56bfeb02b61f351f283026f9a57c4631e368/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f89d8e998d3c936ffbbd1c3686c96f0378f6558aecc5967a3035a857f2bab0ad", size = 1421853, upload-time = "2026-09-08T18:47:23.083Z" }, + { url = "https://files.pythonhosted.org/packages/77/bc/657d5386740c1f4ac518ff05e4c5b132f1e6df2e7c865ba8fda4279adfa5/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0620fa320fa66649e6bfd71e94f3f86115fffebb7e3c6dcece19d1aaff8e07f", size = 1278216, upload-time = "2026-09-08T18:46:30.871Z" }, + { url = "https://files.pythonhosted.org/packages/51/54/2328cdb70489a36634534478d9b238594a269d8bec4630ea0e6897048347/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:4085b61e25d3dcc6c9151d4115269870aee8cdb921611ee5c989b2786449be09", size = 1297593, upload-time = "2026-09-08T18:46:41.395Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ca/d803fa57e3ff7f460f6e262d2b74822cf143343d378501fd3da601b12040/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:b5449a64eb37d9a4aa6ac9cd2ab0fd1a24145adf421ef1536884f73f39824887", size = 1333785, upload-time = "2026-09-08T18:48:25.434Z" }, + { url = "https://files.pythonhosted.org/packages/86/8f/b9799ae6ba6074f151db2c63f9f6f12d844512821ffaba1a3672d7e59f07/hypothesis-6.168.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:91e3de666a6c4f7543000d1710e25055d63ef3032c98bd2ab338b3087bdaa780", size = 675173, upload-time = "2026-09-08T18:47:52.601Z" }, + { url = "https://files.pythonhosted.org/packages/61/54/14c3e277b451ff24128ecc2673cac59dd7e535bce1a433c466912fd682e1/hypothesis-6.168.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:9a2079cd09919956dd388f1a1f8ea5a79f2b2437650fbeda31d8661217ffefef", size = 681491, upload-time = "2026-09-08T18:46:51.437Z" }, + { url = "https://files.pythonhosted.org/packages/99/f3/827e4a48ffee7e40244b0bf064ba47c2171e053ab7edf1cf770105e23401/hypothesis-6.168.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:085c9aa246487c56a40ca89003d285cbffdbb5be4097ba6d0139f9c21003c04a", size = 679197, upload-time = "2026-09-08T18:47:32.112Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/7f15e1b10d13e266b0f09cf3117dd2206104a1f6ac3b3baf6ec4c43b7c2e/hypothesis-6.168.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:16864797de4b024e4c6cebd44598af932f870aad811341bc5bc24c738801ff76", size = 792946, upload-time = "2026-09-08T18:47:35.438Z" }, + { url = "https://files.pythonhosted.org/packages/10/fb/32487bdcf68b3805ec5efcb7f92fb002b573ad3f90c252d2f2913c5d3124/hypothesis-6.168.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:283eda952bcb1987ccba1c8b634db0e8a960e1e92e2daa7003bc2392f19cea01", size = 788783, upload-time = "2026-09-08T18:46:42.862Z" }, + { url = "https://files.pythonhosted.org/packages/13/26/1aa42e0069ebc68d29d980d47420af324fa002494e01c1c0321c34f609ba/hypothesis-6.168.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5427a3c951080c18170486f775df6a82153882b819eca6b8e7ed77693634e5ab", size = 1124762, upload-time = "2026-09-08T18:48:07.512Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4e/99509045ed55aaa05d8408663051a93168d1a8a5bfc132d92ec1584ce47a/hypothesis-6.168.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a838218ff1eab8d7b4bf66b96037fce0a802f61f2fa5fd4b784696cac365ce7", size = 1171748, upload-time = "2026-09-08T18:46:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c9/96e57dfd89913322b5180b031b9e36a29bfe67842108b19904466476cd24/hypothesis-6.168.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:34e3c8b66047ba92f8b8df5e427074058d92db58038f007da4bf9d14e934ad3c", size = 685447, upload-time = "2026-09-08T18:46:35.97Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/12/e2e9ca532cf5a0e08c9489826c4a35c6958c92ba0313fda70e8c6c3912be/librt-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489", size = 148673, upload-time = "2026-08-07T10:46:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/6d/7c/02005e23478bd5950618d9712e0fd2b4c511657857f3efd8ba6a5feabcdd/librt-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52", size = 153547, upload-time = "2026-08-07T10:46:23.931Z" }, + { url = "https://files.pythonhosted.org/packages/a0/90/d8848a735f5642077fc4b3b4bebcdb08edf10178e3add45597f5201a368f/librt-0.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8", size = 494355, upload-time = "2026-08-07T10:46:25.122Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0b/8604f41ea02feace490e9e405a338a15f9905369f55b239a9ce31c946f24/librt-0.15.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1", size = 485459, upload-time = "2026-08-07T10:46:26.447Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ac/84153bda1ce0da609182527ab92b40d961809e544eefdc5a1c2422971416/librt-0.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d", size = 498398, upload-time = "2026-08-07T10:46:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3a/5ca6cd282b2c244bec8ec84102e09773264e9c02891d56ab3a8f0e4d7083/librt-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702", size = 515474, upload-time = "2026-08-07T10:46:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/73/d3/bd34110234779eb843c6ed66aba7c9b2091d3dd85989f1fb9922f564cb7a/librt-0.15.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9", size = 509484, upload-time = "2026-08-07T10:46:30.124Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/43c3f7f071d71631a7daa3b835ef2168ea39f20692d81464d4e47fbaa6d6/librt-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b", size = 532534, upload-time = "2026-08-07T10:46:31.511Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1c/b854adf036ea817c40408873a5b794d65a91d9f0f39826f2ad2a2d5d7f48/librt-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db", size = 537087, upload-time = "2026-08-07T10:46:32.734Z" }, + { url = "https://files.pythonhosted.org/packages/25/5c/c9a890e244e7dd725d3bd8b560e41f0aec787eaf343b46956a290ab7b841/librt-0.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451", size = 536575, upload-time = "2026-08-07T10:46:33.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c5/c8e70b60b704299555f55db468eb46b1c81bfc60201ffbfe20407d89870c/librt-0.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389", size = 517142, upload-time = "2026-08-07T10:46:35.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/d1/767a90c41f5d381b3195bc88ac0ec4afda35777c9c781e1f9848fedd965e/librt-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c", size = 558714, upload-time = "2026-08-07T10:46:36.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/3c0624b8dc8301ab808f2b3a910995bcabe28df070fb9a0e5505ae997dae/librt-0.15.0-cp310-cp310-win32.whl", hash = "sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e", size = 104426, upload-time = "2026-08-07T10:46:38.594Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/e91c0382304bedb2db9c6801897319a9dcb68daac5e975819b562362f20d/librt-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053", size = 125057, upload-time = "2026-08-07T10:46:40.052Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/06790ced2ac7117f890c21bda43c39c958ec82aa665c0718e821d33ff939/librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22", size = 148039, upload-time = "2026-08-07T10:46:41.165Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/8e150b7fc449a1f33c8a760965cc1f43b14fc1577d9d0b50ab2701420e74/librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63", size = 153067, upload-time = "2026-08-07T10:46:42.418Z" }, + { url = "https://files.pythonhosted.org/packages/51/87/a162bc5a66a35599dc619ecb215145f4de7d68e886b479b6d12593139f7c/librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef", size = 493087, upload-time = "2026-08-07T10:46:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3a/aeea1fc620cf48060d3065b37614edbf97043c099d0f50782bc8ca61d897/librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f", size = 485608, upload-time = "2026-08-07T10:46:45.038Z" }, + { url = "https://files.pythonhosted.org/packages/52/ff/fe571ad416f0856fd0d5578ffc2e6dc531891e586e36b647bcf50569cab8/librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8", size = 498723, upload-time = "2026-08-07T10:46:46.35Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/7a65eb5dedb1f00aebd948cdd8e17add48bf066cab3514e9daf84ab45a6c/librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a", size = 516002, upload-time = "2026-08-07T10:46:47.599Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/59832b0ebfbd08c2742e6ece372ceb53f18bf1faef5d33c8daf3abebf749/librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18", size = 508607, upload-time = "2026-08-07T10:46:48.873Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0d/37fa73f3b43ebd8259f91ae9102a15e5a54e65d581e48dea72df3e81d7a4/librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c", size = 530422, upload-time = "2026-08-07T10:46:50.45Z" }, + { url = "https://files.pythonhosted.org/packages/26/02/e046c6fe7a5881ac34623242192f484426ba8a75595fd18f22c53a3f530f/librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a", size = 534303, upload-time = "2026-08-07T10:46:51.693Z" }, + { url = "https://files.pythonhosted.org/packages/95/32/d5e6d861ab0366f3edf74f887ab0c9eb9f535aaf01d32b80b4f734daa179/librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091", size = 536084, upload-time = "2026-08-07T10:46:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/d69d725513fe53fc90c6d7a1f86e4428939bad2fb905b17fe4c18d413dde/librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40", size = 514307, upload-time = "2026-08-07T10:46:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/93/f8aded0d6682b4f25820fa86e0690f87f01df9fd7bd09ddb04d9167ad021/librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0", size = 557686, upload-time = "2026-08-07T10:46:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/ffeb6bdeb6cd862b4272fddc8ad05f938dd25d020ed517e631813917d80a/librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb", size = 104917, upload-time = "2026-08-07T10:46:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/96/28/7e2313a3ffbf0b4de7ba3da58a09e488507b4bd1ea2b5e69378354a23415/librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db", size = 125886, upload-time = "2026-08-07T10:46:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/39/9e/04b8c3cde014ef255ee785730425268354543acc38902093a40afa0dc164/librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56", size = 111885, upload-time = "2026-08-07T10:46:58.787Z" }, + { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, + { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, + { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, + { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, +] + +[[package]] +name = "mypy" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/97/ce2502df2cecf2ef997b6c6527c4a223b92feb9e7b790cdc8dcd683f3a8a/mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4", size = 14457059, upload-time = "2026-04-21T17:06:14.935Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/417ee60b822cc80c0f3dc9f495ad7fd8dbb8d8b2cf4baf22d4046d25d01d/mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997", size = 13346816, upload-time = "2026-04-21T17:10:41.433Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/e20951978702df58379d0bcc2e8f7ccdca4e78cd7dc66dd3ddbf9b29d517/mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14", size = 13772593, upload-time = "2026-04-21T17:08:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/63/a5/5441a13259ec516c56fd5de0fd96a69a9590ae6c5e5d3e5174aa84b97973/mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99", size = 14656635, upload-time = "2026-04-21T17:09:54.042Z" }, + { url = "https://files.pythonhosted.org/packages/3b/51/b89c69157c5e1f19fd125a65d991166a26906e7902f026f00feebbcfa2b9/mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c", size = 14943278, upload-time = "2026-04-21T17:09:15.599Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/6b0eeecfe96d7cce1d71c66b8e03cb304aa70ec11f1955dc1d6b46aca3c3/mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd", size = 10851915, upload-time = "2026-04-21T17:06:03.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/6593dc88545d75fb96416184be5392da5e2a8e8c2802a8597913e16ae25c/mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2", size = 9786676, upload-time = "2026-04-21T17:07:02.035Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/9ebeae211caccbdaddde7ed5e31dfcf57faac66be9b11deb1dc6526c8078/mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c", size = 14371307, upload-time = "2026-04-21T17:08:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/d7/93473d34b61f04fac1aecc01368485c89c5c4af7a4b9a0cab5d77d04b63f/mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3", size = 13258917, upload-time = "2026-04-21T17:05:50.978Z" }, + { url = "https://files.pythonhosted.org/packages/e2/30/3dd903e8bafb7b5f7bf87fcd58f8382086dea2aa19f0a7b357f21f63071b/mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254", size = 13700516, upload-time = "2026-04-21T17:11:33.161Z" }, + { url = "https://files.pythonhosted.org/packages/07/05/c61a140aba4c729ac7bc99ae26fc627c78a6e08f5b9dd319244ea71a3d7e/mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98", size = 14562889, upload-time = "2026-04-21T17:05:27.674Z" }, + { url = "https://files.pythonhosted.org/packages/fd/87/da78243742ffa8a36d98c3010f0d829f93d5da4e6786f1a1a6f2ad616502/mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac", size = 14803844, upload-time = "2026-04-21T17:10:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/37/52/10a1ddf91b40f843943a3c6db51e2df59c9e237f29d355e95eaab427461f/mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67", size = 10846300, upload-time = "2026-04-21T17:12:23.886Z" }, + { url = "https://files.pythonhosted.org/packages/20/02/f9a4415b664c53bd34d6709be59da303abcae986dc4ac847b402edb6fa1e/mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100", size = 9779498, upload-time = "2026-04-21T17:09:23.695Z" }, + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/README.md b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/README.md index bd33a5d2..f832b557 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/README.md +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/README.md @@ -7,6 +7,7 @@ and inspect each failure before starting the next case. | Request boundary | Case | | ----------------------------------------------- | ------------------------- | | Direct skill creation | `create-focused-skill` | +| Cross-platform Python mechanic | `create-python-mechanic` | | Indirect reusable-workflow request | `indirect-discovery` | | Missing goal, destination, and runtime support | `refuse-undefined-skill` | | Ordinary agent guidance without skill intent | `non-skill-guidance` | diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/create-python-mechanic.yaml b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/create-python-mechanic.yaml new file mode 100644 index 00000000..c0ba0a14 --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/create-python-mechanic.yaml @@ -0,0 +1,75 @@ +id: author-agent-skill-create-python-mechanic +invariant: SA-C5,SA-C6,SA-C7,SA-C8 +activation: positive +prompt: >- + Create one cross-platform agent skill named validate-capability-manifest in + the existing plugins/schema-helper plugin. It should validate JSON capability + manifests before review. A valid manifest is an object with non-empty string + `name` and `description` fields and no unknown top-level fields. This is a + native Windows, macOS, and Linux workflow, so put the substantial validation + mechanic in a contained Python package managed by uv, expose a narrow locked + entrypoint, and add appropriate tests. Keep semantic review of the description + in SKILL.md. Do not commit. +fixture: + commits: + - message: "chore: init" + files: + AGENTS.md: | + # Repository instructions + + Plugins are independently installable. Substantial cross-platform + mechanics use a plugin-local Python package with pyproject.toml and + uv.lock. Runtime and development dependencies stay separate. Runtime + entrypoints use uv with frozen resolution and no development group. + Support Python 3.10 through 3.13. Never commit automatically. + plugins/schema-helper/.claude-plugin/plugin.json: | + {"name":"schema-helper","version":"0.1.0","description":"Schema review helpers","author":{"name":"Example"}} + plugins/schema-helper/.codex-plugin/plugin.json: | + {"name":"schema-helper","version":"0.1.0","description":"Schema review helpers","author":{"name":"Example"},"skills":"./skills/"} + valid.json: | + {"name":"release-check","description":"Validate a release before review."} +checks: + - name: focused skill has valid discovery metadata + run: >- + skill=plugins/schema-helper/skills/validate-capability-manifest/SKILL.md; + test -r "$skill"; + grep -E '^name:[[:space:]]*validate-capability-manifest[[:space:]]*$' "$skill" >/dev/null; + grep -Ei '^description:.*(manifest|capability).*(validat|review)|^description:.*(validat|review).*(manifest|capability)' "$skill" >/dev/null + - name: Python package is contained and locked + run: | + skill=plugins/schema-helper/skills/validate-capability-manifest + project=$(find plugins/schema-helper -name pyproject.toml -type f | sed -n '1p') + test -n "$project" + backend=$(dirname "$project") + test -r "$backend/uv.lock" + grep -F 'requires-python = ">=3.10,<3.14"' "$project" >/dev/null + grep -F 'default-groups = []' "$project" >/dev/null + ! find "$skill" -type f -name '*.sh' | grep . + - name: locked runtime entrypoint validates success and failure + run: | + backend=$(dirname "$(find plugins/schema-helper -name pyproject.toml -type f | sed -n '1p')") + entrypoint=$(sed -n '/^\[project.scripts\]/,/^\[/ { /^[a-z][a-z0-9-]*[[:space:]]*=/ { s/[[:space:]]*=.*//; p; q; }; }' "$backend/pyproject.toml") + test -n "$entrypoint" + output=$(uv run --quiet --frozen --no-dev --project "$backend" "$entrypoint" valid.json) + case "$output" in *"$(pwd -P)/valid.json"*) ;; *) exit 1 ;; esac + for invalid in \ + '{"name":"missing-description"}' \ + '{"name":"","description":"empty name"}' \ + '{"name":7,"description":"wrong type"}' \ + '{"name":"unknown-field","description":"reject extras","extra":true}' + do + printf '%s\n' "$invalid" >.git/invalid.json + ! uv run --quiet --frozen --no-dev --project "$backend" "$entrypoint" .git/invalid.json + done + - name: package tests pass without repository dependencies + run: | + backend=$(dirname "$(find plugins/schema-helper -name pyproject.toml -type f | sed -n '1p')") + uv lock --check --project "$backend" + (cd "$backend" && uv run --all-groups pytest -q) + - name: workflow keeps semantic judgment in the skill + run: cat plugins/schema-helper/skills/validate-capability-manifest/SKILL.md + expect_regex: "clear|specific|audience|semantic|meaning|quality" + flags: i + - name: authoring creates no commit + run: git rev-list --all --reflog --count + expect_regex: "^1$" diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/plugin-mechanics.md b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/plugin-mechanics.md new file mode 100644 index 00000000..3d7f692c --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/plugin-mechanics.md @@ -0,0 +1,61 @@ +# Plugin helper mechanics + +Read this reference before adding or changing executable mechanics in a skill. +Choose the smallest runtime surface that supports the target's declared hosts. + +## Choose the implementation + +- Use existing host tools directly when their invocation is already short, + stable, and hard to misuse. +- Use portable Bash for genuinely small POSIX launchers, environment adapters, + and command glue. If Bash owns any logic or tests, also read + `portable-shell.md` from this reference directory. +- Use a contained Python package managed by UV for substantial parsing, + filesystem traversal, state validation, subprocess coordination, or mechanics + that must run on native Windows. Python is warranted by the behavior and host + support, not by the presence of one conditional. +- Do not introduce a shared plugin runtime or depend on a sibling plugin. Keep + contextual judgment, authority, and material choices in `SKILL.md`. + +## Package Python inside the plugin + +Follow the target repository's instructions and existing Python package +conventions. Do not copy inventories, paths, thresholds, or CI structure from +the repository that supplied this skill. If the target has no established +convention, treat the package layout, supported Python range, UV adoption, and +quality bar as contract inputs rather than silently imposing this plugin's own +implementation. + +When the target selects Python plus UV, keep the complete package and its +environment inside the owning plugin, and commit both `pyproject.toml` and +`uv.lock`. Declare the supported Python range, expose narrow console entrypoints, +put runtime dependencies in `project.dependencies`, put quality and test tools +in the development dependency group, and set UV's default groups to empty so a +runtime sync cannot install development tools. + +Invoke a runtime entrypoint through its package without development groups: + +```text +uv run --quiet --frozen --no-dev --project [arguments] +``` + +This form works in POSIX shells and native Windows terminals. A small POSIX +launcher may calculate `` for existing Unix callers, but it only +dispatches to the same locked entrypoint and contains no duplicated mechanics. +Give every public operation the same arguments, output, refusal, and exit-status +contract on each supported platform. + +## Test and release the helper + +Keep fast Python unit and integration tests beside the package. Apply the target +repository's formatting, lint, typing, property-test, and coverage gates rather +than importing thresholds from the skill's source repository. Directly exercise +path normalization, malformed input, failure output, subprocess status, and +platform-specific path forms. + +Copy the complete plugin to a fresh temporary directory, remove local virtual +environments and generated state, sync only locked runtime dependencies, and +invoke each public entrypoint from the copy. Confirm that development tools are +absent from the runtime dependency tree. Run this fresh-artifact check on every +claimed native platform; a POSIX compatibility layer does not establish native +Windows support. diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/portable-shell.md b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/portable-shell.md index 8a6d77f1..8b081996 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/portable-shell.md +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/portable-shell.md @@ -44,8 +44,8 @@ shell test. Apply it to both the implementation and its deterministic tests. Run every target shell test through the authoring skill's bundled matrix helper: -```bash -bash /scripts/verify-shell-tests -- ... +```text +uv run --quiet --frozen --no-dev --project "/backend" verify-shell-tests -- ""... ``` The helper discovers actual Bash 3.2 and Bash 5 interpreters, queries their diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/inspect-skill b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/inspect-skill index a60ddb55..3c49a4e5 100755 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/inspect-skill +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/inspect-skill @@ -1,204 +1,5 @@ -#!/usr/bin/env bash -set -u +#!/bin/sh +set -eu -PROGRAM=${0##*/} - -usage() { - cat <&2 - exit "$code" -} - -absolute_directory() { - candidate=$1 - label=$2 - case "$candidate" in - /*) attempted=$candidate ;; - *) attempted=$(pwd -P)/$candidate ;; - esac - [ -d "$candidate" ] && [ -r "$candidate" ] && [ -x "$candidate" ] || - fail 2 "$label is not a readable directory: $attempted" - (cd "$candidate" 2>/dev/null && pwd -P) || - fail 2 "cannot resolve $label: $attempted" -} - -trim_scalar() { - printf '%s\n' "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' -} - -command=${1:-} -[ "$command" = "inspect" ] || { - usage >&2 - exit 2 -} -shift -[ "$#" -eq 2 ] || fail 2 "inspect requires SKILL_DIRECTORY and PLUGIN_ROOT" - -skill_input=$1 -plugin_input=$2 -plugin_root=$(absolute_directory "$plugin_input" "plugin root") || exit $? - -if [ -d "$skill_input" ]; then - skill_directory=$(absolute_directory "$skill_input" "skill directory") || exit $? -else - case "$skill_input" in - /*) missing_skill=$skill_input/SKILL.md ;; - *) missing_skill=$(pwd -P)/$skill_input/SKILL.md ;; - esac - fail 2 "skill file is not a readable regular file: $missing_skill" -fi - -case "$skill_directory/" in - "$plugin_root/"*) ;; - *) fail 2 "skill directory escapes plugin root: $skill_directory" ;; -esac - -skill_file=$skill_directory/SKILL.md -[ -f "$skill_file" ] && [ -r "$skill_file" ] || - fail 2 "skill file is not a readable regular file: $skill_file" -[ ! -L "$skill_file" ] || fail 2 "skill file must not be a symbolic link: $skill_file" - -metadata=$(awk ' - BEGIN { state = 0; bad = 0; name = ""; description = ""; disable_seen = 0 } - NR == 1 { - if ($0 != "---") bad = 1 - else state = 1 - next - } - state == 1 && $0 == "---" { state = 2; next } - state == 1 && /^[[:space:]]*($|#)/ { next } - state == 1 && /^name[[:space:]]*:/ { - if (name != "") bad = 1 - name = $0 - sub(/^name[[:space:]]*:[[:space:]]*/, "", name) - next - } - state == 1 && /^description[[:space:]]*:/ { - if (description != "") bad = 1 - description = $0 - sub(/^description[[:space:]]*:[[:space:]]*/, "", description) - next - } - state == 1 && /^disable-model-invocation[[:space:]]*:/ { - if (disable_seen) bad = 1 - disable_seen = 1 - value = $0 - sub(/^disable-model-invocation[[:space:]]*:[[:space:]]*/, "", value) - if (value != "true" && value != "false") bad = 1 - next - } - state == 1 { bad = 1 } - END { - if (state != 2 || bad || name == "" || description == "") exit 3 - print name - print description - } -' "$skill_file") || - fail 2 "frontmatter must contain one single-line name and description plus an optional boolean disable-model-invocation: $skill_file" - -name=$(printf '%s\n' "$metadata" | sed -n '1p') -description=$(printf '%s\n' "$metadata" | sed -n '2p') -name=$(trim_scalar "$name") -description=$(trim_scalar "$description") - -case "$name" in - \"*\") name=${name#\"}; name=${name%\"} ;; - \"*) fail 2 "skill name has an unmatched double quote: $skill_file" ;; - \'*\') name=${name#\'}; name=${name%\'} ;; - \'*) fail 2 "skill name has an unmatched single quote: $skill_file" ;; -esac -case "$description" in - \"*\") description=${description#\"}; description=${description%\"} ;; - \"*) fail 2 "skill description has an unmatched double quote: $skill_file" ;; - \'*\') description=${description#\'}; description=${description%\'} ;; - \'*) fail 2 "skill description has an unmatched single quote: $skill_file" ;; -esac - -case "$name" in - ""|*[!a-z0-9-]*|-*|*-) fail 2 "skill name must use lower-case letters, digits, and internal hyphens: $skill_file" ;; -esac -case "$name" in - *--*) fail 2 "skill name must not contain consecutive hyphens: $skill_file" ;; -esac -[ "$name" = "${skill_directory##*/}" ] || - fail 2 "skill name '$name' does not match directory '${skill_directory##*/}': $skill_file" -[ -n "$description" ] || fail 2 "skill description is empty: $skill_file" - -temp_parent=${TMPDIR:-/tmp} -case "$temp_parent" in - /) temp_prefix=/ ;; - */) temp_prefix=$temp_parent ;; - *) temp_prefix=$temp_parent/ ;; -esac - -links_file=$(mktemp "${temp_prefix}darrow-skill-links.XXXXXX") || - fail 2 "cannot create a temporary link inventory" -scripts_file=$(mktemp "${temp_prefix}darrow-skill-scripts.XXXXXX") || { - rm -f "$links_file" - fail 2 "cannot create a temporary script inventory" -} -trap 'rm -f "$links_file" "$scripts_file"' EXIT HUP INT TERM -grep -Eo '\]\([^)]*\)' "$skill_file" >"$links_file" 2>/dev/null || : - -printf 'format\tdarrow-skill-inspection-v1\n' -printf 'plugin_root\t%s\n' "$plugin_root" -printf 'skill_directory\t%s\n' "$skill_directory" -printf 'skill_file\t%s\n' "$skill_file" -printf 'name\t%s\n' "$name" -printf 'description_length\t%s\n' "${#description}" - -while IFS= read -r markdown_link; do - target=${markdown_link#](} - target=${target%)} - case "$target" in - \<*\>) target=${target#<}; target=${target%>} ;; - esac - target=${target%%#*} - [ -n "$target" ] || continue - case "$target" in - http://*|https://*|mailto:*) continue ;; - /*) fail 2 "absolute local references are not portable: $target" ;; - esac - - target_parent=${target%/*} - target_name=${target##*/} - if [ "$target_parent" = "$target" ]; then - target_parent=. - fi - reference_parent=$(cd "$skill_directory/$target_parent" 2>/dev/null && pwd -P) || - fail 2 "local reference parent is not readable: $skill_directory/$target_parent" - reference=$reference_parent/$target_name - case "$reference" in - "$plugin_root"/*) ;; - *) fail 2 "local reference escapes plugin root: $reference" ;; - esac - [ -f "$reference" ] && [ -r "$reference" ] || - fail 2 "local reference is not a readable regular file: $reference" - [ ! -L "$reference" ] || fail 2 "local reference must not be a symbolic link: $reference" - printf 'local_reference\t%s\n' "$reference" -done <"$links_file" - -if [ -e "$skill_directory/scripts" ]; then - [ -d "$skill_directory/scripts" ] && [ -r "$skill_directory/scripts" ] && [ -x "$skill_directory/scripts" ] || - fail 2 "scripts path is not a readable directory: $skill_directory/scripts" - find "$skill_directory/scripts" -type f -print >"$scripts_file" 2>/dev/null || - fail 2 "cannot enumerate bundled scripts: $skill_directory/scripts" - LC_ALL=C sort "$scripts_file" >"$links_file" || - fail 2 "cannot sort bundled scripts: $skill_directory/scripts" - while IFS= read -r bundled_script; do - [ -r "$bundled_script" ] || fail 2 "bundled script is unreadable: $bundled_script" - printf 'bundled_script\t%s\n' "$bundled_script" - done <"$links_file" -fi - -printf 'status\tvalid\n' +skill_dir=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +exec uv run --quiet --frozen --no-dev --project "$skill_dir/backend" inspect-skill "$@" diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/verify-shell-tests b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/verify-shell-tests index 454903c4..224fcafb 100755 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/verify-shell-tests +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/verify-shell-tests @@ -1,178 +1,5 @@ -#!/usr/bin/env bash -set -u +#!/bin/sh +set -eu -TAB=$(printf '\t') - -fail() { - printf 'verify-shell-tests: %s\n' "$2" >&2 - exit "$1" -} - -usage() { - printf '%s\n' \ - 'usage: verify-shell-tests [--shell ]... -- ...' >&2 - exit 2 -} - -canonical_file() { - local input=$1 parent name - case "$input" in - */*) - parent=${input%/*} - name=${input##*/} - [ -n "$parent" ] || parent=/ - ;; - *) - parent=. - name=$input - ;; - esac - parent=$(cd "$parent" 2>/dev/null && pwd -P) || return 1 - case "$parent" in - /) printf '/%s\n' "$name" ;; - *) printf '%s/%s\n' "$parent" "$name" ;; - esac -} - -resolve_interpreter() { - local requested=$1 resolved - case "$requested" in - */*) resolved=$requested ;; - *) resolved=$(command -v "$requested" 2>/dev/null) || return 1 ;; - esac - [ -x "$resolved" ] || return 1 - canonical_file "$resolved" -} - -CANDIDATES=() -explicit_candidates=false -while [ "$#" -gt 0 ]; do - case "$1" in - --shell) - [ "$#" -ge 2 ] || usage - CANDIDATES[${#CANDIDATES[@]}]=$2 - explicit_candidates=: - shift 2 - ;; - --) - shift - break - ;; - -*) usage ;; - *) break ;; - esac -done - -[ "$#" -gt 0 ] || usage - -if ! $explicit_candidates; then - CANDIDATES[${#CANDIDATES[@]}]=/bin/bash - CANDIDATES[${#CANDIDATES[@]}]=bash - CANDIDATES[${#CANDIDATES[@]}]=bash5 - CANDIDATES[${#CANDIDATES[@]}]=/opt/homebrew/bin/bash - CANDIDATES[${#CANDIDATES[@]}]=/usr/local/bin/bash -fi - -TESTS=() -for test_input in "$@"; do - [ -f "$test_input" ] && [ -r "$test_input" ] || - fail 2 "test script is not a readable regular file: $test_input" - test_file=$(canonical_file "$test_input") || - fail 2 "cannot resolve test script: $test_input" - TESTS[${#TESTS[@]}]=$test_file -done - -bash_3_2= -bash_3_2_version= -bash_5= -bash_5_version= - -for candidate in "${CANDIDATES[@]}"; do - if ! interpreter=$(resolve_interpreter "$candidate"); then - if $explicit_candidates; then - fail 2 "shell is not executable: $candidate" - fi - continue - fi - - if ! version_record=$("$interpreter" -c "printf '%s\\t%s\\t%s\\n' \"\${BASH_VERSINFO[0]}\" \"\${BASH_VERSINFO[1]}\" \"\$BASH_VERSION\"" 2>/dev/null); then - if $explicit_candidates; then - fail 2 "cannot read Bash version: $interpreter" - fi - continue - fi - IFS="$TAB" read -r major minor version </dev/null 2>&1 - test_status=$? - if [ "$test_status" -eq 0 ]; then - printf 'test_result%s%s%s%s%spassed\n' \ - "$TAB" "$label" "$TAB" "$test_file" "$TAB" - else - printf 'test_result%s%s%s%s%sfailed%sexit=%s\n' \ - "$TAB" "$label" "$TAB" "$test_file" "$TAB" "$TAB" "$test_status" - failures=$((failures + 1)) - fi - done -} - -if [ -n "$bash_3_2" ]; then - run_target bash-3.2 "$bash_3_2" "$bash_3_2_version" -else - printf 'interpreter%sbash-3.2%sunavailable\n' "$TAB" "$TAB" - missing=$((missing + 1)) -fi - -if [ -n "$bash_5" ]; then - run_target bash-5 "$bash_5" "$bash_5_version" -else - printf 'interpreter%sbash-5%sunavailable\n' "$TAB" "$TAB" - missing=$((missing + 1)) -fi - -if [ "$failures" -ne 0 ]; then - printf 'matrix_status%sfailed\n' "$TAB" - exit 1 -fi -if [ "$missing" -ne 0 ]; then - printf 'matrix_status%sunverified\n' "$TAB" - exit 3 -fi - -printf 'matrix_status%scomplete\n' "$TAB" +skill_dir=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +exec uv run --quiet --frozen --no-dev --project "$skill_dir/backend" verify-shell-tests "$@" diff --git a/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 b/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 new file mode 100644 index 00000000..7b5a793a --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 @@ -0,0 +1,38 @@ +$ErrorActionPreference = "Stop" + +$pluginRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$fixture = Join-Path ([System.IO.Path]::GetTempPath()) ("darrow skill authoring " + [guid]::NewGuid()) +$copy = Join-Path $fixture "plugin" + +try { + New-Item -ItemType Directory -Path $fixture | Out-Null + Copy-Item -LiteralPath $pluginRoot -Destination $copy -Recurse + $backend = Join-Path $copy "skills/author-agent-skill/backend" + $skill = Join-Path $copy "skills/author-agent-skill" + Remove-Item -LiteralPath (Join-Path $backend ".venv") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath (Join-Path $backend ".coverage") -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath (Join-Path $backend "coverage.json") -Force -ErrorAction SilentlyContinue + + & uv sync --locked --no-dev --project $backend + if ($LASTEXITCODE -ne 0) { throw "runtime sync failed" } + $runtimeTree = (& uv tree --locked --no-dev --project $backend) -join "`n" + if ($LASTEXITCODE -ne 0) { throw "runtime tree failed" } + foreach ($tool in @("coverage", "hypothesis", "mypy", "pytest", "ruff")) { + if ($runtimeTree -match "(?m)(^|\s)$tool(\s|$)") { + throw "development dependency installed at runtime: $tool" + } + } + + $inspection = (& uv run --quiet --frozen --no-dev --project $backend inspect-skill inspect $skill $copy) -join "`n" + if ($LASTEXITCODE -ne 0) { throw "inspection failed" } + if ($inspection -notmatch "(?m)^status\s+valid$") { throw "inspection omitted valid status" } + + $testScript = Join-Path $fixture "passing test.sh" + "#!/usr/bin/env bash`nexit 0`n" | Set-Content -Path $testScript -NoNewline + $matrix = (& uv run --quiet --frozen --no-dev --project $backend verify-shell-tests -- $testScript) -join "`n" + if ($LASTEXITCODE -notin @(0, 3)) { throw "shell matrix failed with $LASTEXITCODE" } + if ($matrix -notmatch "(?m)^format\s+darrow-shell-test-matrix-v1$") { throw "matrix omitted format" } +} +finally { + Remove-Item -LiteralPath $fixture -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.sh b/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.sh new file mode 100755 index 00000000..8e829bca --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.sh @@ -0,0 +1,47 @@ +#!/bin/sh +set -eu + +plugin_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +fixture=$(mktemp -d "${TMPDIR:-/tmp}/darrow-skill-authoring-install.XXXXXX") +trap 'rm -rf "$fixture"' EXIT HUP INT TERM + +mkdir -p "$fixture/plugin" +cp -R "$plugin_root/." "$fixture/plugin/" +backend="$fixture/plugin/skills/author-agent-skill/backend" +skill="$fixture/plugin/skills/author-agent-skill" +rm -rf "$backend/.venv" +rm -f "$backend/.coverage" "$backend/coverage.json" + +uv sync --locked --no-dev --project "$backend" +runtime_tree=$(uv tree --locked --no-dev --project "$backend") +for development_tool in coverage hypothesis mypy pytest ruff; do + if printf '%s\n' "$runtime_tree" | grep -E "(^|[[:space:]])${development_tool}([[:space:]]|$)" >/dev/null; then + echo "error: development dependency installed at runtime: $development_tool" >&2 + exit 1 + fi +done + +inspection=$(uv run --quiet --frozen --no-dev --project "$backend" \ + inspect-skill inspect "$skill" "$fixture/plugin") +printf '%s\n' "$inspection" | grep '^status[[:space:]]valid$' >/dev/null + +set +e +matrix=$(uv run --quiet --frozen --no-dev --project "$backend" \ + verify-shell-tests -- \ + "$skill/scripts/inspect-skill.test.sh" \ + "$skill/scripts/verify-shell-tests.test.sh" \ + "$skill/scripts/interpreter-routing.test.sh") +matrix_status=$? +set -e +case "$matrix_status" in + 0|3) ;; + *) printf '%s\n' "$matrix" >&2; exit "$matrix_status" ;; +esac +printf '%s\n' "$matrix" | grep '^format[[:space:]]darrow-shell-test-matrix-v1$' >/dev/null +if printf '%s\n' "$matrix" | grep '^test_result.*failed' >/dev/null; then + printf '%s\n' "$matrix" >&2 + exit 1 +fi +for test_name in inspect-skill.test.sh verify-shell-tests.test.sh interpreter-routing.test.sh; do + printf '%s\n' "$matrix" | grep "test_result.*${test_name}[[:space:]]passed$" >/dev/null +done diff --git a/python-packages.txt b/python-packages.txt index a2376427..174b75a1 100644 --- a/python-packages.txt +++ b/python-packages.txt @@ -1,2 +1,3 @@ # Repository-relative Python package roots, one per line. plugins/capability/darrow-observability-langfuse/backend +plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend From 7ddf7e710bc80406a379b3f3364e4341d20005fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rochel?= Date: Thu, 17 Sep 2026 13:35:26 +0200 Subject: [PATCH 02/10] fix(skill-authoring): correct native Windows checks --- .github/workflows/python-quality.yml | 28 +++++++++++++-- docs/specs/python-quality.md | 10 +++--- .../src/darrow_skill_authoring/shell_tests.py | 5 ++- .../backend/tests/test_shell_tests.py | 14 ++++++++ scripts/check-python | 36 +++++++++++++++++-- 5 files changed, 82 insertions(+), 11 deletions(-) diff --git a/.github/workflows/python-quality.yml b/.github/workflows/python-quality.yml index 4244422b..27ed8559 100644 --- a/.github/workflows/python-quality.yml +++ b/.github/workflows/python-quality.yml @@ -15,9 +15,28 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] python-version: ["3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.os }} + env: + UV_PYTHON: ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + - run: scripts/check-python + + skill-authoring-windows: + name: Skill authoring Python ${{ matrix.python-version }} on windows-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + runs-on: windows-latest defaults: run: shell: bash @@ -31,7 +50,7 @@ jobs: - uses: astral-sh/setup-uv@v6 with: enable-cache: true - - run: scripts/check-python + - run: scripts/check-python --package plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend inventory: name: Python inventory guard @@ -89,17 +108,20 @@ jobs: aggregate: name: Python quality if: always() - needs: [package, inventory, fresh-install, performance] + needs: + [package, skill-authoring-windows, inventory, fresh-install, performance] runs-on: ubuntu-latest steps: - name: Require every Python quality job env: PACKAGE_RESULT: ${{ needs.package.result }} + WINDOWS_RESULT: ${{ needs.skill-authoring-windows.result }} INVENTORY_RESULT: ${{ needs.inventory.result }} INSTALL_RESULT: ${{ needs.fresh-install.result }} PERFORMANCE_RESULT: ${{ needs.performance.result }} run: | test "$PACKAGE_RESULT" = success + test "$WINDOWS_RESULT" = success test "$INVENTORY_RESULT" = success test "$INSTALL_RESULT" = success test "$PERFORMANCE_RESULT" = success diff --git a/docs/specs/python-quality.md b/docs/specs/python-quality.md index af059228..4f2df356 100644 --- a/docs/specs/python-quality.md +++ b/docs/specs/python-quality.md @@ -56,10 +56,12 @@ including property tests, and enforces both coverage measures. The pre-commit hook runs the fast formatting, lint, and type portion when staged Python or Python project files change. -CI runs the canonical gate on Linux, macOS, and native Windows for every -declared Python minor version. A stable aggregate `Python quality` status is the -branch-protection check. The inventory guard itself is tested by introducing a -temporary unregistered package and proving that the gate refuses it. +CI runs the canonical all-package gate on Linux and macOS for every declared +Python minor version. Packages that claim native Windows support run the same +gate in a package-scoped Windows matrix. A stable aggregate `Python quality` +status is the branch-protection check. The inventory guard itself is tested by +introducing a temporary unregistered package and proving that the gate refuses +it. ## Performance and release evidence diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/shell_tests.py b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/shell_tests.py index 24873705..2a24c66e 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/shell_tests.py +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/shell_tests.py @@ -155,9 +155,12 @@ def _test_files(inputs: list[str]) -> list[Path]: def run_test(interpreter: BashInterpreter, test: Path) -> int: + test_argument = str(test) + if os.name == "nt": + test_argument = test_argument.replace("\\", "/") try: returncode = subprocess.run( - [str(interpreter.path), str(test)], + [str(interpreter.path), test_argument], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_shell_tests.py b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_shell_tests.py index abd77cf4..8b2a737e 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_shell_tests.py +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_shell_tests.py @@ -270,6 +270,20 @@ def test_run_test_reports_process_status_and_spawn_failure(tmp_path: Path) -> No assert shell_tests.run_test(interpreter, test) == 137 +def test_run_test_uses_forward_slashes_for_native_windows( + monkeypatch: pytest.MonkeyPatch, +) -> None: + interpreter = BashInterpreter("bash-5", Path("bash.exe"), "test") + test = Path(r"C:\Temp Folder\passing.sh") + monkeypatch.setattr(os, "name", "nt") + with patch( + "darrow_skill_authoring.shell_tests.subprocess.run", + return_value=subprocess.CompletedProcess([], 0), + ) as execute: + assert shell_tests.run_test(interpreter, test) == 0 + assert execute.call_args.args[0] == ["bash.exe", "C:/Temp Folder/passing.sh"] + + def test_entrypoint_returns_run_status(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "argv", ["verify-shell-tests"]) with pytest.raises(SystemExit) as raised: diff --git a/scripts/check-python b/scripts/check-python index c0fada31..af7fb459 100755 --- a/scripts/check-python +++ b/scripts/check-python @@ -3,9 +3,26 @@ set -eu repo_root=$(git rev-parse --show-toplevel) mode=full -if [ "${1:-}" = "--fast" ]; then - mode=fast -fi +selected_package= +while [ "$#" -gt 0 ]; do + case "$1" in + --fast) + mode=fast + shift + ;; + --package) + if [ "$#" -lt 2 ] || [ -n "$selected_package" ]; then + echo "usage: scripts/check-python [--fast] [--package REPOSITORY_PATH]" >&2 + exit 2 + fi + selected_package=$2 + shift 2 + ;; + *) + break + ;; + esac +done if ! command -v uv >/dev/null 2>&1; then echo "error: uv is required for Python quality checks" >&2 @@ -15,6 +32,19 @@ fi "$repo_root/scripts/check-python-inventory" "$repo_root" packages=$(sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' "$repo_root/python-packages.txt") +if [ -n "$selected_package" ]; then + package_registered=false + for relative in $packages; do + if [ "$relative" = "$selected_package" ]; then + package_registered=true + fi + done + if [ "$package_registered" != true ]; then + echo "error: Python package is not registered: $selected_package" >&2 + exit 1 + fi + packages=$selected_package +fi coverage_verifier_tested=false for relative in $packages; do package="$repo_root/$relative" From 00d635daa752c9a6f95f3da729bc30112956c0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rochel?= Date: Thu, 17 Sep 2026 13:38:13 +0200 Subject: [PATCH 03/10] fix(python): invoke quality helpers portably --- .../darrow-skill-authoring/tests/fresh-install.test.ps1 | 2 ++ scripts/check-python | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 b/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 index 7b5a793a..4270a4c3 100644 --- a/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 +++ b/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 @@ -25,11 +25,13 @@ try { $inspection = (& uv run --quiet --frozen --no-dev --project $backend inspect-skill inspect $skill $copy) -join "`n" if ($LASTEXITCODE -ne 0) { throw "inspection failed" } + Write-Output $inspection if ($inspection -notmatch "(?m)^status\s+valid$") { throw "inspection omitted valid status" } $testScript = Join-Path $fixture "passing test.sh" "#!/usr/bin/env bash`nexit 0`n" | Set-Content -Path $testScript -NoNewline $matrix = (& uv run --quiet --frozen --no-dev --project $backend verify-shell-tests -- $testScript) -join "`n" + Write-Output $matrix if ($LASTEXITCODE -notin @(0, 3)) { throw "shell matrix failed with $LASTEXITCODE" } if ($matrix -notmatch "(?m)^format\s+darrow-shell-test-matrix-v1$") { throw "matrix omitted format" } } diff --git a/scripts/check-python b/scripts/check-python index af7fb459..0e31c1d2 100755 --- a/scripts/check-python +++ b/scripts/check-python @@ -68,11 +68,11 @@ for relative in $packages; do if [ "$mode" = full ]; then if [ "$coverage_verifier_tested" = false ]; then - uv run --all-groups "$repo_root/scripts/test-python-coverage" + uv run --all-groups sh "$repo_root/scripts/test-python-coverage" fi uv run --all-groups coverage run -m pytest uv run --all-groups coverage json -o coverage.json - uv run --all-groups "$repo_root/scripts/verify-python-coverage" coverage.json + uv run --all-groups sh "$repo_root/scripts/verify-python-coverage" coverage.json fi ) if [ "$mode" = full ]; then From eda5eb6961766c9da751de63a26c9b4584a06545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rochel?= Date: Thu, 17 Sep 2026 13:40:24 +0200 Subject: [PATCH 04/10] fix(skill-authoring): handle Windows path semantics --- .../backend/src/darrow_skill_authoring/inspector.py | 3 ++- .../darrow-skill-authoring/tests/fresh-install.test.ps1 | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/inspector.py b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/inspector.py index ec537875..dade18ba 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/inspector.py +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/src/darrow_skill_authoring/inspector.py @@ -4,7 +4,7 @@ import re import sys from dataclasses import dataclass -from pathlib import Path, PureWindowsPath +from pathlib import Path, PurePosixPath, PureWindowsPath from typing import TextIO PROGRAM = "inspect-skill" @@ -162,6 +162,7 @@ def _portable_target(raw_target: str) -> str | None: if ( "\\" in target or Path(target).is_absolute() + or PurePosixPath(target).is_absolute() or windows_target.is_absolute() or bool(windows_target.drive) ): diff --git a/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 b/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 index 4270a4c3..7bbc277f 100644 --- a/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 +++ b/plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 @@ -31,10 +31,13 @@ try { $testScript = Join-Path $fixture "passing test.sh" "#!/usr/bin/env bash`nexit 0`n" | Set-Content -Path $testScript -NoNewline $matrix = (& uv run --quiet --frozen --no-dev --project $backend verify-shell-tests -- $testScript) -join "`n" + $matrixStatus = $LASTEXITCODE Write-Output $matrix - if ($LASTEXITCODE -notin @(0, 3)) { throw "shell matrix failed with $LASTEXITCODE" } + if ($matrixStatus -notin @(0, 3)) { throw "shell matrix failed with $matrixStatus" } if ($matrix -notmatch "(?m)^format\s+darrow-shell-test-matrix-v1$") { throw "matrix omitted format" } } finally { Remove-Item -LiteralPath $fixture -Recurse -Force -ErrorAction SilentlyContinue } + +exit 0 From ab37eece64ee0410ea8b7addad3a316aacaeaaae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rochel?= Date: Thu, 17 Sep 2026 13:42:22 +0200 Subject: [PATCH 05/10] test(skill-authoring): cover portable shell path branch --- .../backend/tests/test_shell_tests.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_shell_tests.py b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_shell_tests.py index 8b2a737e..1a20a726 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_shell_tests.py +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend/tests/test_shell_tests.py @@ -284,6 +284,20 @@ def test_run_test_uses_forward_slashes_for_native_windows( assert execute.call_args.args[0] == ["bash.exe", "C:/Temp Folder/passing.sh"] +def test_run_test_preserves_posix_path_spelling( + monkeypatch: pytest.MonkeyPatch, +) -> None: + interpreter = BashInterpreter("bash-5", Path("bash"), "test") + test = Path("passing.sh") + monkeypatch.setattr(os, "name", "posix") + with patch( + "darrow_skill_authoring.shell_tests.subprocess.run", + return_value=subprocess.CompletedProcess([], 0), + ) as execute: + assert shell_tests.run_test(interpreter, test) == 0 + assert execute.call_args.args[0] == ["bash", "passing.sh"] + + def test_entrypoint_returns_run_status(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "argv", ["verify-shell-tests"]) with pytest.raises(SystemExit) as raised: From 52cf62f937a962a3f81c682cc81c69b6ae5ba2e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rochel?= Date: Thu, 17 Sep 2026 14:29:49 +0200 Subject: [PATCH 06/10] fix(skill-authoring): follow target toolchains --- docs/specs/skill-authoring.md | 35 ++++---- .../skills/author-agent-skill/SKILL.md | 18 ++-- .../skills/author-agent-skill/evals/README.md | 19 +++-- .../evals/respect-target-toolchain.yaml | 73 ++++++++++++++++ .../references/plugin-mechanics.md | 85 +++++++++---------- .../references/portable-shell.md | 27 ++++-- 6 files changed, 172 insertions(+), 85 deletions(-) create mode 100644 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/respect-target-toolchain.yaml diff --git a/docs/specs/skill-authoring.md b/docs/specs/skill-authoring.md index 464af29b..9a252a43 100644 --- a/docs/specs/skill-authoring.md +++ b/docs/specs/skill-authoring.md @@ -76,27 +76,30 @@ checkable, and recorded evidence for trigger behavior and workflow quality. - **SA-C5 — Mechanics are executable.** Put deterministic, repeatedly needed, or error-prone checks in a bundled script with narrow inputs and stable machine-readable output. Keep contextual decisions and trade-offs in - `SKILL.md`. Use a contained, locked Python package when substantial mechanics - benefit from structured code or native Windows support; use portable Bash for - genuinely small host-specific launchers and command glue. Do not add a script - when existing tools and instructions are already reliable. + `SKILL.md`. Follow the target repository's instructions and established + toolchain when choosing how to implement executable mechanics. If the target + has no applicable convention, treat the runtime and package manager as + contract inputs instead of importing them from the authoring skill's source + repository. Do not add a script when existing tools and instructions are + already reliable. - **SA-C6 — Fail closed and identify evidence.** A required unreadable skill, manifest, configuration, instruction, or referenced local resource blocks the dependent result. Model-facing script output identifies inspected inputs and local references with absolute paths. Missing evidence is never treated as a successful empty state. -- **SA-C7 — Portable self-containment.** A Python-backed helper keeps its - package, lock, entrypoints, dependencies, and state inside the owning plugin; - separates runtime from development dependencies; uses frozen UV execution; - and verifies its public entrypoints from a fresh artifact on every claimed - native platform. Bundled shell scripts work in Bash 5 and `/bin/bash` 3.2, - avoid GNU-only assumptions, and never require files from a sibling plugin. - Normalize paths before emitting or comparing them. Label shell evidence by - the interpreter's observed version, deduplicate equivalent interpreters, and - leave an unavailable required version explicitly unverified. Shell tests - preserve their executing interpreter in nested implementation calls. A - packaged skill remains useful with no MCP server or other Darrow plugin - installed unless its own manifest declares and supplies that dependency. +- **SA-C7 — Portable self-containment.** A packaged helper keeps its manifest, + lock, entrypoints, dependencies, and state inside the owning plugin; follows + the target repository's runtime and development dependency conventions; uses + reproducible locked execution; and verifies its public entrypoints from a + fresh artifact on every claimed native platform. Bundled shell scripts work + on every shell version claimed by the target, avoid unsupported utility + assumptions, and never require files from a sibling plugin. Normalize paths + before emitting or comparing them. Label shell evidence by the interpreter's + observed version, deduplicate equivalent interpreters, and leave an + unavailable required version explicitly unverified. Shell tests preserve + their executing interpreter in nested implementation calls. A packaged skill + remains useful with no MCP server or other Darrow plugin installed unless its + own manifest declares and supplies that dependency. - **SA-C8 — Evaluation separates prompt from criteria.** Participant-visible eval prompts contain the task and repository evidence but not their pass criteria. Hidden deterministic checks or rubrics cover workflow behavior, diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/SKILL.md b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/SKILL.md index 09bc25ae..cf5c6a8f 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/SKILL.md +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/SKILL.md @@ -24,10 +24,14 @@ validators or decide whether the workflow is well designed. Before adding executable mechanics, read [`references/plugin-mechanics.md`](references/plugin-mechanics.md) and choose -existing tools, small shell glue, or contained Python from the target's actual -complexity and supported hosts. When verifying target shell tests, also read -[`references/portable-shell.md`](references/portable-shell.md) and run the -version-aware helper through the same backend: +existing tools, small glue, or a contained packaged helper from the target +repository's instructions, established toolchain, actual complexity, and +supported hosts. If the target establishes no runtime or package manager, make +that choice a contract input before writing mechanics. When verifying target +shell tests, also read +[`references/portable-shell.md`](references/portable-shell.md). If the target +requires both Bash 3.2 and Bash 5, run the version-aware helper through the same +backend: ```text uv run --quiet --frozen --no-dev --project "/backend" verify-shell-tests -- ""... @@ -200,9 +204,9 @@ resources, and packaging changes, with no unrelated capability added. Run, in order: -1. each bundled package test through its locked quality command and each shell - test through the version-aware helper, preserving unavailable required - versions as unverified; +1. each bundled package test through the target repository's locked quality + command and each shell test on the interpreter versions claimed by the + target, preserving unavailable required versions as unverified; 2. the bundled inspector on the finished skill and plugin root; 3. the repository's skill and native plugin-manifest validators; 4. scoped eval dry validation and live trials on the supported harnesses chosen diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/README.md b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/README.md index f832b557..04cbcf79 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/README.md +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/README.md @@ -4,15 +4,16 @@ Run these cases with the shared repository runner, selecting `--plugin darrow-skill-authoring`. Use one trial and one job while diagnosing, and inspect each failure before starting the next case. -| Request boundary | Case | -| ----------------------------------------------- | ------------------------- | -| Direct skill creation | `create-focused-skill` | -| Cross-platform Python mechanic | `create-python-mechanic` | -| Indirect reusable-workflow request | `indirect-discovery` | -| Missing goal, destination, and runtime support | `refuse-undefined-skill` | -| Ordinary agent guidance without skill intent | `non-skill-guidance` | -| Repair a skill with a sibling-plugin dependency | `repair-skill-boundaries` | -| Validation without mutation authority | `validate-read-only` | +| Request boundary | Case | +| ----------------------------------------------- | -------------------------- | +| Direct skill creation | `create-focused-skill` | +| Target-selected Python mechanic | `create-python-mechanic` | +| Target repository toolchain | `respect-target-toolchain` | +| Indirect reusable-workflow request | `indirect-discovery` | +| Missing goal, destination, and runtime support | `refuse-undefined-skill` | +| Ordinary agent guidance without skill intent | `non-skill-guidance` | +| Repair a skill with a sibling-plugin dependency | `repair-skill-boundaries` | +| Validation without mutation authority | `validate-read-only` | ## Shell evidence diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/respect-target-toolchain.yaml b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/respect-target-toolchain.yaml new file mode 100644 index 00000000..c4c98d1a --- /dev/null +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/respect-target-toolchain.yaml @@ -0,0 +1,73 @@ +id: author-agent-skill-respect-target-toolchain +invariant: SA-C5,SA-C7,SA-C8 +activation: positive +prompt: >- + Create one cross-platform agent skill named validate-capability-manifest in + the existing plugins/schema-helper plugin. It should validate JSON capability + manifests before review. A valid manifest is an object with non-empty string + `name` and `description` fields and no unknown top-level fields. Put the + deterministic validation behind a narrow entrypoint, add appropriate tests, + keep semantic review of the description in SKILL.md, and follow this + repository's established toolchain. Do not commit. +fixture: + commits: + - message: "chore: init" + files: + AGENTS.md: | + # Repository instructions + + Plugins are independently installable. Executable plugin mechanics + use JavaScript ES modules and package-local package.json files. Use + pnpm for package operations. Do not add Python, uv, or shell wrappers. + Never commit automatically. + plugins/schema-helper/.claude-plugin/plugin.json: | + {"name":"schema-helper","version":"0.1.0","description":"Schema review helpers","author":{"name":"Example"}} + plugins/schema-helper/.codex-plugin/plugin.json: | + {"name":"schema-helper","version":"0.1.0","description":"Schema review helpers","author":{"name":"Example"},"skills":"./skills/"} + plugins/schema-helper/package.json: | + {"name":"schema-helper-mechanics","private":true,"type":"module","scripts":{"test":"node --test"}} + valid.json: | + {"name":"release-check","description":"Validate a release before review."} +checks: + - name: focused skill has valid discovery metadata + run: >- + skill=plugins/schema-helper/skills/validate-capability-manifest/SKILL.md; + test -r "$skill"; + grep -E '^name:[[:space:]]*validate-capability-manifest[[:space:]]*$' "$skill" >/dev/null; + grep -Ei '^description:.*(manifest|capability).*(validat|review)|^description:.*(validat|review).*(manifest|capability)' "$skill" >/dev/null + - name: target JavaScript toolchain owns the mechanic + run: | + plugin=plugins/schema-helper + test -r "$plugin/package.json" + mechanic=$(find "$plugin" -type f \( -name '*.js' -o -name '*.mjs' \) ! -name '*.test.js' ! -name '*.test.mjs' ! -path '*/test/*' ! -path '*/tests/*' | sed -n '1p') + test -n "$mechanic" + test -z "$(find "$plugin" -type f \( -name '*.py' -o -name pyproject.toml -o -name uv.lock -o -name '*.sh' \) -print)" + - name: target-native entrypoint validates success and failure + run: | + plugin=plugins/schema-helper + mechanic=$(find "$plugin" -type f \( -name '*.js' -o -name '*.mjs' \) ! -name '*.test.js' ! -name '*.test.mjs' ! -path '*/test/*' ! -path '*/tests/*' | sed -n '1p') + node "$mechanic" valid.json >/dev/null + for invalid in \ + '{"description":"Valid."}' \ + '{"name":"missing-description"}' \ + '{"name":3,"description":"Valid."}' \ + '{"name":"release-check","description":"Valid.","extra":true}' \ + '{"name":"","description":"Valid."}' \ + '{"name":"release-check","description":3}' + do + printf '%s\n' "$invalid" >.git/invalid.json + ! node "$mechanic" .git/invalid.json + done + - name: package tests pass with the target toolchain + run: | + plugin=plugins/schema-helper + tests=$(find "$plugin" \( -name node_modules -o -name .pnpm -o -name dist -o -name build \) -prune -o -type f \( -name '*.test.js' -o -name '*.test.mjs' -o -path '*/test/*.js' -o -path '*/test/*.mjs' -o -path '*/tests/*.js' -o -path '*/tests/*.mjs' \) -print) + test -n "$tests" + cd "$plugin" && pnpm test + - name: workflow keeps semantic judgment in the skill + run: cat plugins/schema-helper/skills/validate-capability-manifest/SKILL.md + expect_regex: "clear|specific|audience|semantic|meaning|quality" + flags: i + - name: authoring creates no commit + run: git rev-list --all --reflog --count + expect_regex: "^1$" diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/plugin-mechanics.md b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/plugin-mechanics.md index 3d7f692c..09ad6682 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/plugin-mechanics.md +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/plugin-mechanics.md @@ -1,61 +1,58 @@ -# Plugin helper mechanics +# Executable helper mechanics Read this reference before adding or changing executable mechanics in a skill. -Choose the smallest runtime surface that supports the target's declared hosts. +Choose the smallest runtime surface that follows the target repository's +instructions and supports its declared hosts. ## Choose the implementation -- Use existing host tools directly when their invocation is already short, - stable, and hard to misuse. -- Use portable Bash for genuinely small POSIX launchers, environment adapters, - and command glue. If Bash owns any logic or tests, also read - `portable-shell.md` from this reference directory. -- Use a contained Python package managed by UV for substantial parsing, - filesystem traversal, state validation, subprocess coordination, or mechanics - that must run on native Windows. Python is warranted by the behavior and host - support, not by the presence of one conditional. +- Inspect the target's repository instructions, manifests, lockfiles, existing + helper code, and supported hosts before selecting an implementation. +- Use existing host or repository tools directly when their invocation is + short, stable, and hard to misuse. +- Use small command glue in an environment the target already supports for + launchers, adapters, and simple composition. If the target selects shell, + also read `portable-shell.md` from this reference directory. +- Use a packaged helper in the target's established runtime for substantial + parsing, filesystem traversal, state validation, subprocess coordination, or + other structured mechanics. +- If no applicable runtime or package-manager convention exists, ask for that + contract input. Do not import the source repository's choice as a default. - Do not introduce a shared plugin runtime or depend on a sibling plugin. Keep contextual judgment, authority, and material choices in `SKILL.md`. -## Package Python inside the plugin +## Contain a packaged helper -Follow the target repository's instructions and existing Python package -conventions. Do not copy inventories, paths, thresholds, or CI structure from -the repository that supplied this skill. If the target has no established -convention, treat the package layout, supported Python range, UV adoption, and -quality bar as contract inputs rather than silently imposing this plugin's own -implementation. +Follow the target repository's package layout, manifest, lockfile, dependency, +entrypoint, and quality conventions. Do not copy inventories, paths, version +ranges, thresholds, or CI structure from the repository that supplied this +skill. -When the target selects Python plus UV, keep the complete package and its -environment inside the owning plugin, and commit both `pyproject.toml` and -`uv.lock`. Declare the supported Python range, expose narrow console entrypoints, -put runtime dependencies in `project.dependencies`, put quality and test tools -in the development dependency group, and set UV's default groups to empty so a -runtime sync cannot install development tools. +Keep the complete package and its generated state inside the owning plugin. +Commit the target ecosystem's manifest and lockfile when it uses them. Expose +narrow public entrypoints, declare the supported runtime range, and separate +runtime dependencies from quality and test tools according to the target's +established conventions. -Invoke a runtime entrypoint through its package without development groups: - -```text -uv run --quiet --frozen --no-dev --project [arguments] -``` - -This form works in POSIX shells and native Windows terminals. A small POSIX -launcher may calculate `` for existing Unix callers, but it only -dispatches to the same locked entrypoint and contains no duplicated mechanics. -Give every public operation the same arguments, output, refusal, and exit-status -contract on each supported platform. +Invoke public operations through the target's locked package command. Do not +invent a generic command when the repository has not selected one. A small +compatibility launcher may calculate the package location for existing callers, +but it only dispatches to the same entrypoint and contains no duplicated +mechanics. Give every public operation the same arguments, output, refusal, and +exit-status contract on each supported platform. ## Test and release the helper -Keep fast Python unit and integration tests beside the package. Apply the target +Keep fast unit and integration tests beside the package. Apply the target repository's formatting, lint, typing, property-test, and coverage gates rather -than importing thresholds from the skill's source repository. Directly exercise -path normalization, malformed input, failure output, subprocess status, and -platform-specific path forms. +than importing thresholds from the skill's source repository. Directly +exercise path normalization, malformed input, failure output, subprocess +status, and platform-specific path forms. Copy the complete plugin to a fresh temporary directory, remove local virtual -environments and generated state, sync only locked runtime dependencies, and -invoke each public entrypoint from the copy. Confirm that development tools are -absent from the runtime dependency tree. Run this fresh-artifact check on every -claimed native platform; a POSIX compatibility layer does not establish native -Windows support. +environments, dependency directories, and generated state, install only locked +runtime dependencies, and invoke each public entrypoint from the copy. Confirm +that development tools are absent from the runtime dependency tree when the +target ecosystem distinguishes them. Run this fresh-artifact check on every +claimed native platform; a compatibility layer does not establish native host +support. diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/portable-shell.md b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/portable-shell.md index 8b081996..80b8d7b7 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/portable-shell.md +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/references/portable-shell.md @@ -30,10 +30,13 @@ shell test. Apply it to both the implementation and its deterministic tests. ## Portable mechanics -- Support both Bash 5 and `/bin/bash` 3.2. Avoid associative arrays, - `${var,,}`, `${var^^}`, and other features newer than Bash 3.2. -- Use baseline Unix utilities and portable options. Account for old BSD `awk`, - `sed`, `grep`, and `find` behavior instead of relying on GNU extensions. +- Support the shells, versions, operating systems, and utilities established by + the target repository. Do not import the authoring skill's source-repository + compatibility range into the target. +- Use syntax and utility options supported by that declared range. For a target + that includes Bash 3.2, avoid associative arrays, `${var,,}`, `${var^^}`, and + other newer features. For a target that includes macOS or BSD utilities, + avoid unsupported GNU extensions. - Under `set -o pipefail`, do not use an early-exit consumer in a pipeline when the producer may receive `SIGPIPE`. Capture bounded output or use a here-string when that is safer. @@ -42,13 +45,17 @@ shell test. Apply it to both the implementation and its deterministic tests. ## Interpreter evidence -Run every target shell test through the authoring skill's bundled matrix helper: +Run target shell tests with each interpreter version the target repository +claims. Use its established test command or matrix when available. + +When the target specifically requires both Bash 3.2 and Bash 5, the authoring +skill's bundled matrix helper can supply that evidence: ```text uv run --quiet --frozen --no-dev --project "/backend" verify-shell-tests -- ""... ``` -The helper discovers actual Bash 3.2 and Bash 5 interpreters, queries their +This helper discovers actual Bash 3.2 and Bash 5 interpreters, queries their versions, deduplicates equivalent candidates, and labels results from observed version data. Pass one or more `--shell ` options before `--` only when repository evidence supplies additional interpreter paths. Never infer a @@ -61,7 +68,8 @@ interpreter executing the test even when `PATH` resolves another Bash. Exercise that boundary with a conflicting `bash` on `PATH`; expected-failure assertions must not hide calls through the wrong interpreter. -Interpret its final `matrix_status` and exit status together: +For this Bash 3.2/5 matrix, interpret its final `matrix_status` and exit status +together: - `complete` / exit `0`: both required versions were observed and all tests passed; @@ -76,8 +84,9 @@ required version explicitly. ## Tests -- Require support for Bash 5 and `/bin/bash` 3.2, but claim a live result only - for versions observed by the matrix helper. +- Require only the shell versions established by the target, and claim a live + result only for versions actually observed. For a Bash 3.2/5 target, use the + bundled matrix helper's observed versions. - When code uses `TMPDIR` or compares paths, run it with `TMPDIR` both with and without a trailing separator. Compare canonical paths, not the raw strings used to construct them. From c77322ef862f365738500d82848fa2e86b6fc60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rochel?= Date: Thu, 17 Sep 2026 15:06:54 +0200 Subject: [PATCH 07/10] refactor(skill-authoring): remove forwarding launchers --- .../darrow-skill-authoring/README.md | 12 +++--- .../evals/create-focused-skill.yaml | 7 ++-- .../evals/repair-skill-boundaries.yaml | 7 ++-- .../author-agent-skill/scripts/inspect-skill | 5 --- .../scripts/inspect-skill.test.sh | 41 +++++++++++-------- .../scripts/verify-shell-tests | 5 --- .../scripts/verify-shell-tests.test.sh | 17 +++++--- 7 files changed, 49 insertions(+), 45 deletions(-) delete mode 100755 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/inspect-skill delete mode 100755 plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/verify-shell-tests diff --git a/plugins/foundation/darrow-skill-authoring/README.md b/plugins/foundation/darrow-skill-authoring/README.md index 327f73de..c4a495e5 100644 --- a/plugins/foundation/darrow-skill-authoring/README.md +++ b/plugins/foundation/darrow-skill-authoring/README.md @@ -37,8 +37,10 @@ From the repository root, run the Python quality checks with `bun run check:python`. From this plugin directory, run the compatibility shell suites through the version-aware matrix: -```sh -bash skills/author-agent-skill/scripts/verify-shell-tests -- \ +```text +uv run --quiet --frozen --no-dev \ + --project skills/author-agent-skill/backend \ + verify-shell-tests -- \ skills/author-agent-skill/scripts/inspect-skill.test.sh \ skills/author-agent-skill/scripts/verify-shell-tests.test.sh \ skills/author-agent-skill/scripts/interpreter-routing.test.sh @@ -63,9 +65,9 @@ Create, revise, or validate one reusable skill. Do not use it for generic instru ## Hosts and prerequisites -Codex and Claude Code; UV and a UV-managed Python `>=3.10,<3.14`. The small -POSIX compatibility launchers use Bash and baseline Unix tools. Live -evaluations need the target's supported host harness. +Codex and Claude Code; UV and a UV-managed Python `>=3.10,<3.14`. Shell-matrix +verification also needs the target interpreters and their baseline Unix tools. +Live evaluations need the target's supported host harness. ## Installation diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/create-focused-skill.yaml b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/create-focused-skill.yaml index b067d746..1c4d7161 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/create-focused-skill.yaml +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/create-focused-skill.yaml @@ -104,9 +104,10 @@ checks: flags: i - name: result is self-contained run: >- - inspector=$(find . -path '*/author-agent-skill/scripts/inspect-skill' -type f | sed -n '1p'); - test -n "$inspector"; - bash "$inspector" inspect plugins/release-helper/skills/validate-release-note plugins/release-helper >/dev/null; + backend_file=$(find . -path '*/author-agent-skill/backend/pyproject.toml' -type f | sed -n '1p'); + test -n "$backend_file"; + backend=${backend_file%/pyproject.toml}; + uv run --quiet --frozen --no-dev --project "$backend" inspect-skill inspect plugins/release-helper/skills/validate-release-note plugins/release-helper >/dev/null; ! grep -R -E '(plugins/[^/]+/skills|~/|/Users/|/home/)' plugins/release-helper/skills/validate-release-note - name: authoring creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/repair-skill-boundaries.yaml b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/repair-skill-boundaries.yaml index 357abf77..734f7e81 100644 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/repair-skill-boundaries.yaml +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/evals/repair-skill-boundaries.yaml @@ -41,9 +41,10 @@ checks: flags: i - name: sibling-plugin dependency is removed run: >- - inspector=$(find . -path '*/author-agent-skill/scripts/inspect-skill' -type f | sed -n '1p'); - test -n "$inspector"; - bash "$inspector" inspect plugins/ops/skills/deploy-checklist plugins/ops >/dev/null; + backend_file=$(find . -path '*/author-agent-skill/backend/pyproject.toml' -type f | sed -n '1p'); + test -n "$backend_file"; + backend=${backend_file%/pyproject.toml}; + uv run --quiet --frozen --no-dev --project "$backend" inspect-skill inspect plugins/ops/skills/deploy-checklist plugins/ops >/dev/null; ! grep -R -E '(plugins/[^/]+/skills|~/|/Users/|/home/)' plugins/ops/skills/deploy-checklist - name: configuration mechanics move to a linked local script run: | diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/inspect-skill b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/inspect-skill deleted file mode 100755 index 3c49a4e5..00000000 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/inspect-skill +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -set -eu - -skill_dir=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) -exec uv run --quiet --frozen --no-dev --project "$skill_dir/backend" inspect-skill "$@" diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/inspect-skill.test.sh b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/inspect-skill.test.sh index 1d8b27a7..0974215d 100755 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/inspect-skill.test.sh +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/inspect-skill.test.sh @@ -1,11 +1,16 @@ #!/usr/bin/env bash set -u -SCRIPT="$(cd "$(dirname "$0")" 2>/dev/null && pwd -P)/inspect-skill" +SCRIPT_DIR=$(cd "$(dirname "$0")" 2>/dev/null && pwd -P) +BACKEND=$(cd "$SCRIPT_DIR/../backend" 2>/dev/null && pwd -P) TAB=$(printf '\t') FAILURES=0 TEMPS=() +inspect_skill() { + uv run --quiet --frozen --no-dev --project "$BACKEND" inspect-skill "$@" +} + cleanup() { local directory for directory in "${TEMPS[@]}"; do @@ -64,7 +69,7 @@ expect_failure() { printf '%s\n' 'valid skill inspection' new_plugin before=$(find "$PLUGIN" -print | LC_ALL=C sort) -output=$("$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN") || fail "valid skill is accepted" +output=$(inspect_skill inspect "$SKILL" "$PLUGIN") || fail "valid skill is accepted" after=$(find "$PLUGIN" -print | LC_ALL=C sort) expect_contains "reports the stable format" "format${TAB}darrow-skill-inspection-v1" "$output" expect_contains "reports the absolute skill directory" "skill_directory${TAB}$SKILL" "$output" @@ -96,12 +101,12 @@ printf '%s\n' \ "exec \"\$REAL_MKTEMP\" \"\$@\"" \ >"$MOCK_BIN/mktemp" chmod +x "$MOCK_BIN/mktemp" -if PATH="$MOCK_BIN:$PATH" TMPDIR=$TEMP_PARENT "$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN" >/dev/null 2>&1; then +if PATH="$MOCK_BIN:$PATH" TMPDIR=$TEMP_PARENT inspect_skill inspect "$SKILL" "$PLUGIN" >/dev/null 2>&1; then pass "temporary path without trailing separator is accepted" else fail "temporary path without trailing separator is accepted" fi -if PATH="$MOCK_BIN:$PATH" TMPDIR=$TEMP_PARENT/ "$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN" >/dev/null 2>&1; then +if PATH="$MOCK_BIN:$PATH" TMPDIR=$TEMP_PARENT/ inspect_skill inspect "$SKILL" "$PLUGIN" >/dev/null 2>&1; then pass "temporary path with trailing separator is accepted" else fail "temporary path with trailing separator is accepted" @@ -110,7 +115,7 @@ fi cp "$SKILL/SKILL.md" "$SKILL/SKILL.portable" sed '/^description:/a\ disable-model-invocation: true' "$SKILL/SKILL.portable" >"$SKILL/SKILL.md" -if "$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN" >/dev/null 2>&1; then +if inspect_skill inspect "$SKILL" "$PLUGIN" >/dev/null 2>&1; then pass "disable-model-invocation true is accepted" else fail "disable-model-invocation true is accepted" @@ -119,7 +124,7 @@ cp "$SKILL/SKILL.portable" "$SKILL/SKILL.md" sed '/^description:/a\ disable-model-invocation: false' "$SKILL/SKILL.portable" >"$SKILL/SKILL.md" -if "$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN" >/dev/null 2>&1; then +if inspect_skill inspect "$SKILL" "$PLUGIN" >/dev/null 2>&1; then pass "disable-model-invocation false is accepted" else fail "disable-model-invocation false is accepted" @@ -128,24 +133,24 @@ cp "$SKILL/SKILL.portable" "$SKILL/SKILL.md" sed '/^description:/a\ disable-model-invocation: sometimes' "$SKILL/SKILL.portable" >"$SKILL/SKILL.md" -expect_failure "disable-model-invocation requires a boolean" "$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN" +expect_failure "disable-model-invocation requires a boolean" inspect_skill inspect "$SKILL" "$PLUGIN" cp "$SKILL/SKILL.portable" "$SKILL/SKILL.md" sed '/^description:/a\ disable-model-invocation: true\ disable-model-invocation: false' "$SKILL/SKILL.portable" >"$SKILL/SKILL.md" -expect_failure "disable-model-invocation cannot be duplicated" "$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN" +expect_failure "disable-model-invocation cannot be duplicated" inspect_skill inspect "$SKILL" "$PLUGIN" cp "$SKILL/SKILL.portable" "$SKILL/SKILL.md" sed '/^description:/a\ unknown-host-field: true' "$SKILL/SKILL.portable" >"$SKILL/SKILL.md" -expect_failure "unknown frontmatter remains fail-closed" "$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN" +expect_failure "unknown frontmatter remains fail-closed" inspect_skill inspect "$SKILL" "$PLUGIN" cp "$SKILL/SKILL.portable" "$SKILL/SKILL.md" printf '%s\n' 'fail-closed inputs' missing="$PLUGIN/skills/missing/SKILL.md" set +e -missing_output=$("$BASH" "$SCRIPT" inspect "$PLUGIN/skills/missing" "$PLUGIN" 2>&1) +missing_output=$(inspect_skill inspect "$PLUGIN/skills/missing" "$PLUGIN" 2>&1) missing_status=$? set -e if [ "$missing_status" -ne 0 ]; then @@ -156,7 +161,7 @@ fi expect_contains "missing error identifies an absolute path" "$missing" "$missing_output" missing_plugin="$PLUGIN/missing-plugin" set +e -plugin_output=$(cd "$PLUGIN" && "$BASH" "$SCRIPT" inspect skills/author-release-skill missing-plugin 2>&1) +plugin_output=$(cd "$PLUGIN" && inspect_skill inspect skills/author-release-skill missing-plugin 2>&1) plugin_status=$? set -e if [ "$plugin_status" -ne 0 ]; then @@ -169,11 +174,11 @@ expect_contains "plugin-root error identifies an absolute path" "$missing_plugin printf '%s\n' 'metadata and containment counterexamples' cp "$SKILL/SKILL.md" "$SKILL/SKILL.valid" sed 's/name: author-release-skill/name: different-name/' "$SKILL/SKILL.valid" >"$SKILL/SKILL.md" -expect_failure "frontmatter name must match the directory" "$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN" +expect_failure "frontmatter name must match the directory" inspect_skill inspect "$SKILL" "$PLUGIN" cp "$SKILL/SKILL.valid" "$SKILL/SKILL.md" sed 's/name: author-release-skill/name: "author-release-skill/' "$SKILL/SKILL.valid" >"$SKILL/SKILL.md" -expect_failure "frontmatter rejects an unmatched quote" "$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN" +expect_failure "frontmatter rejects an unmatched quote" inspect_skill inspect "$SKILL" "$PLUGIN" cp "$SKILL/SKILL.valid" "$SKILL/SKILL.md" EXTERNAL=$(mktemp -d) || exit 1 @@ -182,23 +187,23 @@ TEMPS[${#TEMPS[@]}]=$EXTERNAL printf '%s\n' '---' 'name: symlink-skill' 'description: Validate a symbolic-link boundary.' '---' >"$EXTERNAL/SKILL.md" mkdir -p "$PLUGIN/skills/symlink-skill" ln -s "$EXTERNAL/SKILL.md" "$PLUGIN/skills/symlink-skill/SKILL.md" -expect_failure "skill file symlink is rejected" "$BASH" "$SCRIPT" inspect "$PLUGIN/skills/symlink-skill" "$PLUGIN" +expect_failure "skill file symlink is rejected" inspect_skill inspect "$PLUGIN/skills/symlink-skill" "$PLUGIN" printf '# External policy\n' >"$EXTERNAL/policy.md" rm "$SKILL/references/policy.md" ln -s "$EXTERNAL/policy.md" "$SKILL/references/policy.md" -expect_failure "local reference symlink is rejected" "$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN" +expect_failure "local reference symlink is rejected" inspect_skill inspect "$SKILL" "$PLUGIN" rm "$SKILL/references/policy.md" printf '# Policy\n' >"$SKILL/references/policy.md" mkdir "$SKILL/scripts/private" printf '#!/usr/bin/env bash\nexit 0\n' >"$SKILL/scripts/private/hidden-check" chmod 000 "$SKILL/scripts/private" -expect_failure "unreadable nested script directory fails closed" "$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN" +expect_failure "unreadable nested script directory fails closed" inspect_skill inspect "$SKILL" "$PLUGIN" chmod 700 "$SKILL/scripts/private" printf '\nRead [external](../../../outside-the-plugin.md).\n' >>"$SKILL/SKILL.md" -expect_failure "a local reference cannot escape the plugin" "$BASH" "$SCRIPT" inspect "$SKILL" "$PLUGIN" +expect_failure "a local reference cannot escape the plugin" inspect_skill inspect "$SKILL" "$PLUGIN" printf '%s\n' 'space-bearing absolute paths' temp_parent=${TMPDIR:-/tmp} @@ -212,7 +217,7 @@ SPACE_PLUGIN=$(cd "$SPACE_PLUGIN" 2>/dev/null && pwd -P) || exit 1 TEMPS[${#TEMPS[@]}]=$SPACE_PLUGIN mkdir -p "$SPACE_PLUGIN/skills/space-skill" printf '%s\n' '---' 'name: space-skill' 'description: Inspect a skill stored below a path containing spaces.' '---' >"$SPACE_PLUGIN/skills/space-skill/SKILL.md" -space_output=$("$BASH" "$SCRIPT" inspect "$SPACE_PLUGIN/skills/space-skill" "$SPACE_PLUGIN") || fail "space-bearing path is accepted" +space_output=$(inspect_skill inspect "$SPACE_PLUGIN/skills/space-skill" "$SPACE_PLUGIN") || fail "space-bearing path is accepted" expect_contains "space-bearing path remains absolute" "skill_directory${TAB}$SPACE_PLUGIN/skills/space-skill" "$space_output" if [ "$FAILURES" -ne 0 ]; then diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/verify-shell-tests b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/verify-shell-tests deleted file mode 100755 index 224fcafb..00000000 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/verify-shell-tests +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -set -eu - -skill_dir=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) -exec uv run --quiet --frozen --no-dev --project "$skill_dir/backend" verify-shell-tests "$@" diff --git a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/verify-shell-tests.test.sh b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/verify-shell-tests.test.sh index e467a7e0..28064eae 100755 --- a/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/verify-shell-tests.test.sh +++ b/plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/scripts/verify-shell-tests.test.sh @@ -1,11 +1,16 @@ #!/usr/bin/env bash set -u -SCRIPT="$(cd "$(dirname "$0")" 2>/dev/null && pwd -P)/verify-shell-tests" +SCRIPT_DIR=$(cd "$(dirname "$0")" 2>/dev/null && pwd -P) +BACKEND=$(cd "$SCRIPT_DIR/../backend" 2>/dev/null && pwd -P) TAB=$(printf '\t') FAILURES=0 TEMPS=() +verify_shell_tests() { + uv run --quiet --frozen --no-dev --project "$BACKEND" verify-shell-tests "$@" +} + cleanup() { local directory for directory in "${TEMPS[@]}"; do @@ -79,7 +84,7 @@ printf '%s\n' '#!/usr/bin/env bash' 'exit 9' >"$FAIL_TEST" printf '%s\n' 'complete version matrix' : >"$RUN_LOG" set +e -output=$("$BASH" "$SCRIPT" --shell "$BASH_3" --shell "$BASH_5" -- "$PASS_TEST" 2>&1) +output=$(verify_shell_tests --shell "$BASH_3" --shell "$BASH_5" -- "$PASS_TEST" 2>&1) status=$? set -e expect_status "complete matrix succeeds" 0 "$status" @@ -93,7 +98,7 @@ expect_status "runs once under each distinct target" 2 "$run_count" printf '%s\n' 'duplicate interpreter evidence' : >"$RUN_LOG" set +e -output=$("$BASH" "$SCRIPT" --shell "$BASH_3" --shell "$BASH_3" --shell "$BASH_5" -- "$PASS_TEST" 2>&1) +output=$(verify_shell_tests --shell "$BASH_3" --shell "$BASH_3" --shell "$BASH_5" -- "$PASS_TEST" 2>&1) status=$? set -e expect_status "duplicate matrix succeeds" 0 "$status" @@ -103,7 +108,7 @@ expect_status "equivalent interpreter is deduplicated" 2 "$run_count" printf '%s\n' 'unavailable required version' : >"$RUN_LOG" set +e -output=$("$BASH" "$SCRIPT" --shell "$BASH_3" -- "$PASS_TEST" 2>&1) +output=$(verify_shell_tests --shell "$BASH_3" -- "$PASS_TEST" 2>&1) status=$? set -e expect_status "incomplete matrix has distinct status" 3 "$status" @@ -113,7 +118,7 @@ expect_contains "reports unverified matrix" "matrix_status${TAB}unverified" "$ou printf '%s\n' 'test failure' : >"$RUN_LOG" set +e -output=$("$BASH" "$SCRIPT" --shell "$BASH_3" --shell "$BASH_5" -- "$FAIL_TEST" 2>&1) +output=$(verify_shell_tests --shell "$BASH_3" --shell "$BASH_5" -- "$FAIL_TEST" 2>&1) status=$? set -e expect_status "test failure fails the matrix" 1 "$status" @@ -121,7 +126,7 @@ expect_contains "reports failed matrix" "matrix_status${TAB}failed" "$output" printf '%s\n' 'invalid input' set +e -output=$("$BASH" "$SCRIPT" --shell "$ROOT/missing-bash" -- "$PASS_TEST" 2>&1) +output=$(verify_shell_tests --shell "$ROOT/missing-bash" -- "$PASS_TEST" 2>&1) status=$? set -e expect_status "missing explicit interpreter fails closed" 2 "$status" From 3830692b5d3b242cdf492644217a63bc797b1f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rochel?= Date: Thu, 17 Sep 2026 15:22:27 +0200 Subject: [PATCH 08/10] ci(python): scope quality jobs to changed packages --- .github/workflows/python-quality.yml | 127 +++++++++++++++++++++----- docs/specs/python-quality.md | 15 +-- scripts/detect-python-quality-changes | 73 +++++++++++++++ scripts/test-python-quality-changes | 100 ++++++++++++++++++++ scripts/verify-python-quality-results | 51 +++++++++++ 5 files changed, 339 insertions(+), 27 deletions(-) create mode 100755 scripts/detect-python-quality-changes create mode 100755 scripts/test-python-quality-changes create mode 100755 scripts/verify-python-quality-results diff --git a/.github/workflows/python-quality.yml b/.github/workflows/python-quality.yml index 27ed8559..224a6d85 100644 --- a/.github/workflows/python-quality.yml +++ b/.github/workflows/python-quality.yml @@ -10,8 +10,45 @@ permissions: contents: read jobs: - package: - name: Python ${{ matrix.python-version }} on ${{ matrix.os }} + changes: + name: Python change scopes + runs-on: ubuntu-latest + outputs: + skill_authoring: ${{ steps.scopes.outputs.skill_authoring }} + observability_langfuse: ${{ steps.scopes.outputs.observability_langfuse }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - id: scopes + name: Select changed Python packages + env: + EVENT_NAME: ${{ github.event_name }} + BEFORE_SHA: ${{ github.event.before }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + if [ "$EVENT_NAME" = workflow_dispatch ]; then + scripts/detect-python-quality-changes --all >>"$GITHUB_OUTPUT" + exit 0 + fi + + if [ "$EVENT_NAME" = pull_request ]; then + scripts/detect-python-quality-changes "$BASE_SHA" "$HEAD_SHA" >>"$GITHUB_OUTPUT" + exit 0 + fi + + zero=0000000000000000000000000000000000000000 + if [ "$BEFORE_SHA" = "$zero" ]; then + scripts/detect-python-quality-changes --all >>"$GITHUB_OUTPUT" + else + scripts/detect-python-quality-changes "$BEFORE_SHA" "$GITHUB_SHA" >>"$GITHUB_OUTPUT" + fi + + skill-authoring-package: + name: Skill authoring Python ${{ matrix.python-version }} on ${{ matrix.os }} + needs: changes + if: needs.changes.outputs.skill_authoring == 'true' strategy: fail-fast: false matrix: @@ -28,10 +65,34 @@ jobs: - uses: astral-sh/setup-uv@v6 with: enable-cache: true - - run: scripts/check-python + - run: scripts/check-python --package plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend + + observability-package: + name: Observability Langfuse Python ${{ matrix.python-version }} on ${{ matrix.os }} + needs: changes + if: needs.changes.outputs.observability_langfuse == 'true' + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ["3.10", "3.11", "3.12", "3.13"] + runs-on: ${{ matrix.os }} + env: + UV_PYTHON: ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + - run: scripts/check-python --package plugins/capability/darrow-observability-langfuse/backend skill-authoring-windows: name: Skill authoring Python ${{ matrix.python-version }} on windows-latest + needs: changes + if: needs.changes.outputs.skill_authoring == 'true' strategy: fail-fast: false matrix: @@ -58,27 +119,23 @@ jobs: steps: - uses: actions/checkout@v4 - run: scripts/test-python-inventory + - run: scripts/test-python-quality-changes - fresh-install: - name: Fresh install ${{ matrix.plugin }} on ${{ matrix.os }} + skill-authoring-fresh-install: + name: Fresh install skill-authoring on ${{ matrix.os }} + needs: changes + if: needs.changes.outputs.skill_authoring == 'true' strategy: fail-fast: false matrix: include: - os: ubuntu-latest - plugin: observability-langfuse - shell: bash - test: plugins/capability/darrow-observability-langfuse/tests/fresh-install.test.sh - - os: ubuntu-latest - plugin: skill-authoring shell: bash test: plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.sh - os: macos-latest - plugin: skill-authoring shell: bash test: plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.sh - os: windows-latest - plugin: skill-authoring shell: pwsh test: plugins/foundation/darrow-skill-authoring/tests/fresh-install.test.ps1 runs-on: ${{ matrix.os }} @@ -94,8 +151,23 @@ jobs: shell: pwsh run: "& '${{ matrix.test }}'" + observability-fresh-install: + name: Fresh install observability-langfuse on ubuntu-latest + needs: changes + if: needs.changes.outputs.observability_langfuse == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@v6 + - run: bash plugins/capability/darrow-observability-langfuse/tests/fresh-install.test.sh + performance: name: Python performance invariants + needs: changes + if: needs.changes.outputs.observability_langfuse == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -109,19 +181,32 @@ jobs: name: Python quality if: always() needs: - [package, skill-authoring-windows, inventory, fresh-install, performance] + - changes + - skill-authoring-package + - observability-package + - skill-authoring-windows + - inventory + - skill-authoring-fresh-install + - observability-fresh-install + - performance runs-on: ubuntu-latest steps: - - name: Require every Python quality job + - name: Require every selected Python quality job env: - PACKAGE_RESULT: ${{ needs.package.result }} + CHANGES_RESULT: ${{ needs.changes.result }} + SKILL_AUTHORING_CHANGED: ${{ needs.changes.outputs.skill_authoring }} + OBSERVABILITY_CHANGED: ${{ needs.changes.outputs.observability_langfuse }} + SKILL_PACKAGE_RESULT: ${{ needs.skill-authoring-package.result }} + OBSERVABILITY_PACKAGE_RESULT: ${{ needs.observability-package.result }} WINDOWS_RESULT: ${{ needs.skill-authoring-windows.result }} INVENTORY_RESULT: ${{ needs.inventory.result }} - INSTALL_RESULT: ${{ needs.fresh-install.result }} + SKILL_INSTALL_RESULT: ${{ needs.skill-authoring-fresh-install.result }} + OBSERVABILITY_INSTALL_RESULT: ${{ needs.observability-fresh-install.result }} PERFORMANCE_RESULT: ${{ needs.performance.result }} run: | - test "$PACKAGE_RESULT" = success - test "$WINDOWS_RESULT" = success - test "$INVENTORY_RESULT" = success - test "$INSTALL_RESULT" = success - test "$PERFORMANCE_RESULT" = success + scripts/verify-python-quality-results \ + "$CHANGES_RESULT" "$INVENTORY_RESULT" \ + "$SKILL_AUTHORING_CHANGED" "$SKILL_PACKAGE_RESULT" \ + "$WINDOWS_RESULT" "$SKILL_INSTALL_RESULT" \ + "$OBSERVABILITY_CHANGED" "$OBSERVABILITY_PACKAGE_RESULT" \ + "$OBSERVABILITY_INSTALL_RESULT" "$PERFORMANCE_RESULT" diff --git a/docs/specs/python-quality.md b/docs/specs/python-quality.md index 4f2df356..1040368e 100644 --- a/docs/specs/python-quality.md +++ b/docs/specs/python-quality.md @@ -56,12 +56,15 @@ including property tests, and enforces both coverage measures. The pre-commit hook runs the fast formatting, lint, and type portion when staged Python or Python project files change. -CI runs the canonical all-package gate on Linux and macOS for every declared -Python minor version. Packages that claim native Windows support run the same -gate in a package-scoped Windows matrix. A stable aggregate `Python quality` -status is the branch-protection check. The inventory guard itself is tested by -introducing a temporary unregistered package and proving that the gate refuses -it. +CI runs the canonical package gate on Linux and macOS for every declared Python +minor version when that package or shared Python-quality infrastructure changes. +Packages that claim native Windows support run the same change-scoped gate in a +package-specific Windows matrix. Manual dispatch runs every package. The +inventory guard runs on every workflow invocation, and a stable aggregate +`Python quality` status verifies that every required scope passed or every +unchanged scope was intentionally skipped. The inventory guard itself is tested +by introducing a temporary unregistered package and proving that the gate +refuses it. ## Performance and release evidence diff --git a/scripts/detect-python-quality-changes b/scripts/detect-python-quality-changes new file mode 100755 index 00000000..31b62ea4 --- /dev/null +++ b/scripts/detect-python-quality-changes @@ -0,0 +1,73 @@ +#!/bin/sh +set -eu + +usage() { + echo "usage: scripts/detect-python-quality-changes --all | " >&2 + exit 2 +} + +emit() { + printf 'skill_authoring=%s\n' "$1" + printf 'observability_langfuse=%s\n' "$2" +} + +changed() { + set +e + git -C "$repo_root" diff --quiet "$base" "$head" -- "$@" + status=$? + set -e + case "$status" in + 0) return 1 ;; + 1) return 0 ;; + *) echo "error: unable to compare Python quality paths" >&2; exit "$status" ;; + esac +} + +if [ "$#" -eq 1 ] && [ "$1" = --all ]; then + emit true true + exit 0 +fi + +[ "$#" -eq 2 ] || usage +base=$1 +head=$2 +repo_root=$(git rev-parse --show-toplevel) + +git -C "$repo_root" cat-file -e "$base^{commit}" 2>/dev/null || { + echo "error: base commit is unavailable: $base" >&2 + exit 1 +} +git -C "$repo_root" cat-file -e "$head^{commit}" 2>/dev/null || { + echo "error: head commit is unavailable: $head" >&2 + exit 1 +} + +shared=false +if changed \ + .github/workflows/python-quality.yml \ + docs/specs/python-quality.md \ + python-packages.txt \ + scripts/check-python \ + scripts/check-python-inventory \ + scripts/detect-python-quality-changes \ + scripts/test-python-coverage \ + scripts/test-python-inventory \ + scripts/test-python-quality-changes \ + scripts/verify-python-coverage \ + scripts/verify-python-quality-results; then + shared=true +fi + +skill_authoring=$shared +if [ "$skill_authoring" = false ] && changed \ + plugins/foundation/darrow-skill-authoring; then + skill_authoring=true +fi + +observability_langfuse=$shared +if [ "$observability_langfuse" = false ] && changed \ + plugins/capability/darrow-observability-langfuse; then + observability_langfuse=true +fi + +emit "$skill_authoring" "$observability_langfuse" diff --git a/scripts/test-python-quality-changes b/scripts/test-python-quality-changes new file mode 100755 index 00000000..5961d653 --- /dev/null +++ b/scripts/test-python-quality-changes @@ -0,0 +1,100 @@ +#!/bin/sh +set -eu + +repo_root=$(git rev-parse --show-toplevel) +detector="$repo_root/scripts/detect-python-quality-changes" +verifier="$repo_root/scripts/verify-python-quality-results" +fixture=$(mktemp -d "${TMPDIR:-/tmp}/darrow-python-quality-changes.XXXXXX") +trap 'rm -rf "$fixture"' EXIT HUP INT TERM + +git -C "$fixture" init -q +git -C "$fixture" config user.email test@example.com +git -C "$fixture" config user.name "Darrow Tests" +mkdir -p "$fixture/scripts" +cp "$detector" "$fixture/scripts/detect-python-quality-changes" +chmod +x "$fixture/scripts/detect-python-quality-changes" +printf '%s\n' '# Fixture' >"$fixture/README.md" +git -C "$fixture" add . +git -C "$fixture" commit -qm "chore: init" +base=$(git -C "$fixture" rev-parse HEAD) + +expect_scope() { + path=$1 + expected=$2 + git -C "$fixture" reset -q --hard "$base" + git -C "$fixture" clean -qfdx + mkdir -p "$(dirname "$fixture/$path")" + printf '%s\n' changed >"$fixture/$path" + git -C "$fixture" add "$path" + git -C "$fixture" commit -qm "test: change $path" + actual=$(cd "$fixture" && scripts/detect-python-quality-changes "$base" HEAD) + if [ "$actual" != "$expected" ]; then + printf 'error: unexpected scopes for %s\nexpected:\n%s\nactual:\n%s\n' \ + "$path" "$expected" "$actual" >&2 + exit 1 + fi +} + +expect_scope \ + docs/note.md \ + "skill_authoring=false +observability_langfuse=false" +expect_scope \ + plugins/foundation/darrow-skill-authoring/README.md \ + "skill_authoring=true +observability_langfuse=false" +expect_scope \ + plugins/capability/darrow-observability-langfuse/README.md \ + "skill_authoring=false +observability_langfuse=true" +expect_scope \ + python-packages.txt \ + "skill_authoring=true +observability_langfuse=true" + +all=$(cd "$fixture" && scripts/detect-python-quality-changes --all) +if [ "$all" != "skill_authoring=true +observability_langfuse=true" ]; then + printf 'error: --all did not select every Python quality scope\n' >&2 + exit 1 +fi + +if (cd "$fixture" && scripts/detect-python-quality-changes only-one-ref) >/dev/null 2>&1; then + printf 'error: invalid detector invocation succeeded\n' >&2 + exit 1 +fi + +"$verifier" \ + success success \ + true success success success \ + false skipped skipped skipped +"$verifier" \ + success success \ + false skipped skipped skipped \ + true success success success + +if "$verifier" \ + success success \ + false success skipped skipped \ + false skipped skipped skipped >/dev/null 2>&1; then + printf 'error: aggregate accepted a job that ran for an unchanged scope\n' >&2 + exit 1 +fi + +if "$verifier" \ + success success \ + true success failure success \ + false skipped skipped skipped >/dev/null 2>&1; then + printf 'error: aggregate accepted a failed selected job\n' >&2 + exit 1 +fi + +if "$verifier" \ + failure success \ + true success success success \ + true success success success >/dev/null 2>&1; then + printf 'error: aggregate accepted failed change detection\n' >&2 + exit 1 +fi + +printf '%s\n' 'python quality change detector tests passed' diff --git a/scripts/verify-python-quality-results b/scripts/verify-python-quality-results new file mode 100755 index 00000000..ab65673c --- /dev/null +++ b/scripts/verify-python-quality-results @@ -0,0 +1,51 @@ +#!/bin/sh +set -eu + +if [ "$#" -ne 10 ]; then + echo "usage: scripts/verify-python-quality-results " >&2 + exit 2 +fi + +changes_result=$1 +inventory_result=$2 +skill_selected=$3 +skill_package_result=$4 +skill_windows_result=$5 +skill_install_result=$6 +observability_selected=$7 +observability_package_result=$8 +observability_install_result=$9 +shift 9 +observability_performance_result=$1 + +require_result() { + name=$1 + expected=$2 + actual=$3 + if [ "$actual" != "$expected" ]; then + echo "error: $name expected $expected, got $actual" >&2 + exit 1 + fi +} + +require_scope() { + scope=$1 + selected=$2 + shift 2 + case "$selected" in + true) expected=success ;; + false) expected=skipped ;; + *) echo "error: invalid $scope selection: $selected" >&2; exit 1 ;; + esac + for result do + require_result "$scope job" "$expected" "$result" + done +} + +require_result "change detection" success "$changes_result" +require_result "inventory" success "$inventory_result" +require_scope skill-authoring "$skill_selected" \ + "$skill_package_result" "$skill_windows_result" "$skill_install_result" +require_scope observability-langfuse "$observability_selected" \ + "$observability_package_result" "$observability_install_result" \ + "$observability_performance_result" From 28e66cb89028d7b739a784492013f6deb163172c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rochel?= Date: Thu, 17 Sep 2026 15:25:39 +0200 Subject: [PATCH 09/10] fix(ci): checkout aggregate verifier --- .github/workflows/python-quality.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-quality.yml b/.github/workflows/python-quality.yml index 224a6d85..4e1701b4 100644 --- a/.github/workflows/python-quality.yml +++ b/.github/workflows/python-quality.yml @@ -191,6 +191,7 @@ jobs: - performance runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 - name: Require every selected Python quality job env: CHANGES_RESULT: ${{ needs.changes.result }} From 4ab2c316c5e2fba6223e8235bdd54754d0261016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rochel?= Date: Thu, 17 Sep 2026 15:44:46 +0200 Subject: [PATCH 10/10] fix(ci): derive package matrix from inventory --- .github/workflows/python-quality.yml | 49 +++------- scripts/detect-python-quality-changes | 132 +++++++++++++++++++------- scripts/test-python-quality-changes | 111 ++++++++++++++++++---- scripts/verify-python-quality-results | 20 ++-- 4 files changed, 215 insertions(+), 97 deletions(-) diff --git a/.github/workflows/python-quality.yml b/.github/workflows/python-quality.yml index 4e1701b4..5db6a96a 100644 --- a/.github/workflows/python-quality.yml +++ b/.github/workflows/python-quality.yml @@ -14,6 +14,8 @@ jobs: name: Python change scopes runs-on: ubuntu-latest outputs: + packages: ${{ steps.scopes.outputs.packages }} + package_selected: ${{ steps.scopes.outputs.package_selected }} skill_authoring: ${{ steps.scopes.outputs.skill_authoring }} observability_langfuse: ${{ steps.scopes.outputs.observability_langfuse }} steps: @@ -45,39 +47,19 @@ jobs: scripts/detect-python-quality-changes "$BEFORE_SHA" "$GITHUB_SHA" >>"$GITHUB_OUTPUT" fi - skill-authoring-package: - name: Skill authoring Python ${{ matrix.python-version }} on ${{ matrix.os }} + package: + name: ${{ matrix.package }} Python ${{ matrix.python-version }} on ${{ matrix.os }} needs: changes - if: needs.changes.outputs.skill_authoring == 'true' - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest] - python-version: ["3.10", "3.11", "3.12", "3.13"] - runs-on: ${{ matrix.os }} - env: - UV_PYTHON: ${{ matrix.python-version }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - uses: astral-sh/setup-uv@v6 - with: - enable-cache: true - - run: scripts/check-python --package plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend - - observability-package: - name: Observability Langfuse Python ${{ matrix.python-version }} on ${{ matrix.os }} - needs: changes - if: needs.changes.outputs.observability_langfuse == 'true' + if: needs.changes.outputs.package_selected == 'true' strategy: fail-fast: false matrix: + package: ${{ fromJSON(needs.changes.outputs.packages) }} os: [ubuntu-latest, macos-latest] python-version: ["3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.os }} env: + PACKAGE: ${{ matrix.package }} UV_PYTHON: ${{ matrix.python-version }} steps: - uses: actions/checkout@v4 @@ -87,7 +69,7 @@ jobs: - uses: astral-sh/setup-uv@v6 with: enable-cache: true - - run: scripts/check-python --package plugins/capability/darrow-observability-langfuse/backend + - run: scripts/check-python --package "$PACKAGE" skill-authoring-windows: name: Skill authoring Python ${{ matrix.python-version }} on windows-latest @@ -182,8 +164,7 @@ jobs: if: always() needs: - changes - - skill-authoring-package - - observability-package + - package - skill-authoring-windows - inventory - skill-authoring-fresh-install @@ -195,10 +176,10 @@ jobs: - name: Require every selected Python quality job env: CHANGES_RESULT: ${{ needs.changes.result }} + PACKAGE_SELECTED: ${{ needs.changes.outputs.package_selected }} + PACKAGE_RESULT: ${{ needs.package.result }} SKILL_AUTHORING_CHANGED: ${{ needs.changes.outputs.skill_authoring }} OBSERVABILITY_CHANGED: ${{ needs.changes.outputs.observability_langfuse }} - SKILL_PACKAGE_RESULT: ${{ needs.skill-authoring-package.result }} - OBSERVABILITY_PACKAGE_RESULT: ${{ needs.observability-package.result }} WINDOWS_RESULT: ${{ needs.skill-authoring-windows.result }} INVENTORY_RESULT: ${{ needs.inventory.result }} SKILL_INSTALL_RESULT: ${{ needs.skill-authoring-fresh-install.result }} @@ -207,7 +188,7 @@ jobs: run: | scripts/verify-python-quality-results \ "$CHANGES_RESULT" "$INVENTORY_RESULT" \ - "$SKILL_AUTHORING_CHANGED" "$SKILL_PACKAGE_RESULT" \ - "$WINDOWS_RESULT" "$SKILL_INSTALL_RESULT" \ - "$OBSERVABILITY_CHANGED" "$OBSERVABILITY_PACKAGE_RESULT" \ - "$OBSERVABILITY_INSTALL_RESULT" "$PERFORMANCE_RESULT" + "$PACKAGE_SELECTED" "$PACKAGE_RESULT" \ + "$SKILL_AUTHORING_CHANGED" "$WINDOWS_RESULT" "$SKILL_INSTALL_RESULT" \ + "$OBSERVABILITY_CHANGED" "$OBSERVABILITY_INSTALL_RESULT" \ + "$PERFORMANCE_RESULT" diff --git a/scripts/detect-python-quality-changes b/scripts/detect-python-quality-changes index 31b62ea4..ab1b85e0 100755 --- a/scripts/detect-python-quality-changes +++ b/scripts/detect-python-quality-changes @@ -6,11 +6,6 @@ usage() { exit 2 } -emit() { - printf 'skill_authoring=%s\n' "$1" - printf 'observability_langfuse=%s\n' "$2" -} - changed() { set +e git -C "$repo_root" diff --quiet "$base" "$head" -- "$@" @@ -23,27 +18,73 @@ changed() { esac } -if [ "$#" -eq 1 ] && [ "$1" = --all ]; then - emit true true - exit 0 -fi +plugin_root() { + package=$1 + case "$package" in + plugins/*/*/*) + rest=${package#plugins/} + kind=${rest%%/*} + rest=${rest#*/} + name=${rest%%/*} + printf 'plugins/%s/%s\n' "$kind" "$name" + ;; + *) printf '%s\n' "$package" ;; + esac +} -[ "$#" -eq 2 ] || usage -base=$1 -head=$2 -repo_root=$(git rev-parse --show-toplevel) +append_package() { + package=$1 + if [ "$packages_json" = '[]' ]; then + packages_json="[\"$package\"]" + else + packages_json=${packages_json%]} + packages_json="$packages_json,\"$package\"]" + fi + package_selected=true -git -C "$repo_root" cat-file -e "$base^{commit}" 2>/dev/null || { - echo "error: base commit is unavailable: $base" >&2 - exit 1 + case "$package" in + plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend) + skill_authoring=true + ;; + plugins/capability/darrow-observability-langfuse/backend) + observability_langfuse=true + ;; + esac } -git -C "$repo_root" cat-file -e "$head^{commit}" 2>/dev/null || { - echo "error: head commit is unavailable: $head" >&2 + +repo_root=$(git rev-parse --show-toplevel) +inventory="$repo_root/python-packages.txt" +if [ ! -r "$inventory" ]; then + echo "error: Python package inventory is unreadable: $inventory" >&2 exit 1 -} +fi -shared=false -if changed \ +packages=$(sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' "$inventory") +if [ -z "$packages" ]; then + echo "error: Python package inventory is empty: $inventory" >&2 + exit 1 +fi + +select_all=false +if [ "$#" -eq 1 ] && [ "$1" = --all ]; then + select_all=true +elif [ "$#" -eq 2 ]; then + base=$1 + head=$2 + git -C "$repo_root" cat-file -e "$base^{commit}" 2>/dev/null || { + echo "error: base commit is unavailable: $base" >&2 + exit 1 + } + git -C "$repo_root" cat-file -e "$head^{commit}" 2>/dev/null || { + echo "error: head commit is unavailable: $head" >&2 + exit 1 + } +else + usage +fi + +shared=$select_all +if [ "$shared" = false ] && changed \ .github/workflows/python-quality.yml \ docs/specs/python-quality.md \ python-packages.txt \ @@ -58,16 +99,43 @@ if changed \ shared=true fi -skill_authoring=$shared -if [ "$skill_authoring" = false ] && changed \ - plugins/foundation/darrow-skill-authoring; then - skill_authoring=true -fi +packages_json='[]' +package_selected=false +skill_authoring=false +observability_langfuse=false +seen_packages='|' -observability_langfuse=$shared -if [ "$observability_langfuse" = false ] && changed \ - plugins/capability/darrow-observability-langfuse; then - observability_langfuse=true -fi +while IFS= read -r package; do + case "$package" in + /* | *../* | */.. | *[!A-Za-z0-9._/-]*) + echo "error: invalid Python package path in $inventory: $package" >&2 + exit 1 + ;; + esac + case "$seen_packages" in + *"|$package|"*) + echo "error: duplicate Python package in $inventory: $package" >&2 + exit 1 + ;; + esac + seen_packages="$seen_packages$package|" + + selected=$shared + if [ "$selected" = false ]; then + root=$(plugin_root "$package") + if changed "$root"; then + selected=true + fi + fi + + if [ "$selected" = true ]; then + append_package "$package" + fi +done <"$fixture/python-packages.txt" <<'EOF' +plugins/capability/darrow-observability-langfuse/backend +plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend +EOF printf '%s\n' '# Fixture' >"$fixture/README.md" git -C "$fixture" add . git -C "$fixture" commit -qm "chore: init" @@ -37,28 +41,69 @@ expect_scope() { expect_scope \ docs/note.md \ - "skill_authoring=false -observability_langfuse=false" + 'packages=[] +package_selected=false +skill_authoring=false +observability_langfuse=false' expect_scope \ plugins/foundation/darrow-skill-authoring/README.md \ - "skill_authoring=true -observability_langfuse=false" + 'packages=["plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend"] +package_selected=true +skill_authoring=true +observability_langfuse=false' expect_scope \ plugins/capability/darrow-observability-langfuse/README.md \ - "skill_authoring=false -observability_langfuse=true" + 'packages=["plugins/capability/darrow-observability-langfuse/backend"] +package_selected=true +skill_authoring=false +observability_langfuse=true' expect_scope \ - python-packages.txt \ - "skill_authoring=true -observability_langfuse=true" + scripts/check-python \ + 'packages=["plugins/capability/darrow-observability-langfuse/backend","plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend"] +package_selected=true +skill_authoring=true +observability_langfuse=true' all=$(cd "$fixture" && scripts/detect-python-quality-changes --all) -if [ "$all" != "skill_authoring=true -observability_langfuse=true" ]; then +if [ "$all" != 'packages=["plugins/capability/darrow-observability-langfuse/backend","plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend"] +package_selected=true +skill_authoring=true +observability_langfuse=true' ]; then printf 'error: --all did not select every Python quality scope\n' >&2 exit 1 fi +git -C "$fixture" reset -q --hard "$base" +mkdir -p "$fixture/plugins/capability/darrow-third/backend" +printf '%s\n' 'plugins/capability/darrow-third/backend' >>"$fixture/python-packages.txt" +printf '%s\n' '[project]' 'name = "darrow-third"' > \ + "$fixture/plugins/capability/darrow-third/backend/pyproject.toml" +git -C "$fixture" add . +git -C "$fixture" commit -qm 'test: register third Python package' +third_base=$(git -C "$fixture" rev-parse HEAD) +printf '%s\n' changed >"$fixture/plugins/capability/darrow-third/README.md" +git -C "$fixture" add . +git -C "$fixture" commit -qm 'test: change third Python package' +third=$(cd "$fixture" && scripts/detect-python-quality-changes "$third_base" HEAD) +if [ "$third" != 'packages=["plugins/capability/darrow-third/backend"] +package_selected=true +skill_authoring=false +observability_langfuse=false' ]; then + printf 'error: registered third package was not selected\nactual:\n%s\n' \ + "$third" >&2 + exit 1 +fi + +third_all=$(cd "$fixture" && scripts/detect-python-quality-changes --all) +if [ "$third_all" != 'packages=["plugins/capability/darrow-observability-langfuse/backend","plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend","plugins/capability/darrow-third/backend"] +package_selected=true +skill_authoring=true +observability_langfuse=true' ]; then + printf 'error: --all omitted a registered third package\nactual:\n%s\n' \ + "$third_all" >&2 + exit 1 +fi + if (cd "$fixture" && scripts/detect-python-quality-changes only-one-ref) >/dev/null 2>&1; then printf 'error: invalid detector invocation succeeded\n' >&2 exit 1 @@ -66,33 +111,57 @@ fi "$verifier" \ success success \ - true success success success \ - false skipped skipped skipped + true success \ + true success success \ + false skipped skipped "$verifier" \ success success \ - false skipped skipped skipped \ - true success success success + true success \ + false skipped skipped \ + true success success +"$verifier" \ + success success \ + true success \ + false skipped skipped \ + false skipped skipped +"$verifier" \ + success success \ + false skipped \ + false skipped skipped \ + false skipped skipped if "$verifier" \ success success \ - false success skipped skipped \ - false skipped skipped skipped >/dev/null 2>&1; then + false success \ + false skipped skipped \ + false skipped skipped >/dev/null 2>&1; then printf 'error: aggregate accepted a job that ran for an unchanged scope\n' >&2 exit 1 fi if "$verifier" \ success success \ - true success failure success \ - false skipped skipped skipped >/dev/null 2>&1; then + true failure \ + false skipped skipped \ + false skipped skipped >/dev/null 2>&1; then + printf 'error: aggregate accepted a failed registered-package job\n' >&2 + exit 1 +fi + +if "$verifier" \ + success success \ + true success \ + true failure success \ + false skipped skipped >/dev/null 2>&1; then printf 'error: aggregate accepted a failed selected job\n' >&2 exit 1 fi if "$verifier" \ failure success \ - true success success success \ - true success success success >/dev/null 2>&1; then + true success \ + true success success \ + true success success >/dev/null 2>&1; then printf 'error: aggregate accepted failed change detection\n' >&2 exit 1 fi diff --git a/scripts/verify-python-quality-results b/scripts/verify-python-quality-results index ab65673c..c390eb6b 100755 --- a/scripts/verify-python-quality-results +++ b/scripts/verify-python-quality-results @@ -2,18 +2,18 @@ set -eu if [ "$#" -ne 10 ]; then - echo "usage: scripts/verify-python-quality-results " >&2 + echo "usage: scripts/verify-python-quality-results " >&2 exit 2 fi changes_result=$1 inventory_result=$2 -skill_selected=$3 -skill_package_result=$4 -skill_windows_result=$5 -skill_install_result=$6 -observability_selected=$7 -observability_package_result=$8 +package_selected=$3 +package_result=$4 +skill_selected=$5 +skill_windows_result=$6 +skill_install_result=$7 +observability_selected=$8 observability_install_result=$9 shift 9 observability_performance_result=$1 @@ -44,8 +44,8 @@ require_scope() { require_result "change detection" success "$changes_result" require_result "inventory" success "$inventory_result" +require_scope packages "$package_selected" "$package_result" require_scope skill-authoring "$skill_selected" \ - "$skill_package_result" "$skill_windows_result" "$skill_install_result" + "$skill_windows_result" "$skill_install_result" require_scope observability-langfuse "$observability_selected" \ - "$observability_package_result" "$observability_install_result" \ - "$observability_performance_result" + "$observability_install_result" "$observability_performance_result"