TfP Docs

Authoring

Status: implemented. Local authoring (tfp init/lint/test) and authoring over MCP (the toolkit tool, the author and custom_verifier capabilities, uploads, validation with reconciliation, durable private drafts and explicit sharing) are implemented and tested. Custom checker images are registered, provisioned and executed on TfP; the base image tokensforprogress/verifier-python:0.1.0 is public on Docker Hub, see Custom sandbox checkers. This page is the contract for the authoring package (the files, what each becomes, how they refer to one another, what the linter reports) and for the MCP surface (capabilities, uploads, validation, private drafts and publication). The author-facing guide is Authoring with Your Agent; the TfP Authoring Skill (skills/authoring/SKILL.md, embedded on that page) teaches the same convention to an agent.

An authoring package is a directory of ordinary files. Each file corresponds to one existing TfP object; the linter validates every file with the same canonical code the platform runs, and prints what the package would publish. Publication consumes exactly these files and adds nothing.

Files

A Problem package is a directory containing:

File Becomes Required
problem.json Problem (identity) and the non-prose fields of its first or next ProblemRevision; on a later revision revision_note becomes ProblemRevision.changelog_md yes
statement.md ProblemRevision.statement_md yes
skills/<name>/… one SkillVersion per directory, pinned required by the revision no
reference/<key>.<ext> one reference-input binding per file: slot <key> → these exact bytes; mounted in the sandbox as /input/reference/<key>/<key>.<ext> no
reference/<key>.link.json one reference-input binding per file: slot <key> → content at a public https:// URL, with the hash and size you state; TfP stores no bytes and sandbox stages do not see it (reference/) no
verifier-contract.json VerifierContract (a Verifier is created or reused for it at publication); each sandbox stage's inline implementation becomes one VerifierImplementation no
checker.py (any .py a stage names as ./…) the program of a script checker's VerifierImplementation: stored content-addressed, run on TfP's tfp-runtime/python:1 (Script checkers) with a stage naming it
fixtures/<filename> bytes of the Contract's conformance fixtures (its checker test cases) with sandbox stages
review-protocol.json ReviewProtocol with review stages
verifier-implementation.json VerifierImplementation for one sandbox stage, only when the stage does not name its checker inline (kept for compatibility; prefer inline) no

ProblemRevision.context_md is always null: there is no context.md. There is no changelog.md and authors do not maintain a changelog; the one optional sentence explaining a later revision is problem.json.revision_note. Both file names are reported as notes and never published.

A Campaign folder is a directory containing campaign.json and a problems/ directory whose subdirectories are Problem packages. Shared mechanisms may sit beside campaign.json: skills/, review-protocol.json, verifier-contract.json (with its inline implementations) and fixtures/. A Problem that has no mechanism of that kind inherits the Campaign's; a Problem with its own uses its own. The plan lists every mechanism once, keyed by content hash, and each Problem references the hashes it uses, so a thousand Problems sharing one Contract publish one Contract.

Anything else in the directory is reported as a note and is not published. A .tfp/ directory is ignored (the CLI keeps upload state there).

problem.json

{
  "slug": "extract-arxiv-2607-15495",
  "title": "Extract structured findings from arXiv:2607.15495",
  "summary": "Produce a JSON extraction of the paper; a reviewer checks fidelity.",
  "structure": "SOLVE",
  "priority_weight": 0.5,
  "campaign": "translate-chinarxiv",
  "verifier_contract": "vct_…",
  "skills": ["skv_…"],
  "benchmark": null,
  "revision_note": "Schema v2: added the limitations field."
}

slug, title, summary are required and validated by the Problem schema. structure defaults to SOLVE; BENCHMARK requires a benchmark block (the ProblemRevision schema's shape); DISCOVERY takes no benchmark. priority_weight defaults to 0.5. campaign names an existing Campaign by slug; inside a Campaign folder it is implied and must not disagree. verifier_contract pins a published Contract instead of authoring verifier-contract.json (never both). skills pins published SkillVersions in addition to any under skills/. revision_note is optional metadata for a later revision only: a string of at most 5000 characters saying why this revision replaces the previous one, stored as the new ProblemRevision.changelog_md when reconciliation makes the Problem a revise. On a first publication, and on a noop, it has no effect and changelog_md of a first revision is null. It is not a changelog: nothing accumulates across revisions, and each revision carries at most its own note.

sealing is required exactly when a reference file binds a slot the Contract declares sealed. It declares sealed_until, optional submission_closes_at, score_release ({mode:"immediate",release_at:null} or {mode:"final_only",release_at:"<RFC 3339 UTC>"}), and submission_limit: {max_submissions:n,window_hours:h}, and optional entries: "collaborative" | "independent" (default collaborative). Independent entries require a submission cutoff. Publication requires future release dates and bounded integer quotas. The public revision adds derived evaluation_id, unsealed_at and nullable release_event (the manager, time and public reason for explicit early release); do not author these fields. Revisions with the same complete sealed dataset and owners share one evaluation, immutable policy, quota and final-entry choice. Partial overlaps or different owners are refused. Early release requires preparing and acknowledging its exact scope. Materializing a sealed package with tfp draft commits its release dates, quota and scoring rules even while private. Obtain the owner's authorization for that commitment; rehearse using invented labels in separate fixtures or dummy work. The proposal preparation helpers refuse sealed packages. See Sealed reference inputs.

campaign.json

{ "slug": "translate-chinarxiv", "title": "Translate ChinArxiv into English", "summary": "…", "description_md": "…", "status": "draft" }

slug, title, summary required; description_md defaults to empty; status to draft.

reference/

The file name before its extension is the binding key and must match ^[a-z0-9]+(_[a-z0-9]+)*$. The media type is taken from the extension (.json, .md, .txt, .html, .csv, .pdf, …; anything unknown is application/octet-stream). Size and SHA-256 are computed. The statement refers to inputs by key; the linter notes a key the statement never mentions. A file bound to a sealed slot is uploaded and stored like any other; only who can read it after publication differs.

When the content already lives at a public URL and attaching the bytes is impractical (a campaign of thousands of scanned papers hosted by their publisher under an open licence), bind the slot to the link instead: reference/<key>.link.json holds

{ "url": "https://journals.example.org/archive/1974/paper-0512.pdf",
  "content_hash": "sha256:…", "size_bytes": 1548211,
  "media_type": "application/pdf", "filename": "paper-0512.pdf" }

A linked slot behaves like a stored one everywhere people and agents look for inputs: the Problem page, get_problem and start_attempt (reference_resources[].url), the submission page, the review casefile and the revision's package all carry the URL beside the key, media type, hash and size, and GET /api/problem-revisions/{id}/reference/{key} answers with a redirect to the URL (the stated hash rides in x-tfp-content-hash). It differs in one way: sandbox stages do not mount a linked slot. /input/reference/<key>/ is absent in the run, so a checker that reads the slot must have the bytes bound as a file. Lint says so with a note on every link binding under a Contract with sandbox stages, and names the checker when a builtin is known to read that slot. Conformance fixtures always bind bytes: a fixture whose filename names a .link.json is refused. tfp lint --fix never rewrites a link binding's hash, and tfp verify skips linked slots with a note instead of downloading them.

Changing a link binding's url, hash, size, media type or filename changes the revision's identity, so the next publication revises the Problem, as replacing the bytes of a stored binding would.

skills/<name>/

An Agent Skill package: SKILL.md at its root with name and description frontmatter, any other files beside it, within the registry's limits. Validated by the same rules as the Skills registry; the content hash is the registry's tree digest.

verifier-contract.json

The canonical VerifierContract document (see Verification), with these local conveniences, each resolved by the linter before the canonical validator runs:

Conformance fixtures reference files by filename, found under fixtures/ or, when a fixture uses a real bound input, under reference/; --fix writes their content_hash and size_bytes.

Field by field:

Top level

Field Required Meaning
title yes one line
statement_md yes one sentence: what "verified" means
reference_inputs yes (may be []) the slots a revision binds files to
stages yes ordered checks; stage N+1 runs after stage N passes
submission_requirements yes what a solver must submit
result_kind yes pass_fail or score
score with score { "unit": "…", "description": "…" } plus optional "direction": "higher_is_better" | "lower_is_better"; lint fills null for pass_fail. Leave direction out and the Problem's benchmark.direction decides how results rank, so one scored Contract serves minimised and maximised Problems
conformance_fixtures yes (may be []) checker test cases; at least one verified and one refuted per sandbox stage is the norm

reference_inputs[]

{ "key": "document_schema", "required": true, "media_types": ["application/json"], "max_bytes": 262144, "description": "The JSON Schema the translation must satisfy." }

key matches ^[a-z0-9]+(_[a-z0-9]+)*$ and names the file reference/<key>.<ext> (or the link descriptor reference/<key>.link.json) in the package. A required: true slot must be bound by every Problem using the Contract, and by fixtures of stages selecting that slot (fixtures bind bytes, never a link). sealed (optional, default false) holds the bound bytes back from everyone but selecting sandbox checks and the Problem's managers until the revision unseals (Sealed reference inputs); a sealed slot cannot be bound by link, since TfP would hold no bytes to hold back.

stages[]

{ "key": "structure_check", "kind": "sandbox", "title": "Structural check",
  "description_md": "Parses and satisfies the schema.",
  "evidence_schema": { "type": "object", "required": ["valid"], "properties": { "valid": { "type": "boolean" } } },
  "review_protocol_id": null, "rule_owner": null, "acceptance": null, "external_system": null,
  "implementation": "tfp-builtin/json-schema-check:1" }
Field Meaning
key unique, ^[a-z0-9]+(_[a-z0-9]+)*$
kind sandbox (a checker runs), review (people judge) or external (a partner system attests)
title, description_md for readers
reference_input_slots sandbox only: selected reference keys; required on every sandbox stage if the Contract declares any sealed slot. [] mounts no references. Otherwise omission preserves the legacy all-slots behavior
result_kind sandbox only: pass_fail or score, defaulting to the Contract's result kind. A scored Contract can begin with a public pass/fail format check before a scored evaluation
limits sandbox stages naming an implementation only; {memory_mb, vcpu, disk_mb, timeout_seconds}, each optional; the checker's requested execution envelope (an Implementation detail, stripped from the Contract; see custom checkers)
evidence_schema JSON Schema the checker's evidence/summary.json must satisfy, or null
review_protocol_id for review: "./review-protocol.json" or a published rpt_…; otherwise null
rule_owner, acceptance, external_system for external only; otherwise null
implementation for sandbox only: a builtin, a Python file in this directory (./checker.py), or a public image pinned by digest (registry/repo@sha256:<64 hex>; a tag is refused)

Builtins:

Builtin Checks Reads
tfp-builtin/json-schema-check:1 the artifact in role document parses as JSON and satisfies the schema reference input document_schema
tfp-builtin/sat-assignment-check:1 the artifact in role assignment satisfies the CNF reference input instance

A builtin fixes the artifact role and the reference key it reads; declare both, or lint tells you which is missing.

submission_requirements

{ "required_artifacts": [ { "role": "document", "description": "The English translation as JSON.", "media_types": ["application/json"], "max_bytes": 1048576 } ] }

accepts_text_only (default false) allows a submission that is only its summary text; answer_format_md (default null) tells the solver the expected answer shape. A solver binds each uploaded Artifact to a role explicitly (create_submission { artifacts: [{ role, artifact_id }] }).

conformance_fixtures[]

{ "key": "valid_document", "stage_key": "structure_check", "description": "A complete translation.",
  "inputs": {
    "reference": { "document_schema": { "filename": "document_schema.json", "content_hash": "sha256:…", "size_bytes": 420 } },
    "artifacts": [ { "role": "document", "filename": "valid.json", "content_hash": "sha256:…", "size_bytes": 114 } ]
  },
  "expected_result": "verified" }
Builtin Checks Reads
tfp-builtin/json-schema-check:1 the artifact in role document parses as JSON and satisfies the schema reference input document_schema
tfp-builtin/sat-assignment-check:1 the artifact in role assignment satisfies the CNF reference input instance

review-protocol.json

The canonical tfp.review/v0.1 document (see Reviews), with one local convenience: a node's skills[].skill_version_id may be "./skills/<name>".

Lint

tfp lint <dir> [--fix] [--json] in the toolkit bundle, and the same code as pnpm tfp-lint <dir> in the repository. Runs, in this order, on the Campaign root's shared mechanisms and then on every Problem package found:

  1. problem.json against the Problem schema; campaign.json against the Campaign schema (ids, owner and timestamps are placeholders).
  2. Each skills/<name>/ with the Skill-package rules.
  3. review-protocol.json with the ReviewProtocol validator, rules 1–16, on the production schema engine; local Skill references resolved.
  4. verifier-contract.json with the Contract validator; each stage's implementation resolved to an Implementation and validated against the Contract; fixture bytes hashed; the local protocol reference resolved.
  5. Reference files against the Contract's slots (required slots bound, media types and sizes within limits); sealing policy and date ordering; bounded integer quotas; explicit stage selectors for sealed Contracts; no sealed bytes duplicated elsewhere in the package, including public bindings, fixtures, Skill files and checker source. Publication additionally checks historical hashes of public references, fixtures, Artifacts and checker programs and shared release promises against the registry, which local lint cannot inspect.
  6. The assembled ProblemRevision against its schema.

Every failure is one record:

{ "file": "verifier-contract.json", "path": "/stages/1/review_protocol_id", "code": "not_found", "message": "review-protocol.json is not in this directory" }

file is relative to the package root (problems/<slug>/… inside a Campaign folder). code is one of missing_file, invalid_json, schema_violation, rule_violation (a rule beyond the schema; protocol rules carry their number), not_found (a reference to something not in the package), binding_error, unsupported_runtime, hash_mismatch, unknown_file. Notes carry the same addressing and never fail the run. --json prints {ok, errors, notes, plan, plan_hash}.

The plan

plan is what the package would publish, computed from the files:

{
  "campaign": { "slug": "…", "title": "…", "summary": "…", "description_md": "…", "status": "draft" } | null,
  "skills":                   { "<content_hash>": { "dir": "…", "name": "…", "content_hash": "…", "files": 3 } },
  "review_protocols":         { "<content_hash>": { "file": "…", "title": "…", "content_hash": "…", "skill_dirs": ["…"] } },
  "verifier_contracts":       { "<content_hash>": { "file": "…", "title": "…", "stages": ["schema:sandbox", "fidelity:review"], "content_hash": "…", "local_review_protocol": "<hash>" | null, "fixture_files": ["…"] } },
  "verifier_implementations": { "<content_hash>": { "file": "…", "stage_key": "…", "image_ref": "…", "image_digest": "sha256:…", "custom": false, "content_hash": "…", "program": { "file": "checker.py", "content_hash": "sha256:…", "size_bytes": 1420 } } },
  "problems": [ { "dir": "…", "slug": "…", "structure": "SOLVE", "title": "…", "summary": "…", "priority_weight": 0.5, "campaign_slug": "…" | null,
                  "statement_md": "…", "benchmark": null, "revision_note": null,
                  "skills": ["<hash>"], "published_skills": ["skv_…"],
                  "reference_inputs": [ { "key": "…", "file": "…", "media_type": "…", "size_bytes": 0, "content_hash": "sha256:…" } ],
                  "verifier_contract": "<hash>" | null, "published_verifier_contract": "vct_…" | null,
                  "verifier_implementations": ["<hash>"], "review_protocol": "<hash>" | null } ]
}

plan_hash is sha256: of the canonical JSON of the plan. It identifies a publication exactly: the same files produce the same hash on the author's machine and on the server, and every server-side step below is keyed by it.

The plan is state-independent: it describes the files. verifier_contract is the hash of a Contract authored in the package and published_verifier_contract is a vct_… id the package pins in problem.json; a package that authors its Contract always shows the first and null for the second, whatever the server later reuses. Which existing objects a publication reuses (the same Contract by its canonical document, the same SkillVersion by content hash) is the reconciliation's and the publication result's vocabulary, never the plan's.

Published ids (vct_…, rpt_…, skv_…) are accepted by shape and noted; whether they exist is checked by the server.

One engine

The linter contains no rule of its own. It calls @tokensforprogress/verification-contracts (Contract, Implementation and binding validation, the JSON-Schema subset engine, canonical JSON and hashing), @tokensforprogress/skill-package (package rules and tree digest), and @tokensforprogress/review-protocol (rules 1–16 on the subset engine), plus the canonical JSON Schemas under schemas/. It reads a package as a file tree (path → bytes), so the same function runs from the author's disk and from an uploaded archive on the server. The Worker imports the same packages; the operator seeds use them. A package that lints clean is one the platform would accept, and a rule the platform enforces is one the linter reports.

Stage 8B: authoring over MCP

The surfaces below implement the authoring contract. A package save and an explicit visibility transition are separate operations.

Capabilities

Scope Kind Grants
read scope everything read-only, including the Skill Resource and get_sandbox_image_status
contribute scope doing work, reviewing and preparing private proposals as the connected User
author beta capability materialize ordinary private packages and explicitly open participation
custom_verifier beta capability name public, digest-pinned OCI images as sandbox checkers; images are registered under this capability

Capabilities are granted by an operator to allowlisted Users and recorded with who granted them and when. Signed-in contributors can save incomplete proposals and validate and test them privately (prepare_work_draft) without author access. Uploading and validating are also available to contributors; direct package materialization requires author or administrator access. Unlisted work is not automatically included in discovery. Trusted authors can explicitly list their own work. Platform administrators can publish proposals on behalf of their owners and manage access through the website or MCP. See Drafts, publication and discovery. Existing author grants are kept in user_capabilities (migration 0020). A token's scope is the requested scope intersected with read/contribute plus the capabilities the User holds at grant time; a token never carries a capability its User lacks. The consent screen for author states that publications are immutable. The website groups current abilities into contributor, author, trusted-author and administrator roles. Trusted listing and administration are checked live, independently of OAuth capability claims.

Surface

Surface Scope Purpose Status
pnpm tfp-lint <dir> / tfp lint <dir> none (local) canonical local validation and the plan implemented
website: Propose a campaign or problem, Proposal status and Edit signed-in account, live role checked per action save incomplete proposals, edit text, validate/test, request publication; technical package editing stays in agent tooling implemented
Resource tfp://authoring/skill/latest read the canonical TfP Authoring Skill (skills/authoring/SKILL.md) implemented
get_authoring_toolkit read a short-lived download URL for the tfp bundle; open to every connected User, since the bundle is also the solver's preflight (tfp verify) implemented
request_authoring_upload contribute a signed PUT slot for one package archive implemented
validate_authoring_upload contribute canonical validation plus server checks; the plan, its reconciliation and its hash; registers any custom image implemented
publish_authoring_upload author or live administrator execute exactly the validated reconciliation; new work starts private implemented
save_work_draft, prepare_work_draft contribute save incomplete proposals; materialize valid new work as ordinary private Campaigns/Problems implemented
publish_work_draft, set_work_visibility live role checked explicitly open participation or list the same work implemented
get_sandbox_image_status read provisioning and conformance state of one registered custom image, or of a script runtime (tfp-runtime/python:1) implemented
authorize_cli contribute approve the code tfp login --mcp displays; the CLI receives its own credential implemented
tfp login [--mcp], tfp whoami, tfp logout (CLI) the CLI's credential: browser OAuth + PKCE, or a code approved over MCP; stored in ~/.config/tfp/credentials.json, revocable in Settings implemented
Resources tfp://authoring/skill/latest/ + …/<path> read the Skill package's manifest and files: reference/*.md, examples/** (also /docs/authoring/skill/<path>) implemented
tfp init/test/upload/draft/publish (CLI over the tools above) the author-side loop; draft and its compatibility name publish shard a large Campaign, retry transient failures and resume from .tfp/publish-state.json implemented
tfp verifier build <checker-dir> (CLI over docker buildx) build a custom checker for linux/amd64, push it, confirm it is publicly pullable, pin the digest into the Contract implemented
tfp verifier status <ref-or-digest> (CLI over get_sandbox_image_status) where a custom image stands implemented
tfp verify <problem> --artifact <role>=<path>… (CLI over get_problem and the API) the solver's preflight: the Contract's sandbox stages on local files, the Worker's way (Verification) implemented
custom image execution custom_verifier run a digest-pinned image as a sandbox stage on a runner per runtime digest implemented; provisioning verified live 2026-09-04 (status)
script checker execution author run one Python file (./checker.py) on TfP's tfp-runtime/python:1, the same runner data plane; the runtime bound once per deployment by an operator (POST /admin/sandbox-runtimes) implemented (status)
GET /api/verifier-implementations/{id}/program read (API) the exact program bytes of a script Implementation, with x-tfp-content-hash; also a revision file checkers/<stage_key>/<filename> implemented

Tools

Every result is an MCP text content holding a JSON envelope; errors are MCP tool errors with { "error": … }.

get_authoring_toolkit {} → { version, content_hash, platforms, download_url, expires_at }. download_url is a short-lived (15 minutes) URL for the single-file bundle (tfp-<version>.mjs); content_hash is sha256: of its bytes; platforms lists what it runs on (node>=20). The bundle is TfP's private authoring CLI: internal package name @tokensforprogress/authoring, never published to npm.

request_authoring_upload { size_bytes, content_hash } → { upload_id, upload_url, expires_at, max_bytes }. size_bytes and content_hash describe the archive the client is about to PUT; upload_url is a signed PUT URL valid until expires_at (15 minutes); upload_id (aup_…) names the upload from here on. The server verifies size and hash on receipt and records the upload as uploaded.

validate_authoring_upload { upload_id } → { valid, errors: [{file, path, code, message}], notes, plan, plan_hash, conformance: [{stage_key, image_ref, fixture, expected, actual, passed}], images: [{image_ref, image_digest, state}] }. Unpacks the archive into the same file tree the linter reads, runs the linter unchanged, then the checks only the server can make:

Validation never mutates author content. The upload is recorded as validated with plan_json, plan_hash and the reconciliation it computed (including the current state it was computed against), or invalid with the errors. Validating the same upload again recomputes the reconciliation against current state.

publish_authoring_upload { upload_id, plan_hash } → { campaign_id?, problems: [{slug, problem_id, problem_revision_id, revision_number, visibility, listed_in_explore}], campaign?, skill_version_ids, verifier_contract_ids, verifier_implementation_ids, review_protocol_ids, counts }. Requires a validated upload whose plan_hash matches; a stale hash is refused with stale_plan_hash: validate again. Executes the stored reconciliation; before mutating, it verifies that every affected Campaign and Problem still has the owner and current revision the validator observed, and otherwise refuses with stale_server_state: validate again. Creates, in dependency order: SkillVersions, ReviewProtocol, Verifier (created or reused) and VerifierContract (local protocol reference replaced by the published id), VerifierImplementations, Campaign (owner = caller, initially active and unpublished), then each Problem and its ProblemRevision with pins and bindings, opened. New standalone Problems are private too; Campaign children inherit the Campaign visibility unless an individual exception restricts it. The result reports effective visibility and listed_in_explore. Existing visibility is preserved, including on retries. Idempotent: publishing an upload already published returns the recorded result and creates nothing. For a Campaign folder the CLI may send several uploads (publication batches); that is transport, and the shared mechanisms are reused by hash across batches. Problems whose action is noop create nothing and are returned with their current ids. A package naming a custom image publishes whether or not the image is available; a run reaching that stage waits for the image and ends error if provisioning fails (an execution fault, never a finding; a limit exhausted while running is inconclusive).

A newly created ordinary Campaign is Unpublished after this package step. Its owner and platform administrators can inspect it and test its open Problems privately. New standalone Problems also start private. When ready, use the management visibility controls or set_work_visibility to share as unlisted, then list when authorized. Materializing a Problem or revision within a Campaign keeps the parent's visibility, and updating or retrying an existing Campaign package does not reset its publication setting or individual Problem exceptions.

get_sandbox_image_status { image } → { image_ref, image_digest, runtime_digest, state, state_reason, registered_at, implementations: [{verifier_implementation_id, contract_id, stage_key, conformance}] }. image is the author-supplied reference, a bare sha256: digest, or a script runtime name (tfp-runtime/python:1). state is registered, provisioning, available or provisioning_failed (with state_reason), or unbound for a script runtime whose pinned image has not been enabled on on this deployment yet; runtime_digest is the linux/amd64 image that actually executes (equal to image_digest for a single-platform image); each Implementation's conformance is pending, passed or failed. Semantics in Custom sandbox checkers.

tfp verifier build: a checker image, pinned

tfp verifier build <checker-dir> is a thin wrapper over the Docker Build Cloud flow in Custom sandbox checkers: no new runtime concept, only the commands an author would type. It requires a Dockerfile in <checker-dir>, then picks a builder in order: a builder docker buildx ls lists with the cloud driver; one created from DOCKER_ACCESS_TOKEN + DOCKER_ACCOUNT + CLOUD_BUILDER_NAME (or --builder <account>/<name>) with docker login --password-stdin (the token goes over stdin, never argv or output) and docker buildx create --driver cloud --use, which needs Buildx ≥ 0.37; the local daemon when docker info answers; otherwise it fails naming the docs section and the three variables. It runs docker buildx build --platform linux/amd64 --push --tag <repository>:<tag> --metadata-file …, with --repository defaulting to tokensforprogress/tfp-verifiers (DOCKER_REPOSITORY) and --tag to <dir basename>-<YYYYMMDDHHMMSS>, reads containerimage.digest from the metadata, and confirms the image is anonymously pullable the way the Worker pulls it (an anonymous token from auth.docker.io, then a manifest HEAD at registry-1.docker.io); a private repository is an error. Inside a package (a verifier-contract.json in the directory's parent, or --contract <path>) it writes docker.io/<repository>@sha256:… into the sandbox stage's implementation (--stage <key> when there is more than one) and, with --limits memory_mb=…,vcpu=…,timeout_seconds=…, the limits beside it, keeping 2-space JSON; otherwise it just prints the reference. --json prints { image_ref, digest, repository, tag, builder, contract_file?, stage_key? }; --dry-run prints the commands it would run.

Uploads: transport only

The archive is a ZIP of the package directory, produced by the CLI for transport and never shown, versioned or published. Rules:

The CLI's <dir>/.tfp/upload.json records {upload_id, plan_hash, content_hash, uploaded_at, valid} so draft and publish need no arguments; <dir>/.tfp/publish-state.json records the shard plan of the last tfp draft or tfp publish and which shards completed (below).

tfp draft: shards, retry, resume

tfp draft <dir> [--json] [--fresh] [--verify] [--shard-problems <n>] [--shard-bytes <n>] owns transport for a package of any size. tfp publish is a compatibility name with identical behavior. Neither command changes the visibility of existing work; new Campaigns and standalone Problems start private:

  1. The whole package is linted once locally; errors stop it.
  2. The package is cut into shards at Problem boundaries. Every shard is a sub-tree holding campaign.json, every root-level shared mechanism (skills/, verifier-contract.json, verifier-implementation.json, review-protocol.json, fixtures/) and a subset of problems/<slug>/ directories, in slug order. A shard closes when the next Problem would exceed any ceiling: 300 Problems (--shard-problems), 3000 files, 24 MiB of zipped bytes (--shard-bytes, estimated by deflating each file) or 100 MiB unpacked; all well under the server's 5000 files, 64 MiB and 256 MiB. A single Problem or a small Campaign is one shard and behaves as before; when tfp upload already sent exactly those bytes, that validated upload is what gets materialized.
  3. Each shard goes through request_authoring_upload → PUT → validate_authoring_upload → publish_authoring_upload, with up to 5 attempts on transient failures (a dropped connection, a call that passes the client's deadline of 180 s, 300 s for the PUT, an HTTP 5xx page, an empty or unparseable answer), waiting 2, 4, 8, 16 and 30 s plus jitter. A retry re-sends the whole shard: if the lost answer was to a publish the server had committed, the replay reconciles those Problems as noop, so nothing is duplicated. stale_plan_hash and stale_server_state are answered with exactly one fresh upload + validate + publish of the same shard; validation errors and conflicts stop the run and are printed like lint errors.
  4. <dir>/.tfp/publish-state.json records the plan (shard ids, their slugs, a content hash of each shard's files) and, per shard, the status, attempts, upload id, plan hash, counts and time. A rerun skips shards that are published and whose hash is unchanged, and sends the rest; a crashed or interrupted run is resumed by running the same command again. --verify replays every shard (the server answers noop for each unchanged Problem); --fresh ignores the state file. Changing a root file changes every shard's hash, so all are re-sent (and reconcile as noop unless a Problem's effective task changed).

Progress is one line per shard; --json prints one final document {ok, campaign, problems_total, planned_shards, published_shards, skipped_shards, counts, problems: [{slug, action, problem_id, problem_revision_id, revision_number}], …_ids, shards, published_at} with progress on stderr.

Durable private drafts

tfp draft executes the validated plan under the real Campaign and Problem slugs. The owner can find the work in Campaigns & Problems, edit it, and attempt its open Problems. It has no automatic expiry. Run the command again after changes to update those same objects; immutable revisions, Attempts and submissions keep those identities and their historical pins.

The task, its bound reference-input bytes, Attempts and submitted artifacts stay private. Package Skills and evaluation definitions—including review rubrics, Contracts and checker programs—are separately published reusable registry resources; they do not inherit the task’s privacy. Keep task-specific material in the statement or bound reference inputs, and put only material intended for reuse in those registry definitions.

Use set_work_visibility to open participation as unlisted, then listed when ready and authorized. These are transitions on the same work. Sharing exposes its retained history according to the current visibility and any sealed-entry policy. Updating a package preserves existing visibility; successful validation never shares or lists work. See Drafts, publication and discovery.

Incomplete ideas use save_work_draft. Preparing a valid new proposal materializes ordinary private work; subsequent preparation updates it. A proposed replacement for existing shared work uses an isolated validation candidate until publication, so contributors cannot silently replace a task others are already attempting.

There is no public authoring-preview command, grant or creation/list/revocation tool. Old tfp preview calls stop with replacement guidance. Historical preview records keep their existing access controls and cleanup; they are not draft Campaigns to manage or promote. Isolated developer PR deployments are unrelated.

Testing and review

Private work is available to its owner and administrators. To test with another participant, explicitly share the same work as unlisted and give them its link. A fresh solver uses get_problem, starts an Attempt, submits artifacts by role and receives the normal verification results. Review runs the real protocol: reviewers need the configured eligibility, access to the work, and no conflict. Owning the work does not automatically make its author an eligible reviewer.

Reconciliation against existing state

The local authoring compiler is state-independent: it turns the files in one package into a normalized semantic plan. Server validation then reconciles that plan with the current TfP state.

A Campaign package is partial. Only the Campaign and the Problems present in the package are reconciled. A Problem that already belongs to the Campaign but is absent from the package is untouched. Omission never means deletion, archival or removal.

For every included Problem the server determines one action:

Action Meaning
create No Problem with this slug exists; create it and revision 1.
noop The Problem exists and its current effective task is semantically identical. Create no revision.
update Only mutable Problem metadata changed (title, summary, priority weight); update it without creating a revision.
revise The effective task changed; create the next ProblemRevision and make it current.
conflict The caller cannot safely perform the change: another User owns the slug and the caller is not a live platform administrator, the Problem is archived, or an immutable Problem property (such as structure) differs.

Whether a Problem needs a revision is based on its effective revision semantics, not on whether its source files were uploaded again. The comparison covers the statement, the benchmark block, the pinned SkillVersions, the pinned VerifierContract, and the exact reference-input bindings (keys, media types, content hashes and, for a link binding, the URL), plus any other field whose change canonically requires a new ProblemRevision. revision_note is not part of the comparison. VerifierImplementations are not part of the ProblemRevision fingerprint: a new or corrected Implementation under the same Contract does not revise Problems (same Problem, same Contract, new checker Implementation is a noop for the Problem, while the Implementation is registered and conformed). Mutable Problem metadata is compared separately.

The slug is how reconciliation identifies a Problem. Changing a slug in an authoring package is not treated as renaming an existing Problem; it names a different Problem. Renaming, if supported, is a separate explicit operation. The implementation may store a derived semantic fingerprint for efficient comparison; the fingerprint is infrastructure, not a participant-facing object or authoring field.

An existing owned slug is therefore not automatically a later revision. Republishing an unchanged package is a noop.

Campaign metadata is reconciled separately: a new Campaign slug is create; an existing Campaign owned by the caller is reuse; explicit changes to its mutable metadata are update; an optional Campaign field omitted from campaign.json does not reset an existing value merely because the compiler supplies a default for creation; a Campaign owned by another User is conflict unless the caller is a live platform administrator. Administrators publish their own uploads as themselves; they do not acquire the existing campaign or problem ownership. Publication rechecks current authority and the observed owner, metadata and revision state. Package maintenance is recorded in the append-only work audit history alongside the publishing actor and upload. An earlier successful validation cannot preserve revoked administrator authority.

Validation returns the reconciliation alongside the plan:

"reconciliation": {
  "campaign": { "slug": "translate-openalex-en-zh", "action": "reuse", "campaign_id": "cmp_…" },
  "problems": [
    { "slug": "openalex-en-zh-W123", "action": "noop",   "problem_id": "prb_…", "current_revision_id": "prv_…" },
    { "slug": "openalex-en-zh-W124", "action": "revise", "problem_id": "prb_…", "current_revision_id": "prv_…", "next_revision_number": 2 },
    { "slug": "openalex-en-zh-W125", "action": "create" },
    { "slug": "openalex-en-zh-W126", "action": "conflict", "reason": "owned by another User" }
  ],
  "counts": { "create": 1, "revise": 1, "update": 0, "noop": 1, "conflict": 1 },
  "warnings": [ { "code": "possible_duplicate", "slug": "…", "of": "…" } ]
}

The CLI prints the counts and lists every create, revise, update and conflict by slug. Any conflict makes the upload invalid for publication rather than silently choosing an interpretation.

Validation records the current state it reconciled against (each existing Problem's id and current revision id, the Campaign's owner). publish_authoring_upload executes that stored reconciliation and, before mutating, verifies the state is still what the validator saw; if it changed, publication refuses with stale_server_state and the author validates again. The two refusals are distinct: stale_plan_hash means the author's package changed; stale_server_state means TfP changed since the package was validated. This keeps two authoring sessions, or two publication batches, from creating revisions on stale state: two agents that both planned revision 5 cannot both publish it; the second revalidates and becomes a noop or revision 6.

Duplicate detection

The primary identity mechanism is the Problem slug. Generators over an external corpus should derive it deterministically from the corpus's stable identifier, so the same source reached twice reaches the same Problem: same slug and same semantics is noop; same slug and changed semantics is revise.

As a non-blocking safeguard, validation reports the warning possible_duplicate when a Problem to be created has the same effective semantic fingerprint as another live Problem in the same Campaign. Sharing only a paper or dataset is not by itself a duplicate, because two Problems may ask different questions of the same input. The warning is advisory and never merges Problems.

Publication batches

A large Campaign may be published through arbitrarily many packages over time. Each package contains only a finite batch of ordinary Problem directories, within the upload limits, and names the same Campaign; shared mechanisms are reused by content hash across batches.

Campaign-root material applies to the Problems included in that batch. Changing a root Skill, Contract or protocol does not retroactively change Problems absent from the batch; their existing ProblemRevisions remain pinned to the versions they were published with. To move existing Problems to the new mechanism, include them again; each becomes a revise.

tfp draft (or tfp publish) produces these batches itself (shards, above); an author or a generator may also send them by hand.

There is no generative-Campaign object and no requirement that the author keep the entire Campaign in one local directory. A generator may stream corpus records, emit a batch, publish it, checkpoint its external cursor and continue.

Updating existing Problems and Campaigns

Authoring is incremental. A Problem directory whose slug does not exist creates a new Problem. A directory whose slug exists and is owned by the caller is compared with the current ProblemRevision: no semantic change is noop; only mutable metadata changed is update; the effective task changed is revise. A revise creates the next immutable ProblemRevision, with changelog_md equal to problem.json.revision_note or null, and makes it current. Attempts against earlier revisions remain attached to those revisions. A listed problem can be replaced directly only by its trusted owner or a live platform administrator. Other authors submit a private single-problem proposal for review; revision_review_required leaves the approved revision current. Campaign and sibling listings are preserved. Publication results include visibility and listed_in_explore on campaign/problem entries, evaluated at response time (including replayed receipts); check those fields before announcing a listing. New work is not automatically listed. See Drafts, publication and discovery.

A Campaign directory behaves the same way over a set of Problems. It is a partial batch, not desired-state replacement: Problems omitted from the directory are untouched. Mechanisms with unchanged content hashes are reused rather than re-created. A newly created Campaign starts Unpublished; explicitly share as unlisted or list through the management visibility controls when authorized. Updates preserve the existing Campaign's visibility and individual Problem publication choices.

Archiving is explicit, never by omission: archive_campaign and archive_problem (MCP, owner only) archive the named work; an archived object leaves the default Explore listing, takes no new Attempts, and keeps its record. Campaign archival also archives its open Problems. The browser's Manage Campaigns & Problems screen offers Restore for campaigns and Reopen for problems. Restoration preserves publication and existing revisions; restoring a campaign leaves its archived children archived. Reopen individual problems while their campaign is Live or Unpublished; restore an archived campaign first. Existing Attempts retain their pinned revisions. There is no delete or MCP restore tool. See Publishing and unpublishing work for Unpublished work, private testing, and browser status actions.

Not part of 8A or 8B: deletion or archival by omission, ownership transfer, cross-user Campaign attachment, restoring through an authoring package, yanking a Contract, automatic merging of duplicates, reconstructing authoring files from published state (tfp pull), uploading executable code by any route other than a public digest-pinned image, generative Campaigns, any authoring-specific participant-facing object.

Decisions embedded in this spec

Settled by the owner, 2026-09-03:

  1. Capabilities, not RBAC. author and custom_verifier are named abilities granted per User by an operator and intersected into tokens; no roles, no hierarchy, no per-object ACLs.
  2. Package upload is transport only. The directory is the canonical package; the ZIP exists to move bytes, is unpacked into the same file tree lint reads, and is never shown, versioned or published.
  3. Durable private work uses the ordinary objects (supersedes the September 3 preview workflow, approved October 16). New Campaigns and Problems use permanent IDs and real slugs, remain editable and testable while private, and share/list through explicit visibility transitions. Historical preview access checks remain for existing records.
  4. The plan hash gates publication. validate computes it, draft and publish require it, a mismatch is refused, and publishing an already published upload is a no-op that returns the same ids.
  5. Custom checkers are digest-pinned public OCI images, registered at validation, provisioned per unique digest, conformance-gated. Lint refuses tags; validation registers the image and enqueues provisioning; the server runs the fixtures once the image is available; one runner per unique runtime digest; the only author surface beyond the package is tfp verifier status.
  6. No context.md, no changelog.md. A later revision's reason is the optional problem.json.revision_note, null on first publication; background is a reference file or a Skill; findings are Notes.
  7. Shared mechanisms beside campaign.json, listed once by hash. Bulk Campaigns are a generator script emitting ordinary Problem directories plus publication batches on the wire; no template or generated-Problem object.
  8. Publication is partial reconciliation, not desired-state replacement (2026-09-04). For each included Problem the server computes create / update / revise / noop / conflict against current state; omitted Problems are untouched; revision equality is semantic, not upload-based; generated corpus Problems use deterministic slugs from stable source identifiers; publication rechecks the state validation reconciled against and refuses stale_server_state rather than guessing.

Script runtime enablement does not change the catalogue image: each name pins the same actual OCI digest on every deployment. Unbound script uploads cannot materialize as a draft; validate again after the operator enables the runtime. A bound upload provisions the resource shape requested by its checker. Automatic spare-compute dispatch requires a non-yanked, conformance-passed implementation for every sandbox stage, including when consuming an already cached candidate.