IO contract
trap passes locations, never inlined data. Each subprocess gets one env var holding
a JSON string; the consumer does json.loads(os.environ[VAR]) and reads the files
itself. Override any var name with manifest_envvar in the relevant YAML.
Every value is a directory or file path — not a pre-scanned {name → path} dict — which
keeps the contract lossless for nested input trees and lets each consumer read exactly the
files it authored.
Legacy tasks. Task versions written for the old trapstreet-cli read a
TRAPTASK_PAYLOADenv var instead ofTRAPTASK_MANIFEST, so their judge/grader crash under this CLI (a bareKeyError).tp runrecognises the signature and says so; the fix is task-side — publish a task version that readsTRAPTASK_MANIFEST, or run the old one withuvx --from trapstreet-cli tp run.
Solution — TRAP_MANIFEST
{"inputs_dir": "/abs/task/inputs/<case_id>",
"outputs_dir": "/abs/.trap/.../<case_id>/solution/outputs"}
inputs_dir— your case's input files (you know their names).outputs_dir— write outputs here; trap writes nothing in it, so it holds exactly what the solution produced.- stdout, stderr, exit code and duration are captured automatically. Optionally read one input file from stdin (declare it via
stdin:in trap.yaml).
import json, os
from pathlib import Path
m = json.loads(os.environ["TRAP_MANIFEST"])
cfg = json.loads((Path(m["inputs_dir"]) / "config.json").read_text())
(Path(m["outputs_dir"]) / "result.json").write_text(json.dumps({"answer": 42}))
Judge — TRAPTASK_MANIFEST (per case)
{
"inputs_dir": "/abs/task/inputs/<case_id>",
"expected_dir": "/abs/task/expected/<case_id>", // null if the case has no expected/
"outputs_dir": "/abs/.../solution/outputs",
"run": { "stdout": "<path>", "stderr": "<path>", "meta": "<path>" }
}
run.meta is a JSON file {"exit_code": N, "duration": seconds}. The judge prints
free-form JSON to stdout; trap stores it verbatim as the case's metrics. No reserved
field names.
m = json.loads(os.environ["TRAPTASK_MANIFEST"])
out = Path(m["run"]["stdout"]).read_text().strip()
exp = json.loads((Path(m["expected_dir"]) / "expected.json").read_text())
print(json.dumps({"score": 1.0 if out == exp["answer"] else 0.0}))
Grader — TRAPTASK_MANIFEST (once, all cases)
A JSON list of per-case results:
[{"case_id": "c1", "exit_code": 0, "duration": 0.12,
"metrics": {"score": 1.0}, "cost": null, "judge_exit_code": 0}]
metrics is whatever JSON the judge printed — any value it emits, or null. It never
signals pass/fail. judge_exit_code is the sole pass/fail signal: 0 = passed (it printed
valid JSON, whatever the shape — even null), 124 = timed out, 125 = exited 0 but its
stdout wasn't JSON, any other non-zero = the judge's own exit, null = no judge ran. The
grader works the same way via grader_exit_code. A grader's verdict never affects trap's
exit code; a grader that fails (by its exit code) does — tp run exits 3 (see the CLI
reference, exit codes).
docs for trap v0.0.14 · source: trapstreet/trap