pytest-skill-engineering 1.0.2__tar.gz → 1.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. pytest_skill_engineering-1.0.4/PKG-INFO +175 -0
  2. pytest_skill_engineering-1.0.4/README.md +127 -0
  3. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/pyproject.toml +14 -14
  4. pytest_skill_engineering-1.0.4/src/pytest_skill_engineering/copilot/config.py +133 -0
  5. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/contracts.py +8 -21
  6. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/eval.py +15 -28
  7. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/events.py +72 -3
  8. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/fixtures.py +43 -43
  9. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/personas.py +23 -25
  10. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/result.py +57 -5
  11. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/runner.py +6 -1
  12. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/evals.py +34 -5
  13. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/plugin.py +2 -85
  14. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/result.py +36 -8
  15. pytest_skill_engineering-1.0.4/src/pytest_skill_engineering/core/serialization.py +128 -0
  16. pytest_skill_engineering-1.0.4/src/pytest_skill_engineering/core/validation.py +83 -0
  17. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/execution/cost.py +5 -0
  18. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/execution/servers.py +9 -9
  19. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/plugin.py +57 -29
  20. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/reporting/collector.py +10 -4
  21. pytest_skill_engineering-1.0.2/PKG-INFO +0 -182
  22. pytest_skill_engineering-1.0.2/README.md +0 -134
  23. pytest_skill_engineering-1.0.2/src/pytest_skill_engineering/copilot/config.py +0 -57
  24. pytest_skill_engineering-1.0.2/src/pytest_skill_engineering/core/serialization.py +0 -309
  25. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/.gitignore +0 -0
  26. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/LICENSE +0 -0
  27. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/__init__.py +0 -0
  28. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/__init__.py +0 -0
  29. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/api.py +0 -0
  30. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/client.py +0 -0
  31. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/controls.py +0 -0
  32. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/requests.py +0 -0
  33. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/skills.py +0 -0
  34. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/__init__.py +0 -0
  35. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/errors.py +0 -0
  36. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/prompt.py +0 -0
  37. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/skill.py +0 -0
  38. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/skill_evals.py +0 -0
  39. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/skill_grading.py +0 -0
  40. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/execution/__init__.py +0 -0
  41. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/execution/rate_limiter.py +0 -0
  42. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/execution/skill_tools.py +0 -0
  43. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/fixtures/__init__.py +0 -0
  44. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/fixtures/factories.py +0 -0
  45. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/fixtures/iteration.py +0 -0
  46. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/onboarding.py +0 -0
  47. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/plugin_options.py +0 -0
  48. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/plugin_report.py +0 -0
  49. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/reporting/__init__.py +0 -0
  50. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/reporting/generator.py +0 -0
  51. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/reporting/schema.py +0 -0
  52. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/testing/__init__.py +0 -0
  53. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/testing/banking.py +0 -0
  54. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/testing/banking_mcp.py +0 -0
  55. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/testing/todo.py +0 -0
  56. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/testing/todo_mcp.py +0 -0
  57. {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/testing/types.py +0 -0
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.5
2
+ Name: pytest-skill-engineering
3
+ Version: 1.0.4
4
+ Summary: Reliable Copilot execution and evidence for coding-agent-led testing of MCP servers, CLI tools, skills, and custom agents.
5
+ Project-URL: Homepage, https://github.com/sbroenne/pytest-skill-engineering
6
+ Project-URL: Repository, https://github.com/sbroenne/pytest-skill-engineering
7
+ Author: Stefan Brunner
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,ai,custom-agents,llm,mcp,pytest,skill-engineering,skills,testing
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: Pytest
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Testing
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: github-copilot-sdk>=1.0.28
23
+ Requires-Dist: httpx>=0.28.1
24
+ Requires-Dist: mcp>=2.3.0
25
+ Requires-Dist: pydantic>=2.14.1
26
+ Requires-Dist: pytest-asyncio>=1.4.1
27
+ Requires-Dist: pytest>=9.2.0
28
+ Requires-Dist: python-frontmatter>=1.3.0
29
+ Requires-Dist: tomlkit>=0.15.2
30
+ Requires-Dist: trio>=0.34.0
31
+ Requires-Dist: websockets>=17.3
32
+ Provides-Extra: dev
33
+ Requires-Dist: pre-commit>=4.7.0; extra == 'dev'
34
+ Requires-Dist: pyright>=1.1.414; extra == 'dev'
35
+ Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
36
+ Requires-Dist: python-dotenv>=1.2.4; extra == 'dev'
37
+ Requires-Dist: ruff>=0.17.3; extra == 'dev'
38
+ Requires-Dist: typeguard>=4.6.0; extra == 'dev'
39
+ Provides-Extra: docs
40
+ Requires-Dist: cairosvg>=2.9.1; extra == 'docs'
41
+ Requires-Dist: mkdocs-material<9.8,>=9.7.7; extra == 'docs'
42
+ Requires-Dist: mkdocs>=1.6.1; extra == 'docs'
43
+ Requires-Dist: mkdocstrings[python]>=1.0.8; extra == 'docs'
44
+ Requires-Dist: pillow>=13.0.0; extra == 'docs'
45
+ Provides-Extra: test
46
+ Requires-Dist: syrupy<7,>=6.1.1; extra == 'test'
47
+ Description-Content-Type: text/markdown
48
+
49
+ # pytest-skill-engineering
50
+
51
+ [![PyPI version](https://img.shields.io/pypi/v/pytest-skill-engineering)](https://pypi.org/project/pytest-skill-engineering/)
52
+ [![CI](https://github.com/sbroenne/pytest-skill-engineering/actions/workflows/ci.yml/badge.svg)](https://github.com/sbroenne/pytest-skill-engineering/actions/workflows/ci.yml)
53
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
54
+
55
+ **Test whether GitHub Copilot can complete tasks with your tools and instructions.**
56
+
57
+ pytest-skill-engineering runs real Copilot sessions from pytest. Give Copilot a
58
+ task, check the actual output with ordinary assertions, and save the execution
59
+ as JSON.
60
+
61
+ Use it to test MCP servers, CLI tools, skills, system prompts, plugins, and custom
62
+ agents. It answers a different question from your tool's unit tests: **can
63
+ Copilot use the interface correctly?** A working tool can still have confusing
64
+ descriptions or arguments that lead the agent to the wrong result.
65
+
66
+ You define what correctness means. The framework manages the Copilot session,
67
+ execution limits, usage, and evidence; it does not grade answers with another AI.
68
+
69
+ ## Quick start
70
+
71
+ You need Python 3.11+, [uv](https://docs.astral.sh/uv/), and a GitHub account with
72
+ Copilot access.
73
+
74
+ Run these commands in an existing Python project with a `pyproject.toml`:
75
+
76
+ ```bash
77
+ uv add pytest-skill-engineering
78
+ gh auth login --hostname github.com
79
+ uv run pytest-skill-engineering init
80
+ uv run pytest-skill-engineering doctor
81
+ uv run python -m pytest tests/test_copilot_eval.py -v
82
+ ```
83
+
84
+ `init` creates a test using the bundled Todo MCP server, configures JSON output,
85
+ and adds starter-model rates to `pricing.toml`. It reports conflicts rather than
86
+ overwriting existing files or settings. `doctor` checks setup and Copilot access.
87
+ Real task execution may consume Copilot premium requests.
88
+
89
+ The test checks the tool's arguments and returned task state. pytest prints the
90
+ result; `aitest-reports/results.json` preserves the execution. See the
91
+ [setup guide](docs/getting-started/index.md) or the standalone
92
+ [quickstart project](examples/quickstart/).
93
+
94
+ ## A test that checks the result
95
+
96
+ ```python
97
+ from __future__ import annotations
98
+
99
+ import json
100
+
101
+ from pytest_skill_engineering.copilot import CopilotEval
102
+
103
+
104
+ async def test_order_total(copilot_eval, tmp_path):
105
+ agent = CopilotEval(
106
+ name="order-total",
107
+ model="gpt-5.6-sol",
108
+ instructions="Save the requested output as JSON. Use integer cents.",
109
+ working_directory=str(tmp_path),
110
+ )
111
+ result = await copilot_eval(
112
+ agent,
113
+ "Save order.json for two items at 1250 cents each plus 20% tax. "
114
+ "Include subtotal_cents, tax_cents, and total_cents.",
115
+ )
116
+ assert result.success, result.error
117
+ output = tmp_path / "order.json"
118
+ assert output.is_file()
119
+ assert json.loads(output.read_text(encoding="utf-8")) == {
120
+ "subtotal_cents": 2500,
121
+ "tax_cents": 500,
122
+ "total_cents": 3000,
123
+ }
124
+ ```
125
+
126
+ `CopilotEval` configures the session. The second argument to `copilot_eval` is
127
+ the user task. `result.success` checks execution completion; the file assertions
128
+ check that the task produced the right result.
129
+
130
+ For your own tools, check the resulting application state, saved files, or
131
+ operation output. A tool call or a model's claim is not proof of task success.
132
+ Use approved isolation for live tests: a temporary directory is not a sandbox,
133
+ and generated code or tool calls can act with your user's access.
134
+
135
+ ## What the framework provides
136
+
137
+ | You need | Use |
138
+ | --- | --- |
139
+ | Real task execution | `CopilotEval` and the `copilot_eval` pytest fixture |
140
+ | Controlled runs | Timeouts, tool budgets, request controls, and permission settings |
141
+ | Debugging evidence | Configuration, calls, arguments, outputs, errors, and completion records |
142
+ | Repeatable comparisons | `ab_run` and pytest repetitions |
143
+ | Usage visibility | Captured tokens, Copilot premium requests, and explicit price estimates |
144
+ | Saved results | Native JSON with execution evidence and ordinary pytest outcomes |
145
+
146
+ Your tests supply application fixtures, guarded tool adapters, and independent
147
+ output checks. The framework makes one execution attempt per call; it does not
148
+ retry failures automatically. Missing usage, prices, and incomplete evidence
149
+ remain visible.
150
+
151
+ ## Investigate and compare
152
+
153
+ Give your coding agent a failed test, its JSON evidence, the requirements, and
154
+ the relevant source. It can investigate the failure and help you change the
155
+ interface. Keep the success criteria fixed when rerunning a comparison.
156
+
157
+ For A/B tests, record verification separately for each side: both saved
158
+ executions share one pytest outcome. That outcome alone cannot establish
159
+ independent pass rates or a causal improvement. See
160
+ [comparing configurations](docs/getting-started/comparing.md).
161
+
162
+ ## Documentation
163
+
164
+ - [Getting started](https://sbroenne.github.io/pytest-skill-engineering/getting-started/)
165
+ - [Task guides](docs/how-to/index.md) and [API reference](docs/reference/api.md)
166
+ - [Examples](examples/README.md), including historical skill comparisons
167
+ - [Upgrading from 0.6.x](docs/migration.md): 1.0 removed AI judging, renderers,
168
+ rankings, and skill refinement
169
+
170
+ No framework companion skill is required or distributed. Testing your own
171
+ domain skills is supported.
172
+
173
+ ## License
174
+
175
+ MIT. Inspired by [agent-benchmark](https://github.com/mykhaliev/agent-benchmark).
@@ -0,0 +1,127 @@
1
+ # pytest-skill-engineering
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/pytest-skill-engineering)](https://pypi.org/project/pytest-skill-engineering/)
4
+ [![CI](https://github.com/sbroenne/pytest-skill-engineering/actions/workflows/ci.yml/badge.svg)](https://github.com/sbroenne/pytest-skill-engineering/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+
7
+ **Test whether GitHub Copilot can complete tasks with your tools and instructions.**
8
+
9
+ pytest-skill-engineering runs real Copilot sessions from pytest. Give Copilot a
10
+ task, check the actual output with ordinary assertions, and save the execution
11
+ as JSON.
12
+
13
+ Use it to test MCP servers, CLI tools, skills, system prompts, plugins, and custom
14
+ agents. It answers a different question from your tool's unit tests: **can
15
+ Copilot use the interface correctly?** A working tool can still have confusing
16
+ descriptions or arguments that lead the agent to the wrong result.
17
+
18
+ You define what correctness means. The framework manages the Copilot session,
19
+ execution limits, usage, and evidence; it does not grade answers with another AI.
20
+
21
+ ## Quick start
22
+
23
+ You need Python 3.11+, [uv](https://docs.astral.sh/uv/), and a GitHub account with
24
+ Copilot access.
25
+
26
+ Run these commands in an existing Python project with a `pyproject.toml`:
27
+
28
+ ```bash
29
+ uv add pytest-skill-engineering
30
+ gh auth login --hostname github.com
31
+ uv run pytest-skill-engineering init
32
+ uv run pytest-skill-engineering doctor
33
+ uv run python -m pytest tests/test_copilot_eval.py -v
34
+ ```
35
+
36
+ `init` creates a test using the bundled Todo MCP server, configures JSON output,
37
+ and adds starter-model rates to `pricing.toml`. It reports conflicts rather than
38
+ overwriting existing files or settings. `doctor` checks setup and Copilot access.
39
+ Real task execution may consume Copilot premium requests.
40
+
41
+ The test checks the tool's arguments and returned task state. pytest prints the
42
+ result; `aitest-reports/results.json` preserves the execution. See the
43
+ [setup guide](docs/getting-started/index.md) or the standalone
44
+ [quickstart project](examples/quickstart/).
45
+
46
+ ## A test that checks the result
47
+
48
+ ```python
49
+ from __future__ import annotations
50
+
51
+ import json
52
+
53
+ from pytest_skill_engineering.copilot import CopilotEval
54
+
55
+
56
+ async def test_order_total(copilot_eval, tmp_path):
57
+ agent = CopilotEval(
58
+ name="order-total",
59
+ model="gpt-5.6-sol",
60
+ instructions="Save the requested output as JSON. Use integer cents.",
61
+ working_directory=str(tmp_path),
62
+ )
63
+ result = await copilot_eval(
64
+ agent,
65
+ "Save order.json for two items at 1250 cents each plus 20% tax. "
66
+ "Include subtotal_cents, tax_cents, and total_cents.",
67
+ )
68
+ assert result.success, result.error
69
+ output = tmp_path / "order.json"
70
+ assert output.is_file()
71
+ assert json.loads(output.read_text(encoding="utf-8")) == {
72
+ "subtotal_cents": 2500,
73
+ "tax_cents": 500,
74
+ "total_cents": 3000,
75
+ }
76
+ ```
77
+
78
+ `CopilotEval` configures the session. The second argument to `copilot_eval` is
79
+ the user task. `result.success` checks execution completion; the file assertions
80
+ check that the task produced the right result.
81
+
82
+ For your own tools, check the resulting application state, saved files, or
83
+ operation output. A tool call or a model's claim is not proof of task success.
84
+ Use approved isolation for live tests: a temporary directory is not a sandbox,
85
+ and generated code or tool calls can act with your user's access.
86
+
87
+ ## What the framework provides
88
+
89
+ | You need | Use |
90
+ | --- | --- |
91
+ | Real task execution | `CopilotEval` and the `copilot_eval` pytest fixture |
92
+ | Controlled runs | Timeouts, tool budgets, request controls, and permission settings |
93
+ | Debugging evidence | Configuration, calls, arguments, outputs, errors, and completion records |
94
+ | Repeatable comparisons | `ab_run` and pytest repetitions |
95
+ | Usage visibility | Captured tokens, Copilot premium requests, and explicit price estimates |
96
+ | Saved results | Native JSON with execution evidence and ordinary pytest outcomes |
97
+
98
+ Your tests supply application fixtures, guarded tool adapters, and independent
99
+ output checks. The framework makes one execution attempt per call; it does not
100
+ retry failures automatically. Missing usage, prices, and incomplete evidence
101
+ remain visible.
102
+
103
+ ## Investigate and compare
104
+
105
+ Give your coding agent a failed test, its JSON evidence, the requirements, and
106
+ the relevant source. It can investigate the failure and help you change the
107
+ interface. Keep the success criteria fixed when rerunning a comparison.
108
+
109
+ For A/B tests, record verification separately for each side: both saved
110
+ executions share one pytest outcome. That outcome alone cannot establish
111
+ independent pass rates or a causal improvement. See
112
+ [comparing configurations](docs/getting-started/comparing.md).
113
+
114
+ ## Documentation
115
+
116
+ - [Getting started](https://sbroenne.github.io/pytest-skill-engineering/getting-started/)
117
+ - [Task guides](docs/how-to/index.md) and [API reference](docs/reference/api.md)
118
+ - [Examples](examples/README.md), including historical skill comparisons
119
+ - [Upgrading from 0.6.x](docs/migration.md): 1.0 removed AI judging, renderers,
120
+ rankings, and skill refinement
121
+
122
+ No framework companion skill is required or distributed. Testing your own
123
+ domain skills is supported.
124
+
125
+ ## License
126
+
127
+ MIT. Inspired by [agent-benchmark](https://github.com/mykhaliev/agent-benchmark).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pytest-skill-engineering"
7
- version = "1.0.2"
7
+ version = "1.0.4"
8
8
  description = "Reliable Copilot execution and evidence for coding-agent-led testing of MCP servers, CLI tools, skills, and custom agents."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -26,25 +26,25 @@ classifiers = [
26
26
  "Topic :: Software Development :: Testing",
27
27
  ]
28
28
  dependencies = [
29
- "pytest>=9.1.1",
30
- "mcp>=2.2.0",
31
- "pydantic>=2.13.5",
29
+ "pytest>=9.2.0",
30
+ "mcp>=2.3.0",
31
+ "pydantic>=2.14.1",
32
32
  "python-frontmatter>=1.3.0",
33
- "github-copilot-sdk>=1.0.16",
33
+ "github-copilot-sdk>=1.0.28",
34
34
  "httpx>=0.28.1",
35
35
  "trio>=0.34.0",
36
- "tomlkit>=0.15.1",
37
- "pytest-asyncio>=1.4.0",
38
- "websockets>=17.1",
36
+ "tomlkit>=0.15.2",
37
+ "pytest-asyncio>=1.4.1",
38
+ "websockets>=17.3",
39
39
  ]
40
40
 
41
41
  [project.optional-dependencies]
42
42
  dev = [
43
43
  "pytest-cov>=7.1.0",
44
- "python-dotenv>=1.2.3",
45
- "ruff>=0.16.9",
44
+ "python-dotenv>=1.2.4",
45
+ "ruff>=0.17.3",
46
46
  "pyright>=1.1.414",
47
- "pre-commit>=4.6.2",
47
+ "pre-commit>=4.7.0",
48
48
  "typeguard>=4.6.0",
49
49
  ]
50
50
  test = [
@@ -53,8 +53,8 @@ test = [
53
53
  docs = [
54
54
  "mkdocs>=1.6.1",
55
55
  "mkdocs-material>=9.7.7,<9.8",
56
- "mkdocstrings[python]>=1.0.6",
57
- "pillow>=12.3.0",
56
+ "mkdocstrings[python]>=1.0.8",
57
+ "pillow>=13.0.0",
58
58
  "cairosvg>=2.9.1",
59
59
  ]
60
60
 
@@ -98,7 +98,7 @@ quote-style = "double"
98
98
  indent-style = "space"
99
99
 
100
100
  [tool.pyright]
101
- include = ["src", "tests", "examples/skill-dogfood"]
101
+ include = ["src", "tests", "examples/skill-dogfood", "examples/excel-skill-value"]
102
102
  pythonVersion = "3.11"
103
103
  typeCheckingMode = "basic"
104
104
 
@@ -0,0 +1,133 @@
1
+ """Configuration file loaders for Copilot and Claude Code projects.
2
+
3
+ Provides utilities for loading MCP server configurations from standard
4
+ config files (``.mcp.json``, ``.vscode/mcp.json``).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from copy import deepcopy
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from pytest_skill_engineering.core.plugin import _read_json
14
+ from pytest_skill_engineering.core.validation import _validate_mcp_servers
15
+
16
+ if TYPE_CHECKING:
17
+ from pytest_skill_engineering.copilot.contracts import CopilotEvalConfig
18
+
19
+
20
+ def snapshot_session_configuration(
21
+ agent: CopilotEvalConfig, session_config: dict[str, Any]
22
+ ) -> dict[str, Any]:
23
+ """Capture prepared settings without handlers, credentials, or server environments."""
24
+ system = session_config.get("system_message") or {}
25
+ snapshot = {
26
+ "name": agent.name,
27
+ "model": session_config.get("model"),
28
+ "instructions": system.get("content"),
29
+ "system_message_mode": system.get("mode"),
30
+ "reasoning_effort": session_config.get("reasoning_effort"),
31
+ "client_mode": agent.client_mode,
32
+ "image_detail": agent.image_detail,
33
+ "audit_requests": agent.audit_requests,
34
+ "max_tool_calls": agent.max_tool_calls,
35
+ "max_turns": agent.max_turns,
36
+ "timeout_s": agent.timeout_s,
37
+ "auto_confirm": agent.auto_confirm,
38
+ "allowed_tools": session_config.get("available_tools"),
39
+ "excluded_tools": session_config.get("excluded_tools"),
40
+ "skill_directories": session_config.get("skill_directories", []),
41
+ "disabled_skills": session_config.get("disabled_skills", []),
42
+ "active_agent": session_config.get("agent"),
43
+ "working_directory": session_config.get("working_directory"),
44
+ "persona": type(agent.persona).__name__,
45
+ "tools": [
46
+ {
47
+ "name": tool.name,
48
+ "description": tool.description,
49
+ "parameters": tool.parameters,
50
+ "metadata": tool.metadata,
51
+ }
52
+ for tool in session_config.get("tools") or []
53
+ ],
54
+ "mcp_servers": {
55
+ name: {"type": config.get("type"), "tools": config.get("tools")}
56
+ for name, config in (session_config.get("mcp_servers") or {}).items()
57
+ },
58
+ "custom_agents": [
59
+ {
60
+ key: value
61
+ for key, value in custom.items()
62
+ if key
63
+ in {
64
+ "name",
65
+ "prompt",
66
+ "description",
67
+ "display_name",
68
+ "model",
69
+ "reasoning_effort",
70
+ "tools",
71
+ "infer",
72
+ "skills",
73
+ }
74
+ }
75
+ for custom in session_config.get("custom_agents") or []
76
+ ],
77
+ }
78
+ for key in (
79
+ "enable_config_discovery",
80
+ "enable_file_hooks",
81
+ "enable_on_demand_instruction_discovery",
82
+ "skip_custom_instructions",
83
+ "enable_managed_settings",
84
+ "enable_skills",
85
+ ):
86
+ if key in session_config:
87
+ snapshot[key] = session_config[key]
88
+ return deepcopy(snapshot)
89
+
90
+
91
+ def load_mcp_config(path: str | Path) -> dict[str, dict[str, Any]]:
92
+ """Load MCP server configs from a ``.mcp.json`` file.
93
+
94
+ Supports both Claude Code / root-level ``.mcp.json`` and VS Code
95
+ ``.vscode/mcp.json`` formats. Handles two common top-level keys:
96
+
97
+ * ``mcpServers`` — Claude Code / standard MCP convention
98
+ * ``servers`` — VS Code ``mcp.json`` convention
99
+
100
+ Exactly one supported key must be present. An explicit empty mapping is valid;
101
+ missing keys, conflicting keys, and malformed server definitions raise errors.
102
+
103
+ Returns:
104
+ Dict of ``server_name → config`` compatible with
105
+ ``CopilotEval(mcp_servers=...)``.
106
+
107
+ Raises:
108
+ FileNotFoundError: If the file does not exist.
109
+ ValueError: If the file is not valid JSON or has unexpected structure.
110
+
111
+ Example::
112
+
113
+ from pytest_skill_engineering.copilot.config import load_mcp_config
114
+
115
+ servers = load_mcp_config(".mcp.json")
116
+ agent = CopilotEval(mcp_servers=servers)
117
+ """
118
+ path = Path(path)
119
+ if not path.is_file():
120
+ msg = f"MCP config file not found: {path}"
121
+ raise FileNotFoundError(msg)
122
+
123
+ raw = _read_json(path)
124
+
125
+ if not isinstance(raw, dict):
126
+ msg = f"{path}: MCP config must be a JSON object, got {type(raw).__name__}"
127
+ raise ValueError(msg)
128
+
129
+ fields = [key for key in ("mcpServers", "servers") if key in raw]
130
+ if len(fields) != 1:
131
+ raise ValueError(f"{path}: specify exactly one of 'mcpServers' and 'servers'")
132
+
133
+ return _validate_mcp_servers(raw[fields[0]], path, field=fields[0])
@@ -3,24 +3,16 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Awaitable, Callable, Mapping
6
- from dataclasses import dataclass
7
6
  from typing import Any, Literal, Protocol, TypeAlias
8
7
 
9
8
  from copilot.session import CustomAgentConfig, MCPServerConfig, ReasoningEffort, SessionHooks
10
9
 
10
+ from pytest_skill_engineering.copilot.result import CopilotResult, CopilotResultAgent
11
+
11
12
  CopilotCustomAgentConfig: TypeAlias = CustomAgentConfig
12
13
  CopilotMCPServerConfig: TypeAlias = MCPServerConfig
13
14
  CopilotReasoningEffort: TypeAlias = ReasoningEffort
14
15
  CopilotSessionHooks: TypeAlias = SessionHooks
15
- SubagentStatus: TypeAlias = Literal["selected", "started", "completed", "failed"]
16
-
17
-
18
- class CopilotResultAgent(Protocol):
19
- """Minimal agent surface stored on :class:`CopilotResult`."""
20
-
21
- @property
22
- def working_directory(self) -> str | None:
23
- raise NotImplementedError
24
16
 
25
17
 
26
18
  class CopilotRunResult(Protocol):
@@ -53,6 +45,11 @@ class CopilotEventMapper(Protocol):
53
45
  def record_subagent_failed(self, *, invocation_id: str, name: str) -> None:
54
46
  raise NotImplementedError
55
47
 
48
+ def record_subagent_result(
49
+ self, *, invocation_id: str, name: str, result: CopilotResult
50
+ ) -> None:
51
+ raise NotImplementedError
52
+
56
53
 
57
54
  class CopilotEvalConfig(CopilotResultAgent, Protocol):
58
55
  """Configuration surface consumed by the runner and personas."""
@@ -141,7 +138,7 @@ class CopilotEvalConfig(CopilotResultAgent, Protocol):
141
138
 
142
139
  CopilotNestedRunner: TypeAlias = Callable[
143
140
  [CopilotEvalConfig, str],
144
- Awaitable[CopilotRunResult],
141
+ Awaitable["CopilotResult"],
145
142
  ]
146
143
 
147
144
 
@@ -158,16 +155,6 @@ class CopilotPersona(Protocol):
158
155
  raise NotImplementedError
159
156
 
160
157
 
161
- @dataclass(slots=True)
162
- class SubagentInvocation:
163
- """A single custom-agent dispatch observed during a run."""
164
-
165
- invocation_id: str
166
- name: str
167
- status: SubagentStatus
168
- duration_ms: float | None = None
169
-
170
-
171
158
  def declared_agent_tools(
172
159
  agent_config: CopilotCustomAgentConfig,
173
160
  ) -> list[str] | None:
@@ -18,7 +18,6 @@ from pytest_skill_engineering.copilot.personas import ClaudeCodePersona, VSCodeP
18
18
  from pytest_skill_engineering.core.evals import load_custom_agent, load_instruction_file
19
19
  from pytest_skill_engineering.core.plugin import (
20
20
  _discover_skills,
21
- _validate_mcp_servers,
22
21
  _validate_skill_reference_names,
23
22
  )
24
23
 
@@ -34,30 +33,20 @@ def _parse_agent_file(path: Path) -> CopilotCustomAgentConfig:
34
33
  fields and uses the Markdown body as the custom agent's system prompt.
35
34
  Parsing and metadata errors retain the definition's file path.
36
35
  """
37
- loaded = load_custom_agent(path)
38
- frontmatter = loaded["metadata"]
39
- agent: CopilotCustomAgentConfig = {
40
- "name": frontmatter.get("name", loaded["name"]),
41
- "prompt": loaded["prompt"],
42
- }
43
- for key in (
44
- "description",
45
- "display_name",
46
- "infer",
47
- "skills",
48
- "model",
49
- "reasoning_effort",
50
- "tools",
51
- ):
52
- if key in frontmatter:
53
- agent[key] = frontmatter[key]
54
- if "mcp-servers" in frontmatter:
55
- agent["mcp_servers"] = cast(
56
- dict[str, CopilotMCPServerConfig],
57
- _validate_mcp_servers(frontmatter["mcp-servers"], path, field="mcp-servers"),
58
- )
36
+ return cast(CopilotCustomAgentConfig, load_custom_agent(path))
37
+
59
38
 
60
- return agent
39
+ def _parse_agent_files(paths: list[Path]) -> list[CopilotCustomAgentConfig]:
40
+ agents: list[CopilotCustomAgentConfig] = []
41
+ names: set[str] = set()
42
+ for path in paths:
43
+ agent = _parse_agent_file(path)
44
+ name = require_custom_agent_name(agent)
45
+ if name in names:
46
+ raise ValueError(f"{path}: Duplicate custom agent name '{name}'")
47
+ names.add(name)
48
+ agents.append(agent)
49
+ return agents
61
50
 
62
51
 
63
52
  def _default_persona() -> CopilotPersona:
@@ -361,8 +350,7 @@ class CopilotEval:
361
350
  if agents_dir.exists():
362
351
  if not agents_dir.is_dir():
363
352
  raise ValueError(f"{agents_dir}: custom agents path must be a directory")
364
- for agent_file in sorted(agents_dir.rglob("*.agent.md")):
365
- agents.append(_parse_agent_file(agent_file))
353
+ agents = _parse_agent_files(sorted(agents_dir.rglob("*.agent.md")))
366
354
 
367
355
  config: dict[str, Any] = {
368
356
  "instructions": instructions,
@@ -527,8 +515,7 @@ class CopilotEval:
527
515
  if not agents_dir.is_dir():
528
516
  raise ValueError(f"{agents_dir}: custom agents path must be a directory")
529
517
  # Claude Code uses plain .md files for agents
530
- for agent_file in sorted(agents_dir.glob("*.md")):
531
- agents.append(_parse_agent_file(agent_file))
518
+ agents = _parse_agent_files(sorted(agents_dir.glob("*.md")))
532
519
 
533
520
  # 3. Discover skill directories from .claude/skills/
534
521
  skills = _discover_skills(claude_dir)