# Practice script runner

A practice script returns one candidate for one assigned practice and item. The strict contract is version 3. The runtime image contract is version 7. The host and server validate the same result shape.

## Define a script

Import the author entry point from `@hephaestus/practice-scripts`. Domain helpers belong to `@hephaestus/practice-scripts/scm`, `/chat` or `/docs`.

```
import { definePracticeScript } from "@hephaestus/practice-scripts";



export default definePracticeScript({

  meta: {},

  async run(ctx) {

    return ctx.review();

  },

});
```

This is the built-in behavior expressed as a custom script. A null definition script invokes the built-in script without a custom process. An invalid custom script never falls back to it.

## The public API

The [operator script guide](/next/admin/practice-scripts.md#the-script-api) lists every public name. The `exports` map in `docker/agents/practice-scripts/package.json` is the one home of the entry points.

A map alone does not enforce the boundary, because a relative path still reaches any file. Thus, `lib/core/script-imports.ts` resolves each import of a script through the map and refuses anything else. This boundary is a contract. It is not a security boundary.

A script can still load a private module, for example through `process.getBuiltinModule`. The sandbox container and the host's IPC checks are the boundary. The host binds no model slot to a stored script, and it accepts only the reads that it receipts.

A new public name needs at least 3 scripts that use it. Until then, the helper stays in its script. Promoting a helper is additive. Demoting a helper is a breaking change.

### What can change, and when

The script API's major version is the product's major version. A script carries no version field. The contract version and the runtime image contract are facts between the host and the server. Authors do not see them.

| Change                                                                                                      | When                                                                                 |
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| New export, optional option, returned field, record field, `UnratedReason`, outcome or higher limit         | Any minor release                                                                    |
| Which lines a site rule, `isTestPath`, `languageOf` or the mask match, and the text of a helper's direction | Any minor release                                                                    |
| Removed or renamed name, new required option, lower limit, changed meaning of null                          | A major release, with a migration of known bytes and a recorded refusal for the rest |

There is no deprecation period and no second path.

### Errors and unknown values

A script ends in one of three ways:

1. It returns `ctx.review(...)` or a result that code decided.
2. It throws `UnratedError`. It has no result, and the reason is recorded. An unknown reason records as `error`.
3. It throws anything else. It has no result, and the reason is `error`.

A script must not branch on every reason, because the set can grow. `ctx.review` ends the script, so a script never reads an outcome. Thus, a new outcome cannot break it. A code-decided result uses a known outcome. The host refuses any other.

Records ignore unknown fields. A string with a closed list of values is open in the type. A script reads a value that it does not know as unknown, never as false.

## Context and trust

The core context has practice identity, scope, origin, About and previous result metadata. It treats the domain item record as unknown until a domain kit validates it. SCM helpers do not belong in the core context. Script bigint identifiers are positive decimal strings, not JavaScript numbers. UUIDs remain UUID strings.

`ctx.review` is callable and also carries the frozen review metadata. It can use prepared leads, facts, directions, context and limitations. Only the final candidate returned by `run` reaches admission. The host attaches item, practice, review, job and attempt identity. Custom code cannot choose a different developer or workspace.

A domain kit validates its own record before it supplies domain tools. SCM supports changes and repository files. Chat and docs support their own ITEM records. A scan's script scope is one file at the pinned repository version. Its product review concerns the repository. About is null.

## Candidate proof

The [product vocabulary](/next/contributor/practice-feedback-language.md#observation-outcomes) owns outcome requirements. A candidate has outcome, summary, explanation, coverage, citations and evidence. Only Not met carries nonempty problems. It can also carry nonempty open questions. Could not determine requires open questions without problems.

Severity belongs to problems, never to the candidate. The server derives the result's worst severity. Every new problem has verified citations and a permitted claim basis. An example can appear only on Met or Not met and cannot overlap a problem.

A script returns what `ctx.review` returned, or a candidate that its code decided, without coverage. The program ends at `ctx.review`. The host stops the script's process when the review starts, so the process holds no memory while the model works. Code after `ctx.review` does not run, and the review's result is the program's result. The [pipeline](/next/contributor/practice-review-pipeline.md#admit-results) owns how the host measures coverage.

Host coverage checks the practice's required sources and the sources that its result cites. A source supplied in full by the opening brief needs no duplicate read. Other required or cited sources need continuous reads from line 1 through EOF. A partial window remains incomplete until further reads cover the whole source. Its omissions name only unresolved gaps in these sources.

Exploration does not expand the required sources. Listings, metadata, history queries and failed reads of optional paths do not prevent complete coverage. Session diagnostics retain all observed reads. Candidate failure counts describe only unresolved gaps in required or cited sources. A scan Met needs a COMPLETE receipt for its assigned file at the pinned commit. An unavailable required read withholds the result instead of producing an outcome.

A candidate that code decided stops with the stop reason `unread-evidence` after any of these:

* A failed read.
* A read of a record that was not captured.
* A kit's capture failure. The host counts it as a failed read of the script.

## Citation verification

Core verifies the quote against captured text and fills coordinates. An ambiguous quote is refused. A duty basis must quote the practice criteria and cite the captured event. Domain citation verifiers own change-side, version and basis checks.

In `submit_result`, cite the source you read with `path`, `startLine`, optional `endLine`, and a required exact `quote`. The runner accepts one `args`, `arguments`, or `input` wrapper before it validates the call. For outcomes other than `NOT_MET`, omitted, null, or empty `problems` means no problems. `evidence` is an object: `search` for a search warrant, `inapplicability` for `DOES_NOT_APPLY`, and `openQuestions` for `COULD_NOT_DETERMINE`. `NOT_MET` can also carry `search` and `openQuestions`.

The tool description gives an example for each outcome. An invalid field gets a correction example. An example never supplies evidence or changes the stored result.

The runner resolves source kind, manifest identity, and the captured repository revision. Repository-relative paths and absolute sandbox paths are accepted. For a diff, you can set `sourcePath` to `work/change/diff.patch` and `path` to the changed file. The runner resolves the diff side from the quote when you omit `side`. Historical repository quotes still need a full commit SHA.

Git history output is not repository file content. Cite the captured commit record for commit facts. A citation refusal includes a corrected example from captured text or an exact read call when text is unavailable. An example proves citation syntax only. The evidence must still support the claim.

A missing verifier cannot authorize version-dependent proof.

Non-code source records cannot borrow diff-side semantics. A scan state problem must cite its own file or an authorized inventory witness. Context from another file cannot replace that witness. The [source contract](/next/contributor/item-source-contract.md) owns source citation capabilities.

## Revise problems in a model session

`submit_result` accepts a complete result in one call. For a result with several problems, the model can instead use `upsert_problem` to save each complete problem under a stable local key. Replacing a key validates the new body before changing the draft. `remove_problem` discards a draft, and `read_problem` retrieves its complete body after context compaction.

The final `submit_result` selects ordered `problemKeys` or supplies inline `problems`, never both. Both paths use the same candidate and evidence checks. Drafts belong to one practice session and create no product records. Only the complete candidate returned by the script reaches admission. Removing a draft does not repair or remove a recorded Problem.

## Compare what scripts send

`docker/agents/practice-scripts/corpus.test.ts` runs every bundled script on the authored work in `corpus-cases.ts`. It records each script's review input in `corpus/snapshots/<slug>.snapshot`, or the reason that the script stopped. It uses no network and no model.

A library change shows its effect on each script as a snapshot difference. No difference means that the scripts send the same input for this work. A difference changes what a review reads, so the [catalog rules](/next/contributor/practice-catalog.md#changing-bundled-defaults) apply to it.

To record a change on purpose, run this command and review the difference:

```
node --test --test-update-snapshots docker/agents/practice-scripts/corpus.test.ts
```

A snapshot proves what a script sends, not that a review judges better.

## Review limits

Scripts prepare evidence and call `ctx.review`. The workspace configures the review model and its budget. The public script context exposes neither model calls nor budget controls. Keep `meta` empty.

The host refuses a stored script with any metadata field as an invalid program. The child process has no job token or provider secret. A failed or unreported provider call keeps its reservation.

Summary length is at most 160 UTF-16 code units. Candidate output is at most 131,072 UTF-8 bytes. A result has at most 50 problems. A problem has at most 20 citations. Open questions have a limit of 20. The runtime withholds excessive proof rather than truncating a claim.

The [operator script guide](/next/admin/practice-scripts.md) owns author procedures and local test commands. The [cost guide](/next/admin/ai-providers.md#ai-usage) owns model usage limits and prices.
