fix: address PR review + upgrade ruff/ty so CI matches local

Review feedback:
- codex.py: Codex marketplace entries now conform to the openai/codex
  core-plugins schema — source {source: "local", path}, policy
  {installation, authentication}, and category (was missing required
  fields; flagged P1). Top-level trimmed to {name, plugins}.
- codex.py: interface.shortDescription cuts on a word boundary + ellipsis
  instead of a mid-word [:120] slice in committed .codex-plugin manifests.
- cursor.py: top-level .cursor-plugin/plugin.json displayName is now the
  short "Claude Code Workflows" instead of the long marketplace description.

Tooling (local drives CI):
- Bump ruff 0.15.14 -> 0.15.15 and ty 0.0.39 -> 0.0.40 in plugin-eval's
  uv.lock so CI installs the same versions we run locally, and reformat the
  tools/ + plugin-eval sources to ruff 0.15.15 (fixes the failing
  ruff format --check; the lock-hash change also busts CI's stale cache).

make validate STRICT=1 clean / garden 0 errors / 442 tests pass; regeneration
remains drift-free.
This commit is contained in:
Seth Hobson
2026-05-29 13:18:48 -04:00
parent 608c3840ca
commit d31e8b8fea
44 changed files with 1111 additions and 432 deletions
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-code-workflows",
"displayName": "Production-ready workflow orchestration with 83 marketplace plugins, 191 local specialized agents, and 155 local skills - optimized for granular installation and minimal token usage",
"displayName": "Claude Code Workflows",
"version": "1.7.1",
"description": "Production-ready workflow orchestration with 83 marketplace plugins, 191 local specialized agents, and 155 local skills - optimized for granular installation and minimal token usage",
"author": {
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Accessibility Compliance",
"shortDescription": "WCAG accessibility auditing, compliance validation, UI testing for screen readers, keyboard navigation, and inclusive de",
"shortDescription": "WCAG accessibility auditing, compliance validation, UI testing for screen readers, keyboard navigation, and\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Agent Teams",
"shortDescription": "Orchestrate multi-agent teams for parallel code review, hypothesis-driven debugging, and coordinated feature development",
"shortDescription": "Orchestrate multi-agent teams for parallel code review, hypothesis-driven debugging, and coordinated feature\u2026",
"category": "Coding"
}
}
@@ -9,7 +9,7 @@
"license": "MIT",
"interface": {
"displayName": "Block No Verify",
"shortDescription": "PreToolUse hook that prevents AI agents from using --no-verify, --no-gpg-sign, and other bypass flags that skip git hook",
"shortDescription": "PreToolUse hook that prevents AI agents from using --no-verify, --no-gpg-sign, and other bypass flags that skip git\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "C4 Architecture",
"shortDescription": "Comprehensive C4 architecture documentation workflow with bottom-up code analysis, component synthesis, container mappin",
"shortDescription": "Comprehensive C4 architecture documentation workflow with bottom-up code analysis, component synthesis, container\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Cloud Infrastructure",
"shortDescription": "Cloud architecture design for AWS/Azure/GCP/OCI, Kubernetes cluster configuration, Terraform infrastructure-as-code, hyb",
"shortDescription": "Cloud architecture design for AWS/Azure/GCP/OCI, Kubernetes cluster configuration, Terraform infrastructure-as-code\u2026",
"category": "Coding"
}
}
+1 -1
View File
@@ -10,7 +10,7 @@
"license": "Apache-2.0",
"interface": {
"displayName": "Conductor",
"shortDescription": "Context-Driven Development plugin that transforms Claude Code into a project management tool with structured workflow: C",
"shortDescription": "Context-Driven Development plugin that transforms Claude Code into a project management tool with structured\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Developer Essentials",
"shortDescription": "Essential developer skills including Git workflows, SQL optimization, error handling, code review, E2E testing, authenti",
"shortDescription": "Essential developer skills including Git workflows, SQL optimization, error handling, code review, E2E testing\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Documentation Standards",
"shortDescription": "HADS (Human-AI Document Standard) \u2014 semantic tagging convention for writing documentation that works efficiently for bot",
"shortDescription": "HADS (Human-AI Document Standard) \u2014 semantic tagging convention for writing documentation that works efficiently for\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Dotnet Contribution",
"shortDescription": "Comprehensive .NET backend development with C#, ASP.NET Core, Entity Framework Core, and Dapper for production-grade app",
"shortDescription": "Comprehensive .NET backend development with C#, ASP.NET Core, Entity Framework Core, and Dapper for production-grade\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Hr Legal Compliance",
"shortDescription": "HR policy documentation, legal compliance templates (GDPR/SOC2/HIPAA), employment contracts, and regulatory documentatio",
"shortDescription": "HR policy documentation, legal compliance templates (GDPR/SOC2/HIPAA), employment contracts, and regulatory\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Julia Development",
"shortDescription": "Modern Julia development with Julia 1.10+, package management, scientific computing, high-performance numerical code, an",
"shortDescription": "Modern Julia development with Julia 1.10+, package management, scientific computing, high-performance numerical\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Kubernetes Operations",
"shortDescription": "Kubernetes manifest generation, networking configuration, security policies, observability setup, GitOps workflows, and ",
"shortDescription": "Kubernetes manifest generation, networking configuration, security policies, observability setup, GitOps workflows\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Llm Application Dev",
"shortDescription": "LLM application development with LangGraph, RAG systems, vector search, and AI agent architectures for Claude 4.6 and GP",
"shortDescription": "LLM application development with LangGraph, RAG systems, vector search, and AI agent architectures for Claude 4.6\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Machine Learning Ops",
"shortDescription": "ML model training pipelines, hyperparameter tuning, model deployment automation, experiment tracking, and MLOps workflow",
"shortDescription": "ML model training pipelines, hyperparameter tuning, model deployment automation, experiment tracking, and MLOps\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Meigen Ai Design",
"shortDescription": "AI image generation with creative workflow orchestration, parallel multi-direction output, prompt engineering, and a 1,3",
"shortDescription": "AI image generation with creative workflow orchestration, parallel multi-direction output, prompt engineering, and a\u2026",
"category": "Coding"
}
}
+38 -38
View File
@@ -955,27 +955,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.15.14"
version = "0.15.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" }
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
{ url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
{ url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
{ url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
{ url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
{ url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
{ url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
{ url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
{ url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
{ url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
{ url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
{ url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
{ url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
{ url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
{ url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
]
[[package]]
@@ -1024,27 +1024,27 @@ wheels = [
[[package]]
name = "ty"
version = "0.0.39"
version = "0.0.40"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/8d/7b5c74dc287fbcb37bae9853cec13bf44717c1735298500e4aeba31579a9/ty-0.0.39.tar.gz", hash = "sha256:f750277e76a01ecd86185960eca73823c26a53c51103568d56d4d904575159fd", size = 5702365, upload-time = "2026-05-22T21:09:56.403Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/f8/a754c96967b71de8723f88be17df8738216bd382ffed229cd500b7a24d13/ty-0.0.40.tar.gz", hash = "sha256:883b53dd98f6e5b33ab1c8e1a3cd94b0f29c762ef22cdf1e86aaffb4fd711c67", size = 5726484, upload-time = "2026-05-27T17:55:43.615Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/17/9b89802c26d12d0f7a27bc25d4066d941d42891e8898f9f26499f0067e32/ty-0.0.39-py3-none-linux_armv6l.whl", hash = "sha256:c1bb7ac70f1f7d70cc6655fd96558039e4562b10f489fa49c7ebfd5fcee73ad1", size = 11360431, upload-time = "2026-05-22T21:09:18.689Z" },
{ url = "https://files.pythonhosted.org/packages/9c/c6/663ded50e823dbf9fb9d002eca46b7cb1fb2c72b744b84f22ce732a0ee0b/ty-0.0.39-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3435b64c1e59c14c9aa39c20cc018823937cd38d55db853e74d95b8f420569b0", size = 11096281, upload-time = "2026-05-22T21:09:15.383Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ae/5d38ba9a6456ff4c78d212cf464fd8b9a25d8118465197b0b2dc891c0b19/ty-0.0.39-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5f136377ce46c73677701a9e1ad730bf72f699bcec046e422eb79d0886cac3ab", size = 10529674, upload-time = "2026-05-22T21:09:46.471Z" },
{ url = "https://files.pythonhosted.org/packages/be/6f/43638cb8106445d3c8817256a0731cde9dd7b6a53ae2e881294bc1930ca3/ty-0.0.39-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36b65fb0cc17f03e851d40e210d420be94ab8bc52d041328ad1e45f616036a61", size = 11055561, upload-time = "2026-05-22T21:09:36.981Z" },
{ url = "https://files.pythonhosted.org/packages/91/17/95e62cf4458527ce78dc386eba18f8b10c3fb64cd8c9e7e59b262ff6029d/ty-0.0.39-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4967967bfadf3860ff84c3fccdbaec8edf8aa20d0d727521084733d853de6657", size = 11127185, upload-time = "2026-05-22T21:09:31.395Z" },
{ url = "https://files.pythonhosted.org/packages/4e/c0/93666c213db5c71ab1b1f1a0db5f66bf8c7c0e0b0bf59859f5da8f0b3c36/ty-0.0.39-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e10ecb1297099ddf9a1f054f8bd921d1863ce85fb819a3c96ed27865a1ba6ed", size = 11608459, upload-time = "2026-05-22T21:09:12.862Z" },
{ url = "https://files.pythonhosted.org/packages/79/85/3b26585afc8b50230d6464bb0642feef4fab3f847e38b1f0ffa971a81446/ty-0.0.39-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b19cca70e465d71b0510656343883d62372bbe74b7845cae7c0e701d6d5264b", size = 12177101, upload-time = "2026-05-22T21:09:40.519Z" },
{ url = "https://files.pythonhosted.org/packages/49/4a/1039e4f6afc576dc1c3a4d22a6478904a1ad3766597cd0b93c077ab9dfce/ty-0.0.39-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:56c6704b01b9b3d80ff26b2918423b742516d1e469bef830e9254dcedc9185bf", size = 11827815, upload-time = "2026-05-22T21:09:49.89Z" },
{ url = "https://files.pythonhosted.org/packages/e2/c5/4688652870e350a76a8157f7ffb59ad54f37d5d10725aa7076f66ac94ec8/ty-0.0.39-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b7840ff46764b6a6757f4ade1cd0530fc3e8a0b435ca93e7602360e4cb90b6", size = 11694429, upload-time = "2026-05-22T21:09:21.568Z" },
{ url = "https://files.pythonhosted.org/packages/fc/72/8a1c4e823bb5bdc935a1c8140e100304e36a68a4139592f170aa9736fdb7/ty-0.0.39-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1c62a3a87ce26b50819f0dbf03bd95f23f19eeb87bbc7aa732ec64277c77f1aa", size = 11869846, upload-time = "2026-05-22T21:09:28.053Z" },
{ url = "https://files.pythonhosted.org/packages/17/9f/cf982457b861ae22d657c5dcdbc631199f7f90264279db1d17230dfbc3ff/ty-0.0.39-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f8c34bc81a9c3516e49904e9d8330aac385377cca98390193ea02b903a40fcf0", size = 11029763, upload-time = "2026-05-22T21:09:06.791Z" },
{ url = "https://files.pythonhosted.org/packages/46/c9/95b64f6d43ae6e8f0b7e13dacf9c196d35819af22b1924171fba31383156/ty-0.0.39-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:66f5ab11586a64e79cb692ad685ee5469325c31b5f30bd3554f52f36dbe28cc4", size = 11146761, upload-time = "2026-05-22T21:09:10.178Z" },
{ url = "https://files.pythonhosted.org/packages/52/69/0a89cfb06f7632a05bf56c78e0affb4a40f81759e275376cea75c9c5abe9/ty-0.0.39-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e8d89732bcbbcb091f439e556dfc4932f198b118b47d5b85212c60662099670e", size = 11281843, upload-time = "2026-05-22T21:09:34.234Z" },
{ url = "https://files.pythonhosted.org/packages/0e/53/64c4a27067a46643fea2b3fcf21a8a2f838d91a65ffdd14f2e82945b9538/ty-0.0.39-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:eceb6c91dcd05a231119f82abdd9aa337513de23ca6ac990bc44f88791dc1799", size = 11792477, upload-time = "2026-05-22T21:09:24.923Z" },
{ url = "https://files.pythonhosted.org/packages/1a/e8/02f4dd4a12bcdbda0006f9c7ff3b99a4be06bd0d257d3bd4a5b66de074e6/ty-0.0.39-py3-none-win32.whl", hash = "sha256:891c3262314dbc80bf3e872634d23dd216306945daa9a9fcc206ce5ed21ac4c9", size = 10615377, upload-time = "2026-05-22T21:09:43.167Z" },
{ url = "https://files.pythonhosted.org/packages/b5/5a/aaeb22faa8d4dae90a287d4c3636c671edcff3b99be5f4fc8b79ad71eef6/ty-0.0.39-py3-none-win_amd64.whl", hash = "sha256:ba7f2d54452535419e90f6f03ff39282999e87b43c21c00559f6d7ad711a36d5", size = 11710711, upload-time = "2026-05-22T21:09:53.179Z" },
{ url = "https://files.pythonhosted.org/packages/a3/17/ae7339651bfcaa5f54698c8c70eaf5031baa400ecb67baec31d03a56cbd4/ty-0.0.39-py3-none-win_arm64.whl", hash = "sha256:eb4cf0fefbbfedf9a352597bb2431ebdcb7eb3a595c0f825f228e897a0ec285d", size = 11081409, upload-time = "2026-05-22T21:09:03.741Z" },
{ url = "https://files.pythonhosted.org/packages/2c/42/d029a72165ad39f95228b67355927fbd35c821dc8e3e475d49f47c2eeb1e/ty-0.0.40-py3-none-linux_armv6l.whl", hash = "sha256:9defb4742450e569a6a09de286a04008d6c2e815112da4362c88b6eaa2f52a36", size = 11406372, upload-time = "2026-05-27T17:55:49.633Z" },
{ url = "https://files.pythonhosted.org/packages/23/99/7f8ea09b7e49afbf795cb3341a3217f30f228db7e62a2268ed8cbbf813d6/ty-0.0.40-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:868258a3330db88b683fcafe2c4e936d6226a6312799bf15b585d93557b2d38c", size = 11159782, upload-time = "2026-05-27T17:55:47.405Z" },
{ url = "https://files.pythonhosted.org/packages/04/d8/1ea745ee97a98b26ae9564d19a430a76a35297cd450e84dcaad22e1f7ee8/ty-0.0.40-py3-none-macosx_11_0_arm64.whl", hash = "sha256:589c81060cf1e7a9ffa2f45bfa35ffd9b9fbd214104e3f13959f113627efcd91", size = 10594139, upload-time = "2026-05-27T17:55:37.206Z" },
{ url = "https://files.pythonhosted.org/packages/39/1a/fbef21273c6617ff4715b4827ee1c0b6550aa7d1df4b8c43b325545c1cf4/ty-0.0.40-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b06108990cb338d941c315ae6e9ba2fff8f518bc15d3f33e5619ff6a6c9beab", size = 11114156, upload-time = "2026-05-27T17:55:56.11Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f9/389fc4976d7ec016a7473cf1274bf9c4f491bb54c66649bd022bff9f2b6a/ty-0.0.40-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3913ef37336bec4f96bd2512f8c3a543ca34c259b7170f7eb5adf75b3ed7f04c", size = 11189050, upload-time = "2026-05-27T17:55:54.099Z" },
{ url = "https://files.pythonhosted.org/packages/fa/a9/4ecabbf4bdda7df0d99d8d3892c6edac0efc8c4cae756a5109178a3d0e86/ty-0.0.40-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fd1486bd5fe48779a8aa857137f3642a0a9161f5cf57d4380f4a0ecea01c8f3", size = 11664266, upload-time = "2026-05-27T17:55:28.17Z" },
{ url = "https://files.pythonhosted.org/packages/45/02/0aa78730116507c265afb1d6d5961c583b49d4c2e368c4a49fd81bcae6dc/ty-0.0.40-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1668364d5254a734329917ee66c2c5fdd5665389d41043f6fce0f22ddb32b749", size = 12187743, upload-time = "2026-05-27T17:56:04.337Z" },
{ url = "https://files.pythonhosted.org/packages/e6/68/ccabf2d173523598271a385c1d3f864dbda23e5ebdc67f5969b9e830ea05/ty-0.0.40-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f77a73edb91e5dfa2ab9af7c4cac64614f8cc121f38a8875f22e830d3aba6a", size = 11862999, upload-time = "2026-05-27T17:55:58.087Z" },
{ url = "https://files.pythonhosted.org/packages/03/8d/6d7ec22771bb23d534797cdb446eb644bccfe7a62b729bb99e7235a02fc3/ty-0.0.40-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1274ce0212ecbfed01bda7c3659c46e8bd0068e32d00c46c790466a95274c3df", size = 11743896, upload-time = "2026-05-27T17:56:00.017Z" },
{ url = "https://files.pythonhosted.org/packages/cd/a4/f9fa076b010c91cb249b1fcc3476569b7b8462cb4b688da2d04c23a0622f/ty-0.0.40-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5ee1261dbc363e5cc1a0c5bb0c8612c192bfe53491214df8bc85a540835685f9", size = 11883581, upload-time = "2026-05-27T17:56:02.319Z" },
{ url = "https://files.pythonhosted.org/packages/fd/0f/5b776a2328c756d574dd4d6afbd30fc24e1ab4b76935c7c3c23f27ebbcb9/ty-0.0.40-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6220e2cd5cdc4683dd87fb150d195bbd9f1a021395e04cb08bd3c66ea6da6ef8", size = 11093946, upload-time = "2026-05-27T17:55:33.284Z" },
{ url = "https://files.pythonhosted.org/packages/64/c4/eb23154bae83ad7c2935e9e5916660fb3e31598a92ee232aebd79410480c/ty-0.0.40-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:46b9ed69d01d98ef046afac9983c68336f572605ea2a27b90fbe6f80bfc8d6b7", size = 11210737, upload-time = "2026-05-27T17:55:45.523Z" },
{ url = "https://files.pythonhosted.org/packages/ff/19/1fb2529703f708cacfd13a89f98613cae2907dfa941b26976467e6119803/ty-0.0.40-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ddbca9fab4406260f141674ab5efcfe7b02bd468e6985e4cdde0a21626e69ffe", size = 11332563, upload-time = "2026-05-27T17:55:41.674Z" },
{ url = "https://files.pythonhosted.org/packages/87/69/b3f5a8ef26c31204e0391147b3adcdb0674eda3e7d99868478ef168a41c6/ty-0.0.40-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1fcc082a749e6dc11b68fe9aab0420238bbf2a2374c2c7aa3c22e8c1618b136", size = 11843216, upload-time = "2026-05-27T17:55:35.367Z" },
{ url = "https://files.pythonhosted.org/packages/ac/e8/20193069d32787f3e1a6ec8940aaa3759d3de8f48f9281bcc0c5cb0939da/ty-0.0.40-py3-none-win32.whl", hash = "sha256:75feb115b3587824c5bdf8f8305e9547b0d1e398e3077b0addc7a1988ea9bb50", size = 10670731, upload-time = "2026-05-27T17:55:31.316Z" },
{ url = "https://files.pythonhosted.org/packages/a3/f9/8b2aa4da61db81322d4a2f9db227afeb48110ca15ae31d380f64c64ceb63/ty-0.0.40-py3-none-win_amd64.whl", hash = "sha256:b0f905edaad788bd61f779a85801b60a267a25ed57fca05aaddd168d9d8896be", size = 11766211, upload-time = "2026-05-27T17:55:51.898Z" },
{ url = "https://files.pythonhosted.org/packages/04/87/369056ed46f1b235130ec0595393262f9cd2061ca3dab276d490980f9343/ty-0.0.40-py3-none-win_arm64.whl", hash = "sha256:07da2b09d9130e2c9a257d2a29beb53105835b0256ee5fdb288fe1aab83fee47", size = 11117369, upload-time = "2026-05-27T17:55:39.329Z" },
]
[[package]]
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Protect Mcp",
"shortDescription": "Cedar policy enforcement + Ed25519 signed receipts for every Claude Code tool call. First cryptographic governance plugi",
"shortDescription": "Cedar policy enforcement + Ed25519 signed receipts for every Claude Code tool call. First cryptographic governance\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Reverse Engineering",
"shortDescription": "Binary reverse engineering, malware analysis, firmware security, and software protection research for authorized securit",
"shortDescription": "Binary reverse engineering, malware analysis, firmware security, and software protection research for authorized\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Review Agent Governance",
"shortDescription": "Require a human approval signal before an AI agent can post PR reviews, comments, merges, or writes to CI config. Cedar-",
"shortDescription": "Require a human approval signal before an AI agent can post PR reviews, comments, merges, or writes to CI config\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Security Scanning",
"shortDescription": "SAST analysis, dependency vulnerability scanning, OWASP Top 10 compliance, container security scanning, and automated se",
"shortDescription": "SAST analysis, dependency vulnerability scanning, OWASP Top 10 compliance, container security scanning, and\u2026",
"category": "Coding"
}
}
+1 -1
View File
@@ -21,7 +21,7 @@
],
"interface": {
"displayName": "Ship Mate",
"shortDescription": "Your AI development teammate. Turns a story file into a shipped, reviewed, and tested feature via orchestrator \u2192 archite",
"shortDescription": "Your AI development teammate. Turns a story file into a shipped, reviewed, and tested feature via orchestrator \u2192\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Signed Audit Trails",
"shortDescription": "Teaching skill: signed audit trails for Claude Code tool calls. Cookbook-style walkthrough of Cedar-gated tool calls wit",
"shortDescription": "Teaching skill: signed audit trails for Claude Code tool calls. Cookbook-style walkthrough of Cedar-gated tool calls\u2026",
"category": "Coding"
}
}
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Startup Business Analyst",
"shortDescription": "Comprehensive startup business analysis with market sizing (TAM/SAM/SOM), financial modeling, team planning, and strateg",
"shortDescription": "Comprehensive startup business analysis with market sizing (TAM/SAM/SOM), financial modeling, team planning, and\u2026",
"category": "Coding"
}
}
+1 -1
View File
@@ -10,7 +10,7 @@
"license": "MIT",
"interface": {
"displayName": "Ui Design",
"shortDescription": "Comprehensive UI/UX design plugin for mobile (iOS, Android, React Native) and web applications with design systems, acce",
"shortDescription": "Comprehensive UI/UX design plugin for mobile (iOS, Android, React Native) and web applications with design systems\u2026",
"category": "Coding"
}
}
+23 -7
View File
@@ -99,7 +99,9 @@ def parse_frontmatter(content: str) -> tuple[dict, str]:
text = line.strip()
if text:
existing = fields.get(current_key) or ""
fields[current_key] = (existing + " " + text).strip() if existing else text
fields[current_key] = (
(existing + " " + text).strip() if existing else text
)
elif in_list and (
isinstance(fields.get(current_key), list)
or (isinstance(fields.get(current_key), str) and fields[current_key] == "")
@@ -123,7 +125,11 @@ def parse_frontmatter(content: str) -> tuple[dict, str]:
item = stripped.strip('",[] ')
if item and item != "]":
fields[current_key].append(item)
elif current_key and isinstance(fields.get(current_key), str) and line.startswith(" "):
elif (
current_key
and isinstance(fields.get(current_key), str)
and line.startswith(" ")
):
fields[current_key] += " " + line.strip().strip('"')
return fields, body
@@ -341,7 +347,9 @@ def load_plugin(plugin_name: str) -> PluginSource | None:
for md in sorted(agents_dir.glob("*.md")):
fm, body = parse_frontmatter(read_file(md))
plugin.agents.append(
AgentSource(plugin=plugin_name, name=md.stem, path=md, frontmatter=fm, body=body)
AgentSource(
plugin=plugin_name, name=md.stem, path=md, frontmatter=fm, body=body
)
)
skills_dir = plugin_dir / "skills"
@@ -352,7 +360,9 @@ def load_plugin(plugin_name: str) -> PluginSource | None:
continue
fm, body = parse_frontmatter(read_file(skill_file))
plugin.skills.append(
SkillSource(plugin=plugin_name, name=sd.name, dir=sd, frontmatter=fm, body=body)
SkillSource(
plugin=plugin_name, name=sd.name, dir=sd, frontmatter=fm, body=body
)
)
commands_dir = plugin_dir / "commands"
@@ -360,7 +370,9 @@ def load_plugin(plugin_name: str) -> PluginSource | None:
for md in sorted(commands_dir.glob("*.md")):
fm, body = parse_frontmatter(read_file(md))
plugin.commands.append(
CommandSource(plugin=plugin_name, name=md.stem, path=md, frontmatter=fm, body=body)
CommandSource(
plugin=plugin_name, name=md.stem, path=md, frontmatter=fm, body=body
)
)
return plugin
@@ -428,7 +440,9 @@ class HarnessAdapter(ABC):
target = (self.output_root / rel_path).resolve()
root = self.output_root.resolve()
if not target.is_relative_to(root):
raise ValueError(f"refusing to write outside output_root: {target} (root={root})")
raise ValueError(
f"refusing to write outside output_root: {target} (root={root})"
)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
return target
@@ -438,7 +452,9 @@ class HarnessAdapter(ABC):
target = (self.output_root / rel_path).resolve()
root = self.output_root.resolve()
if not target.is_relative_to(root):
raise ValueError(f"refusing to write outside output_root: {target} (root={root})")
raise ValueError(
f"refusing to write outside output_root: {target} (root={root})"
)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(content)
return target
+46 -26
View File
@@ -439,7 +439,9 @@ class CodexAdapter(HarnessAdapter):
# ── Internals ──────────────────────────────────────────────────────────
def _emit_skill(self, plugin: PluginSource, skill: SkillSource, result: EmitResult) -> None:
def _emit_skill(
self, plugin: PluginSource, skill: SkillSource, result: EmitResult
) -> None:
skill_id = f"{plugin.name}__{skill.name}"
skill_dir = Path(".codex") / "skills" / skill_id
@@ -453,7 +455,8 @@ class CodexAdapter(HarnessAdapter):
# If source already has references/details.md, route overflow to _overflow.md
# so the source mirror pass below doesn't clobber it.
source_has_details = (
skill.references_dir is not None and (skill.references_dir / "details.md").is_file()
skill.references_dir is not None
and (skill.references_dir / "details.md").is_file()
)
overflow_rel = "_overflow.md" if source_has_details else "details.md"
result.warnings.append(
@@ -461,7 +464,9 @@ class CodexAdapter(HarnessAdapter):
f"split into references/{overflow_rel}"
)
result.written.append(
self.write(skill_dir / "references" / overflow_rel, overflow.rstrip() + "\n")
self.write(
skill_dir / "references" / overflow_rel, overflow.rstrip() + "\n"
)
)
content = _frontmatter_block(fm) + "\n\n" + head
@@ -476,9 +481,13 @@ class CodexAdapter(HarnessAdapter):
if not ref.is_file():
continue
rel = ref.relative_to(skill.references_dir)
result.written.append(self.mirror_file(ref, skill_dir / "references" / rel))
result.written.append(
self.mirror_file(ref, skill_dir / "references" / rel)
)
def _emit_agent(self, plugin: PluginSource, agent: AgentSource, result: EmitResult) -> None:
def _emit_agent(
self, plugin: PluginSource, agent: AgentSource, result: EmitResult
) -> None:
agent_id = f"{plugin.name}__{agent.name}"
rel = Path(".codex") / "agents" / f"{agent_id}.toml"
@@ -508,7 +517,9 @@ class CodexAdapter(HarnessAdapter):
lines = [
_toml_kv("name", agent_id),
_toml_kv("description", agent.description or f"{agent.name} (from {plugin.name})"),
_toml_kv(
"description", agent.description or f"{agent.name} (from {plugin.name})"
),
_toml_kv("model", model),
_toml_kv("sandbox_mode", sandbox_mode),
_toml_kv("developer_instructions", developer_instructions),
@@ -549,7 +560,8 @@ class CodexAdapter(HarnessAdapter):
fm = {
"name": skill_id,
"description": cmd.description or f"Command: {cmd.name} (from {plugin.name})",
"description": cmd.description
or f"Command: {cmd.name} (from {plugin.name})",
}
body = _rewrite_body_for_codex(cmd.body).rstrip() + "\n"
head, overflow = _split_body_if_oversized(body, self.SKILL_BODY_CAP)
@@ -558,7 +570,9 @@ class CodexAdapter(HarnessAdapter):
f"command-skill `{skill_id}` body exceeded {self.SKILL_BODY_CAP}B; split into references/details.md"
)
result.written.append(
self.write(skill_dir / "references" / "details.md", overflow.rstrip() + "\n")
self.write(
skill_dir / "references" / "details.md", overflow.rstrip() + "\n"
)
)
if cmd.argument_hint:
fm["metadata"] = {"argument-hint": cmd.argument_hint}
@@ -568,7 +582,9 @@ class CodexAdapter(HarnessAdapter):
# ── Marketplace manifests (native install; reads source skills) ──────────
def _emit_codex_plugin_manifest(self, plugin: PluginSource, result: EmitResult) -> None:
def _emit_codex_plugin_manifest(
self, plugin: PluginSource, result: EmitResult
) -> None:
"""Write `plugins/<plugin>/.codex-plugin/plugin.json`, next to `.claude-plugin/`.
The Codex marketplace installs the SOURCE plugin directory directly
@@ -601,36 +617,40 @@ class CodexAdapter(HarnessAdapter):
for key in ("homepage", "repository", "license", "keywords"):
if pj.get(key):
manifest[key] = pj[key]
desc = plugin.description or plugin.name
if len(desc) > 120:
# Cut on a word boundary so committed manifests never render mid-word.
desc = desc[:117].rsplit(" ", 1)[0].rstrip(" ,.;:!?-") + ""
manifest["interface"] = {
"displayName": plugin.name.replace("-", " ").title(),
"shortDescription": (plugin.description or plugin.name)[:120],
"shortDescription": desc,
"category": pj.get("category") or "Coding",
}
return manifest
def _codex_marketplace(self, plugins: list[PluginSource]) -> dict:
"""`.agents/plugins/marketplace.json` — entries install the source plugin dirs."""
"""`.agents/plugins/marketplace.json` — entries install the source plugin dirs.
Schema per openai/codex `core-plugins`: each entry needs `name`, a `source`
(local variant `{"source": "local", "path": ...}`), a `policy`
(`installation`/`authentication`), and `category`. Entries install the source
plugin dir directly; per-plugin display metadata comes from each plugin's own
`.codex-plugin/plugin.json`, so we don't repeat version/description here.
"""
root = self._read_marketplace_root()
owner = root.get("owner")
if not isinstance(owner, dict):
owner = {"name": "Unknown", "email": ""}
entries = []
for p in plugins:
pj = p.plugin_json or {}
entry: dict = {
"name": p.name,
"source": {"path": f"./plugins/{p.name}"},
"version": p.version,
}
if p.description:
entry["description"] = p.description
if pj.get("category"):
entry["category"] = pj["category"]
entries.append(entry)
entries.append(
{
"name": p.name,
"source": {"source": "local", "path": f"./plugins/{p.name}"},
"policy": {"installation": "AVAILABLE", "authentication": "ON_USE"},
"category": pj.get("category") or "Coding",
}
)
return {
"name": root.get("name", "claude-code-workflows"),
"owner": owner,
"metadata": root.get("metadata", {}),
"plugins": entries,
}
+20 -7
View File
@@ -81,7 +81,9 @@ class CopilotAdapter(HarnessAdapter):
harness_id = "copilot"
def __init__(self, output_root: Path | None = None, repo_root: Path | None = None) -> None:
def __init__(
self, output_root: Path | None = None, repo_root: Path | None = None
) -> None:
"""Set output root (defaults to WORKTREE) and optional repo root."""
super().__init__(output_root=output_root)
if repo_root is not None:
@@ -106,7 +108,9 @@ class CopilotAdapter(HarnessAdapter):
"""No cross-plugin artifacts needed for Copilot."""
return EmitResult()
def _emit_agent(self, plugin: PluginSource, agent: AgentSource, result: EmitResult) -> None:
def _emit_agent(
self, plugin: PluginSource, agent: AgentSource, result: EmitResult
) -> None:
"""Emit one .agent.md profile into the agents/ directory.
Builds frontmatter (name, description, model, tools), rewrites tool
@@ -134,7 +138,9 @@ class CopilotAdapter(HarnessAdapter):
content = _copilot_frontmatter(fm) + "\n\n" + body
result.written.append(self.write(rel, content))
def _emit_skill(self, plugin: PluginSource, skill: SkillSource, result: EmitResult) -> None:
def _emit_skill(
self, plugin: PluginSource, skill: SkillSource, result: EmitResult
) -> None:
"""Emit one SKILL.md into the skills/ directory.
Preserves the source skill's frontmatter (name, description, trigger
@@ -184,13 +190,20 @@ class CopilotAdapter(HarnessAdapter):
def _emit_command_index(self, plugin: PluginSource, result: EmitResult) -> None:
"""Emit a plugin entrypoint command that points at the plugin's subcommands."""
command_names = (
", ".join(f"`/{plugin.name}:{cmd.name}`" for cmd in plugin.commands) or "none"
", ".join(f"`/{plugin.name}:{cmd.name}`" for cmd in plugin.commands)
or "none"
)
agent_names = ", ".join(
f"`{plugin.name}__{agent.name}`" for agent in plugin.agents
)
skill_names = ", ".join(
f"`{plugin.name}__{skill.name}`" for skill in plugin.skills
)
agent_names = ", ".join(f"`{plugin.name}__{agent.name}`" for agent in plugin.agents)
skill_names = ", ".join(f"`{plugin.name}__{skill.name}`" for skill in plugin.skills)
parts = [
(plugin.description or f"{plugin.name.replace('-', ' ').title()} plugin").rstrip(".")
(
plugin.description or f"{plugin.name.replace('-', ' ').title()} plugin"
).rstrip(".")
+ ".",
"",
f"This is the entry point for the `{plugin.name}` plugin.",
+5 -3
View File
@@ -56,7 +56,10 @@ def _normalize_author(author) -> dict | None:
m = _AUTHOR_STRING_RE.match(author.strip())
if not m:
return {"name": author.strip(), "email": ""}
return {"name": m.group("name").strip(), "email": (m.group("email") or "").strip()}
return {
"name": m.group("name").strip(),
"email": (m.group("email") or "").strip(),
}
if isinstance(author, (list, tuple)) and author:
# Multi-author lists are legal in npm; Cursor wants a single author so pick the first.
return _normalize_author(author[0])
@@ -163,8 +166,7 @@ class CursorAdapter(HarnessAdapter):
if marketplace["plugins"]:
top_manifest = {
"name": marketplace["name"],
"displayName": marketplace.get("metadata", {}).get("description")
or marketplace["name"],
"displayName": marketplace["name"].replace("-", " ").title(),
"version": marketplace.get("metadata", {}).get("version", "0.0.0"),
"description": marketplace.get("metadata", {}).get("description", ""),
"author": {
+18 -6
View File
@@ -79,7 +79,9 @@ class GeminiAdapter(HarnessAdapter):
# ── Internals ──────────────────────────────────────────────────────────
def _emit_skill(self, plugin: PluginSource, skill: SkillSource, result: EmitResult) -> None:
def _emit_skill(
self, plugin: PluginSource, skill: SkillSource, result: EmitResult
) -> None:
"""Mirror skill to skills/<plugin>__<skill>/SKILL.md at extension root.
Gemini CLI auto-discovers these paths (Dec 2025 SKILL.md spec).
@@ -97,9 +99,13 @@ class GeminiAdapter(HarnessAdapter):
for ref in sorted(skill.references_dir.rglob("*")):
if ref.is_file():
rel = ref.relative_to(skill.references_dir)
result.written.append(self.mirror_file(ref, rel_dir / "references" / rel))
result.written.append(
self.mirror_file(ref, rel_dir / "references" / rel)
)
def _emit_agent(self, plugin: PluginSource, agent: AgentSource, result: EmitResult) -> None:
def _emit_agent(
self, plugin: PluginSource, agent: AgentSource, result: EmitResult
) -> None:
"""Emit a Gemini subagent (April 2026 spec) at agents/<plugin>__<agent>.md."""
agent_id = f"{plugin.name}__{agent.name}"
rel = Path("agents") / f"{agent_id}.md"
@@ -121,7 +127,9 @@ class GeminiAdapter(HarnessAdapter):
content = _gemini_frontmatter(fm) + "\n\n" + agent.body.rstrip() + "\n"
result.written.append(self.write(rel, content))
def _emit_command(self, plugin: PluginSource, cmd: CommandSource, result: EmitResult) -> None:
def _emit_command(
self, plugin: PluginSource, cmd: CommandSource, result: EmitResult
) -> None:
"""Emit a Gemini TOML command at commands/<plugin>/<command>.toml.
Use `@{path}` file injection for large bodies, inline for small ones (the Google
@@ -137,11 +145,15 @@ class GeminiAdapter(HarnessAdapter):
else:
prompt = self._inject_command_prompt(plugin, cmd, description)
result.written.append(self.write(rel, _generate_command_toml(description, prompt)))
result.written.append(
self.write(rel, _generate_command_toml(description, prompt))
)
def _emit_plugin_entry(self, plugin: PluginSource, result: EmitResult) -> None:
"""Top-level commands/<plugin>.toml — Gemini exposes this as `/<plugin>`."""
description = plugin.description or f"{plugin.name.replace('-', ' ').title()} plugin"
description = (
plugin.description or f"{plugin.name.replace('-', ' ').title()} plugin"
)
rel = Path("commands") / f"{plugin.name}.toml"
agent_list = ", ".join(f"`{plugin.name}__{a.name}`" for a in plugin.agents)
+15 -5
View File
@@ -189,12 +189,16 @@ class OpenCodeAdapter(HarnessAdapter):
config = {
"$schema": "https://opencode.ai/config.json",
}
result.written.append(self.write("opencode.json", json.dumps(config, indent=2) + "\n"))
result.written.append(
self.write("opencode.json", json.dumps(config, indent=2) + "\n")
)
return result
# ── Internals ──────────────────────────────────────────────────────────
def _emit_skill(self, plugin: PluginSource, skill: SkillSource, result: EmitResult) -> None:
def _emit_skill(
self, plugin: PluginSource, skill: SkillSource, result: EmitResult
) -> None:
skill_id = _opencode_skill_id(plugin, skill)
source_id = f"{plugin.name}/{skill.name}"
existing_source = self._seen_skill_ids.get(skill_id)
@@ -221,7 +225,9 @@ class OpenCodeAdapter(HarnessAdapter):
rel = src.relative_to(skill.dir)
result.written.append(self.mirror_file(src, skill_dir / rel))
def _emit_agent(self, plugin: PluginSource, agent: AgentSource, result: EmitResult) -> None:
def _emit_agent(
self, plugin: PluginSource, agent: AgentSource, result: EmitResult
) -> None:
agent_id = f"{plugin.name}__{agent.name}"
rel = Path(".opencode") / "agents" / f"{agent_id}.md"
@@ -237,7 +243,9 @@ class OpenCodeAdapter(HarnessAdapter):
}
has_tools_field = "tools" in agent.frontmatter
permission = _build_permission_block(agent.tools, has_tools_field=has_tools_field)
permission = _build_permission_block(
agent.tools, has_tools_field=has_tools_field
)
if permission:
fm["permission"] = permission
@@ -245,7 +253,9 @@ class OpenCodeAdapter(HarnessAdapter):
content = _opencode_frontmatter(fm) + "\n\n" + body
result.written.append(self.write(rel, content))
def _emit_command(self, plugin: PluginSource, cmd: CommandSource, result: EmitResult) -> None:
def _emit_command(
self, plugin: PluginSource, cmd: CommandSource, result: EmitResult
) -> None:
cmd_id = f"{plugin.name}__{cmd.name}"
rel = Path(".opencode") / "commands" / f"{cmd_id}.md"
+19 -8
View File
@@ -59,9 +59,7 @@ class Finding:
rel = self.path.relative_to(WORKTREE)
except ValueError:
rel = self.path
return (
f"[{self.severity:7}] {self.kind:24} {rel}: {self.message}\n Fix: {self.fix}"
)
return f"[{self.severity:7}] {self.kind:24} {rel}: {self.message}\n Fix: {self.fix}"
@dataclass
@@ -122,7 +120,12 @@ def check_stale_artifacts(report: Report) -> None:
pairs.append((real_skill_src, skill_md))
continue
if leaf.endswith("__command"):
src = PLUGINS_DIR / plugin / "commands" / f"{leaf[: -len('__command')]}.md"
src = (
PLUGINS_DIR
/ plugin
/ "commands"
/ f"{leaf[: -len('__command')]}.md"
)
elif leaf.endswith("__cmd"):
# Second-order collision suffix
src = PLUGINS_DIR / plugin / "commands" / f"{leaf[: -len('__cmd')]}.md"
@@ -396,15 +399,21 @@ CHECKS = {
def main() -> int:
parser = argparse.ArgumentParser(description="Recurring drift detection (doc-gardener).")
parser.add_argument("--strict", action="store_true", help="Exit nonzero on any finding.")
parser = argparse.ArgumentParser(
description="Recurring drift detection (doc-gardener)."
)
parser.add_argument(
"--strict", action="store_true", help="Exit nonzero on any finding."
)
parser.add_argument(
"--check",
choices=list(CHECKS.keys()),
action="append",
help="Run only the named check (repeat for multiple). Default: all.",
)
parser.add_argument("--quiet", action="store_true", help="Only print findings, no summary.")
parser.add_argument(
"--quiet", action="store_true", help="Only print findings, no summary."
)
args = parser.parse_args()
selected = args.check or list(CHECKS.keys())
@@ -446,7 +455,9 @@ def main() -> int:
warnings = report.by_severity("warning")
infos = report.by_severity("info")
print()
print(f"Totals: {len(errors)} error(s), {len(warnings)} warning(s), {len(infos)} info.")
print(
f"Totals: {len(errors)} error(s), {len(warnings)} warning(s), {len(infos)} info."
)
if report.by_severity("error"):
return 1
+3 -1
View File
@@ -60,7 +60,9 @@ def get_adapter(harness_id: str, output_root: Path) -> HarnessAdapter:
from tools.adapters.copilot import CopilotAdapter
return CopilotAdapter(output_root=output_root)
raise ValueError(f"Unknown harness: {harness_id}. Supported: {supported_harnesses()}")
raise ValueError(
f"Unknown harness: {harness_id}. Supported: {supported_harnesses()}"
)
def _validate_output_root(output_root: Path) -> str | None:
+9 -3
View File
@@ -21,7 +21,9 @@ from tools.adapters.base import ( # noqa: E402
)
def _make_skill(plugin_dir: Path, name: str, frontmatter: str, body: str) -> SkillSource:
def _make_skill(
plugin_dir: Path, name: str, frontmatter: str, body: str
) -> SkillSource:
"""Build a SkillSource on disk under plugin_dir/skills/<name>/SKILL.md."""
skill_dir = plugin_dir / "skills" / name
skill_dir.mkdir(parents=True, exist_ok=True)
@@ -37,7 +39,9 @@ def _make_skill(plugin_dir: Path, name: str, frontmatter: str, body: str) -> Ski
)
def _make_agent(plugin_dir: Path, name: str, frontmatter: str, body: str) -> AgentSource:
def _make_agent(
plugin_dir: Path, name: str, frontmatter: str, body: str
) -> AgentSource:
agents_dir = plugin_dir / "agents"
agents_dir.mkdir(parents=True, exist_ok=True)
content = f"---\n{frontmatter}\n---\n\n{body}\n"
@@ -53,7 +57,9 @@ def _make_agent(plugin_dir: Path, name: str, frontmatter: str, body: str) -> Age
)
def _make_command(plugin_dir: Path, name: str, frontmatter: str, body: str) -> CommandSource:
def _make_command(
plugin_dir: Path, name: str, frontmatter: str, body: str
) -> CommandSource:
cmds_dir = plugin_dir / "commands"
cmds_dir.mkdir(parents=True, exist_ok=True)
content = f"---\n{frontmatter}\n---\n\n{body}\n"
+117 -38
View File
@@ -85,7 +85,9 @@ class TestCodexAdapter:
# No `tools` key in Codex TOML (silently ignored anyway)
assert "tools" not in parsed
def test_agent_with_write_tools_gets_workspace_write(self, tmp_path: Path, output_root: Path):
def test_agent_with_write_tools_gets_workspace_write(
self, tmp_path: Path, output_root: Path
):
from tools.tests.conftest import _make_agent
plugin_dir = tmp_path / "demo"
@@ -124,7 +126,9 @@ class TestCodexAdapter:
"## Section A\n\n" + ("a" * 4000) + "\n\n"
"## Section B\n\n" + ("b" * 4000) + "\n"
)
skill = _make_skill(plugin_dir, "big", "name: big\ndescription: Use when big.", body)
skill = _make_skill(
plugin_dir, "big", "name: big\ndescription: Use when big.", body
)
plugin = PluginSource(
name="demo", dir=plugin_dir, plugin_json={"name": "demo"}, skills=[skill]
)
@@ -132,7 +136,12 @@ class TestCodexAdapter:
head_path = output_root / ".codex" / "skills" / "demo__big" / "SKILL.md"
overflow_path = (
output_root / ".codex" / "skills" / "demo__big" / "references" / "details.md"
output_root
/ ".codex"
/ "skills"
/ "demo__big"
/ "references"
/ "details.md"
)
assert head_path.is_file()
assert overflow_path.is_file()
@@ -180,7 +189,9 @@ class TestCodexAdapter:
# Stage an oversized AGENTS.md at the repo root (not output_root)
fake_repo = tmp_path / "fake_repo"
fake_repo.mkdir()
(fake_repo / "AGENTS.md").write_text("# Big AGENTS.md\n\n" + ("filler line\n" * 200))
(fake_repo / "AGENTS.md").write_text(
"# Big AGENTS.md\n\n" + ("filler line\n" * 200)
)
adapter = CodexAdapter(output_root=output_root, repo_root=fake_repo)
adapter.emit_plugin(synthetic_plugin)
result = adapter.emit_global([synthetic_plugin])
@@ -229,13 +240,17 @@ class TestCodexAdapter:
# Emitted file uses namespaced ID
assert (output_root / ".codex" / "agents" / "demo__worker.toml").is_file()
def test_command_becomes_skill(self, synthetic_plugin: PluginSource, output_root: Path):
def test_command_becomes_skill(
self, synthetic_plugin: PluginSource, output_root: Path
):
CodexAdapter(output_root=output_root).emit_plugin(synthetic_plugin)
# Command should be present as a skill (Codex deprecated ~/.codex/prompts/)
cmd_skill = output_root / ".codex" / "skills" / "demo__say-hi" / "SKILL.md"
assert cmd_skill.is_file()
def test_skill_command_name_collision_namespaced(self, tmp_path: Path, output_root: Path):
def test_skill_command_name_collision_namespaced(
self, tmp_path: Path, output_root: Path
):
"""A skill and command with the same name in one plugin must NOT overwrite."""
from tools.tests.conftest import _make_command, _make_skill
@@ -262,7 +277,9 @@ class TestCodexAdapter:
result = CodexAdapter(output_root=output_root).emit_plugin(plugin)
skill_path = output_root / ".codex" / "skills" / "demo__review" / "SKILL.md"
cmd_skill_path = output_root / ".codex" / "skills" / "demo__review__command" / "SKILL.md"
cmd_skill_path = (
output_root / ".codex" / "skills" / "demo__review__command" / "SKILL.md"
)
assert skill_path.is_file()
assert cmd_skill_path.is_file()
assert any("collides" in w for w in result.warnings)
@@ -347,7 +364,9 @@ class TestCodexAdapter:
assert "## ##" not in overflow, f"double-prepend bug: {overflow[:80]!r}"
assert "## ##" not in head, f"double-prepend bug in head: {head[-80:]!r}"
def test_empty_tools_field_yields_read_only_sandbox(self, tmp_path: Path, output_root: Path):
def test_empty_tools_field_yields_read_only_sandbox(
self, tmp_path: Path, output_root: Path
):
"""An explicit `tools: []` in source should map to read-only sandbox, not workspace-write."""
from tools.tests.conftest import _make_agent
@@ -373,7 +392,9 @@ class TestCodexAdapter:
)
assert parsed["sandbox_mode"] == "read-only", parsed
def test_missing_tools_field_yields_workspace_write(self, tmp_path: Path, output_root: Path):
def test_missing_tools_field_yields_workspace_write(
self, tmp_path: Path, output_root: Path
):
"""A source agent with NO `tools:` field should map to workspace-write (Claude default)."""
from tools.tests.conftest import _make_agent
@@ -399,7 +420,9 @@ class TestCodexAdapter:
)
assert parsed["sandbox_mode"] == "workspace-write"
def test_second_order_collision_routes_to_cmd(self, tmp_path: Path, output_root: Path):
def test_second_order_collision_routes_to_cmd(
self, tmp_path: Path, output_root: Path
):
"""Skill `foo`, command `foo`, AND skill `foo__command` → command goes to `foo__cmd`."""
from tools.tests.conftest import _make_command, _make_skill
@@ -407,14 +430,18 @@ class TestCodexAdapter:
plugin_dir.mkdir()
(plugin_dir / ".claude-plugin").mkdir()
(plugin_dir / ".claude-plugin" / "plugin.json").write_text('{"name": "demo"}')
s1 = _make_skill(plugin_dir, "foo", "name: foo\ndescription: Use when foo.", "# foo\n")
s1 = _make_skill(
plugin_dir, "foo", "name: foo\ndescription: Use when foo.", "# foo\n"
)
s2 = _make_skill(
plugin_dir,
"foo__command",
"name: foo__command\ndescription: Use when foo command.",
"# foo__command\n",
)
cmd = _make_command(plugin_dir, "foo", 'description: "foo command"', "# foo cmd\n")
cmd = _make_command(
plugin_dir, "foo", 'description: "foo command"', "# foo cmd\n"
)
plugin = PluginSource(
name="demo",
dir=plugin_dir,
@@ -427,9 +454,13 @@ class TestCodexAdapter:
# Real skill `foo`
assert (output_root / ".codex" / "skills" / "demo__foo" / "SKILL.md").is_file()
# Real skill `foo__command`
assert (output_root / ".codex" / "skills" / "demo__foo__command" / "SKILL.md").is_file()
assert (
output_root / ".codex" / "skills" / "demo__foo__command" / "SKILL.md"
).is_file()
# Command-derived skill routed to `__cmd` to avoid second-order clash
assert (output_root / ".codex" / "skills" / "demo__foo__cmd" / "SKILL.md").is_file()
assert (
output_root / ".codex" / "skills" / "demo__foo__cmd" / "SKILL.md"
).is_file()
assert any("second-order" in w for w in result.warnings)
def test_rewriter_matches_lint_pattern(self):
@@ -451,7 +482,9 @@ class TestCodexAdapter:
class TestCursorAdapter:
def test_emits_plugin_manifest(self, synthetic_plugin: PluginSource, output_root: Path):
def test_emits_plugin_manifest(
self, synthetic_plugin: PluginSource, output_root: Path
):
adapter = CursorAdapter(output_root=output_root)
result = adapter.emit_plugin(synthetic_plugin)
manifest_path = output_root / ".cursor-plugin" / "plugins" / "demo.json"
@@ -480,7 +513,9 @@ class TestCursorAdapter:
# First plugin entry uses `source`, not `path` or `url`
assert data["plugins"][0]["source"] == "./plugins/demo"
def test_emits_curated_rules_present(self, synthetic_plugin: PluginSource, output_root: Path):
def test_emits_curated_rules_present(
self, synthetic_plugin: PluginSource, output_root: Path
):
adapter = CursorAdapter(output_root=output_root)
result = adapter.emit_global([synthetic_plugin])
rule_files = [p for p in result.written if p.suffix == ".mdc"]
@@ -511,7 +546,9 @@ class TestCursorAdapter:
)
assert manifest["author"] == {"name": "Jane Doe", "email": "jane@example.com"}
def test_curated_rules_validate(self, synthetic_plugin: PluginSource, output_root: Path):
def test_curated_rules_validate(
self, synthetic_plugin: PluginSource, output_root: Path
):
"""Each emitted .mdc has only the three allowed frontmatter keys."""
adapter = CursorAdapter(output_root=output_root)
adapter.emit_global([synthetic_plugin])
@@ -553,7 +590,9 @@ class TestCursorAdapter:
class TestOpenCodeAdapter:
def test_emits_subagent_markdown(self, synthetic_plugin: PluginSource, output_root: Path):
def test_emits_subagent_markdown(
self, synthetic_plugin: PluginSource, output_root: Path
):
OpenCodeAdapter(output_root=output_root).emit_plugin(synthetic_plugin)
agent_md = output_root / ".opencode" / "agents" / "demo__greeter.md"
assert agent_md.is_file()
@@ -576,7 +615,9 @@ class TestOpenCodeAdapter:
assert re.search(r"write:\s*deny", content)
assert re.search(r"bash:\s*deny", content)
def test_no_permission_block_when_no_tools_field(self, tmp_path: Path, output_root: Path):
def test_no_permission_block_when_no_tools_field(
self, tmp_path: Path, output_root: Path
):
from tools.tests.conftest import _make_agent
plugin_dir = tmp_path / "demo"
@@ -596,13 +637,17 @@ class TestOpenCodeAdapter:
content = (output_root / ".opencode" / "agents" / "demo__free.md").read_text()
assert "permission:" not in content
def test_lowercases_tool_refs_in_body(self, synthetic_plugin: PluginSource, output_root: Path):
def test_lowercases_tool_refs_in_body(
self, synthetic_plugin: PluginSource, output_root: Path
):
OpenCodeAdapter(output_root=output_root).emit_plugin(synthetic_plugin)
# Commands and agents need OpenCode's lowercase tool vocabulary.
cmd_md = output_root / ".opencode" / "commands" / "demo__say-hi.md"
assert cmd_md.is_file()
def test_emits_minimal_opencode_json(self, synthetic_plugin: PluginSource, output_root: Path):
def test_emits_minimal_opencode_json(
self, synthetic_plugin: PluginSource, output_root: Path
):
adapter = OpenCodeAdapter(output_root=output_root)
adapter.emit_plugin(synthetic_plugin)
result = adapter.emit_global([synthetic_plugin])
@@ -626,7 +671,9 @@ class TestOpenCodeAdapter:
assert "`Read`" not in body
assert "`Bash`" not in body
def test_emits_opencode_skill_support_files(self, tmp_path: Path, output_root: Path):
def test_emits_opencode_skill_support_files(
self, tmp_path: Path, output_root: Path
):
from tools.tests.conftest import _make_skill
plugin_dir = tmp_path / "demo"
@@ -659,7 +706,9 @@ class TestOpenCodeAdapter:
plugin_dir = tmp_path / "bad_plugin"
plugin_dir.mkdir()
(plugin_dir / ".claude-plugin").mkdir()
(plugin_dir / ".claude-plugin" / "plugin.json").write_text('{"name": "bad_plugin"}')
(plugin_dir / ".claude-plugin" / "plugin.json").write_text(
'{"name": "bad_plugin"}'
)
skill = _make_skill(
plugin_dir,
"hello",
@@ -704,7 +753,9 @@ class TestOpenCodeAdapter:
else:
raise AssertionError("too-long OpenCode skill id was accepted")
def test_rejects_ambiguous_opencode_skill_id_collision(self, tmp_path: Path, output_root: Path):
def test_rejects_ambiguous_opencode_skill_id_collision(
self, tmp_path: Path, output_root: Path
):
from tools.tests.conftest import _make_skill
first_dir = tmp_path / "data-analysis"
@@ -777,7 +828,9 @@ class TestOpenCodeAdapter:
)
OpenCodeAdapter(output_root=output_root).emit_plugin(plugin)
content = (output_root / ".opencode" / "agents" / "demo__locked-advisor.md").read_text()
content = (
output_root / ".opencode" / "agents" / "demo__locked-advisor.md"
).read_text()
# Permission block MUST be present (locked agent), with skill/task allow + all else deny.
assert "permission:" in content
assert re.search(r"read:\s*deny", content)
@@ -809,7 +862,9 @@ class TestOpenCodeAdapter:
)
OpenCodeAdapter(output_root=output_root).emit_plugin(plugin)
content = (output_root / ".opencode" / "agents" / "demo__open-agent.md").read_text()
content = (
output_root / ".opencode" / "agents" / "demo__open-agent.md"
).read_text()
assert "permission:" not in content
def test_subtask_inference_word_boundary(self, tmp_path: Path, output_root: Path):
@@ -846,7 +901,9 @@ class TestOpenCodeAdapter:
OpenCodeAdapter(output_root=output_root).emit_plugin(plugin)
lint = (output_root / ".opencode" / "commands" / "demo__lint.md").read_text()
delegate = (output_root / ".opencode" / "commands" / "demo__delegate.md").read_text()
delegate = (
output_root / ".opencode" / "commands" / "demo__delegate.md"
).read_text()
assert "subtask:" not in lint # NO false positive on substring matches
assert "subtask: true" in delegate # genuine orchestration still detected
@@ -865,7 +922,9 @@ class TestGeminiAdapter:
fm, _ = parse_frontmatter(skill_md.read_text())
assert fm["name"] == "demo__hello"
def test_emits_native_subagent(self, synthetic_plugin: PluginSource, output_root: Path):
def test_emits_native_subagent(
self, synthetic_plugin: PluginSource, output_root: Path
):
GeminiAdapter(output_root=output_root).emit_plugin(synthetic_plugin)
agent_md = output_root / "agents" / "demo__greeter.md"
assert agent_md.is_file()
@@ -934,7 +993,9 @@ class TestGeminiAdapter:
class TestCopilotAdapter:
def test_emits_agent_profile(self, synthetic_plugin: PluginSource, output_root: Path):
def test_emits_agent_profile(
self, synthetic_plugin: PluginSource, output_root: Path
):
adapter = CopilotAdapter(output_root=output_root)
result = adapter.emit_plugin(synthetic_plugin)
@@ -967,7 +1028,9 @@ class TestCopilotAdapter:
)
CopilotAdapter(output_root=output_root).emit_plugin(plugin)
content = (output_root / ".copilot" / "agents" / "demo__tool-user.agent.md").read_text()
content = (
output_root / ".copilot" / "agents" / "demo__tool-user.agent.md"
).read_text()
fm, body = parse_frontmatter(content)
assert fm["tools"] == ["read", "edit", "execute"]
assert "`read`" in body
@@ -1021,9 +1084,13 @@ class TestCopilotAdapter:
}
for name, exp_model in expected.items():
fm, _ = parse_frontmatter(
(output_root / ".copilot" / "agents" / f"demo__{name}.agent.md").read_text()
(
output_root / ".copilot" / "agents" / f"demo__{name}.agent.md"
).read_text()
)
assert fm["model"] == exp_model, (
f"{name}: expected {exp_model}, got {fm['model']}"
)
assert fm["model"] == exp_model, f"{name}: expected {exp_model}, got {fm['model']}"
def test_emits_skill(self, synthetic_plugin: PluginSource, output_root: Path):
CopilotAdapter(output_root=output_root).emit_plugin(synthetic_plugin)
@@ -1035,7 +1102,9 @@ class TestCopilotAdapter:
assert fm["description"] == "Use when greeting users."
assert "# Hello" in body
def test_emits_command_prompt_files(self, synthetic_plugin: PluginSource, output_root: Path):
def test_emits_command_prompt_files(
self, synthetic_plugin: PluginSource, output_root: Path
):
CopilotAdapter(output_root=output_root).emit_plugin(synthetic_plugin)
entry = output_root / ".copilot" / "commands" / "demo" / "index.md"
@@ -1051,7 +1120,9 @@ class TestCopilotAdapter:
assert cmd_fm["description"] == "Send a greeting"
assert "Greet the user named $ARGUMENTS." in cmd_body
def test_emit_global_returns_empty(self, synthetic_plugin: PluginSource, output_root: Path):
def test_emit_global_returns_empty(
self, synthetic_plugin: PluginSource, output_root: Path
):
adapter = CopilotAdapter(output_root=output_root)
result = adapter.emit_global([synthetic_plugin])
assert result.written == []
@@ -1095,7 +1166,9 @@ class TestCopilotAdapter:
)
CopilotAdapter(output_root=output_root).emit_plugin(plugin)
content = (output_root / ".copilot" / "agents" / "demo__advisory.agent.md").read_text()
content = (
output_root / ".copilot" / "agents" / "demo__advisory.agent.md"
).read_text()
fm, body = parse_frontmatter(content)
assert fm["name"] == "demo__advisory"
assert fm["description"] == "Use when advising."
@@ -1120,7 +1193,9 @@ class TestCopilotAdapter:
)
CopilotAdapter(output_root=output_root).emit_plugin(plugin)
content = (output_root / ".copilot" / "agents" / "demo__unrestricted.agent.md").read_text()
content = (
output_root / ".copilot" / "agents" / "demo__unrestricted.agent.md"
).read_text()
fm, body = parse_frontmatter(content)
assert fm["name"] == "demo__unrestricted"
assert fm["description"] == "Use when unrestricted."
@@ -1139,7 +1214,9 @@ class TestLoadPlugin:
# Build a fake plugins dir with a bad name
bad_plugin = tmp_path / "plugins" / "bad__name"
(bad_plugin / ".claude-plugin").mkdir(parents=True)
(bad_plugin / ".claude-plugin" / "plugin.json").write_text('{"name": "bad__name"}')
(bad_plugin / ".claude-plugin" / "plugin.json").write_text(
'{"name": "bad__name"}'
)
monkeypatch.setattr(base, "PLUGINS_DIR", tmp_path / "plugins")
# Should return None with a stderr warning
@@ -1178,7 +1255,9 @@ class TestFrontmatterParser:
def test_block_scalar_description(self):
from tools.adapters.base import parse_frontmatter
fm, _ = parse_frontmatter("---\nname: x\ndescription: >\n multi\n line\n---\nbody")
fm, _ = parse_frontmatter(
"---\nname: x\ndescription: >\n multi\n line\n---\nbody"
)
assert fm["description"] == "multi line"
+9 -3
View File
@@ -148,7 +148,9 @@ class TestCodexSmoke:
tomllib.loads(toml_path.read_text())
except tomllib.TOMLDecodeError as e:
broken.append(f"{toml_path.name}: {e}")
assert not broken, "Codex agent TOMLs that fail to parse:\n " + "\n ".join(broken)
assert not broken, "Codex agent TOMLs that fail to parse:\n " + "\n ".join(
broken
)
# ── Claude Code CLI ──────────────────────────────────────────────────────────
@@ -169,7 +171,9 @@ class TestClaudeCodeSmoke:
assert mp.get("plugins"), "marketplace.json has no plugins[]"
# Owner/metadata are required for Claude Code's marketplace loader.
assert mp.get("owner"), "marketplace.json missing top-level 'owner'"
assert mp.get("metadata", {}).get("version"), "marketplace.json missing metadata.version"
assert mp.get("metadata", {}).get("version"), (
"marketplace.json missing metadata.version"
)
# ── Cross-CLI sanity: marketplace + adapter agreement ────────────────────────
@@ -186,7 +190,9 @@ class TestMarketplaceAgreement:
source = entry.get("source")
if not (isinstance(source, str) and source.startswith("./plugins/")):
continue
pj_path = WORKTREE / source.removeprefix("./") / ".claude-plugin" / "plugin.json"
pj_path = (
WORKTREE / source.removeprefix("./") / ".claude-plugin" / "plugin.json"
)
if not pj_path.is_file():
continue
pj = json.loads(pj_path.read_text())
+45 -15
View File
@@ -23,7 +23,9 @@ def _patch_paths(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(dg, "WORKTREE", tmp_path)
monkeypatch.setattr(dg, "PLUGINS_DIR", tmp_path / "plugins")
monkeypatch.setattr(dg, "DOCS_DIR", tmp_path / "docs")
monkeypatch.setattr(dg, "MARKETPLACE_JSON", tmp_path / ".claude-plugin" / "marketplace.json")
monkeypatch.setattr(
dg, "MARKETPLACE_JSON", tmp_path / ".claude-plugin" / "marketplace.json"
)
# Also patch the base module's WORKTREE / PLUGINS_DIR since list_plugins() uses them
import tools.adapters.base as base
@@ -35,18 +37,24 @@ def _patch_paths(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
class TestStaleArtifacts:
def test_fresh_artifacts_no_finding(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_fresh_artifacts_no_finding(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_paths(monkeypatch, tmp_path)
# Set up source
plugin = tmp_path / "plugins" / "demo"
(plugin / "agents").mkdir(parents=True)
src = plugin / "agents" / "greeter.md"
src.write_text("---\nname: greeter\ndescription: Use when greeting.\n---\nBody.\n")
src.write_text(
"---\nname: greeter\ndescription: Use when greeting.\n---\nBody.\n"
)
# Set up generated artifact that's newer
gen_dir = tmp_path / ".codex" / "agents"
gen_dir.mkdir(parents=True)
gen = gen_dir / "demo__greeter.toml"
gen.write_text('name = "demo__greeter"\ndescription = "x"\ndeveloper_instructions = "y"\n')
gen.write_text(
'name = "demo__greeter"\ndescription = "x"\ndeveloper_instructions = "y"\n'
)
# Force gen mtime to be after source
future = src.stat().st_mtime + 100
import os
@@ -57,16 +65,22 @@ class TestStaleArtifacts:
check_stale_artifacts(report)
assert [f for f in report.findings if f.kind == "STALE_ARTIFACT"] == []
def test_stale_artifact_warns(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_stale_artifact_warns(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_paths(monkeypatch, tmp_path)
plugin = tmp_path / "plugins" / "demo"
(plugin / "agents").mkdir(parents=True)
src = plugin / "agents" / "greeter.md"
src.write_text("---\nname: greeter\ndescription: Use when greeting.\n---\nBody.\n")
src.write_text(
"---\nname: greeter\ndescription: Use when greeting.\n---\nBody.\n"
)
gen_dir = tmp_path / ".codex" / "agents"
gen_dir.mkdir(parents=True)
gen = gen_dir / "demo__greeter.toml"
gen.write_text('name = "demo__greeter"\ndescription = "x"\ndeveloper_instructions = "y"\n')
gen.write_text(
'name = "demo__greeter"\ndescription = "x"\ndeveloper_instructions = "y"\n'
)
# Force src to be much newer
import os
@@ -94,7 +108,9 @@ class TestStaleArtifacts:
report = Report()
check_stale_artifacts(report)
findings = [f for f in report.findings if f.kind == "opencode-skill-id-collision"]
findings = [
f for f in report.findings if f.kind == "opencode-skill-id-collision"
]
assert findings
assert "data-analysis-report" in findings[0].message
@@ -111,14 +127,18 @@ class TestStaleArtifacts:
report = Report()
check_stale_artifacts(report)
assert [f for f in report.findings if f.kind == "opencode-skill-id-collision"] == []
assert [
f for f in report.findings if f.kind == "opencode-skill-id-collision"
] == []
# ── Context file size ────────────────────────────────────────────────────────
class TestContextFiles:
def test_within_budget_no_finding(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_within_budget_no_finding(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_paths(monkeypatch, tmp_path)
(tmp_path / "AGENTS.md").write_text("\n".join(["line"] * 80))
report = Report()
@@ -138,7 +158,9 @@ class TestContextFiles:
class TestDeadLinks:
def test_valid_links_no_finding(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_valid_links_no_finding(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_paths(monkeypatch, tmp_path)
(tmp_path / "docs").mkdir()
(tmp_path / "docs" / "a.md").write_text("[link to b](b.md)\n")
@@ -156,7 +178,9 @@ class TestDeadLinks:
findings = [f for f in report.findings if f.kind == "DEAD_LINK"]
assert findings
def test_external_links_skipped(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_external_links_skipped(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_paths(monkeypatch, tmp_path)
(tmp_path / "docs").mkdir()
(tmp_path / "docs" / "a.md").write_text(
@@ -171,7 +195,9 @@ class TestDeadLinks:
class TestCodexSkillCaps:
def test_under_cap_no_finding(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_under_cap_no_finding(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_paths(monkeypatch, tmp_path)
sk = tmp_path / "plugins" / "demo" / "skills" / "small"
sk.mkdir(parents=True)
@@ -233,7 +259,9 @@ class TestMarketplaceConsistency:
check_marketplace_consistency(report)
assert [f for f in report.findings if f.kind == "MARKETPLACE_ORPHAN"]
def test_external_plugin_not_orphaned(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_external_plugin_not_orphaned(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
"""git-subdir / git source plugins legitimately have no plugins/<name>/."""
_patch_paths(monkeypatch, tmp_path)
(tmp_path / "plugins").mkdir()
@@ -255,7 +283,9 @@ class TestMarketplaceConsistency:
check_marketplace_consistency(report)
assert not [f for f in report.findings if f.kind == "MARKETPLACE_ORPHAN"]
def test_unregistered_local_plugin_info(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_unregistered_local_plugin_info(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_paths(monkeypatch, tmp_path)
plug = tmp_path / "plugins" / "unregistered"
plug.mkdir(parents=True)
+2 -1
View File
@@ -82,7 +82,8 @@ def test_force_replaces_conflicting_symlink_only(tmp_path: Path):
assert not blocked.ok
assert forced.ok
assert (
target.resolve() == (repo_root / ".copilot" / "agents" / "demo__agent.agent.md").resolve()
target.resolve()
== (repo_root / ".copilot" / "agents" / "demo__agent.agent.md").resolve()
)
+4 -1
View File
@@ -80,7 +80,10 @@ def test_force_replaces_conflicting_symlink_only(tmp_path: Path):
assert not blocked.ok
assert forced.ok
assert target.resolve() == (repo_root / ".opencode" / "agents" / "demo__agent.md").resolve()
assert (
target.resolve()
== (repo_root / ".opencode" / "agents" / "demo__agent.md").resolve()
)
def test_uninstall_removes_only_repo_owned_symlinks(tmp_path: Path):
+53 -19
View File
@@ -50,7 +50,9 @@ _CODEX_BUILTIN_AGENTS = {"default", "worker", "explorer"}
# `subagent_type`, otherwise later phases cannot read earlier outputs.
_PHASE_HEADING_PATTERN = re.compile(r"^##\s+Phase\s+(\d+)\b")
_TASK_SUBAGENT_PATTERN = re.compile(r'^\s*subagent_type:\s*"?([^"\n]+?)"?\s*$')
_PHASE_OUTPUT_REF_PATTERN = re.compile(r"\{phase(?P<phase>\d+)\.(?P<task>[^{}.]+)\.output\}")
_PHASE_OUTPUT_REF_PATTERN = re.compile(
r"\{phase(?P<phase>\d+)\.(?P<task>[^{}.]+)\.output\}"
)
# ── Marketplace consistency ─────────────────────────────────────────────────
@@ -63,14 +65,18 @@ class TestMarketplaceConsistency:
source = entry.get("source")
if isinstance(source, str) and source.startswith("./plugins/"):
plugin_path = WORKTREE / source.removeprefix("./")
assert plugin_path.is_dir(), f"{entry['name']}: source {source} does not exist"
assert plugin_path.is_dir(), (
f"{entry['name']}: source {source} does not exist"
)
assert (plugin_path / ".claude-plugin" / "plugin.json").is_file(), (
f"{entry['name']}: missing .claude-plugin/plugin.json"
)
def test_every_local_plugin_dir_appears_in_marketplace(self):
mp = json.loads((WORKTREE / ".claude-plugin" / "marketplace.json").read_text())
listed = {e["name"] for e in mp.get("plugins", []) if isinstance(e.get("source"), str)}
listed = {
e["name"] for e in mp.get("plugins", []) if isinstance(e.get("source"), str)
}
actual = set(list_plugins())
missing = actual - listed
assert not missing, f"plugins/ dirs not in marketplace.json: {sorted(missing)}"
@@ -85,12 +91,16 @@ class TestMarketplaceConsistency:
continue
name = entry["name"]
mp_version = entry.get("version", "")
pj_path = WORKTREE / source.removeprefix("./") / ".claude-plugin" / "plugin.json"
pj_path = (
WORKTREE / source.removeprefix("./") / ".claude-plugin" / "plugin.json"
)
if not pj_path.is_file():
continue
pj_version = json.loads(pj_path.read_text()).get("version", "")
if mp_version != pj_version:
mismatches.append(f"{name}: marketplace={mp_version} vs plugin.json={pj_version}")
mismatches.append(
f"{name}: marketplace={mp_version} vs plugin.json={pj_version}"
)
assert not mismatches, (
"Version drift between marketplace.json and per-plugin plugin.json:\n "
+ "\n ".join(mismatches)
@@ -121,8 +131,12 @@ class TestPluginSourceIntegrity:
if not agent.frontmatter.get("name"):
problems.append(f"{plugin_name}/agents/{agent.name}: missing name")
if not agent.description:
problems.append(f"{plugin_name}/agents/{agent.name}: missing description")
assert not problems, "Agents missing required frontmatter:\n " + "\n ".join(problems)
problems.append(
f"{plugin_name}/agents/{agent.name}: missing description"
)
assert not problems, "Agents missing required frontmatter:\n " + "\n ".join(
problems
)
def test_every_skill_has_trigger_phrase(self):
"""Per plugin-eval's MISSING_TRIGGER check: every description must include a recognized phrase."""
@@ -137,7 +151,9 @@ class TestPluginSourceIntegrity:
f"{plugin_name}/skills/{skill.name}: description has no trigger phrase "
f"(saw: {skill.description[:80]!r})"
)
assert not problems, "Skills with missing trigger phrases:\n " + "\n ".join(problems[:20])
assert not problems, "Skills with missing trigger phrases:\n " + "\n ".join(
problems[:20]
)
def test_no_agent_collides_with_codex_builtin(self):
"""Codex has built-in agents `default`, `worker`, `explorer`. Custom agents must not use these names."""
@@ -163,9 +179,13 @@ class TestPluginSourceIntegrity:
for agent in plugin.agents:
name = (agent.frontmatter.get("name") or "").strip()
if name:
by_name.setdefault(name, []).append(f"{plugin_name}/agents/{agent.name}.md")
by_name.setdefault(name, []).append(
f"{plugin_name}/agents/{agent.name}.md"
)
duplicates = {name: paths for name, paths in sorted(by_name.items()) if len(paths) > 1}
duplicates = {
name: paths for name, paths in sorted(by_name.items()) if len(paths) > 1
}
assert not duplicates, "Duplicate agent frontmatter names:\n " + "\n ".join(
f"{name}: {', '.join(paths)}" for name, paths in duplicates.items()
)
@@ -183,11 +203,17 @@ class TestPluginSourceIntegrity:
current_phase = int(match.group(1))
phase_tasks.setdefault(current_phase, set())
if (match := _TASK_SUBAGENT_PATTERN.match(line)) and current_phase is not None:
phase_tasks.setdefault(current_phase, set()).add(match.group(1).strip())
if (
match := _TASK_SUBAGENT_PATTERN.match(line)
) and current_phase is not None:
phase_tasks.setdefault(current_phase, set()).add(
match.group(1).strip()
)
for match in _PHASE_OUTPUT_REF_PATTERN.finditer(line):
references.append((lineno, int(match.group("phase")), match.group("task")))
references.append(
(lineno, int(match.group("phase")), match.group("task"))
)
for lineno, phase, task_key in references:
available = phase_tasks.get(phase, set())
@@ -198,7 +224,9 @@ class TestPluginSourceIntegrity:
f"in Phase {phase} (available: {sorted(available)})"
)
assert not problems, "Broken command phase output references:\n " + "\n ".join(problems)
assert not problems, (
"Broken command phase output references:\n " + "\n ".join(problems)
)
# ── Progressive-disclosure refactor integrity ────────────────────────────────
@@ -224,7 +252,9 @@ class TestProgressiveDisclosureIntegrity:
for plugin_name, _, details in self._modified_skills():
size = len(details.read_text().encode("utf-8"))
if size < 500:
too_small.append(f"{plugin_name}/{details.relative_to(PLUGINS_DIR)}: {size}B")
too_small.append(
f"{plugin_name}/{details.relative_to(PLUGINS_DIR)}: {size}B"
)
assert not too_small, (
"references/details.md files are suspiciously small (<500B), "
"suggesting failed extraction:\n " + "\n ".join(too_small)
@@ -237,9 +267,12 @@ class TestProgressiveDisclosureIntegrity:
for plugin_name, skill_md, _ in self._modified_skills():
body = skill_md.read_text()
if "references/" not in body:
missing_pointer.append(f"{plugin_name}/{skill_md.relative_to(PLUGINS_DIR)}")
missing_pointer.append(
f"{plugin_name}/{skill_md.relative_to(PLUGINS_DIR)}"
)
assert not missing_pointer, (
"Refactored skills missing pointer to references/:\n " + "\n ".join(missing_pointer)
"Refactored skills missing pointer to references/:\n "
+ "\n ".join(missing_pointer)
)
def test_extracted_skills_preserve_when_to_use(self):
@@ -260,8 +293,9 @@ class TestProgressiveDisclosureIntegrity:
body = skill_md.read_text()
if not any(marker in body for marker in nav_markers):
missing.append(f"{plugin_name}/{skill_md.relative_to(PLUGINS_DIR)}")
assert not missing, "Refactored skills missing navigation-tier section:\n " + "\n ".join(
missing
assert not missing, (
"Refactored skills missing navigation-tier section:\n "
+ "\n ".join(missing)
)
def test_no_refactored_skill_is_stub_only(self):
+56 -18
View File
@@ -80,7 +80,9 @@ class TestCodexRoundTrip:
size = len(skill_md.read_text().encode("utf-8"))
if size > 8 * 1024:
oversized.append(f"{skill_md.relative_to(WORKTREE)}: {size}B")
assert not oversized, "Codex skills over 8 KB injection cap:\n " + "\n ".join(oversized)
assert not oversized, "Codex skills over 8 KB injection cap:\n " + "\n ".join(
oversized
)
def test_every_codex_agent_toml_parses_and_has_required_fields(self):
required = {"name", "description", "developer_instructions"}
@@ -146,7 +148,9 @@ class TestOpenCodeRoundTrip:
problems.append(f"{agent_md.name}: invalid mode {fm.get('mode')!r}")
model = fm.get("model", "")
if model and "/" not in model:
problems.append(f"{agent_md.name}: model {model!r} not provider-prefixed")
problems.append(
f"{agent_md.name}: model {model!r} not provider-prefixed"
)
assert not problems, "OpenCode agent issues:\n " + "\n ".join(problems[:20])
def test_locked_agents_have_proper_permission_block(self):
@@ -174,12 +178,18 @@ class TestOpenCodeRoundTrip:
continue
# Must allow skill + task (base capabilities), deny others.
if not re.search(r"skill:\s*allow", content):
problems.append(f"{agent_id}: skill not allowed in permission block")
problems.append(
f"{agent_id}: skill not allowed in permission block"
)
if not re.search(r"task:\s*allow", content):
problems.append(f"{agent_id}: task not allowed in permission block")
if not re.search(r"read:\s*deny", content):
problems.append(f"{agent_id}: read should be denied for locked agent")
assert not problems, "Locked-agent permission regressions:\n " + "\n ".join(problems)
problems.append(
f"{agent_id}: read should be denied for locked agent"
)
assert not problems, "Locked-agent permission regressions:\n " + "\n ".join(
problems
)
def test_every_opencode_skill_has_valid_frontmatter(self):
name_re = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
@@ -188,7 +198,9 @@ class TestOpenCodeRoundTrip:
fm, _ = parse_frontmatter(skill_md.read_text())
name = str(fm.get("name") or "")
if name != skill_md.parent.name:
problems.append(f"{skill_md}: name {name!r} != directory {skill_md.parent.name!r}")
problems.append(
f"{skill_md}: name {name!r} != directory {skill_md.parent.name!r}"
)
if not name_re.fullmatch(name):
problems.append(f"{skill_md}: invalid OpenCode skill name {name!r}")
if len(name) > 64:
@@ -221,7 +233,9 @@ class TestCursorRoundTrip:
manifest_names = {p.stem for p in per_plugin.glob("*.json")}
local_plugins = set(list_plugins())
missing = local_plugins - manifest_names
assert not missing, f"Cursor per-plugin manifests missing for: {sorted(missing)}"
assert not missing, (
f"Cursor per-plugin manifests missing for: {sorted(missing)}"
)
def test_cursor_rules_only_use_allowed_keys(self):
rules_dir = WORKTREE / ".cursor" / "rules"
@@ -256,8 +270,12 @@ class TestGeminiRoundTrip:
for match in at_pattern.findall(prompt):
target = WORKTREE / match
if not target.is_file():
broken.append(f"{toml_path.relative_to(WORKTREE)}: @{{{match}}} -> missing")
assert not broken, "Broken Gemini @{path} injections:\n " + "\n ".join(broken[:20])
broken.append(
f"{toml_path.relative_to(WORKTREE)}: @{{{match}}} -> missing"
)
assert not broken, "Broken Gemini @{path} injections:\n " + "\n ".join(
broken[:20]
)
def test_every_gemini_command_has_prompt_and_args(self):
problems = []
@@ -268,11 +286,15 @@ class TestGeminiRoundTrip:
problems.append(f"{toml_path.relative_to(WORKTREE)}: parse error {e}")
continue
if "description" not in data:
problems.append(f"{toml_path.relative_to(WORKTREE)}: missing description")
problems.append(
f"{toml_path.relative_to(WORKTREE)}: missing description"
)
if "prompt" not in data:
problems.append(f"{toml_path.relative_to(WORKTREE)}: missing prompt")
elif "{{args}}" not in data["prompt"]:
problems.append(f"{toml_path.relative_to(WORKTREE)}: prompt missing {{{{args}}}}")
problems.append(
f"{toml_path.relative_to(WORKTREE)}: prompt missing {{{{args}}}}"
)
assert not problems, "Gemini TOML issues:\n " + "\n ".join(problems[:20])
def test_gemini_md_within_cap(self):
@@ -304,7 +326,11 @@ class TestCopilotRoundTrip:
def test_copilot_command_count_matches_source(self):
n = len(
[p for p in (WORKTREE / ".copilot" / "commands").rglob("*.md") if p.name != "index.md"]
[
p
for p in (WORKTREE / ".copilot" / "commands").rglob("*.md")
if p.name != "index.md"
]
)
assert n == _source_command_count(), (
f"command count mismatch: source={_source_command_count()} copilot={n}"
@@ -316,7 +342,9 @@ class TestCopilotRoundTrip:
entry = WORKTREE / ".copilot" / "commands" / plugin_name / "index.md"
if not entry.is_file():
missing.append(plugin_name)
assert not missing, f"missing Copilot command entrypoints for: {sorted(missing)}"
assert not missing, (
f"missing Copilot command entrypoints for: {sorted(missing)}"
)
def test_every_copilot_agent_has_required_frontmatter(self):
required = {"name", "description"}
@@ -329,7 +357,9 @@ class TestCopilotRoundTrip:
missing = required - set(fm.keys())
if missing:
problems.append(f"{agent_md.name}: missing {sorted(missing)}")
assert not problems, "Copilot agent frontmatter issues:\n " + "\n ".join(problems[:20])
assert not problems, "Copilot agent frontmatter issues:\n " + "\n ".join(
problems[:20]
)
# ── Native-install manifests (always run; these are committed source) ─────────
@@ -346,7 +376,9 @@ class TestNativeInstallManifests:
path = WORKTREE / "gemini-extension.json"
assert path.is_file(), "gemini-extension.json missing"
data = json.loads(path.read_text())
assert data.get("contextFileName") == "AGENTS.md", "Gemini contextFileName must be AGENTS.md"
assert data.get("contextFileName") == "AGENTS.md", (
"Gemini contextFileName must be AGENTS.md"
)
assert data.get("name"), "gemini-extension.json needs a name"
assert data.get("version"), "gemini-extension.json needs a version"
@@ -356,7 +388,9 @@ class TestNativeInstallManifests:
data = json.loads(marketplace.read_text())
names = {p["name"] for p in data["plugins"]}
local = set(list_plugins())
assert local.issubset(names), f"Codex marketplace missing plugins: {local - names}"
assert local.issubset(names), (
f"Codex marketplace missing plugins: {local - names}"
)
for p in data["plugins"]:
assert p["source"]["path"] == f"./plugins/{p['name']}", (
f"{p['name']}: marketplace source.path must point at the source plugin dir"
@@ -367,7 +401,9 @@ class TestNativeInstallManifests:
for name in list_plugins():
manifest = WORKTREE / "plugins" / name / ".codex-plugin" / "plugin.json"
if not manifest.is_file():
problems.append(f"{name}: plugins/{name}/.codex-plugin/plugin.json missing")
problems.append(
f"{name}: plugins/{name}/.codex-plugin/plugin.json missing"
)
continue
try:
data = json.loads(manifest.read_text())
@@ -378,7 +414,9 @@ class TestNativeInstallManifests:
problems.append(f"{name}: manifest name is {data.get('name')!r}")
if data.get("skills") != "./skills/":
problems.append(f"{name}: manifest skills is {data.get('skills')!r}")
assert not problems, "Codex per-plugin manifest issues:\n " + "\n ".join(problems[:20])
assert not problems, "Codex per-plugin manifest issues:\n " + "\n ".join(
problems[:20]
)
def test_claude_md_is_symlink_to_agents_md(self):
claude = WORKTREE / "CLAUDE.md"
+90 -30
View File
@@ -27,7 +27,9 @@ def _patch_worktree(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
class TestCodexValidator:
def test_clean_output_no_findings(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_clean_output_no_findings(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
(tmp_path / ".codex" / "agents").mkdir(parents=True)
(tmp_path / ".codex" / "agents" / "demo.toml").write_text(
@@ -45,16 +47,22 @@ class TestCodexValidator:
errors = report.errors()
assert errors == [], [e.render() for e in errors]
def test_malformed_toml_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_malformed_toml_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
(tmp_path / ".codex" / "agents").mkdir(parents=True)
(tmp_path / ".codex" / "agents" / "bad.toml").write_text("not valid = toml = anywhere")
(tmp_path / ".codex" / "agents" / "bad.toml").write_text(
"not valid = toml = anywhere"
)
report = Report()
validate_codex(report)
assert any("TOML parse" in f.message for f in report.errors())
def test_skill_name_mismatch_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_skill_name_mismatch_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
sk = tmp_path / ".codex" / "skills" / "demo"
sk.mkdir(parents=True)
@@ -64,9 +72,13 @@ class TestCodexValidator:
report = Report()
validate_codex(report)
assert any("name" in f.message and "directory" in f.message for f in report.errors())
assert any(
"name" in f.message and "directory" in f.message for f in report.errors()
)
def test_oversized_skill_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_oversized_skill_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
"""Codex skill exceeding 8 KB injection cap is an ERROR (was warning before round 4)."""
_patch_worktree(monkeypatch, tmp_path)
sk = tmp_path / ".codex" / "skills" / "demo"
@@ -79,7 +91,9 @@ class TestCodexValidator:
validate_codex(report)
assert any("8192" in f.message for f in report.errors())
def test_oversized_agents_md_warns(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_oversized_agents_md_warns(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
(tmp_path / "AGENTS.md").write_text("\n".join(["line"] * 200))
# Force the directory check to pass (validate_codex returns early if no .codex/)
@@ -88,7 +102,8 @@ class TestCodexValidator:
report = Report()
validate_codex(report)
assert any(
"AGENTS.md" in str(f.path) and "cap: 150" in f.message for f in report.warnings()
"AGENTS.md" in str(f.path) and "cap: 150" in f.message
for f in report.warnings()
)
@@ -128,7 +143,9 @@ class TestCursorValidator:
validate_cursor(report)
assert any("source" in f.message for f in report.errors())
def test_invalid_mdc_keys_error(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_invalid_mdc_keys_error(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
rules = tmp_path / ".cursor" / "rules"
rules.mkdir(parents=True)
@@ -150,15 +167,22 @@ class TestCursorValidator:
class TestCopilotValidator:
def test_non_string_description_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_non_string_description_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
agents = tmp_path / ".copilot" / "agents"
agents.mkdir(parents=True)
(agents / "bad.agent.md").write_text("---\nname: bad\ndescription: [oops]\n---\n\nBody.\n")
(agents / "bad.agent.md").write_text(
"---\nname: bad\ndescription: [oops]\n---\n\nBody.\n"
)
report = Report()
validate_copilot(report)
assert any("description" in f.message and "string" in f.message for f in report.errors())
assert any(
"description" in f.message and "string" in f.message
for f in report.errors()
)
def test_missing_name_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
_patch_worktree(monkeypatch, tmp_path)
@@ -184,7 +208,9 @@ class TestCopilotValidator:
validate_copilot(report)
assert any("is empty" in f.message for f in report.errors())
def test_missing_description_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_missing_description_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
agents = tmp_path / ".copilot" / "agents"
agents.mkdir(parents=True)
@@ -194,7 +220,9 @@ class TestCopilotValidator:
validate_copilot(report)
assert any("description" in f.message for f in report.errors())
def test_empty_description_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_empty_description_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
agents = tmp_path / ".copilot" / "agents"
agents.mkdir(parents=True)
@@ -218,11 +246,15 @@ class TestCopilotValidator:
validate_copilot(report)
assert not report.errors()
def test_skill_missing_name_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_skill_missing_name_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
skill_dir = tmp_path / ".copilot" / "skills" / "test__skill"
skill_dir.mkdir(parents=True)
(skill_dir / "SKILL.md").write_text("---\ndescription: Use when testing.\n---\n\nBody.\n")
(skill_dir / "SKILL.md").write_text(
"---\ndescription: Use when testing.\n---\n\nBody.\n"
)
report = Report()
validate_copilot(report)
@@ -257,7 +289,9 @@ class TestOpenCodeValidator:
validate_opencode(report)
assert any("mode" in f.message for f in report.errors())
def test_bare_model_alias_warns(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_bare_model_alias_warns(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
agents = tmp_path / ".opencode" / "agents"
agents.mkdir(parents=True)
@@ -269,7 +303,9 @@ class TestOpenCodeValidator:
validate_opencode(report)
assert any("provider-prefixed" in f.message for f in report.warnings())
def test_unknown_permission_key_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_unknown_permission_key_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
agents = tmp_path / ".opencode" / "agents"
agents.mkdir(parents=True)
@@ -305,7 +341,9 @@ class TestOpenCodeValidator:
# The nested permission's `fly_drone` must NOT show up as an invalid top-level key.
assert not any("fly_drone" in f.message for f in report.errors())
def test_invalid_permission_value_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_invalid_permission_value_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
agents = tmp_path / ".opencode" / "agents"
agents.mkdir(parents=True)
@@ -316,9 +354,14 @@ class TestOpenCodeValidator:
report = Report()
validate_opencode(report)
assert any("permission.read" in f.message and "maybe" in f.message for f in report.errors())
assert any(
"permission.read" in f.message and "maybe" in f.message
for f in report.errors()
)
def test_skill_name_mismatch_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_skill_name_mismatch_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
skill = tmp_path / ".opencode" / "skills" / "demo-hello"
skill.mkdir(parents=True)
@@ -330,7 +373,9 @@ class TestOpenCodeValidator:
validate_opencode(report)
assert any("directory" in f.message for f in report.errors())
def test_invalid_skill_name_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_invalid_skill_name_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
skill = tmp_path / ".opencode" / "skills" / "demo__hello"
skill.mkdir(parents=True)
@@ -342,7 +387,9 @@ class TestOpenCodeValidator:
validate_opencode(report)
assert any("OpenCode-safe" in f.message for f in report.errors())
def test_empty_skill_description_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_empty_skill_description_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
skill = tmp_path / ".opencode" / "skills" / "demo-hello"
skill.mkdir(parents=True)
@@ -352,7 +399,9 @@ class TestOpenCodeValidator:
validate_opencode(report)
assert any("empty description" in f.message for f in report.errors())
def test_too_long_skill_name_errors(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_too_long_skill_name_errors(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
name = "x" * 65
skill = tmp_path / ".opencode" / "skills" / name
@@ -376,23 +425,31 @@ class TestGeminiValidator:
_patch_worktree(monkeypatch, tmp_path)
cmds = tmp_path / "commands"
cmds.mkdir()
(cmds / "incomplete.toml").write_text('description = "Just a desc, no prompt"\n')
(cmds / "incomplete.toml").write_text(
'description = "Just a desc, no prompt"\n'
)
report = Report()
validate_gemini(report)
assert any("missing keys" in f.message for f in report.errors())
def test_prompt_without_args_warns(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_prompt_without_args_warns(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
cmds = tmp_path / "commands"
cmds.mkdir()
(cmds / "no_args.toml").write_text('description = "Test"\nprompt = """Run this."""\n')
(cmds / "no_args.toml").write_text(
'description = "Test"\nprompt = """Run this."""\n'
)
report = Report()
validate_gemini(report)
assert any("{{args}}" in f.message for f in report.warnings())
def test_non_gemini_model_warns(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_non_gemini_model_warns(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
agents = tmp_path / "agents"
agents.mkdir()
@@ -404,12 +461,15 @@ class TestGeminiValidator:
validate_gemini(report)
assert any("Gemini model id" in f.message for f in report.warnings())
def test_oversized_gemini_md_warns(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def test_oversized_gemini_md_warns(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_worktree(monkeypatch, tmp_path)
(tmp_path / "GEMINI.md").write_text("\n".join(["line"] * 200))
report = Report()
validate_gemini(report)
assert any(
"GEMINI.md" in str(f.path) and "cap: 150" in f.message for f in report.warnings()
"GEMINI.md" in str(f.path) and "cap: 150" in f.message
for f in report.warnings()
)
+29 -9
View File
@@ -39,7 +39,11 @@ class Finding:
remediation: str = ""
def render(self) -> str:
rel = self.path.relative_to(WORKTREE) if self.path.is_relative_to(WORKTREE) else self.path
rel = (
self.path.relative_to(WORKTREE)
if self.path.is_relative_to(WORKTREE)
else self.path
)
tail = f"\n fix: {self.remediation}" if self.remediation else ""
return f"[{self.severity}] {self.harness}: {rel}: {self.message}{tail}"
@@ -118,7 +122,9 @@ def validate_codex(report: Report) -> None:
# 1. Every agent .toml parses and has required fields.
required_agent_fields = {"name", "description", "developer_instructions"}
for toml_path in (root / "agents").glob("*.toml") if (root / "agents").is_dir() else []:
for toml_path in (
(root / "agents").glob("*.toml") if (root / "agents").is_dir() else []
):
try:
data = tomllib.loads(toml_path.read_text(encoding="utf-8"))
except tomllib.TOMLDecodeError as e:
@@ -630,8 +636,12 @@ def validate_copilot(report: Report) -> None:
remediation="Regenerate via `make generate HARNESS=copilot`.",
)
continue
_check_nonempty_str_field(report, fm, "name", "copilot", agent_md, label="agent")
_check_nonempty_str_field(report, fm, "description", "copilot", agent_md, label="agent")
_check_nonempty_str_field(
report, fm, "name", "copilot", agent_md, label="agent"
)
_check_nonempty_str_field(
report, fm, "description", "copilot", agent_md, label="agent"
)
# 3. Skills: validate .copilot/skills/*/SKILL.md exists and has valid frontmatter.
skills_dir = WORKTREE / ".copilot" / "skills"
if skills_dir.is_dir():
@@ -648,8 +658,12 @@ def validate_copilot(report: Report) -> None:
remediation="Regenerate via `make generate HARNESS=copilot`.",
)
continue
_check_nonempty_str_field(report, fm, "name", "copilot", skill_md, label="skill")
_check_nonempty_str_field(report, fm, "description", "copilot", skill_md, label="skill")
_check_nonempty_str_field(
report, fm, "name", "copilot", skill_md, label="skill"
)
_check_nonempty_str_field(
report, fm, "description", "copilot", skill_md, label="skill"
)
commands_dir = WORKTREE / ".copilot" / "commands"
if commands_dir.is_dir():
@@ -698,11 +712,15 @@ _VALIDATORS = {
def main() -> int:
parser = argparse.ArgumentParser(description="Validate generated harness artifacts.")
parser = argparse.ArgumentParser(
description="Validate generated harness artifacts."
)
parser.add_argument(
"--harness", choices=supported_harnesses(), help="Only validate one harness."
)
parser.add_argument("--strict", action="store_true", help="Exit nonzero on any warning.")
parser.add_argument(
"--strict", action="store_true", help="Exit nonzero on any warning."
)
args = parser.parse_args()
targets = [args.harness] if args.harness else supported_harnesses()
@@ -728,7 +746,9 @@ def main() -> int:
print(f.render())
print()
print(f"Total: {len(errors)} error(s), {len(warnings)} warning(s), {len(infos)} info.")
print(
f"Total: {len(errors)} error(s), {len(warnings)} warning(s), {len(infos)} info."
)
if errors:
return 1
if args.strict and warnings: