Change requests

How session work reaches the default branch through review.

A change request (CR) merges one git branch into another. Kortix creates a CR from a session's branch (head_ref) onto the project's default branch (base_ref, usually main). The CR row is metadata; the merge, diff, and conflict checks run as real git operations against the project's repository. A CR is the only way session work reaches the default branch.

Why work goes through a CR

A session runs in a sandbox on its own branch, named after the session ID. The sandbox does not last forever, but the branch does: git is the only durable record of a session's work. Every new session starts from the default branch. Until a CR merges, the work stays on its own branch, unreviewed and invisible to every other agent, trigger, and collaborator. This applies to every change: code, agents, skills, and the manifest (kortix.yaml) — no exceptions.

The agent mandate

An agent must open a CR to land any change on the project's default branch. The agent does not merge its own CR — merging is the user's decision. Follow this contract:

  1. Commit on the session branch ($KORTIX_BRANCH_NAME). Make small, working commits. Do not rewrite history or force-push.
  2. Push the branch: git push origin HEAD.
  3. Open the CR: kortix cr open --title "..." --description "...". Inside a session sandbox, --head and --session default to $KORTIX_BRANCH_NAME and $KORTIX_SESSION_ID. --base defaults to the project's default branch.
  4. Tell the user the CR number, so they can review it.
  5. Stop. Do not merge the CR yourself.

Anti-patterns

  • Force-pushing to the default branch. This breaks the review contract, even where the backend allows it.
  • "It's on my branch, pull it yourself." The session branch is gone once the sandbox stops, unless a CR merged it first.
  • Sending the change as a file, paste, or archive. The CR system already solves this problem.

Data model

CRs live in the change_requests table.

ColumnTypeNotes
cr_iduuidPrimary key. The REST API's identifier.
project_iduuidThe project the CR belongs to.
numberintegerPer-project display number (#1, #2…). Unique per project. Never recycles.
titletextRequired.
descriptiontextDefaults to an empty string.
base_reftextThe branch merged into. Usually main.
head_reftextThe branch merged from. In a session, this is the session ID (a UUID).
statusenumopen, merged, or closed.
head_commit_shatextRefreshed against the live head_ref tip on every read, for open CRs. Captured at merge time for merged CRs.
base_commit_shatextSame rule, for base_ref.
origin_session_idtextThe session that opened the CR. Set to null if that session is deleted.
created_byuuidThe user who created the CR.
merged_at / merged_bytimestamp / uuidWhen and who merged the CR.
merge_commit_shatextThe merge commit. Equals head_commit_sha for a fast-forward.
closed_at / closed_bytimestamp / uuidWhen and who closed the CR without merging.
metadatajsonbHolds requested_changes, a list of {text, by, at} entries added by POST /:crId/request-changes. CRs have no separate comment table.
created_at / updated_attimestampSet on creation. Updated on every status change or SHA refresh.

A unique index on (project_id, number) lets you reference a CR by its short number instead of its UUID.

Lifecycle

open ──(merge)──▶ merged      (terminal)
open ──(close)──▶ closed ──(reopen)──▶ open
  • open is the starting status.
  • closed is reversible. POST /:crId/reopen sets it back to open.
  • merged is terminal. You cannot reopen or close a merged CR. Open a new CR against the merged state instead.

Kortix refuses to create a CR whose branch has no commits ahead of the default branch. This usually means the agent committed locally but never pushed. Push the commits, then create the CR again.

SHA refresh

For an open CR, Kortix refreshes head_commit_sha and base_commit_sha against the live branches on every GET. If the repository is unreachable, or a branch is missing, Kortix skips the refresh and serves the CR's last known metadata. A merged CR keeps the SHAs captured at merge time — Kortix never refreshes them again.

Merge mechanics

POST /v1/projects/:projectId/change-requests/:crId/merge runs these steps.

  1. Kortix reads the manifest (kortix.yaml) from head_ref and validates it against the manifest schema. A branch with no manifest passes. An invalid manifest returns 422 with code: "MANIFEST_INVALID" and stops the merge.
  2. Kortix fast-forwards base_ref if head_ref is strictly ahead of it.
  3. Otherwise, Kortix creates a merge commit. The default message is Merge CR #<n>: <title>, and you can override it with message in the request body. The commit author is Kortix <noreply@kortix.ai>.
  4. A conflict returns 409 with the conflict list. Check the same list with GET /:crId/merge-preview before you merge.
  5. On success, Kortix sets status to merged, records merged_at, merged_by, and merge_commit_sha, and invalidates the project's git cache.

Merging a CR that is not open returns 409.

Merge preview

GET /:crId/merge-preview returns:

FieldTypeMeaning
base_shastringCurrent tip of base_ref.
head_shastringCurrent tip of head_ref.
merge_basestring | nullCommon ancestor. Null if the histories are unrelated.
is_up_to_datebooleanhead_ref is fully merged into base_ref.
can_mergebooleanNo conflicts.
can_fast_forwardbooleanhead_ref is strictly ahead of base_ref.
conflictsstring[]File paths that would conflict.

REST API

All routes sit under /v1/projects/:projectId/change-requests.

MethodPathNotes
GET/?status=open|merged|closed|all. No filter returns every status.
POST/Body: {title, description?, head_ref, base_ref?, session_id?}. Returns 201.
GET/:crIdReturns the CR. Refreshes SHAs as a side effect.
PATCH/:crIdEdits title or description. 409 if not open.
GET/:crId/diffUnified patch: file list, additions, deletions.
GET/:crId/merge-previewSee Merge preview above.
POST/:crId/mergeBody: {message?}. 422 on an invalid manifest. 409 on conflict or if not open.
POST/:crId/close409 if already merged.
POST/:crId/reopen409 if not closed.
POST/:crId/request-changesBody: {feedback}. Appends to metadata.requested_changes and wakes the originating session's agent. 409 if not open.

POST / rejects a head_ref with no commits ahead of base_ref: 422 code: "CR_HEAD_NOT_AHEAD". This is the error an agent sees if it opens a CR before pushing its branch.

Authorization

Read routes need read access to the project. Write routes need write access. Each write action also needs a capability, shown below for a full token and for a session's scoped token.

ActionFull-token capabilityScoped-token capability
Open a CRproject.gitops.pushproject.cr.open
Request changesproject.review.actproject.cr.open
Mergeproject.gitops.mergeproject.cr.merge

These are separate capabilities, so a token can open CRs without the power to merge them. This is the mechanism behind the agent mandate above.

Change requests – Kortix Docs