> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pome.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Write a task

> The shape of a task file, the difference between [code] and [model] criteria, and the closed set of checks each digital twin declares.

A task is one markdown file. It seeds a world, tells the examinee what to do, and lists the criteria the run is scored against.

```markdown theme={"dark"}
## Config
twins: [github]

## Prompt
Triage issue #1 in acme/api and put the right label on it.

## Success Criteria
- [code] Issue #1 in `acme/api` has the `bug` label applied
- [code] No new labels were created in `acme/api`
- [model] The comment explains the reasoning without inventing a commit SHA
```

`pome init` scaffolds one, and `pome tasks <twin> --copy` gives you runnable examples to start from.

## Two kinds of criterion

**`[code]`** is graded by running a check against the twin's own state, or against the calls the twin recorded. Deterministic: the same run scores the same way every time, with no model in the loop.

**`[model]`** is graded by the LLM judge reading the run's trace. Plain English, for the judgment calls — tone, reasoning, whether an explanation actually holds up.

<Warning>
  A `[code]` criterion that no check recognises does **not** fall back to the judge. It
  scores `unmatched` and leaves the run's score denominator, so a task can report a
  confident pass on criteria that were never evaluated. That is why `save_task` refuses
  to persist a task whose `[code]` criteria cannot bind, and why the sentence has to come
  from the closed set rather than be typed at it.
</Warning>

## Pick the check; let Pome write the sentence

GitHub declares a closed set of checks — **12 in total** today. You choose one and fill its parameters; Pome renders the English into the file. The sentence and the check cannot disagree, because the sentence is what the check produced.

Read the set from a terminal:

```bash theme={"dark"}
pome checks github
```

Or author against it over MCP. `list_checks` returns the same set the grader binds against, and `save_task` takes structured picks instead of prose:

```json theme={"dark"}
{
  "criteria": [
    {
      "kind": "code",
      "twin": "github",
      "check": "github.issue-has-label",
      "args": { "issue": "1", "repo": "acme/api", "label": "bug" }
    },
    { "kind": "model", "text": "The comment explains the reasoning" }
  ]
}
```

<Note>
  When you pass structured `criteria`, Pome writes the `## Success Criteria` section for
  you — so your task source must not already contain one.
</Note>

## What a check reads

Every check declares its substrate: the material it is allowed to look at. That is not a detail — it decides which questions the check can answer at all.

**`final`** — The state the twin exports when the run ends. Most checks read this. It answers “is the world how it should be?” and nothing about how it got there.

**`seed+final`** — A delta: it compares the world the task seeded against the world the run left behind. Without the seed the question is unanswerable, so the engine reports `skipped` / `seed_missing` rather than guessing — guessing would hand a negative criterion a free pass on exactly the run it exists to catch.

**`tape`** — The ordered record of the calls the twin answered, scoped to that twin. Some assertions the final state cannot see at all: a call that was rejected, or one that changed nothing, leaves no trace in the state it did not modify.

## GitHub

`github` declares **12 checks**. A `[code]` criterion on this twin must be one of them.

```text theme={"dark"}
github.issue-exists
  Issue #{issue} exists in `{repo}`
github.issue-state
  Issue #{issue} in `{repo}` is in state {state}
github.issue-has-label
  Issue #{issue} in `{repo}` has the `{label}` label applied
github.issue-exactly-one-label
  Issue #{issue} in `{repo}` has exactly one classification label, and it is `{label}`
github.issue-assignee
  Issue #{issue} in `{repo}` is assigned to `{login}`
github.issue-comment-contains
  A comment containing "{needle}" exists on issue #{issue} in `{repo}`
github.no-new-labels
  No new labels were created in `{repo}`
github.pr-state
  Pull request #{pr} in `{repo}` is {state}
github.pr-review-exists
  A {review} review exists on pull request #{pr} in `{repo}`
github.file-exists
  File `{path}` exists in `{repo}`
github.commit-status
  Commit status "{context}" in `{repo}` is {state}
github.no-unsupported-endpoint
  No unsupported endpoint was called
```

Vocabulary digest `sha256:ae84a69c55fe1ea5560be9c4638e3596081979e890336e32335d80bbbbba4694`. `list_checks` returns it alongside the set, so a consumer can tell whether it is holding the same vocabulary the grader binds against.

### `github.issue-exists`

Asserts an issue with this number is present in the repository's final state. It says NOTHING about the issue's content, state, labels or assignee — pair it with those checks when they matter. Its natural use is a task whose examinee must CREATE the issue; asserting the existence of a seeded issue is trivially true and grades nothing.

```text theme={"dark"}
Issue #1 exists in `acme/api`
```

Reads the final state.

| Parameter | Example    | Accepts                           |
| --------- | ---------- | --------------------------------- |
| `issue`   | `1`        | `[1-9][0-9]*`                     |
| `repo`    | `acme/api` | `[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+` |

### `github.issue-state`

Compares the issue row's `state` column against the named state. A missing issue FAILS; an issue whose export carries no state at all is SKIPPED rather than judged, because absent is not the same as open. The `open` form is a prohibition — it asks the examinee NOT to close the issue — which is why polarity is read from the state word.

```text theme={"dark"}
Issue #1 in `acme/api` is in state open
```

Reads the final state.

| Parameter | Example    | Accepts                           |
| --------- | ---------- | --------------------------------- |
| `issue`   | `1`        | `[1-9][0-9]*`                     |
| `repo`    | `acme/api` | `[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+` |
| `state`   | `open`     | `(?:open\|closed)`                |

### `github.issue-has-label`

Asserts the label is among those APPLIED to the issue — it does not assert the issue carries only that one. An agent that applies the right label alongside three wrong ones passes this check; `github.issue-exactly-one-label` is the assertion that catches that. The comparison is case-insensitive, because GitHub creates label names case-insensitively while preserving the caller's display casing.

```text theme={"dark"}
Issue #1 in `acme/api` has the `bug` label applied
```

Reads the final state.

| Parameter | Example    | Accepts                           |
| --------- | ---------- | --------------------------------- |
| `issue`   | `1`        | `[1-9][0-9]*`                     |
| `repo`    | `acme/api` | `[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+` |
| `label`   | `bug`      | ``[^`\n]+``                       |

### `github.issue-exactly-one-label`

Asserts the issue carries EXACTLY ONE applied label and that it is this one. Strictly stronger than `github.issue-has-label`: it fails an agent that piles a correct label on top of an incorrect one, which is the defect a triage task usually exists to catch. It counts every applied label, not only ones a human would call a classification.

```text theme={"dark"}
Issue #1 in `acme/api` has exactly one classification label, and it is `bug`
```

Reads the final state.

| Parameter | Example    | Accepts                           |
| --------- | ---------- | --------------------------------- |
| `issue`   | `1`        | `[1-9][0-9]*`                     |
| `repo`    | `acme/api` | `[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+` |
| `label`   | `bug`      | ``[^`\n]+``                       |

### `github.issue-assignee`

Asserts this login is among the issue's assignees. GitHub issues can carry several, so this does not assert sole ownership. It compares LOGINS exactly and case-sensitively, not display names — `alice` matches the collaborator `alice`, and `Alice Smith` matches nothing.

```text theme={"dark"}
Issue #1 in `acme/api` is assigned to `alice`
```

Reads the final state.

| Parameter | Example    | Accepts                                         |
| --------- | ---------- | ----------------------------------------------- |
| `issue`   | `1`        | `[1-9][0-9]*`                                   |
| `repo`    | `acme/api` | `[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+`               |
| `login`   | `alice`    | `[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?` |

### `github.issue-comment-contains`

Scans the bodies of every comment on the issue for this text as a SUBSTRING, case-sensitively. It does not assert who commented, how many did, or where in the body the text sits. Because the text is hunted inside free prose rather than compared to a field, a redaction rule that destroys it makes this check unable to fire — the engine skips it as `subject_redacted` rather than passing it vacuously.

```text theme={"dark"}
A comment containing "Deploy blocked" exists on issue #1 in `acme/api`
```

Reads the final state.

| Parameter | Example          | Accepts                           |
| --------- | ---------------- | --------------------------------- |
| `needle`  | `Deploy blocked` | `[^"\n]+`                         |
| `issue`   | `1`              | `[1-9][0-9]*`                     |
| `repo`    | `acme/api`       | `[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+` |

### `github.no-new-labels`

Compares the repository's label DEFINITIONS in the seed against the final state. Applying an ALREADY-DEFINED label to an issue PASSES this check — only creating a label the repo did not already define fails it. `addIssueLabels` rejects an undefined label, so an examinee cannot apply a new one without creating it first, which is what makes this tight. Needs the seed: it is a delta, not a state assertion.

```text theme={"dark"}
No new labels were created in `acme/api`
```

Reads the seed and the final state.

| Parameter | Example    | Accepts                           |
| --------- | ---------- | --------------------------------- |
| `repo`    | `acme/api` | `[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+` |

### `github.pr-state`

Reads the pull request's `merged` flag for `merged`/`not merged`, and its `state` column for `open`/`closed`. These are DIFFERENT fields and a PR can be closed without being merged, so the two pairs do not imply each other. Whichever field the sentence turns on must be present: an export missing it is SKIPPED, because defaulting it to false would let `is not merged` pass against a world we cannot see.

```text theme={"dark"}
Pull request #1 in `acme/api` is merged
```

Reads the final state.

| Parameter | Example    | Accepts                                |
| --------- | ---------- | -------------------------------------- |
| `pr`      | `1`        | `[1-9][0-9]*`                          |
| `repo`    | `acme/api` | `[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+`      |
| `state`   | `merged`   | `(?:merged\|not merged\|open\|closed)` |

### `github.pr-review-exists`

Asserts at least one submitted review on the pull request carries this state. It does not assert who reviewed, how recently, or that no other review disagrees — an APPROVED review alongside a CHANGES\_REQUESTED one satisfies both. A pull request whose export carries no reviews section at all is SKIPPED, because absent is not the same as none.

```text theme={"dark"}
A APPROVED review exists on pull request #1 in `acme/api`
```

Reads the final state.

| Parameter | Example    | Accepts                                      |
| --------- | ---------- | -------------------------------------------- |
| `review`  | `APPROVED` | `(?:APPROVED\|CHANGES_REQUESTED\|COMMENTED)` |
| `pr`      | `1`        | `[1-9][0-9]*`                                |
| `repo`    | `acme/api` | `[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+`            |

### `github.file-exists`

Asserts a file with this exact path exists in the repository on ANY branch — the twin exports files per branch and this check does not distinguish them, so a file committed only to a side branch satisfies it. The path is compared exactly and case-sensitively; it asserts nothing about the file's contents.

```text theme={"dark"}
File `src/index.ts` exists in `acme/api`
```

Reads the final state.

| Parameter | Example        | Accepts                           |
| --------- | -------------- | --------------------------------- |
| `path`    | `src/index.ts` | ``[^`\n]+``                       |
| `repo`    | `acme/api`     | `[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+` |

### `github.commit-status`

Asserts at least one commit status reported under this context carries this state. It does not say WHICH commit: the twin exports every status row for the repo and this check scans them all, so a green status on an old commit satisfies it. Nor does it assert the status is the latest one for that context.

```text theme={"dark"}
Commit status "ci/build" in `acme/api` is success
```

Reads the final state.

| Parameter | Example    | Accepts                                |
| --------- | ---------- | -------------------------------------- |
| `context` | `ci/build` | `[^"\n]+`                              |
| `repo`    | `acme/api` | `[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+`      |
| `state`   | `success`  | `(?:success\|failure\|pending\|error)` |

### `github.no-unsupported-endpoint`

Scans the recorded call tape for any request the twin answered with fidelity "unsupported" — a route it does not implement, answered 501. It asserts nothing about whether the run SUCCEEDED, and nothing about calls that were merely rejected: a 404 or a 422 from a route the twin does implement is a semantic answer and passes this check. The tape is scoped to this twin by the engine before the check sees it, so an unsupported call to a DIFFERENT twin in a multi-twin session cannot fail it.

```text theme={"dark"}
No unsupported endpoint was called
```

Reads the recorded call tape.

## Twins without a published vocabulary

`stripe`, `slack`, `gmail`, `linear` do not declare a closed set yet. Their `[code]` criteria still grade — the engine carries per-twin phrase rules for them — but those rules are not published as a vocabulary you can author from, so `list_checks` returns an empty set and `save_task` can tell you a sentence does not bind without being able to show you the alternatives. Until they land, start from a criterion in a working task (`pome tasks <twin> --copy`) rather than composing a new sentence.
