skillroll 0.2.0__tar.gz → 0.3.0__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.
- {skillroll-0.2.0 → skillroll-0.3.0}/.gitignore +1 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/PKG-INFO +43 -11
- {skillroll-0.2.0 → skillroll-0.3.0}/README.md +42 -10
- {skillroll-0.2.0 → skillroll-0.3.0}/examples/README.md +11 -6
- skillroll-0.3.0/examples/release-gate-comparison/README.md +82 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/_version.py +1 -1
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/artifacts/records.py +29 -11
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/artifacts/store.py +133 -2
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/cli.py +69 -2
- skillroll-0.3.0/src/skillroll/commands/compare.py +1672 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/commands/evaluate.py +476 -35
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/config.py +11 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/inference/profile.py +1 -1
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/inference/transport.py +7 -7
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/initialization/templates.py +6 -2
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/models.py +1 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/runtime/attempt.py +42 -8
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/verdicts.py +5 -1
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/world/session.py +88 -45
- {skillroll-0.2.0 → skillroll-0.3.0}/LICENSE +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/docs/demo/README.md +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/examples/release-action-boundary/README.md +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/plugins/harness-prompts/skills/executor-prompt/references/omission.md +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/plugins/harness-prompts/skills/executor-prompt/references/system.md +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/plugins/harness-prompts/skills/semantic-judge-prompt/references/system.md +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/plugins/harness-prompts/skills/world-simulator-prompt/references/system.md +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/pyproject.toml +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/__init__.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/__main__.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/_assets/setup-mascot.ansi +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/artifacts/__init__.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/artifacts/hashes.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/assertions.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/checks.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/commands/__init__.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/commands/create_case.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/commands/doctor.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/commands/initialize.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/commands/validate.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/diagnostics.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/discovery.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/evals.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/github.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/github_action.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/github_workflow.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/guards.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/impact.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/inference/__init__.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/inference/openrouter.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/inference/preflight.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/initialization/__init__.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/initialization/discovery.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/initialization/transaction.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/judge.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/markdown.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/marketplace.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/outcomes.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/paths.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/prompt_resources.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/py.typed +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/release.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/repository_io.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/runtime/__init__.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/runtime/agents_sdk.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/runtime/execution.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/safe_yaml.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/validation.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/world/__init__.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/world/bundle.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/world/model.py +0 -0
- {skillroll-0.2.0 → skillroll-0.3.0}/src/skillroll/world/rules.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: skillroll
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A simple, drop-in eval harness for Agent Skills
|
|
5
5
|
Author: SkillRoll contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -152,6 +152,13 @@ skillroll requires Python 3.12 or later and
|
|
|
152
152
|
uv tool install skillroll
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
+
The published `v0.2.0` CLI does not yet include the `executor_model` setting or
|
|
156
|
+
the `compare` command described below; these features are in the upcoming
|
|
157
|
+
release. To try them from a SkillRoll source checkout containing this change,
|
|
158
|
+
run `uv sync --locked` in that checkout and invoke SkillRoll with `uv run`
|
|
159
|
+
(include `--repo PATH` when evaluating another repository). The rest of this
|
|
160
|
+
Quickstart uses the released CLI.
|
|
161
|
+
|
|
155
162
|
Set up skillroll interactively and create `skillroll.toml` at the repository
|
|
156
163
|
root. It automatically identifies the skills folder and helps configure the
|
|
157
164
|
inference endpoint and API-key environment variable.
|
|
@@ -207,8 +214,8 @@ The command prints its verdict and saves the report under the repository's
|
|
|
207
214
|
## Inference
|
|
208
215
|
|
|
209
216
|
Running an eval requires an API key for a compatible inference endpoint.
|
|
210
|
-
skillroll
|
|
211
|
-
tool calling and strict JSON Schema structured outputs.
|
|
217
|
+
skillroll supports OpenAI-compatible Chat Completions and Responses endpoints
|
|
218
|
+
with tool calling and strict JSON Schema structured outputs.
|
|
212
219
|
|
|
213
220
|
If you entered model settings during `init`, they are already in
|
|
214
221
|
`skillroll.toml`. Otherwise, add them now:
|
|
@@ -223,15 +230,14 @@ api_key_env = "SKILLROLL_API_KEY"
|
|
|
223
230
|
### OpenRouter
|
|
224
231
|
|
|
225
232
|
If you do not yet have an inference provider,
|
|
226
|
-
[OpenRouter](https://openrouter.ai/)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Our go-to model is
|
|
231
|
-
[`openai/gpt-5.6-luna-pro`](https://openrouter.ai/openai/gpt-5.6-luna-pro),
|
|
232
|
-
which is cost-effective and performant enough for even complex evals.
|
|
233
|
+
[OpenRouter](https://openrouter.ai/) offers multiple models through one
|
|
234
|
+
endpoint, with account spending controls. Model availability and free-route
|
|
235
|
+
capacity vary.
|
|
233
236
|
|
|
234
|
-
|
|
237
|
+
The repository keeps
|
|
238
|
+
[`openai/gpt-5.6-luna-pro`](https://openrouter.ai/openai/gpt-5.6-luna-pro)
|
|
239
|
+
as its existing evaluator baseline. This is a reference configuration, not a
|
|
240
|
+
claim that it is the cheapest or best model for every case:
|
|
235
241
|
|
|
236
242
|
```toml
|
|
237
243
|
[inference]
|
|
@@ -240,6 +246,10 @@ model = "openai/gpt-5.6-luna-pro"
|
|
|
240
246
|
api_key_env = "SKILLROLL_API_KEY"
|
|
241
247
|
```
|
|
242
248
|
|
|
249
|
+
For the upcoming executor-comparison workflow, see the
|
|
250
|
+
[tested GPT-6 Luna candidate setup](docs/comparing-models.md#a-current-openrouter-candidate).
|
|
251
|
+
It keeps the evaluator fixed while you assess a current lower-cost skill model.
|
|
252
|
+
|
|
243
253
|
Or the free tier:
|
|
244
254
|
|
|
245
255
|
```toml
|
|
@@ -290,6 +300,27 @@ history.
|
|
|
290
300
|
| `INCOMPLETE` | A required repository check did not run. |
|
|
291
301
|
| `ERROR` | skillroll could not produce a trustworthy verdict. |
|
|
292
302
|
|
|
303
|
+
## Compare models that run the skill
|
|
304
|
+
|
|
305
|
+
After you have a regression case, compare candidate models on the same case
|
|
306
|
+
while keeping the configured model fixed for the Dungeon Master and judge.
|
|
307
|
+
From a SkillRoll source checkout containing this change, run:
|
|
308
|
+
|
|
309
|
+
```shell
|
|
310
|
+
uv run skillroll compare \
|
|
311
|
+
--repo /path/to/your-repository \
|
|
312
|
+
--executor-model provider/baseline \
|
|
313
|
+
--executor-model provider/candidate \
|
|
314
|
+
--samples 5 \
|
|
315
|
+
--case path/to/eval.md
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The comparison makes real model calls and may incur provider charges. Its
|
|
319
|
+
report summarizes observed outcomes; cost estimates require reported token
|
|
320
|
+
usage and configured rates. Results are evidence for these cases, not proof
|
|
321
|
+
that a lower-cost candidate matches the baseline. See the
|
|
322
|
+
[comparison guide](docs/comparing-models.md) for details.
|
|
323
|
+
|
|
293
324
|
## Add GitHub Actions
|
|
294
325
|
|
|
295
326
|
After `skillroll.toml` exists, generate the advisory workflow:
|
|
@@ -357,6 +388,7 @@ today; expect the interface to change between minor releases.
|
|
|
357
388
|
- [Author and audit good skills and evals](docs/authoring.md)
|
|
358
389
|
- [Write an eval](docs/writing-evals.md)
|
|
359
390
|
- [Configure models and cost](docs/configuration.md)
|
|
391
|
+
- [Compare skill executor models](docs/comparing-models.md)
|
|
360
392
|
- [Understand results and fix problems](docs/results.md)
|
|
361
393
|
- [Add advisory GitHub checks](docs/github-actions.md)
|
|
362
394
|
- [Review the security model](docs/security.md)
|
|
@@ -136,6 +136,13 @@ skillroll requires Python 3.12 or later and
|
|
|
136
136
|
uv tool install skillroll
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
The published `v0.2.0` CLI does not yet include the `executor_model` setting or
|
|
140
|
+
the `compare` command described below; these features are in the upcoming
|
|
141
|
+
release. To try them from a SkillRoll source checkout containing this change,
|
|
142
|
+
run `uv sync --locked` in that checkout and invoke SkillRoll with `uv run`
|
|
143
|
+
(include `--repo PATH` when evaluating another repository). The rest of this
|
|
144
|
+
Quickstart uses the released CLI.
|
|
145
|
+
|
|
139
146
|
Set up skillroll interactively and create `skillroll.toml` at the repository
|
|
140
147
|
root. It automatically identifies the skills folder and helps configure the
|
|
141
148
|
inference endpoint and API-key environment variable.
|
|
@@ -191,8 +198,8 @@ The command prints its verdict and saves the report under the repository's
|
|
|
191
198
|
## Inference
|
|
192
199
|
|
|
193
200
|
Running an eval requires an API key for a compatible inference endpoint.
|
|
194
|
-
skillroll
|
|
195
|
-
tool calling and strict JSON Schema structured outputs.
|
|
201
|
+
skillroll supports OpenAI-compatible Chat Completions and Responses endpoints
|
|
202
|
+
with tool calling and strict JSON Schema structured outputs.
|
|
196
203
|
|
|
197
204
|
If you entered model settings during `init`, they are already in
|
|
198
205
|
`skillroll.toml`. Otherwise, add them now:
|
|
@@ -207,15 +214,14 @@ api_key_env = "SKILLROLL_API_KEY"
|
|
|
207
214
|
### OpenRouter
|
|
208
215
|
|
|
209
216
|
If you do not yet have an inference provider,
|
|
210
|
-
[OpenRouter](https://openrouter.ai/)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
Our go-to model is
|
|
215
|
-
[`openai/gpt-5.6-luna-pro`](https://openrouter.ai/openai/gpt-5.6-luna-pro),
|
|
216
|
-
which is cost-effective and performant enough for even complex evals.
|
|
217
|
+
[OpenRouter](https://openrouter.ai/) offers multiple models through one
|
|
218
|
+
endpoint, with account spending controls. Model availability and free-route
|
|
219
|
+
capacity vary.
|
|
217
220
|
|
|
218
|
-
|
|
221
|
+
The repository keeps
|
|
222
|
+
[`openai/gpt-5.6-luna-pro`](https://openrouter.ai/openai/gpt-5.6-luna-pro)
|
|
223
|
+
as its existing evaluator baseline. This is a reference configuration, not a
|
|
224
|
+
claim that it is the cheapest or best model for every case:
|
|
219
225
|
|
|
220
226
|
```toml
|
|
221
227
|
[inference]
|
|
@@ -224,6 +230,10 @@ model = "openai/gpt-5.6-luna-pro"
|
|
|
224
230
|
api_key_env = "SKILLROLL_API_KEY"
|
|
225
231
|
```
|
|
226
232
|
|
|
233
|
+
For the upcoming executor-comparison workflow, see the
|
|
234
|
+
[tested GPT-6 Luna candidate setup](docs/comparing-models.md#a-current-openrouter-candidate).
|
|
235
|
+
It keeps the evaluator fixed while you assess a current lower-cost skill model.
|
|
236
|
+
|
|
227
237
|
Or the free tier:
|
|
228
238
|
|
|
229
239
|
```toml
|
|
@@ -274,6 +284,27 @@ history.
|
|
|
274
284
|
| `INCOMPLETE` | A required repository check did not run. |
|
|
275
285
|
| `ERROR` | skillroll could not produce a trustworthy verdict. |
|
|
276
286
|
|
|
287
|
+
## Compare models that run the skill
|
|
288
|
+
|
|
289
|
+
After you have a regression case, compare candidate models on the same case
|
|
290
|
+
while keeping the configured model fixed for the Dungeon Master and judge.
|
|
291
|
+
From a SkillRoll source checkout containing this change, run:
|
|
292
|
+
|
|
293
|
+
```shell
|
|
294
|
+
uv run skillroll compare \
|
|
295
|
+
--repo /path/to/your-repository \
|
|
296
|
+
--executor-model provider/baseline \
|
|
297
|
+
--executor-model provider/candidate \
|
|
298
|
+
--samples 5 \
|
|
299
|
+
--case path/to/eval.md
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
The comparison makes real model calls and may incur provider charges. Its
|
|
303
|
+
report summarizes observed outcomes; cost estimates require reported token
|
|
304
|
+
usage and configured rates. Results are evidence for these cases, not proof
|
|
305
|
+
that a lower-cost candidate matches the baseline. See the
|
|
306
|
+
[comparison guide](docs/comparing-models.md) for details.
|
|
307
|
+
|
|
277
308
|
## Add GitHub Actions
|
|
278
309
|
|
|
279
310
|
After `skillroll.toml` exists, generate the advisory workflow:
|
|
@@ -341,6 +372,7 @@ today; expect the interface to change between minor releases.
|
|
|
341
372
|
- [Author and audit good skills and evals](docs/authoring.md)
|
|
342
373
|
- [Write an eval](docs/writing-evals.md)
|
|
343
374
|
- [Configure models and cost](docs/configuration.md)
|
|
375
|
+
- [Compare skill executor models](docs/comparing-models.md)
|
|
344
376
|
- [Understand results and fix problems](docs/results.md)
|
|
345
377
|
- [Add advisory GitHub checks](docs/github-actions.md)
|
|
346
378
|
- [Review the security model](docs/security.md)
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# SkillRoll examples
|
|
2
2
|
|
|
3
|
-
These
|
|
3
|
+
These small repositories are checked-in examples of different Agent
|
|
4
4
|
Skill boundaries. They use the current `skillroll.toml` and Markdown eval
|
|
5
|
-
format
|
|
5
|
+
format. Their active configuration requires no inference endpoint or API key
|
|
6
|
+
for validation.
|
|
7
|
+
|
|
8
|
+
The [release-gate comparison](release-gate-comparison/) follows a queued-CI
|
|
9
|
+
failure through a prompt repair and compares two models running the revised
|
|
10
|
+
skill. It includes recorded live action traces and a provider configuration
|
|
11
|
+
for reproducing the runs.
|
|
6
12
|
|
|
7
13
|
Start with the [release-action-boundary](release-action-boundary/) example. It
|
|
8
14
|
checks required CI before merging and keeps an independent approval requirement
|
|
@@ -49,7 +55,6 @@ api_key_env = "SKILLROLL_API_KEY"
|
|
|
49
55
|
|
|
50
56
|
Then set the named environment variable in the current shell, run
|
|
51
57
|
`skillroll doctor`, and only after it passes run `skillroll eval --all`.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
refund, or customer action was performed.
|
|
58
|
+
Both commands contact the configured provider. Model behavior depends on the
|
|
59
|
+
provider, model, and case revision. Each example uses a simulated environment
|
|
60
|
+
for releases, refunds, and other external actions.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Repair a release skill and compare its executors
|
|
2
|
+
|
|
3
|
+
A release skill sees a queued CI check for the current revision. The original
|
|
4
|
+
prompt requires successful CI and approval for that revision. The revised
|
|
5
|
+
prompt turns those requirements into an ordered checklist and explicitly
|
|
6
|
+
says that queued CI is not a successful check.
|
|
7
|
+
|
|
8
|
+
The example uses synthetic responses from a simulated release service. The
|
|
9
|
+
action trace shows whether the skill tried to deploy and which evidence it
|
|
10
|
+
used. The case also asks the judge to check the skill's explanation.
|
|
11
|
+
|
|
12
|
+
## Run the example
|
|
13
|
+
|
|
14
|
+
Use SkillRoll 0.3.0 or later. To use a local release candidate, install the
|
|
15
|
+
wheel with `uv tool install /path/to/skillroll-0.3.0-py3-none-any.whl`.
|
|
16
|
+
From a checkout of this repository, copy the example into a temporary
|
|
17
|
+
directory:
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
demo_repo="$(mktemp -d)"
|
|
21
|
+
cp -R examples/release-gate-comparison/. "$demo_repo/"
|
|
22
|
+
cd "$demo_repo"
|
|
23
|
+
skillroll validate --all
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Validation checks the case files without contacting a model. For the live
|
|
27
|
+
steps, set `SKILLROLL_API_KEY` in your shell and copy the recorded OpenRouter
|
|
28
|
+
configuration into place:
|
|
29
|
+
|
|
30
|
+
```shell
|
|
31
|
+
cp recorded-run.toml skillroll.toml
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The configuration uses GPT-5.6 Luna Pro for the World and judge. The four
|
|
35
|
+
cases specify exact tool replies, so the simulator can handle the expected
|
|
36
|
+
actions without model calls. The evaluator can still judge the resulting
|
|
37
|
+
evidence differently on repeated runs. The live commands below make billable
|
|
38
|
+
provider calls.
|
|
39
|
+
|
|
40
|
+
## Reproduce the failure and repair
|
|
41
|
+
|
|
42
|
+
Run the original prompt twice against the queued-CI case:
|
|
43
|
+
|
|
44
|
+
```shell
|
|
45
|
+
cp weak/SKILL.md skills/release-gate/SKILL.md
|
|
46
|
+
skillroll eval --case release-gate/evals/test-ci-queued.eval.md \
|
|
47
|
+
--executor-model openai/gpt-4.1-nano --samples 2
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Restore the revised prompt and repeat the same case with the same executor:
|
|
51
|
+
|
|
52
|
+
```shell
|
|
53
|
+
cp repaired/SKILL.md skills/release-gate/SKILL.md
|
|
54
|
+
skillroll eval --case release-gate/evals/test-ci-queued.eval.md \
|
|
55
|
+
--executor-model openai/gpt-4.1-nano --samples 2
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Inspect `report.md` and `transcript.jsonl` under the printed run directories.
|
|
59
|
+
The prompt is the only input file that changes between these commands. In
|
|
60
|
+
particular, the queued-CI case and the provider configuration stay byte-identical.
|
|
61
|
+
|
|
62
|
+
## Compare the revised skill on two models
|
|
63
|
+
|
|
64
|
+
```shell
|
|
65
|
+
skillroll compare --all --samples 2 --seed 20261109 \
|
|
66
|
+
--executor-model openai/gpt-5.6-luna-pro \
|
|
67
|
+
--executor-model openai/gpt-6-luna
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This runs 16 attempts. The suite covers queued CI, stale CI for another
|
|
71
|
+
revision, a ready release, and an instruction embedded in a failed-CI tool
|
|
72
|
+
response. The executor changes and the evaluator stays fixed. The seed
|
|
73
|
+
controls the schedule order. Each attempt has its own model responses.
|
|
74
|
+
|
|
75
|
+
Read the comparison report under `.skillroll/comparisons/`. The recorded
|
|
76
|
+
configuration uses a November 8, 2026 price snapshot. Replace those rates
|
|
77
|
+
with your provider's rates for fresh estimates. The per-role token costs
|
|
78
|
+
are estimates and the executor timing measures work inside this harness.
|
|
79
|
+
Inspect the observed actions separately from the judge verdict when
|
|
80
|
+
reviewing a deployment decision.
|
|
81
|
+
|
|
82
|
+
[Recorded results and action traces](EVIDENCE.md)
|
|
@@ -72,6 +72,7 @@ class RunFacts:
|
|
|
72
72
|
profile_name: str | None = None
|
|
73
73
|
profile_purpose: str | None = None
|
|
74
74
|
skill_available: bool = True
|
|
75
|
+
executor_model: str | None = None
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
def run_bytes(facts: RunFacts) -> bytes:
|
|
@@ -91,6 +92,11 @@ def run_bytes(facts: RunFacts) -> bytes:
|
|
|
91
92
|
"name": facts.profile_name,
|
|
92
93
|
"purpose": facts.profile_purpose,
|
|
93
94
|
},
|
|
95
|
+
"models": {
|
|
96
|
+
"executor": facts.executor_model or facts.model,
|
|
97
|
+
"world": facts.model,
|
|
98
|
+
"judge": facts.model,
|
|
99
|
+
},
|
|
94
100
|
"configured_limits": dict(facts.configured_limits),
|
|
95
101
|
"effective_limits": dict(facts.effective_limits),
|
|
96
102
|
"input_manifest": {
|
|
@@ -119,7 +125,8 @@ def report_bytes(facts: RunFacts) -> bytes:
|
|
|
119
125
|
"",
|
|
120
126
|
f"- Skill: `{facts.skill}`",
|
|
121
127
|
f"- Eval case: `{facts.case}`",
|
|
122
|
-
f"-
|
|
128
|
+
f"- Executor model: `{facts.executor_model or facts.model}`",
|
|
129
|
+
f"- World and judge model: `{facts.model}`",
|
|
123
130
|
"- Skill instructions: "
|
|
124
131
|
+ ("available" if facts.skill_available else "intentionally omitted"),
|
|
125
132
|
"- Effective limits: "
|
|
@@ -202,6 +209,7 @@ def final_report_bytes(
|
|
|
202
209
|
model_profile: str | None = None,
|
|
203
210
|
model_profile_purpose: str | None = None,
|
|
204
211
|
skill_available: bool = True,
|
|
212
|
+
executor_model: str | None = None,
|
|
205
213
|
) -> bytes:
|
|
206
214
|
"""Render a final report in terms a new maintainer can act on."""
|
|
207
215
|
if finished is None:
|
|
@@ -223,7 +231,8 @@ def final_report_bytes(
|
|
|
223
231
|
"- Skill finished: " + ("yes" if finished else "no"),
|
|
224
232
|
]
|
|
225
233
|
if model is not None:
|
|
226
|
-
lines.insert(4, f"-
|
|
234
|
+
lines.insert(4, f"- World and judge model: `{model}`")
|
|
235
|
+
lines.insert(5, f"- Executor model: `{executor_model or model}`")
|
|
227
236
|
if model_profile is not None:
|
|
228
237
|
lines.insert(5, f"- Model profile: `{model_profile}`")
|
|
229
238
|
if model_profile_purpose is not None:
|
|
@@ -351,17 +360,23 @@ def execution_bytes(
|
|
|
351
360
|
usage: object,
|
|
352
361
|
*,
|
|
353
362
|
turns_source: str = "unavailable",
|
|
363
|
+
executor_model: str | None = None,
|
|
364
|
+
world_judge_model: str | None = None,
|
|
354
365
|
) -> bytes:
|
|
355
366
|
"""Render completed execution facts before a repository check can begin."""
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
367
|
+
value: dict[str, object] = {
|
|
368
|
+
"format_version": ARTIFACT_FORMAT_VERSION,
|
|
369
|
+
"final_output": final_output,
|
|
370
|
+
"turns_used": turns,
|
|
371
|
+
"turns_source": turns_source,
|
|
372
|
+
"usage": usage,
|
|
373
|
+
}
|
|
374
|
+
if executor_model is not None or world_judge_model is not None:
|
|
375
|
+
value["models"] = {
|
|
376
|
+
"executor": executor_model,
|
|
377
|
+
"world_and_judge": world_judge_model,
|
|
363
378
|
}
|
|
364
|
-
)
|
|
379
|
+
return canonical_json(value)
|
|
365
380
|
|
|
366
381
|
|
|
367
382
|
def judge_bytes(judge: object, exact_fact_checks: object) -> bytes:
|
|
@@ -412,11 +427,14 @@ def experiment_report_bytes(summary: Mapping[str, object]) -> bytes:
|
|
|
412
427
|
interpretation = summary.get("interpretation")
|
|
413
428
|
if not isinstance(interpretation, Mapping):
|
|
414
429
|
interpretation = {}
|
|
430
|
+
executor_model = summary.get("executor_model", summary.get("model", "unknown"))
|
|
431
|
+
evaluator_model = summary.get("model", "unknown")
|
|
415
432
|
lines = [
|
|
416
433
|
"# SkillRoll comparison report",
|
|
417
434
|
"",
|
|
418
435
|
f"- Eval case: `{summary.get('case', 'selected cases')}`",
|
|
419
|
-
f"-
|
|
436
|
+
f"- Executor model: `{executor_model}`",
|
|
437
|
+
f"- World and judge model: `{evaluator_model}`",
|
|
420
438
|
f"- Samples: {summary.get('samples_requested', 'unknown')}",
|
|
421
439
|
f"- Interpretation: **{interpretation.get('status', 'unknown')}**",
|
|
422
440
|
"",
|
|
@@ -216,6 +216,7 @@ class ArtifactStore:
|
|
|
216
216
|
value: bytes,
|
|
217
217
|
*,
|
|
218
218
|
trusted_directory: bool = False,
|
|
219
|
+
directory_check: Callable[[], Path] | None = None,
|
|
219
220
|
) -> None:
|
|
220
221
|
"""Write one file on platforms without descriptor-relative operations.
|
|
221
222
|
|
|
@@ -226,7 +227,14 @@ class ArtifactStore:
|
|
|
226
227
|
temporary: Path | None = None
|
|
227
228
|
try:
|
|
228
229
|
for _ in range(3):
|
|
229
|
-
|
|
230
|
+
checked = (
|
|
231
|
+
directory_check()
|
|
232
|
+
if directory_check is not None
|
|
233
|
+
else directory
|
|
234
|
+
if trusted_directory
|
|
235
|
+
else self._checked_fallback_directory(directory)
|
|
236
|
+
)
|
|
237
|
+
candidate = checked / f".{name}.{uuid.uuid4().hex}.tmp"
|
|
230
238
|
try:
|
|
231
239
|
candidate.lstat()
|
|
232
240
|
except FileNotFoundError:
|
|
@@ -253,7 +261,9 @@ class ArtifactStore:
|
|
|
253
261
|
finally:
|
|
254
262
|
os.close(file_fd)
|
|
255
263
|
checked = (
|
|
256
|
-
|
|
264
|
+
directory_check()
|
|
265
|
+
if directory_check is not None
|
|
266
|
+
else directory
|
|
257
267
|
if trusted_directory
|
|
258
268
|
else self._checked_fallback_directory(directory)
|
|
259
269
|
)
|
|
@@ -483,3 +493,124 @@ class ArtifactStore:
|
|
|
483
493
|
f"SkillRoll could not atomically write {name} in the "
|
|
484
494
|
"experiment-evidence folder."
|
|
485
495
|
) from error
|
|
496
|
+
|
|
497
|
+
def create_comparison(self) -> tuple[str, Path]:
|
|
498
|
+
"""Reserve a private comparison folder below ``.skillroll``."""
|
|
499
|
+
if not self._root.is_dir() or self._root.is_symlink():
|
|
500
|
+
raise ArtifactError(
|
|
501
|
+
"SkillRoll cannot save comparison evidence because the selected "
|
|
502
|
+
"repository folder is unsafe."
|
|
503
|
+
)
|
|
504
|
+
base = self._root / ".skillroll"
|
|
505
|
+
comparisons = base / "comparisons"
|
|
506
|
+
try:
|
|
507
|
+
if base.is_symlink() or (base.exists() and not base.is_dir()):
|
|
508
|
+
raise ArtifactError(
|
|
509
|
+
"SkillRoll cannot save comparison evidence because .skillroll "
|
|
510
|
+
"is not an ordinary folder."
|
|
511
|
+
)
|
|
512
|
+
base.mkdir(exist_ok=True)
|
|
513
|
+
if comparisons.is_symlink() or (
|
|
514
|
+
comparisons.exists() and not comparisons.is_dir()
|
|
515
|
+
):
|
|
516
|
+
raise ArtifactError(
|
|
517
|
+
"SkillRoll cannot save comparison evidence because "
|
|
518
|
+
".skillroll/comparisons is not an ordinary folder."
|
|
519
|
+
)
|
|
520
|
+
comparisons.mkdir(exist_ok=True)
|
|
521
|
+
for _ in range(3):
|
|
522
|
+
comparison_id = f"comparison-{self._id_factory()}"
|
|
523
|
+
destination = comparisons / comparison_id
|
|
524
|
+
try:
|
|
525
|
+
destination.mkdir(mode=0o700)
|
|
526
|
+
except FileExistsError:
|
|
527
|
+
continue
|
|
528
|
+
return comparison_id, destination
|
|
529
|
+
except OSError as error:
|
|
530
|
+
raise ArtifactError(
|
|
531
|
+
"SkillRoll could not create its private comparison-evidence folder."
|
|
532
|
+
) from error
|
|
533
|
+
raise ArtifactError(
|
|
534
|
+
"SkillRoll could not reserve a unique comparison-evidence directory."
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
def _expected_comparison_directory(self, directory: Path) -> Path:
|
|
538
|
+
expected = self._root / ".skillroll" / "comparisons" / directory.name
|
|
539
|
+
if directory != expected or not directory.name.startswith("comparison-"):
|
|
540
|
+
raise ArtifactError(
|
|
541
|
+
"SkillRoll refused to write comparison evidence outside its "
|
|
542
|
+
"private comparisons folder."
|
|
543
|
+
)
|
|
544
|
+
return expected
|
|
545
|
+
|
|
546
|
+
def _open_comparison_directory(self, directory: Path) -> int:
|
|
547
|
+
"""Open a reserved comparison directory through no-follow POSIX handles."""
|
|
548
|
+
self._expected_comparison_directory(directory)
|
|
549
|
+
flags = (
|
|
550
|
+
os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
551
|
+
)
|
|
552
|
+
handles: list[int] = []
|
|
553
|
+
try:
|
|
554
|
+
handles.append(os.open(self._root, flags))
|
|
555
|
+
handles.append(os.open(".skillroll", flags, dir_fd=handles[-1]))
|
|
556
|
+
handles.append(os.open("comparisons", flags, dir_fd=handles[-1]))
|
|
557
|
+
handles.append(os.open(directory.name, flags, dir_fd=handles[-1]))
|
|
558
|
+
except OSError as error:
|
|
559
|
+
for handle in reversed(handles):
|
|
560
|
+
os.close(handle)
|
|
561
|
+
raise ArtifactError(
|
|
562
|
+
"SkillRoll could not safely reopen this comparison-evidence folder. "
|
|
563
|
+
"It may have changed since the comparison started."
|
|
564
|
+
) from error
|
|
565
|
+
for handle in handles[:-1]:
|
|
566
|
+
os.close(handle)
|
|
567
|
+
return handles[-1]
|
|
568
|
+
|
|
569
|
+
def _checked_comparison_directory(self, directory: Path) -> Path:
|
|
570
|
+
expected = self._expected_comparison_directory(directory)
|
|
571
|
+
for component in (
|
|
572
|
+
self._root,
|
|
573
|
+
self._root / ".skillroll",
|
|
574
|
+
self._root / ".skillroll" / "comparisons",
|
|
575
|
+
expected,
|
|
576
|
+
):
|
|
577
|
+
try:
|
|
578
|
+
mode = component.lstat().st_mode
|
|
579
|
+
except OSError as error:
|
|
580
|
+
raise ArtifactError(
|
|
581
|
+
"SkillRoll could not safely reopen this comparison-evidence "
|
|
582
|
+
"folder. It may have changed since the comparison started."
|
|
583
|
+
) from error
|
|
584
|
+
if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode):
|
|
585
|
+
raise ArtifactError(
|
|
586
|
+
"SkillRoll refused this comparison-evidence folder because one "
|
|
587
|
+
"of its folders changed or is a link."
|
|
588
|
+
)
|
|
589
|
+
return expected
|
|
590
|
+
|
|
591
|
+
def write_comparison(
|
|
592
|
+
self, directory: Path, values: tuple[tuple[str, bytes], ...]
|
|
593
|
+
) -> None:
|
|
594
|
+
"""Safely write the fixed comparison manifest, checkpoint, and report."""
|
|
595
|
+
allowed = {"manifest.json", "comparison.json", "report.md"}
|
|
596
|
+
if any(name not in allowed for name, _ in values):
|
|
597
|
+
raise ArtifactError("SkillRoll refused an unsafe comparison file name.")
|
|
598
|
+
safe_values = tuple((name, self._safe(value)) for name, value in values)
|
|
599
|
+
if _descriptor_safety_available():
|
|
600
|
+
directory_fd = self._open_comparison_directory(directory)
|
|
601
|
+
try:
|
|
602
|
+
for name, value in safe_values:
|
|
603
|
+
self._write(directory_fd, name, value)
|
|
604
|
+
finally:
|
|
605
|
+
os.close(directory_fd)
|
|
606
|
+
else:
|
|
607
|
+
self._checked_comparison_directory(directory)
|
|
608
|
+
for name, value in safe_values:
|
|
609
|
+
self._write_fallback(
|
|
610
|
+
directory,
|
|
611
|
+
name,
|
|
612
|
+
value,
|
|
613
|
+
directory_check=lambda: self._checked_comparison_directory(
|
|
614
|
+
directory
|
|
615
|
+
),
|
|
616
|
+
)
|
|
@@ -217,7 +217,12 @@ def _parser() -> SkillRollParser:
|
|
|
217
217
|
evaluate_parser.add_argument(
|
|
218
218
|
"--model-profile",
|
|
219
219
|
metavar="NAME",
|
|
220
|
-
help="
|
|
220
|
+
help="Evaluator model profile to use for the World and judge.",
|
|
221
|
+
)
|
|
222
|
+
evaluate_parser.add_argument(
|
|
223
|
+
"--executor-model",
|
|
224
|
+
metavar="MODEL",
|
|
225
|
+
help="Run the skill on MODEL while keeping the configured evaluator fixed.",
|
|
221
226
|
)
|
|
222
227
|
evaluate_parser.add_argument(
|
|
223
228
|
"--concurrency",
|
|
@@ -238,6 +243,53 @@ def _parser() -> SkillRollParser:
|
|
|
238
243
|
action="store_true",
|
|
239
244
|
help="Also run without the skill to check whether the eval depends on it.",
|
|
240
245
|
)
|
|
246
|
+
compare_parser = commands.add_parser(
|
|
247
|
+
"compare", help="Compare executor models with a fixed evaluator."
|
|
248
|
+
)
|
|
249
|
+
compare_parser.add_argument(
|
|
250
|
+
"--repo", metavar="PATH", help="Repository directory override."
|
|
251
|
+
)
|
|
252
|
+
compare_selection = compare_parser.add_mutually_exclusive_group()
|
|
253
|
+
compare_selection.add_argument(
|
|
254
|
+
"--skill", metavar="PATH", help="Skill path relative to skills_path."
|
|
255
|
+
)
|
|
256
|
+
compare_selection.add_argument(
|
|
257
|
+
"--case", metavar="PATH", help="Eval-case path relative to skills_path."
|
|
258
|
+
)
|
|
259
|
+
compare_selection.add_argument(
|
|
260
|
+
"--all", action="store_true", help="Compare all cases in the repository."
|
|
261
|
+
)
|
|
262
|
+
compare_parser.add_argument(
|
|
263
|
+
"--executor-model",
|
|
264
|
+
action="append",
|
|
265
|
+
dest="executor_models",
|
|
266
|
+
metavar="MODEL",
|
|
267
|
+
help="Candidate model; repeat for 2-8 distinct models, baseline first.",
|
|
268
|
+
)
|
|
269
|
+
compare_parser.add_argument(
|
|
270
|
+
"--model-profile", metavar="NAME", help="Fixed evaluator model profile."
|
|
271
|
+
)
|
|
272
|
+
compare_parser.add_argument(
|
|
273
|
+
"--samples",
|
|
274
|
+
type=int,
|
|
275
|
+
default=5,
|
|
276
|
+
metavar="N",
|
|
277
|
+
help="Attempts per case/model (1-10; default: 5).",
|
|
278
|
+
)
|
|
279
|
+
compare_parser.add_argument(
|
|
280
|
+
"--seed",
|
|
281
|
+
type=int,
|
|
282
|
+
default=0,
|
|
283
|
+
metavar="N",
|
|
284
|
+
help=(
|
|
285
|
+
"Run-order seed (0-4294967295; default: 0); does not seed model responses."
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
compare_parser.add_argument(
|
|
289
|
+
"--run-commands",
|
|
290
|
+
action="store_true",
|
|
291
|
+
help="Run trusted repository checks declared by cases; comparison is serial.",
|
|
292
|
+
)
|
|
241
293
|
return parser
|
|
242
294
|
|
|
243
295
|
|
|
@@ -277,7 +329,7 @@ def _missing_command() -> CommandResult:
|
|
|
277
329
|
"Choose one SkillRoll command to continue.",
|
|
278
330
|
next_action=(
|
|
279
331
|
"Run 'skillroll --help' and choose init, new, doctor, validate, "
|
|
280
|
-
"or
|
|
332
|
+
"eval, or compare."
|
|
281
333
|
),
|
|
282
334
|
),
|
|
283
335
|
),
|
|
@@ -380,10 +432,25 @@ def main(
|
|
|
380
432
|
all_cases=namespace.all,
|
|
381
433
|
run_commands=namespace.run_commands,
|
|
382
434
|
model_profile=namespace.model_profile,
|
|
435
|
+
executor_model=namespace.executor_model,
|
|
383
436
|
samples=namespace.samples,
|
|
384
437
|
concurrency=namespace.concurrency,
|
|
385
438
|
with_skill_control=namespace.with_skill_control,
|
|
386
439
|
)
|
|
440
|
+
elif namespace.command == "compare" and commands is None:
|
|
441
|
+
from skillroll.commands import compare
|
|
442
|
+
|
|
443
|
+
result = compare.run(
|
|
444
|
+
repo=namespace.repo,
|
|
445
|
+
skill=namespace.skill,
|
|
446
|
+
case=namespace.case,
|
|
447
|
+
all_cases=namespace.all,
|
|
448
|
+
executor_models=tuple(namespace.executor_models or ()),
|
|
449
|
+
model_profile=namespace.model_profile,
|
|
450
|
+
samples=namespace.samples,
|
|
451
|
+
seed=namespace.seed,
|
|
452
|
+
run_commands=namespace.run_commands,
|
|
453
|
+
)
|
|
387
454
|
else:
|
|
388
455
|
result = (COMMANDS if commands is None else commands)[
|
|
389
456
|
namespace.command
|