zstack keeps two complementary regression manifests:
evals/cases.yamltests what an agent does after a skill is selected.evals/routing.yamltests whether frontmatter descriptions select the right skill—or correctly selectnone—before any skill body is loaded.
doctor.sh validates both manifests and requires at least three behavioral
cases per active skill—including one happy-path and one edge or
adversarial case—plus two routing cases per active skill and at least two
routing no-match cases. It does not pretend that static validation proves
model behavior.
Use any isolated runner for repeatable no-web response cases:
# Start with one case while iterating; this makes a real model call.
ruby scripts/run-codex-behavioral-evals.rb --case market-tools-blocked
ruby scripts/run-claude-behavioral-evals.rb \
--case market-tools-blocked \
--model sonnet
ruby scripts/run-grok-behavioral-evals.rb \
--case market-tools-blocked \
--model grok-4.5
# Run the whole response or artifact partition without listing every case.
ruby scripts/run-codex-behavioral-evals.rb --execution response
ruby scripts/run-codex-behavioral-evals.rb --execution artifact
# Inspect exact snapshots and prompts without calling a model.
ruby scripts/run-claude-behavioral-evals.rb \
--dry-run \
--case-prefix market-The runner copies only the selected skills into the run directory. It bounds
each source tree, captures every regular file through stable non-link binary
reads, writes those exact bytes exclusively, and then recaptures all selected
sources before finalizing the snapshot manifest. File, byte, inventory, or
content drift aborts the run rather than creating a mixed snapshot. It hashes
every copied source file, then gives each case a separate initialized Git
workspace containing that exact snapshot. Codex and Claude receive the
generation-time prompt bytes through an owner-private descriptor that is
unlinked before spawn; the structured invocation hash is computed from those
same in-memory bytes, never by reopening the retained prompt pathname. Grok
receives the exact same bytes through --prompt-file /dev/fd/9; fd 9 is mapped
to a separate anonymous descriptor in each child process, so the apparent file
path cannot be replaced and contains no case identity.
Before any host launch, the runner also stable-reads the retained public prompt
and requires it to equal the generation-time bytes; early pathname drift aborts
before model execution, while later drift is caught by output-tree monitoring.
Private workspaces and host homes use independent random 128-bit tokens rather
than semantic case ids. Those
tokens never enter public prompts, responses, or artifacts; the runner rejects
a final response that exposes one, and the archiver independently verifies the
private path relationships before discarding invocation records.
Cases default to execution: response. A case that must create real
files declares execution: artifact and must be run separately from response
cases; the runner switches to an isolated writable sandbox (workspace-write
for Codex/Claude, workspace for Grok), permits
writes only under artifacts/, verifies that the copied skill did not change,
rejects any other workspace file, and hashes every retained deliverable.
Each case copy is materialized directly from the runner's captured in-memory
bytes rather than re-reading a pathname-backed snapshot. The runner verifies
the exact file inventory and bytes immediately before launch and on every
runtime-tree poll, so a changed copy terminates the case and cannot be archived.
Artifact responses must cite only workspace-relative artifacts/... paths; a
machine-local workspace path makes the run incomplete before review.
The Codex response adapter uses a read-only sandbox and approval policy never,
copies only its auth file into a disposable per-case CODEX_HOME, removes
ambient desktop task/permission/origin variables, ignores user config/rules,
disables anonymous Codex analytics with the documented per-run
analytics.enabled=false override, and disables web/browser/apps/computer
access and subagents. This prevents
installed plugins, MCP servers, and unrelated skill descriptions from entering
the evaluation context. The Claude adapter uses safe mode, a Read-only tool allowlist,
permission mode dontAsk, no session persistence, and disables Chrome and MCP;
host authentication/provider settings can still affect requested-model
resolution, so requested and actual model ids are recorded separately. The
runner classifies failed Claude API results into a small, non-secret category
(for example model-unavailable, authentication, or rate-limit) and shows
the HTTP status when present. It never prints the provider's raw error text;
inspect private logs/*.json only when the category is insufficient. A
model-unavailable result with no model usage is a provider/configuration
failure, not behavioral evidence, and must not be reviewed or archived. The
Grok adapter uses an isolated ephemeral GROK_HOME, fixed system-prompt
override, read-only sandbox, Read-only tool allowlist, permission mode
dontAsk, no memory/plan/subagents/web, and an auth copy inside that isolated
home. Both auth adapters accept only a current-user, owner-private, single-link
regular file of at most 1 MiB; they use a no-follow stable copy and remove a
partial destination on failure. A missing auth file remains a supported way to
exercise redacted authentication-failure handling.
Its system prompt requires one exact standalone final-response marker. The
runner retains only bytes after that marker; missing, repeated, or empty marked
output fails closed. Any host/tool narration before the marker remains in the
private JSON result, while its discarded byte count is carried into the
reviewed archive as a runner incident. The archiver independently re-extracts
the marked response and corroborates both the retained bytes and byte count.
For Grok API failures, the runner extracts a defensible HTTP status from the
private structured result when available and reports only a small category such
as authentication, rate-limit, or provider-error; the provider message is
never copied into the public response or console diagnosis.
Artifact cases use Grok's built-in workspace sandbox with Read,Write,Edit
and bypassPermissions; the disposable-workspace boundary and post-run audit
then reject writes outside artifacts/ or changes to the copied skill.
Grok can still discover the standard Agent Skills hub outside its isolated
host home, so this limitation is retained in controls and archive summaries;
the fixed prompts require use of only the exact copied target snapshot. MCP
startup warnings may still appear privately, but MCP tools are excluded. Both
structured adapters record requested and actual model ids separately. The case
receives its registered prompt, but never must or must_not.
The runner records execution completion, not a semantic grade. Its
results.json intentionally leaves grade as null. It does mechanically
validate the shared two-column Evidence handoff contract against retained
response and Markdown artifact bytes, recording the result and pre-filling
only that criterion in review.json. Review the retained output against every
remaining criterion: set a public reviewer label, case grade, each semantic
observed boolean, and concise evidence. Keep logs/ private because host
results and invocation records contain ephemeral session identifiers and
machine-local paths.
Every host process runs in its own process group. Response cases have a
600-second wall-clock limit by default; artifact cases get 1200 seconds because
bounded incremental file writes and transport retries take longer. Override the
limit with --timeout-seconds N when a deliberately large artifact needs more
time. A timeout terminates the whole process group, sets timedOut: true, and
leaves the run incomplete and ineligible for archive.
Runner resource flags fail closed before creating an output directory:
one invocation may select at most 256 cases, --jobs is limited to 32,
--timeout-seconds to 86,400, and Grok
--max-turns to 100. Claude's per-case budget must be a positive finite
number. These ceilings bound local processes and accidental API exposure while
remaining above the complete registered case matrices.
Generated prompts are written exclusively with a 512-KiB per-case limit and a
16-MiB whole-run limit; controls.json records both ceilings and the exact
retained prompt-byte total for archive revalidation.
Before creating those files, runners independently require unique bounded
kebab-case case and skill ids, and resolve every catalog skill through the real
skills/ tree without symlink traversal. This remains enforced even when a
runner is invoked directly without a preceding doctor check.
Before any model call, the host --version probe also runs in a supervised
process group with a 10-second timeout and 64-KiB combined stdout/stderr limit;
a stalled, noisy, background-spawning, or non-UTF-8 probe fails the run.
The runner resolves the host executable to one real absolute file before that
probe and verifies its device, inode, size, mode, and timestamps before and
after every case, preventing PATH or symlink drift from mixing versions in one
evidence run.
Each case workspace is initialized through a template-free git init with a
10-second timeout and 64-KiB output limit, so ambient Git templates cannot add
hooks or files and a broken Git executable cannot stall the runner.
Archive manifest generation and evidence validation use the same process-group
supervision, with a five-minute timeout and 2-MiB combined output limit. A
broken evidence tool therefore cannot hold the archive publication lock or
grow captured diagnostics without bound.
Coverage reporting uses the same five-minute inner bound, while documentation
checks give each nested report another 30 seconds to finish validation and
cleanup before terminating its process group.
After reviewing the run, promote it atomically:
ruby scripts/archive-codex-behavioral-run.rb \
--run-dir "$RUN_DIR" \
--archive 2026-08-01-codex-behavioral-example \
--phase after-fix
ruby scripts/archive-claude-behavioral-run.rb \
--run-dir "$RUN_DIR" \
--archive 2026-08-01-claude-behavioral-example \
--phase after-fix
ruby scripts/archive-grok-behavioral-run.rb \
--run-dir "$RUN_DIR" \
--archive 2026-08-01-grok-behavioral-example \
--phase after-fixThe archiver rejects incomplete or contradictory reviews, a handoff judgment
that disagrees with its own recomputation, prompt/response drift, changed
runner controls, incomplete snapshot hashes, and source changes since
execution. It corroborates controls and bytes against private host results and
invocation records,
copies exact prompts, responses, and skill snapshots but not logs, creates both
manifest layers, publishes by atomic rename, and rolls back a new archive when
post-publish evidence validation fails.
For artifact cases it additionally revalidates the runner artifact manifest,
copies the exact deliverable bytes under raw/artifacts/<case>/, and includes
them in both the raw and whole-archive manifests.
For a case that genuinely needs web access or another capability intentionally excluded by all isolated runners:
- Start a clean agent task with no prior zstack discussion.
- Install or directly reference the skill under test.
- Give the agent only the case
prompt; do not revealmustormust_not. - If the skill may write files, run it in a disposable, initialized workspace and allow writes only there. This avoids trust prompts and protects the repository while preserving realistic artifact behavior.
- Save the raw response or produced artifacts. Record an artifact manifest
when output size or file count is itself part of the grade. Use either
sha256 bytes pathor the legacybytes sha256 pathcolumn order; paths are relative to the manifest'sraw/directory. - Grade every
mustandmust_notitem, citing the response.
A case passes only when every must is present and every must_not is absent.
For a material skill revision, prefer independent runs in two supported hosts or
model families. Treat disagreement as a reason to sharpen the skill or case,
not as permission to average away a failure.
ruby scripts/report-evidence-coverage.rb
ruby scripts/report-evidence-coverage.rb --json
ruby scripts/report-evidence-coverage.rb --require-current-complete-pass-host codex
ruby scripts/report-evidence-coverage.rb \
--suite routing \
--require-current-complete-pass-host codex \
--require-current-complete-pass-host grokThe report distinguishes any archived pass from a pass whose archive has
complete execution provenance, both overall and for each supported host family.
Every report invocation first runs the full archive validator, so standalone
coverage gates never trust unhashed, linked, or structurally invalid run data.
The report then re-reads every run.json, review, result, control, prompt, and
skill-snapshot file through the same bounded stable-file contract. It rejects
linked path components and treats any file or tree that changes during the
current-source comparison as non-current, so the report does not rely on the
preflight validator alone.
Routing runners record disabled web search and subagents in their controls;
archivers verify those fields before copying them into public provenance.
Use --require-current-complete-pass for an any-host release gate, or repeat
--require-current-complete-pass-host for required Claude, Codex, or Grok
matrices. These gates bind each pass to the current skill-source bytes, so a
skill edit makes its older pass insufficient until a reviewed rerun is
published. Current gates also require the supported host's recorded isolation
contract, including disabled web search and subagents, an ephemeral session,
and its host-specific persistence or sandbox controls. Behavioral current
passes also recheck the published pass grade and every present/absent review
outcome against today's case criteria, plus the exact current prompt wrapper.
Routing current passes likewise reconstruct the current candidate rotation and
prompt wrapper, then require the retained selection, pass flag, format,
completion, and exit status to agree with today's expected route. The
non-current --require-complete-pass variants are useful only
for historical audits. Historical partial evidence remains valid but does not
satisfy any complete-provenance gate.
Use --suite routing to apply the same any-host and host-specific gates to the
current description-routing manifest; the default suite remains behavioral.
- Start a clean agent task with no prior zstack or case discussion.
- Give it only the current frontmatter descriptions of all candidate active
skills and the case
prompt; do not expose skill bodies, repository files,expected_skill, or grading notes. - Require exactly one selection from the candidates, or
nonewhen none fits. - Save the raw selection and concise reason before grading it against
expected_skill.
Routing prompts must express natural user intent and must not name a z-* skill
or use a host-specific explicit invocation. Include positive cases, close
boundaries between neighboring skills, and unrelated no-match requests. A
routing case passes only when the selected id exactly matches expected_skill.
Use the repository runner instead of rebuilding isolation prompts by hand:
# Real model calls; filters are recommended while iterating.
ruby scripts/run-codex-routing-evals.rb --case-prefix zh-
ruby scripts/run-claude-routing-evals.rb --case boundary-public-research
ruby scripts/run-grok-routing-evals.rb --case boundary-public-research
# Inspect a mechanical truncation prompt without calling Codex.
ruby scripts/run-codex-routing-evals.rb \
--dry-run \
--case boundary-public-research \
--truncate-descriptions 180The shared runner reads active descriptions and registered requests from source,
rotates candidate order deterministically, and starts one ephemeral host process
per case in a separate initialized Git workspace. It never places
expected_skill in the model prompt. Codex receives only generated stdin, uses
a read-only sandbox, uses a disposable auth-only CODEX_HOME, removes ambient
desktop context, ignores user config/rules, disables anonymous analytics with
analytics.enabled=false, and disables skill loading, apps, browser/computer
access, and subagents. Claude uses safe mode, an empty
built-in tool list, dontAsk, no Chrome/MCP, and no session persistence. Grok
uses an isolated host home, fixed system prompt, anonymous-descriptor
prompt-file transport, an empty
tool list, read-only sandbox, and disables web, memory, plan, subagents, managed
MCP, and marketplace auto-registration. Its workspace, host-home, and private
workspace and host-home paths use the same random opaque-token contract as
behavioral runs; the semantic routing case id is retained only in public runner
filenames, while the prompt path is the fixed child descriptor /dev/fd/9. Grok can
still discover the standard
Agent Skills hub outside its isolated home, so both routing prompts prohibit
skill loading and file inspection. Its final-response marker uses the same
fail-closed extraction and independent archive corroboration as behavioral
runs, so setup narration cannot corrupt the public two-line routing response.
--case, --case-prefix, --jobs,
--model, --reasoning, --timeout-seconds, --wrapper,
--truncate-descriptions, and --output control every host; host-specific
budget/turn/auth options are shown by --help. The same per-process-group
600-second default prevents one stalled routing call from blocking a matrix.
Routing applies the same resource ceilings; --truncate-descriptions accepts
80–4,096 Unicode characters.
Default and explicit output directories are owner-private. An existing
--output directory must be empty, owned by the current user, and deny all
group/other access because it retains private host logs and invocation records.
The runner holds an exclusive lease inside that directory until every child is
reaped and cleanup finishes, so concurrent runs cannot share an output path. A
hard-killed run leaves the lease in place and the non-empty directory must be
inspected or replaced instead of being silently reused.
The printed RUN_DIR contains exact candidate descriptions, per-case prompts,
verbatim final responses, machine-readable controls and grades, plus private
host records. Review failures before changing a description. Do not copy
logs/ into a public evidence archive: logs, invocation records, and
structured host results contain ephemeral identifiers and machine-local paths.
Use the matching archiver to publish only exact candidates, prompts, responses,
and sanitized metadata. A full matrix makes real model calls, so prefer the
smallest relevant selector during iteration and run the complete set only for
release-facing evidence.
After reviewing the runner directory, promote it without hand-copying files:
ruby scripts/archive-codex-routing-run.rb \
--run-dir "$RUN_DIR" \
--archive 2026-08-01-codex-routing-example \
--phase after-fix
ruby scripts/archive-grok-routing-run.rb \
--run-dir "$RUN_DIR" \
--archive 2026-08-01-grok-routing-example \
--phase after-fixThe archive name must be a new YYYY-MM-DD-kebab-slug directory beginning with
a real ISO calendar date, and the phase must be kebab-case. The archiver
re-parses every final response, checks grades
against the current routing manifest, rejects prompt/response drift and private
runner identifiers, retains exact prompts and responses, and uses private host
records to corroborate requested/actual model, permissions, isolation,
workspace, prompt bytes, response bytes, turns/stops, and incident counts. It
never copies those private records. It creates both manifest layers and validates
the complete schema-v2 evidence set, building in a hidden sibling directory and
publishing by atomic rename. An existing target is never overwritten, and a
failed post-publish validation removes only the newly created archive.
For a release-facing run, store immutable raw responses in
evals/runs/<YYYY-MM-DD>[-<kind-or-runner>]/raw/ and the grades in the same
directory's summary.md. Keep failed first attempts when they led to a skill
change; they explain why the regression rule exists. Do not overwrite a same-day
run from a different host or model—append a short runner slug to the directory
name instead.
Publish a run only when it has a durable evidence role: it establishes missing current-source or host coverage, preserves a failure that caused a rule or fix, validates a new runner/host contract, or records a release-facing complete matrix. Keep exploratory runs and redundant passing reruns in an ignored local directory. Archives are immutable audit evidence, not a general model-call log.
Each runner also limits combined per-case host-produced output to 2 MiB. For
Codex this is the CLI log plus the host-written final-response file; for
structured Claude/Grok adapters it is the raw JSON result plus stderr. The
runner-extracted Claude/Grok response is separately bounded to 2 MiB but is not
counted twice as host output. The supervisor applies an operating-system
per-file size limit and terminates the process group when the monitored total
crosses the bound; controls.json and failed results retain the limit and
observed byte count.
Artifact cases additionally retain at most 400 files, 2,000 total tree entries,
2 MiB per file, and 8 MiB total under artifacts/. Stable binary reads, a
second inventory/content pass, and exclusive destination writes reject changed
or oversized trees before treating them as durable runner evidence. The
artifact manifest is generated from those captured in-memory bytes, while each
retained file is re-read stably and required to match before final publication.
Retained
paths must also be canonical relative UTF-8 without leading/trailing whitespace
or control characters so JSON, line manifests, and every supported host encode
the same artifact identity. The 400-file and 8-MiB retained-artifact budgets
also apply cumulatively across every case in one invocation, preventing
individually valid cases from producing a run that exceeds its declared
publication envelope.
Every real-host case applies a 250-ms baseline-tree poll to its allowed
top-level layout, template-free .git tree, copied skill package when present,
and isolated Codex/Grok home. This prevents a damaged or hostile local CLI from
ignoring its declared sandbox and using a private subtree to grow disk state
without bound. A violation terminates the process group and is recorded as
workspaceLimitError; archivers reject that result and preserve the exact
limits in public execution provenance. Runners prepare the complete batch
layout first, start the monitor, and dynamically register each PID immediately
after spawn returns; no earlier process waits for later case setup before it
is supervised. The monitor checks every concurrent PID independently rather
than waiting for join order, and the parent performs one atomic final check
when each PID exits. The same batch check binds the
shared temporary-root layout and every isolated-home container, so a process
cannot evade its case budget by writing a sibling directory. Grok's inherited
prompt descriptor is anonymous and unlinked; its exact bytes are bound before
spawn and the parent closes its copy immediately afterward. The output
reservation separately pins both the output-directory and lease-file identities;
replacements are detected during every batch and before
and after final evidence writes. Each batch also captures the output-tree
inventory before spawning a host. Only that batch's declared log, invocation,
and response paths may appear or grow; prior evidence remains identity- and
timestamp-bound, while the whole tree stays below 4,096 files, 8,192 entries,
and 640 MiB. Runner-created logs, invocations, controls, manifests, results,
and review scaffolds use owner-private exclusive creation, so a pre-existing
file or symbolic link fails the run instead of being followed or truncated.
A workspace/output-tree violation stops scheduling later batches and leaves a
partial, non-archivable diagnostic run; Grok prompt copies use the same stable
read and exclusive-create boundary before their captured hash is recorded.
Every runtime workspace, artifact, host-home, and runner-output file must also
be owned by the current user and have exactly one hard link, preventing an
allowed path from aliasing an inode outside the monitored tree.
Repository inputs and retained runner sources use the same owner and
single-hard-link rule. Their parent-directory identities and file stat state
are checked across each bounded read or snapshot. Immediately after open, the
path's pre-open identity must match the opened descriptor before the first byte
is read, copied, or hashed; the identity is checked again after the operation.
Thus a manifest, auth file, retained source, control file, prompt, response, or
log cannot enter evidence through an external hardlink, a replacement between
lstat and open, or a concurrently replaced trusted directory.
The standalone Evidence handoff validator and current-evidence documentation
checker use this same 2 MiB stable-input contract for path-backed Markdown;
missing documents still fail normally, while symlinks, hardlinks, special
files, oversized inputs, and replacement races are rejected before their bytes
can influence a result.
Published evidence archives extend this rule to every directory and regular
file. Manifest generation binds archive and evals/runs directory identities
through its two preflight passes and atomic replacement; validation records
ownership and link counts in the whole-tree state that it rechecks at exit.
Coverage reporting captures the complete evidence collection before invoking
the validator, requires the same content-bound tree immediately afterward,
reads only the captured run.json set, verifies every consumed file against
that snapshot, and rechecks the bounded tree before emitting a report. The
collection is capped at 100,000 entries across all archives.
Artifact execution is additionally single-process so the whole-run budget can
be enforced while the model is still running. The same poll checks artifact
file, tree-entry, per-file, file-count, and cumulative-byte limits before the
final content audit.
Archivers do not parse or publish directly from a mutable --run-dir. They
first capture each referenced regular file into a private, stable snapshot,
reject symbolic-link path components and files above 2 MiB, and verify file
identity, size, and timestamps across the copy. A promotion captures at most
1,000 source files and 64 MiB total. The same bounded read retains an immutable
in-memory copy as the authority for every later parse, digest, byte count,
privacy check, Evidence handoff check, and exclusive archive write. The private
snapshot pathname remains only a compatibility handle; replacing it after
capture cannot change validation or publication. Binary bytes remain
ASCII-8BIT; JSON, manifests, prompts, responses, and logs must pass an
explicit UTF-8 view before text processing, avoiding locale-dependent implicit
file encodings. Later edits to either the compatibility pathname or the runner
directory therefore cannot change the evidence being promoted.
The archiver also keeps a relative-path-to-bytes map for every public raw/
file. It derives the raw artifact manifest from that map instead of rescanning
pathnames, then requires the complete directory/file inventory, byte counts,
and SHA-256 digests to match immediately after candidate validation and again
after publication. Replacing a raw file and rewriting both manifest layers into
a new self-consistent archive therefore still fails because the bytes no longer
match the runner capture.
After the whole-archive manifest is created, the archiver captures one bounded,
content-bound snapshot of the complete hidden candidate. Candidate validation
must leave every directory and file in that tree unchanged; after atomic rename,
the same inode-backed child tree must appear at the public name. This extends
the raw-byte guarantee to generated summaries, metadata, controls, reviews, and
both manifest layers, even when an attempted replacement updates its manifest
entry to remain internally self-consistent.
Generated top-level files use a transaction-root-bound writer: paths are
canonical single-component names, writes are exclusive and no-follow, each file
is capped at 2 MiB, and both the root inode and created file inode are checked
before and after fsync. A raced existing file or symlink is preserved and
causes the promotion to fail instead of being overwritten or followed outside
the hidden archive.
Temporary evaluation directories are removed through an identity-bound
quarantine: cleanup first moves the expected dev/inode under a random
owner-private sibling, rechecks it there, and only then traverses it. If the
caller-visible pathname was replaced during that move, cleanup restores and
preserves the replacement instead of recursively deleting it.
Archive publication is serialized across routing and behavioral archivers with
an owner-private operating-system file lock keyed to the repository's real
evals/runs path. The lock covers baseline validation, archive construction,
the final target-existence check, rename, post-publish validation, and rollback.
Concurrent archive commands therefore fail before creating a target, and a
crashed publisher releases the lock through the kernel.
Direct archive-manifest generation and checking acquire the same lock. An
archiver's bounded generator subprocess uses the lock already held by its
parent, so manifest maintenance cannot race a promotion or another generator.
The pathname, opened file descriptor, owner metadata, and device/inode identity
are rebound before every publication boundary. A generator subprocess retains
its own read lease on the parent's exact lock inode and rechecks that the
parent's exclusive flock still conflicts. Replacing the temporary lock pathname
therefore aborts and rolls back identity-matched outputs instead of creating a
second publication lane.
Before rename, the archiver asks the evidence validator to check the hidden candidate directory alongside all existing archives while applying its intended final archive name. The candidate must pass the same schema, provenance, manifest, mapping, path, and byte checks as a published archive. Post-rename validation remains as a repository-wide defense, but a process killed in that final window can leave only an archive that already passed complete validation.
The publication lock does not freeze evaluation definitions or skill sources,
so archivers bind those separately. A routing promotion retains the exact
evals/routing.yaml bytes it graded; a behavioral promotion retains the exact
evals/cases.yaml bytes plus bounded SHA-256 state for every file in each
selected current skill tree. Stable non-link reads and a second inventory pass
capture that state. Direct archiver use enforces the same 100-file, 5-MiB
per-skill package bounds as the runner, stops a skill-tree walk after 1,000
entries, and retains the existing 1,000-file / 64-MiB whole-promotion budget.
The archiver rechecks the state after candidate validation before rename and
again after post-publish validation. Any concurrent drift aborts the candidate
or rolls back only the archive that transaction just published.
Every archive must include a machine-readable run.json using schema version 2.
Record schemaVersion, archiveDate, the behavioral or routing suite, a host
object, provenance status, a non-empty runs array, and runnerIncidents. Each
run has a unique kebab-case id, its own registered case, phase, execution
controls, grade, and the exact retained paths it uses under raw/. This
per-run mapping supports multi-case archives and shared routing-response files
without inferring ownership from filenames.
Set provenance to complete only when the host version and every run's model,
workspace, permissions, web/subagent flags, turn limit or count, and stop status
were recorded. For historical evidence with gaps, use partial, list the gaps,
and store unknown values as JSON null; never reconstruct them from memory.
executionDefaults may hold values shared by every run. Token usage, cost,
reasoning effort, failures, host revision, and runner notes remain optional.
When recorded, exit status must be a non-negative integer, elapsed time and
cost must be finite non-negative numbers, and every numeric value nested in a
token-usage object must also be finite and non-negative; historical unknowns
may remain JSON null.
Omit account, session, and request identifiers; they do not help reproduce
behavior.
Every archive also needs at least one raw artifact-manifest*.txt. These raw
manifests preserve runner paths, sizes, hashes, and intentional inventory-only
entries. After the prompts, summary, metadata, and raw evidence are final, build
the whole-archive manifest:
ruby scripts/generate-evidence-manifests.rb evals/runs/<archive>
ruby scripts/generate-evidence-manifests.rb --check
ruby scripts/validate-evidence.rbDo not hand-edit archive-manifest.txt. The generator deterministically hashes
every regular archive file except that manifest itself, so control-document and
raw-manifest drift is detected as well as raw-byte drift. Git history anchors
the resulting digest; the manifest is an integrity check, not a substitute for
a signed release.
The manifest generator enumerates hidden top-level entries, rejects
non-canonical or impossible-date archive names, and computes every requested
manifest before writing any of them; one invalid target therefore cannot cause
an earlier target to be partially updated. It also rejects oversized inputs
before hashing and refuses to replace a symbolic-link or special-file manifest
target. The validator independently checks schema, provenance, suite/case references,
run-to-artifact mapping, complete archive and raw-file coverage, byte counts,
and SHA-256 hashes. Parsed JSON inputs require unique object keys and finite
numbers. YAML
manifests use one document with scalar, unique mapping keys; aliases remain
disabled. Parsed control files are captured as stable UTF-8 while retained
artifacts are hashed as arbitrary binary bytes. The validator records the initial type,
identity, mode, size, and timestamps of every archive entry and rechecks the
whole tree before success, binding all checks to one unchanged snapshot. A
published archive is bounded to 500 regular files, 2,000 total tree entries,
2 MiB per file, and 10 MiB total; one validation or default manifest pass
accepts at most 1,000 top-level archive entries. Split a larger durable run or retain large outputs
as explicit inventory-only entries instead of committing unbounded model logs
or binary artifacts. Archive only regular files; symbolic links are rejected
so evidence cannot resolve outside its directory. Manifest paths must be
canonical relative paths without . / .. aliases, leading or trailing
whitespace, or control characters. Reviewer labels, criterion evidence, and
generated summaries must cite portable archive-relative evidence rather than
machine-local paths; the archiver and validator reject common local home and
temporary-directory roots. If a failure raw manifest
inventories files whose bytes were intentionally not retained, add an
Unarchived entries are inventory-only: line followed by the exact
comma-separated manifest paths. Those allowlisted entries remain historical
inventory, not cryptographically reverifiable evidence; other missing or
unmapped files still fail validation.
Never place secrets, private customer data, or unpublished product material in the archive. Use a local ignored directory instead when a real-world artifact cannot be published safely.
Behavioral cases use response mode unless they explicitly require a written deliverable:
- id: smoke-complete-pack
skill: z-landing-smoke
kind: happy-path
execution: artifact
prompt: Create a complete smoke-test package...Use artifact only when file existence or deployability is part of the grade.
Keep ordinary reports and plans in the default response mode. Run the two modes
in separate invocations so their permission evidence remains unambiguous.
- Give each case a stable kebab-case
idand a registeredskill. - Set
kindtohappy-path,edge, oradversarial; every active skill needs a successful full-output case as well as resilience coverage. - Test a decision boundary or failure mode, not superficial wording.
- Keep prompts realistic and free of the intended answer.
- Write observable grading criteria; avoid "high quality" or "good reasoning".
- Never include private customer data, secrets, or the expected response itself.
- Give each case a stable kebab-case
id, a realisticprompt, and anexpected_skillthat is either an active catalog id ornone. - Add at least two positive cases per active skill and at least two unrelated no-match cases across the manifest.
- Test ambiguous neighboring intents as explicit boundary cases.
- When descriptions advertise non-English triggers, include at least one positive request for every such skill. Add neighboring boundary cases where skills overlap, and keep at least one unrelated no-match request in that language across the suite.
- Never put a skill id, host-specific explicit invocation, or expected selection in the prompt.
- Re-run changed descriptions in clean contexts; static manifest validation is only a guardrail, not a routing result.
Run ./scripts/test.sh after changing either manifest.