Skip to content
Design a class

Design a class

A class is the schema for one kind of shared record. You write it once, as YAML, before any agent runs. This page designs one from scratch for a problem that is not an incident: a fleet of agents reviewing pull requests.

Start from the agents

Write down the agents you have and what each one produces.

AgentProduces
summariserA short description of what the PR changes
testerResults of running the test suite
reviewer (several copies)Findings on the code, and a final verdict

Each thing an agent produces becomes a field. Each agent becomes a role.

Pick a mode for each field

Ask one question per field: could two agents both be right about this at the same time?

FieldBoth right at once?ModeWhy
summaryNo. One current description.versionedIf two summarisers run, the second must know the first wrote.
findingsYes. Two reviewers can each find something.appendEvery finding stays, with who found it and what evidence.
testsYes. Each run is a result.appendTest runs accumulate. A later run does not erase an earlier one.
verdictNo. One decision.versionedAnd it must cite evidence: what finding or test run it rests on.
reviewOnly one reviewer should work a PR at a time.taskA reviewer claims it. Other copies skip.

When in doubt, choose append. It is the mode that loses nothing.

Set the rules on each field

  • verdict gets enum: [approve, request_changes]. The gateway rejects anything else.
  • verdict gets evidence: required. A verdict with no evidence is refused.
  • review gets a lease. A review takes a reviewer a few minutes, so 600s leaves room to renew.
  • The PR number is the record id, so idPattern: "[0-9]+". An id that is not a number is refused.

Roles

Each agent is a role. Give it the fields it writes and nothing more. Every role here may read everything, which is usual.

Reviewers run as several copies with different identities, so the role uses a pattern. A * stands for one whole segment and never crosses a /, so apikey:reviewer/* matches reviewer/r1 and reviewer/r2. A Kubernetes identity is always sa:<namespace>/<name>, two segments, so the pattern has to be sa:<namespace>/*. That matches every ServiceAccount in the namespace, which is why the reviewers get a namespace of their own.

Add an operator role for the CLI, with no write access.

The class

apiVersion: kslate.dev/v1alpha1
kind: SharedContext
metadata: { name: pr-reviews }
spec:
  idPattern: "[0-9]+"
  fields:
    summary:   { type: string, mode: versioned }
    findings:  { type: append }
    tests:     { type: append }
    review:    { type: task, lease: 600s }
    verdict:   { type: string, mode: versioned, enum: [approve, request_changes], evidence: required }
  roles:
    summariser: { identities: [sa:ci/summariser, apikey:summariser], read: ["*"], write: [summary] }
    reviewer:   { identities: ["sa:ci-reviewers/*", "apikey:reviewer/*"], read: ["*"], write: [findings, verdict], claim: [review] }
    tester:     { identities: [sa:ci/tester, apikey:tester], read: ["*"], write: [tests] }
    operator:   { identities: [sa:ci/kslate-cli], read: ["*"], write: [] }
  ttl: { after: 720h, onExpire: archive }
  checkpoint:
    required: [reviewer]
    sections: [goal, decisions, progress, open_questions]

Two more lines explained:

  • ttl says records expire 30 days after creation and are archived with their history rather than deleted.
  • checkpoint.required: [reviewer] means a reviewer must have saved a checkpoint before anyone can set status to resolved. This class has no status field, so the gate never fires here. It is kept to show the shape.

Load it

Locally, add the document to your policy file and start the gateway with --policy. On Kubernetes, kubectl apply it. Then check it is there:

KSLATE_APIKEY=rev-token bin/kslate classes --server http://127.0.0.1:8080
pr-reviews

GET /v1/classes/pr-reviews returns the class as the gateway loaded it. The verdict field, for example:

"verdict": {
  "mode": "versioned",
  "type": "string",
  "evidence": "required",
  "enum": ["approve", "request_changes"]
}

Try the rules

A reviewer claims the review on PR 4812. The record did not exist, so the claim creates it:

curl -s -X PUT http://127.0.0.1:8080/v1/records/pr-reviews/4812/fields/review/claim \
  -H 'Authorization: Bearer rev-token' -d '{}'
HTTP 201
{"record":"pr-reviews/4812","field":"review","owner":"apikey:reviewer/r1","lease_expires_at":"2026-10-02T18:52:39.648026-07:00","version":1,"record_version":1}

An id that is not a PR number:

HTTP 400
{"error":"MALFORMED","message":"id does not match the class idPattern","record":"pr-reviews/abc"}

A verdict without evidence:

HTTP 422
{"error":"EVIDENCE_REQUIRED","field":"verdict","record":"pr-reviews/4812"}

A verdict outside the enum:

HTTP 400
{"error":"MALFORMED","field":"verdict","message":"value must be one of the field's enum: approve, request_changes","record":"pr-reviews/4812"}

How the agents would use it

  • The summariser runs once per PR: run(agent, client, record=f"pr-reviews/{pr}") with a decide that returns one put to summary.
  • The tester appends to tests after each run, with the CI job URL as evidence.
  • Reviewers run in class-scoped mode with claim_field="review". Each copy claims a PR, reads summary and tests, appends findings, and puts verdict with based_on set to the tests version it read. If the tester appends a new run while a reviewer is deciding, the verdict is rejected and the reviewer decides again.

Checklist

  • One field per thing an agent produces.
  • append unless exactly one value can be true at a time.
  • evidence: required on any field another agent acts on.
  • enum on any field with a fixed set of values.
  • A task field for anything one agent should work at a time, with a lease two to three times your step time.
  • A role per agent, writing only its own fields. A pattern for roles that run as several copies.
  • An operator role for the CLI.
  • idPattern if the id has a shape.
  • A TTL. Add onExpire: archive if you want the history kept.