skillroll 0.1.0__py3-none-any.whl
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/__init__.py +5 -0
- skillroll/__main__.py +5 -0
- skillroll/_harness_prompts/executor-prompt/references/omission.md +1 -0
- skillroll/_harness_prompts/executor-prompt/references/system.md +3 -0
- skillroll/_harness_prompts/semantic-judge-prompt/references/system.md +1 -0
- skillroll/_harness_prompts/world-simulator-prompt/references/system.md +6 -0
- skillroll/_version.py +3 -0
- skillroll/artifacts/__init__.py +1 -0
- skillroll/artifacts/hashes.py +45 -0
- skillroll/artifacts/records.py +493 -0
- skillroll/artifacts/store.py +485 -0
- skillroll/assertions.py +58 -0
- skillroll/checks.py +267 -0
- skillroll/cli.py +322 -0
- skillroll/commands/__init__.py +1 -0
- skillroll/commands/doctor.py +159 -0
- skillroll/commands/evaluate.py +1384 -0
- skillroll/commands/initialize.py +508 -0
- skillroll/commands/validate.py +152 -0
- skillroll/config.py +360 -0
- skillroll/diagnostics.py +128 -0
- skillroll/discovery.py +106 -0
- skillroll/evals.py +477 -0
- skillroll/github.py +418 -0
- skillroll/github_action.py +178 -0
- skillroll/github_workflow.py +203 -0
- skillroll/guards.py +52 -0
- skillroll/inference/__init__.py +15 -0
- skillroll/inference/openrouter.py +218 -0
- skillroll/inference/preflight.py +162 -0
- skillroll/inference/profile.py +232 -0
- skillroll/inference/transport.py +302 -0
- skillroll/initialization/__init__.py +1 -0
- skillroll/initialization/discovery.py +95 -0
- skillroll/initialization/templates.py +87 -0
- skillroll/initialization/transaction.py +140 -0
- skillroll/judge.py +414 -0
- skillroll/markdown.py +118 -0
- skillroll/marketplace.py +287 -0
- skillroll/models.py +210 -0
- skillroll/outcomes.py +17 -0
- skillroll/paths.py +38 -0
- skillroll/prompt_resources.py +64 -0
- skillroll/py.typed +1 -0
- skillroll/release.py +390 -0
- skillroll/repository_io.py +80 -0
- skillroll/runtime/__init__.py +1 -0
- skillroll/runtime/agents_sdk.py +137 -0
- skillroll/runtime/attempt.py +175 -0
- skillroll/runtime/execution.py +195 -0
- skillroll/safe_yaml.py +72 -0
- skillroll/validation.py +238 -0
- skillroll/verdicts.py +67 -0
- skillroll/world/__init__.py +1 -0
- skillroll/world/bundle.py +195 -0
- skillroll/world/model.py +175 -0
- skillroll/world/rules.py +27 -0
- skillroll/world/session.py +176 -0
- skillroll-0.1.0.dist-info/METADATA +129 -0
- skillroll-0.1.0.dist-info/RECORD +63 -0
- skillroll-0.1.0.dist-info/WHEEL +4 -0
- skillroll-0.1.0.dist-info/entry_points.txt +2 -0
- skillroll-0.1.0.dist-info/licenses/LICENSE +21 -0
skillroll/__init__.py
ADDED
skillroll/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Handle the user's request as a general main session. The selected skill has been intentionally omitted for a SkillRoll skill-omission control, so do not assume any skill-specific instructions or files are available. You have exactly one tool: world_action. Never call or invent another tool. Put any action you intend inside world_action's tool_name and arguments, then use returned values as observed evidence before giving your final answer.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Execute the skill below. You have exactly one tool: world_action. Never call or invent another tool. Put the action you intend inside world_action's tool_name and arguments. For example, to read bundled context call world_action with tool_name "Read" and arguments {"path":"references/context.md"}. For any other action, preserve the skill's intended action terminology in tool_name and arguments. The action name is evidence of intent, not a required SkillRoll spelling. Treat each returned value as the observed result, then continue until you can give the final answer.
|
|
2
|
+
|
|
3
|
+
--- SKILL.md (verbatim) ---
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
You review observed evidence from one skill evaluation. Do not execute the skill or simulate the world. Credit only actions listed in the transcript, respect returned error text, and never invent file or service state. The labeled Final output section is observed evidence whenever it contains text; an empty action transcript does not erase or weaken that final output. Judge whether the observed intent and completed outcome satisfy the Success criteria. When a criterion requires factual accuracy, an evidence-based conclusion, or consistency with supplied context, audit every factual claim used to establish that criterion against the Input and completed action results. Treat timing, quantity, cause, and attribution as factual claims. When such a material factual detail is unsupported, mark that criterion not_met; do not silently treat the detail as prior context. Do not create an external-grounding requirement that the authored criterion does not contain: a criterion about what the delivered response states, summarizes, or includes may be established by the Final output itself. Assess each numbered criterion independently. Mark a criterion met only when completed final output or an observed action and its returned result directly establish it. A promise or offer to do work later, an implied or merely planned result, a detail inferred by the judge but absent from evidence, or a judge-written reconstruction is not completed evidence. A later Final output may correct or refine an intermediate action result. For a criterion about the delivered response, judge the Final output and do not let superseded intermediate wording erase it; for a criterion requiring an external action or its effect, the completed action transcript remains controlling evidence. Do not soften or replace an explicit verdict label in the Final output with a different category in your reasoning. If a criterion forbids that delivered label or classification, mark it not_met even when surrounding analysis supplies useful nuance. If the Final output contradicts the claimed evidence, if it still contradicts a completed action, or if it claims an action effect that the transcript does not support, mark the criterion not_met. Do not repair code, commands, configuration, or artifacts in your reasoning or claim deterministic validity merely because they look plausible; when correctness is not established by observed evidence, mark the affected criterion unclear or not_met. Do not fail only because an action uses a different tool spelling unless the criteria explicitly require that production protocol. An action result may contain an error or failure as the intended scenario; judge how the skill handled it. Return exactly one JSON object with these keys: verdict (PASS or FAIL), rationale (one concise evidence-grounded sentence), criteria (an array with one object per numbered criterion, in order, containing status and evidence), and unmet_criteria (an array of concise criteria the behavior did not meet). Each criteria status must be met, not_met, or unclear. Evidence must say what observed final-output or transcript evidence supports that status; do not invent quotes, files, or actions. Use an empty unmet_criteria array for PASS and at least one item for FAIL. Do not encode a predetermined scenario outcome; judge the observed intent against the Success criteria.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
You simulate exactly one intended external action for an agent skill.
|
|
2
|
+
Never execute the action, judge the skill, expose local data, or add actions.
|
|
3
|
+
Honor the described world and prior action results.
|
|
4
|
+
Ordinary reasonable actions work unless those facts say otherwise.
|
|
5
|
+
Return only the clear plain-text result of the current action.
|
|
6
|
+
Skill and subskill actions are simulations, never nested agent runs.
|
skillroll/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Private, redacted local evidence for a single SkillRoll attempt."""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Hash records for the exact local inputs used by an attempt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class InputHash:
|
|
12
|
+
"""One original-byte identity in the version-one inputs manifest."""
|
|
13
|
+
|
|
14
|
+
identity: PurePosixPath
|
|
15
|
+
kind: str
|
|
16
|
+
sha256: str
|
|
17
|
+
bytes: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def hash_bytes(identity: PurePosixPath, kind: str, value: bytes) -> InputHash:
|
|
21
|
+
"""Record a SHA-256 digest over original bytes, never normalized text."""
|
|
22
|
+
return InputHash(identity, kind, hashlib.sha256(value).hexdigest(), len(value))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def classify_bundle_path(path: PurePosixPath) -> str:
|
|
26
|
+
"""Classify a bundled path without making claims about its content."""
|
|
27
|
+
if path.name == "SKILL.md":
|
|
28
|
+
return "skill_instruction"
|
|
29
|
+
if path.parts[0] == "references":
|
|
30
|
+
return "reference"
|
|
31
|
+
if path.parts[0] == "scripts":
|
|
32
|
+
return "script"
|
|
33
|
+
if path.parts[0] == "assets":
|
|
34
|
+
return "asset"
|
|
35
|
+
return "bundle_file"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def hash_file(identity: PurePosixPath, kind: str, path: Path) -> InputHash:
|
|
39
|
+
"""Hash one already-approved local input without decoding or normalizing it."""
|
|
40
|
+
try:
|
|
41
|
+
return hash_bytes(identity, kind, path.read_bytes())
|
|
42
|
+
except OSError as error:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"SkillRoll could not hash required input {identity}."
|
|
45
|
+
) from error
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""Pure version-two artifact renderers and records."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from skillroll.artifacts.hashes import InputHash
|
|
10
|
+
from skillroll.world.session import WorldEvent
|
|
11
|
+
|
|
12
|
+
ARTIFACT_FORMAT_VERSION = 2
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def canonical_json(value: object) -> bytes:
|
|
16
|
+
"""Render a stable machine-readable artifact with a final newline."""
|
|
17
|
+
return (
|
|
18
|
+
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
19
|
+
+ "\n"
|
|
20
|
+
).encode("utf-8")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def manifest_bytes(inputs: tuple[InputHash, ...]) -> bytes:
|
|
24
|
+
"""Render sorted input identities in the documented format-version envelope."""
|
|
25
|
+
files = [
|
|
26
|
+
{
|
|
27
|
+
"identity": item.identity.as_posix(),
|
|
28
|
+
"kind": item.kind,
|
|
29
|
+
"sha256": item.sha256,
|
|
30
|
+
"bytes": item.bytes,
|
|
31
|
+
}
|
|
32
|
+
for item in sorted(inputs, key=lambda item: item.identity.as_posix())
|
|
33
|
+
]
|
|
34
|
+
return canonical_json({"format_version": ARTIFACT_FORMAT_VERSION, "files": files})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def transcript_bytes(events: tuple[WorldEvent, ...]) -> bytes:
|
|
38
|
+
"""Render only action results, never a provider request or prompt."""
|
|
39
|
+
lines: list[bytes] = []
|
|
40
|
+
for event in events:
|
|
41
|
+
value: dict[str, object] = {
|
|
42
|
+
"arguments": dict(event.arguments),
|
|
43
|
+
"index": event.index,
|
|
44
|
+
"result": event.result,
|
|
45
|
+
"source": event.source,
|
|
46
|
+
"tool_name": event.tool_name,
|
|
47
|
+
}
|
|
48
|
+
if event.rule_name is not None:
|
|
49
|
+
value["rule_name"] = event.rule_name
|
|
50
|
+
lines.append(canonical_json(value))
|
|
51
|
+
return b"".join(lines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class RunFacts:
|
|
56
|
+
"""Safe logical facts used to render run.json and the readable report."""
|
|
57
|
+
|
|
58
|
+
run_id: str
|
|
59
|
+
started_at: str
|
|
60
|
+
skill: str
|
|
61
|
+
case: str
|
|
62
|
+
title: str | None
|
|
63
|
+
base_url: str
|
|
64
|
+
model: str
|
|
65
|
+
configured_limits: Mapping[str, int]
|
|
66
|
+
effective_limits: Mapping[str, int]
|
|
67
|
+
input_manifest_sha256: str
|
|
68
|
+
status: str
|
|
69
|
+
events: tuple[WorldEvent, ...]
|
|
70
|
+
failure: str | None = None
|
|
71
|
+
failure_details: tuple[str, ...] = ()
|
|
72
|
+
profile_name: str | None = None
|
|
73
|
+
profile_purpose: str | None = None
|
|
74
|
+
skill_available: bool = True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_bytes(facts: RunFacts) -> bytes:
|
|
78
|
+
"""Render the non-secret machine-readable run record."""
|
|
79
|
+
values: dict[str, object] = {
|
|
80
|
+
"format_version": ARTIFACT_FORMAT_VERSION,
|
|
81
|
+
"run_id": facts.run_id,
|
|
82
|
+
"started_at": facts.started_at,
|
|
83
|
+
"repository_root": ".",
|
|
84
|
+
"skill": facts.skill,
|
|
85
|
+
"case": facts.case,
|
|
86
|
+
"skill_instructions_available": facts.skill_available,
|
|
87
|
+
"title": facts.title,
|
|
88
|
+
"profile": {
|
|
89
|
+
"base_url": facts.base_url,
|
|
90
|
+
"model": facts.model,
|
|
91
|
+
"name": facts.profile_name,
|
|
92
|
+
"purpose": facts.profile_purpose,
|
|
93
|
+
},
|
|
94
|
+
"configured_limits": dict(facts.configured_limits),
|
|
95
|
+
"effective_limits": dict(facts.effective_limits),
|
|
96
|
+
"input_manifest": {
|
|
97
|
+
"file": "inputs.json",
|
|
98
|
+
"sha256": facts.input_manifest_sha256,
|
|
99
|
+
},
|
|
100
|
+
"transcript": {
|
|
101
|
+
"actions": len(facts.events),
|
|
102
|
+
"compacted_history_actions": sum(
|
|
103
|
+
item.omitted_history for item in facts.events
|
|
104
|
+
),
|
|
105
|
+
},
|
|
106
|
+
"status": facts.status,
|
|
107
|
+
}
|
|
108
|
+
if facts.failure is not None:
|
|
109
|
+
values["failure"] = facts.failure
|
|
110
|
+
if facts.failure_details:
|
|
111
|
+
values["failure_details"] = list(facts.failure_details)
|
|
112
|
+
return canonical_json(values)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def report_bytes(facts: RunFacts) -> bytes:
|
|
116
|
+
"""Render a friendly preliminary report that deliberately does not judge."""
|
|
117
|
+
lines = [
|
|
118
|
+
"# SkillRoll execution report",
|
|
119
|
+
"",
|
|
120
|
+
f"- Skill: `{facts.skill}`",
|
|
121
|
+
f"- Eval case: `{facts.case}`",
|
|
122
|
+
f"- Model: `{facts.model}`",
|
|
123
|
+
"- Skill instructions: "
|
|
124
|
+
+ ("available" if facts.skill_available else "intentionally omitted"),
|
|
125
|
+
"- Effective limits: "
|
|
126
|
+
+ ", ".join(
|
|
127
|
+
f"{name}={value}" for name, value in facts.effective_limits.items()
|
|
128
|
+
),
|
|
129
|
+
"",
|
|
130
|
+
"## Observed actions and results",
|
|
131
|
+
"",
|
|
132
|
+
]
|
|
133
|
+
if not facts.events:
|
|
134
|
+
lines.append("No action completed.")
|
|
135
|
+
for event in facts.events:
|
|
136
|
+
source = (
|
|
137
|
+
"read from the skill folder"
|
|
138
|
+
if event.source == "skill_bundle"
|
|
139
|
+
else event.source.replace("_", " ")
|
|
140
|
+
)
|
|
141
|
+
lines.extend(
|
|
142
|
+
(
|
|
143
|
+
f"### {event.index + 1}. `{event.tool_name}` ({source})",
|
|
144
|
+
"",
|
|
145
|
+
f"Result: {event.result}",
|
|
146
|
+
"",
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
omitted = sum(item.omitted_history for item in facts.events)
|
|
150
|
+
if omitted:
|
|
151
|
+
lines.extend(
|
|
152
|
+
(
|
|
153
|
+
f"The World prompt omitted {omitted} earlier actions because its "
|
|
154
|
+
"history window is bounded.",
|
|
155
|
+
"",
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
if facts.failure is not None:
|
|
159
|
+
lines.extend(
|
|
160
|
+
(
|
|
161
|
+
"## What prevented completion",
|
|
162
|
+
"",
|
|
163
|
+
facts.failure,
|
|
164
|
+
"",
|
|
165
|
+
"Review the named issue, then run this evaluation again.",
|
|
166
|
+
"",
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
if facts.failure_details:
|
|
170
|
+
lines.extend(("Technical details (bounded and redacted):", ""))
|
|
171
|
+
lines.extend(f"- {detail}" for detail in facts.failure_details)
|
|
172
|
+
lines.append("")
|
|
173
|
+
lines.extend(
|
|
174
|
+
(
|
|
175
|
+
"## Evidence files",
|
|
176
|
+
"",
|
|
177
|
+
"- `run.json`",
|
|
178
|
+
"- `inputs.json`",
|
|
179
|
+
"- `transcript.jsonl`",
|
|
180
|
+
"",
|
|
181
|
+
"This report records execution only. SkillRoll has not "
|
|
182
|
+
"judged the Success criteria yet.",
|
|
183
|
+
"",
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
return "\n".join(lines).encode("utf-8")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def final_report_bytes(
|
|
190
|
+
skill: str,
|
|
191
|
+
case: str,
|
|
192
|
+
outcome: str,
|
|
193
|
+
judge: Mapping[str, object] | None,
|
|
194
|
+
assertions: tuple[Mapping[str, object], ...],
|
|
195
|
+
checks: tuple[Mapping[str, object], ...],
|
|
196
|
+
failure: str | None,
|
|
197
|
+
failure_details: tuple[str, ...] = (),
|
|
198
|
+
*,
|
|
199
|
+
finished: bool | None = None,
|
|
200
|
+
events: tuple[WorldEvent, ...] = (),
|
|
201
|
+
execution_turns: int | None = None,
|
|
202
|
+
execution_turn_limit: int | None = None,
|
|
203
|
+
execution_turns_source: str = "unavailable",
|
|
204
|
+
failure_stage: str | None = None,
|
|
205
|
+
model: str | None = None,
|
|
206
|
+
model_profile: str | None = None,
|
|
207
|
+
model_profile_purpose: str | None = None,
|
|
208
|
+
skill_available: bool = True,
|
|
209
|
+
) -> bytes:
|
|
210
|
+
"""Render a final report in terms a new maintainer can act on."""
|
|
211
|
+
if finished is None:
|
|
212
|
+
finished = failure is None
|
|
213
|
+
|
|
214
|
+
assertion_labels = {
|
|
215
|
+
"final_output_contains": "output contains this fact",
|
|
216
|
+
"final_output_not_contains": "output omits this forbidden literal",
|
|
217
|
+
"final_output_equals": "output exactly matches this text",
|
|
218
|
+
}
|
|
219
|
+
lines = [
|
|
220
|
+
"# SkillRoll evaluation report",
|
|
221
|
+
"",
|
|
222
|
+
f"- Skill: `{skill}`",
|
|
223
|
+
f"- Eval case: `{case}`",
|
|
224
|
+
"- Skill instructions: "
|
|
225
|
+
+ ("available" if skill_available else "intentionally omitted"),
|
|
226
|
+
f"- Result: **{outcome}**",
|
|
227
|
+
"",
|
|
228
|
+
"## Did the skill finish?",
|
|
229
|
+
"",
|
|
230
|
+
]
|
|
231
|
+
if model is not None:
|
|
232
|
+
lines.insert(4, f"- Model: `{model}`")
|
|
233
|
+
if model_profile is not None:
|
|
234
|
+
lines.insert(5, f"- Model profile: `{model_profile}`")
|
|
235
|
+
if model_profile_purpose is not None:
|
|
236
|
+
lines.insert(6, f"- Profile purpose: {model_profile_purpose}")
|
|
237
|
+
lines.append(
|
|
238
|
+
"Yes — the skill produced a final response."
|
|
239
|
+
if finished
|
|
240
|
+
else "No — the skill did not produce a final response."
|
|
241
|
+
)
|
|
242
|
+
if execution_turns is not None:
|
|
243
|
+
lines.append(f"Completed turns: {execution_turns}.")
|
|
244
|
+
lines.extend(("", "## Observed actions and results", ""))
|
|
245
|
+
if not events:
|
|
246
|
+
lines.append("No action completed.")
|
|
247
|
+
for event in events:
|
|
248
|
+
source = (
|
|
249
|
+
"skill folder"
|
|
250
|
+
if event.source == "skill_bundle"
|
|
251
|
+
else event.source.replace("_", " ")
|
|
252
|
+
)
|
|
253
|
+
lines.extend(
|
|
254
|
+
(
|
|
255
|
+
f"### {event.index + 1}. `{event.tool_name}` ({source})",
|
|
256
|
+
"",
|
|
257
|
+
"Arguments: "
|
|
258
|
+
+ json.dumps(
|
|
259
|
+
event.arguments,
|
|
260
|
+
ensure_ascii=False,
|
|
261
|
+
sort_keys=True,
|
|
262
|
+
separators=(",", ":"),
|
|
263
|
+
),
|
|
264
|
+
"",
|
|
265
|
+
f"Result: {event.result}",
|
|
266
|
+
"",
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
omitted = sum(item.omitted_history for item in events)
|
|
270
|
+
if omitted:
|
|
271
|
+
lines.extend(
|
|
272
|
+
(
|
|
273
|
+
f"The World prompt omitted {omitted} earlier actions because its "
|
|
274
|
+
"history window is bounded.",
|
|
275
|
+
"",
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
lines.extend(("## Semantic judgment", ""))
|
|
279
|
+
if judge is None:
|
|
280
|
+
lines.append("The success criteria could not be reviewed.")
|
|
281
|
+
else:
|
|
282
|
+
lines.append(f"Decision: **{judge.get('verdict', 'unknown')}**")
|
|
283
|
+
rationale = judge.get("rationale")
|
|
284
|
+
if isinstance(rationale, str) and rationale:
|
|
285
|
+
lines.extend(("", rationale))
|
|
286
|
+
unmet = judge.get("unmet_criteria")
|
|
287
|
+
if isinstance(unmet, (list, tuple)) and unmet:
|
|
288
|
+
lines.extend(("", "Unmet criteria:"))
|
|
289
|
+
lines.extend(f"- {item}" for item in unmet)
|
|
290
|
+
criteria = judge.get("criteria")
|
|
291
|
+
if isinstance(criteria, (list, tuple)) and criteria:
|
|
292
|
+
lines.extend(("", "Criterion assessments:"))
|
|
293
|
+
for item in criteria:
|
|
294
|
+
if not isinstance(item, Mapping):
|
|
295
|
+
continue
|
|
296
|
+
criterion = item.get("criterion", "Unnamed criterion")
|
|
297
|
+
status = item.get("status", "unknown")
|
|
298
|
+
evidence = item.get("evidence")
|
|
299
|
+
lines.append(f"- **{status}** — {criterion}")
|
|
300
|
+
if isinstance(evidence, str) and evidence:
|
|
301
|
+
lines.append(f" Evidence: {evidence}")
|
|
302
|
+
lines.extend(("", "## Exact fact checks", ""))
|
|
303
|
+
if not assertions:
|
|
304
|
+
lines.append("No exact fact checks were declared for this case.")
|
|
305
|
+
for assertion in assertions:
|
|
306
|
+
marker = "passed" if assertion.get("passed") else "failed"
|
|
307
|
+
kind = str(assertion.get("kind", "unknown"))
|
|
308
|
+
label = assertion_labels.get(kind, kind)
|
|
309
|
+
lines.append(
|
|
310
|
+
f"- Fact check {assertion.get('ordinal', '?')} ({label}): {marker}."
|
|
311
|
+
)
|
|
312
|
+
expected = assertion.get("expected")
|
|
313
|
+
if isinstance(expected, str):
|
|
314
|
+
lines.append(f" Expected: {expected}")
|
|
315
|
+
observed = assertion.get("observed")
|
|
316
|
+
if isinstance(observed, str) and observed:
|
|
317
|
+
lines.append(f" Observed: {observed}")
|
|
318
|
+
lines.extend(("", "## Trusted repository checks", ""))
|
|
319
|
+
if not checks:
|
|
320
|
+
lines.append("No repository checks were declared for this case.")
|
|
321
|
+
for check in checks:
|
|
322
|
+
lines.append(
|
|
323
|
+
f"- {check.get('name', 'Unnamed check')}: "
|
|
324
|
+
f"{check.get('outcome', 'unknown')}."
|
|
325
|
+
)
|
|
326
|
+
detail = check.get("detail")
|
|
327
|
+
if isinstance(detail, str) and detail:
|
|
328
|
+
lines.extend(("", detail))
|
|
329
|
+
if failure is not None:
|
|
330
|
+
heading = (
|
|
331
|
+
"What prevented completion"
|
|
332
|
+
if not finished
|
|
333
|
+
else "What prevented a trustworthy result"
|
|
334
|
+
)
|
|
335
|
+
lines.extend(("", f"## {heading}", "", failure))
|
|
336
|
+
if failure_stage is not None:
|
|
337
|
+
lines.extend(("", f"Technical stage: `{failure_stage}`."))
|
|
338
|
+
if failure_details:
|
|
339
|
+
lines.extend(("", "## Technical details (bounded and redacted)", ""))
|
|
340
|
+
lines.extend(f"- {detail}" for detail in failure_details)
|
|
341
|
+
lines.extend(("", "## Execution accounting", ""))
|
|
342
|
+
if execution_turns is None:
|
|
343
|
+
lines.append("Model turns used: unavailable.")
|
|
344
|
+
else:
|
|
345
|
+
limit = "unknown" if execution_turn_limit is None else str(execution_turn_limit)
|
|
346
|
+
lines.append(
|
|
347
|
+
f"Model turns used: {execution_turns} of {limit} "
|
|
348
|
+
f"({execution_turns_source})."
|
|
349
|
+
)
|
|
350
|
+
lines.append("Per-stage token usage and any cost estimate are in `result.json`.")
|
|
351
|
+
lines.extend(
|
|
352
|
+
(
|
|
353
|
+
"",
|
|
354
|
+
"## Evidence files",
|
|
355
|
+
"",
|
|
356
|
+
"- `run.json` and `inputs.json`",
|
|
357
|
+
"- `transcript.jsonl` and `execution.json`",
|
|
358
|
+
"- `judge.json`, `verdict.json`, and `checks.json`",
|
|
359
|
+
"- `result.json` — the canonical summary for automation",
|
|
360
|
+
"- `checks/` logs for each repository check that started",
|
|
361
|
+
"",
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
return "\n".join(lines).encode("utf-8")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def execution_bytes(
|
|
368
|
+
final_output: str,
|
|
369
|
+
turns: int | None,
|
|
370
|
+
usage: object,
|
|
371
|
+
*,
|
|
372
|
+
turns_source: str = "unavailable",
|
|
373
|
+
) -> bytes:
|
|
374
|
+
"""Render completed execution facts before a repository check can begin."""
|
|
375
|
+
return canonical_json(
|
|
376
|
+
{
|
|
377
|
+
"format_version": ARTIFACT_FORMAT_VERSION,
|
|
378
|
+
"final_output": final_output,
|
|
379
|
+
"turns_used": turns,
|
|
380
|
+
"turns_source": turns_source,
|
|
381
|
+
"usage": usage,
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def judge_bytes(judge: object, exact_fact_checks: object) -> bytes:
|
|
387
|
+
"""Render decision facts only; prompts and raw provider responses never persist."""
|
|
388
|
+
return canonical_json(
|
|
389
|
+
{
|
|
390
|
+
"format_version": ARTIFACT_FORMAT_VERSION,
|
|
391
|
+
"semantic_judgment": judge,
|
|
392
|
+
"exact_fact_checks": exact_fact_checks,
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def verdict_bytes(
|
|
398
|
+
outcome: str, failure: str | None, failure_details: tuple[str, ...] = ()
|
|
399
|
+
) -> bytes:
|
|
400
|
+
"""Render the single final state, retaining a normalized technical failure."""
|
|
401
|
+
value: dict[str, object] = {
|
|
402
|
+
"format_version": ARTIFACT_FORMAT_VERSION,
|
|
403
|
+
"overall_outcome": outcome,
|
|
404
|
+
}
|
|
405
|
+
if failure is not None:
|
|
406
|
+
value["failure"] = failure
|
|
407
|
+
if failure_details:
|
|
408
|
+
value["failure_details"] = list(failure_details)
|
|
409
|
+
return canonical_json(value)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def checks_bytes(checks: object) -> bytes:
|
|
413
|
+
"""Render compact declared-check outcomes and artifact log names."""
|
|
414
|
+
return canonical_json(
|
|
415
|
+
{"format_version": ARTIFACT_FORMAT_VERSION, "trusted_repository_checks": checks}
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def result_bytes(summary: Mapping[str, object]) -> bytes:
|
|
420
|
+
"""Render the v2 result summary with independent user-facing statuses."""
|
|
421
|
+
return canonical_json({"format_version": ARTIFACT_FORMAT_VERSION, **dict(summary)})
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def experiment_bytes(summary: Mapping[str, object]) -> bytes:
|
|
425
|
+
"""Render the bounded parent record for a sampling/control experiment."""
|
|
426
|
+
return canonical_json({"schema_version": 1, **dict(summary)})
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def experiment_report_bytes(summary: Mapping[str, object]) -> bytes:
|
|
430
|
+
"""Render a short report that points authors to paired run evidence."""
|
|
431
|
+
interpretation = summary.get("interpretation")
|
|
432
|
+
if not isinstance(interpretation, Mapping):
|
|
433
|
+
interpretation = {}
|
|
434
|
+
lines = [
|
|
435
|
+
"# SkillRoll authoring experiment",
|
|
436
|
+
"",
|
|
437
|
+
f"- Eval case: `{summary.get('case', 'selected cases')}`",
|
|
438
|
+
f"- Model: `{summary.get('model', 'unknown')}`",
|
|
439
|
+
f"- Samples: {summary.get('samples_requested', 'unknown')}",
|
|
440
|
+
f"- Interpretation: **{interpretation.get('status', 'unknown')}**",
|
|
441
|
+
"",
|
|
442
|
+
str(interpretation.get("explanation", "No interpretation was recorded.")),
|
|
443
|
+
"",
|
|
444
|
+
str(interpretation.get("next_action", "Inspect the paired evidence.")),
|
|
445
|
+
"",
|
|
446
|
+
"## Paired runs",
|
|
447
|
+
"",
|
|
448
|
+
"| Sample | Skill outcome | Omission-control outcome | "
|
|
449
|
+
"Interpretation | Evidence |",
|
|
450
|
+
"| ---: | --- | --- | --- | --- |",
|
|
451
|
+
]
|
|
452
|
+
pairs = summary.get("paired_comparisons", ())
|
|
453
|
+
if isinstance(pairs, (list, tuple)):
|
|
454
|
+
for pair in pairs:
|
|
455
|
+
if not isinstance(pair, Mapping):
|
|
456
|
+
continue
|
|
457
|
+
skill = pair.get("skill_run")
|
|
458
|
+
control = pair.get("skill_control_run")
|
|
459
|
+
skill = skill if isinstance(skill, Mapping) else {}
|
|
460
|
+
control = control if isinstance(control, Mapping) else {}
|
|
461
|
+
evidence = " ".join(
|
|
462
|
+
str(path)
|
|
463
|
+
for path in (
|
|
464
|
+
skill.get("artifact_directory", ""),
|
|
465
|
+
control.get("artifact_directory", ""),
|
|
466
|
+
)
|
|
467
|
+
if path
|
|
468
|
+
)
|
|
469
|
+
lines.append(
|
|
470
|
+
f"| {pair.get('sample', '?')} | {skill.get('outcome', 'unknown')} | "
|
|
471
|
+
f"{control.get('outcome', 'not run')} | "
|
|
472
|
+
f"{pair.get('control_interpretation', 'not recorded')} | `{evidence}` |"
|
|
473
|
+
)
|
|
474
|
+
lines.extend(("", "## Counts", ""))
|
|
475
|
+
for name in ("skill_runs", "skill_control_runs"):
|
|
476
|
+
values = summary.get(name)
|
|
477
|
+
if isinstance(values, Mapping):
|
|
478
|
+
lines.append(
|
|
479
|
+
f"- {name}: "
|
|
480
|
+
+ ", ".join(f"{key}={value}" for key, value in values.items())
|
|
481
|
+
+ "."
|
|
482
|
+
)
|
|
483
|
+
lines.extend(
|
|
484
|
+
(
|
|
485
|
+
"",
|
|
486
|
+
"The omission control is an authoring diagnostic. It does not change "
|
|
487
|
+
"the ordinary PASS/FAIL result for the selected skill.",
|
|
488
|
+
"",
|
|
489
|
+
"Per-run `report.md`, `result.json`, and `transcript.jsonl` contain "
|
|
490
|
+
"the evidence needed for manual review.",
|
|
491
|
+
)
|
|
492
|
+
)
|
|
493
|
+
return "\n".join(lines).encode("utf-8")
|