Open one change from any part of it
RepoOps gives every merged pull request one id, taken from its merge commit, and joins the prompt, the agent session, the diff, the review, the merge, the deploy, how it behaved in production, the incident it caused and the fix under it. Every place that names a change opens the same page, so a change opened from Production and from Attribution lands on the same address.
How to
The RepoOps desktop app, tracking the repository, with git in its checkout. On the hosted dashboard, the change page, Attribution's Changes tab and Cost per outcome list the receipts your desktops earned, with the session each one names; the prompt, diff and review stay on the desktop that signed the record.
The gh command line signed in, for the review stage. Without it the review falls back to the local pull request record.
- Open a change from where you are.
On a Production finding, the Diagnosis chain has a Change record step when the case knows its commit; press Open in the change record. Anywhere in the app, press Ctrl+K (Cmd+K on a Mac), type a commit, a chg_ change id or a pull request number, and pick Open the change record. The address works too: /v2/change.html?id= with any of those three.
- Read the nine stages on the rail.
Prompt, Agent session, Diff, Review, Merge, Deploy, Production, Incident and Fix, all from the record. A stage with nothing behind it says why, in the record's own words.
- Follow the Linked card.
Opened from a session, the card lists what the same session also shipped: the newest 12 changes as links to their own pages, and the rest as a count.
- Press Sign this record on The record card.
The record is built again and signed with this install's Ed25519 key as version 1, and logged in the install's transparency log. When a stage changes later, the card says changed since version 1 and the button signs version 2. Each version names the digest of the one before.
- Export it as OKF, NDJSON or Parquet, or share it with Copy link.
An export holds every signed version. The link names the change, so it opens the same record on any machine that tracks the repository.
Use cases
From a slow route back to the change and its fix
- Situation
- A Production finding shows a route got slower after a deploy, the case names the commit behind it, and you want the change and whether anyone fixed it.
- You do
- Open the finding, press the Change record step in the Diagnosis chain, then Open in the change record.
- You see
- The page opens on the merged pull request that holds the commit. Production lists the errors and findings that name the merge commit or the deploy, Incident lists the cases opened from them with their status, and Fix shows what each case recorded: the remediation, its pull request, a rollback.
Everything one session shipped
- Situation
- You have a session id and want each pull request it produced.
- You do
- Open /v2/change.html?session= with the session id.
- You see
- The record opens on the newest pull request the session merged, and the Linked card lists the others under Same session also shipped.
Expected outcome
Next step: The pull request is not merged into this checkout's history, or its squash subject does not end in (#N). Fetch, or open it by its merge commit.
Next step: The merge commit has no Session-Id trailer, or the session ran on another machine. Run npx repoops install in the checkout for future commits.
Next step: No deploy event names the merge commit, a later deploy containing it, or the pull request branch. Connect the host's deploy events.
Next step: No production errors or findings are recorded on this machine for the 30 days after the merge, or the change was never deployed. Connect a production source, or turn on the hourly job.
Next step: A receipt holds ids and hashes only. Open the change in the desktop app that signed it to read those stages.
Next step: No desktop of your team has sent a change record yet, so the rows come from the pull requests and commits your synced sessions recorded. Sign the desktop app in to the team, in a repository the team authorized, and its next signed version replaces them.
Next step: No version is signed yet, or a version on this machine does not verify. Press Sign this record, or read the failed check on The record card and on Memory proof.
Next step: The merge commit names no session, or it already carries a Git AI note that RepoOps did not write. Hover the button for which one; RepoOps never overwrites a team's own note.
Next step: The line under it says why: no declared configuration or observation is stored on this machine, the session launched more subagent runs than were captured, a subagent run was captured before runs were checked, or a transcript line over 64 MB was skipped. The session ran on another machine or before the hooks were installed; future sessions are checked.
Next step: The session ran on another machine, or the trailer was typed or copied. Open the change on the machine that ran the session; a teammate's sessions are recorded on theirs.
Next step: The file was checked against the key it carries, which anyone can make. Pass --pubkey with the key shown in the Verify line on The record card.
What else to consider
How the joins are made
The change id is chg_ plus the first 12 hex characters of the merge commit. It is derived, not stored, so every machine computes the same id. The session comes from the Session-Id trailer on the merge commit and is never guessed from a time window. A commit that is not a merge commit opens the pull request GitHub names as containing it. A later deploy counts only when git confirms its commit contains the merge. A production event or an incident case joins only when it names the merge commit or the deploy that shipped it, and a join through the deploy is labelled, because one deploy can carry several changes. False positives and duplicates are left out.
Git AI notes on the merge commit
If your team runs Git AI, its line attribution is stored as a git note under refs/notes/ai. When the merge commit carries one, the record reads it and shows it on the Agent session stage as the Git AI note, next to what the RepoOps capture says. The note reports whether a person or an agent wrote the change, the agent, the model, the session and the lines covered in each file. When the note and the capture disagree on who wrote it, the session or the model, the stage shows both values for each field and does not choose one. The note is treated as text: RepoOps keeps its declared fields, cut to one line each, and never runs or fetches anything in it. A merge commit with no note adds nothing to the record, so a version signed earlier keeps its digest. Only the merge commit is read, so a note left on a branch commit before a squash merge is not picked up. Fetch notes with git fetch origin refs/notes/ai:refs/notes/ai if they live on the remote.
Writing the record back as a Git AI note
On The record card, Write as a Git AI note puts the change record on the merge commit as a git note under refs/notes/ai, in the shape Git AI reads: an agent wrote it, with the session's tool, model and id, and the files changed. RepoOps reads the note back to check it came out the same. The note has no line ranges, because the record counts lines per file and does not hold ranges. It stays on your machine until you run git push origin refs/notes/ai. The button is greyed out when the merge commit names no session, or when it already carries a Git AI note that RepoOps did not write, which RepoOps never overwrites.
Crash Override marks on the deploy
If you build with Crash Override's Chalk, put the marks of what a deploy shipped on its deploy event as payload.chalk. The Deploy stage then names the artifact, its hash, its chalk id and the commit it was built from, and says so when a mark names a different commit from the deploy. Mark fields are kept as short text, any user name and password in the origin address are removed, and nothing in a mark is run or fetched.
What the signature covers
Each version is an in-toto statement in a DSSE envelope, signed with the install's Ed25519 attestation key, the same envelope RepoOps uses for authorship attestations. It covers the record, the version number, the signing time and the previous version's digest, so a dropped or edited version breaks the chain, and repoops verify on an export exits 0 only for the pinned key over a whole chain. A version is signed when a person presses the button, not on every change. The OKF export is one JSON document; Parquet has one row per version and loads in DuckDB.
The configuration check
Each session declares a configuration: the model, the instruction files, the MCP servers and the skills on disk when a turn ended, hashed into its agent version ID. The configuration check reads what the session actually used from its transcript on the machine that ran it: the models its messages came from, the MCP servers and tools it called, the skills it invoked and the instruction files the client loaded. Each subagent run is read from its own transcript and checked against its own declared version, which carries the run's model and agent definition. A model no declared version names, an MCP server the configuration does not include, a tool outside a server's declared tools, or a declared CLAUDE.md that was never loaded is a difference, and the Configuration check card lists each one with the thread it came from.
A server's declared tools are the mcp__server__tool entries in the permissions allow and ask lists of your settings files. A server allowed whole, or named by no entry, has no narrower list, so any tool on it counts as declared. The tools the client offered during the session are kept beside the check but never stand in for the declared list. When part of a transcript could not be read, for example a single line over 64 MB, the check reads not checked and says which part, never a match. The result is signed into the merge-time attestation with the rest of the record, so it is tamper-evident after the merge-time signature, not tamper-proof: an edit after signing fails verify, but a compromised machine can still sign a consistent record. The public signature note on the commit carries only the verdict and what it covered.
Commit trailers and outside agent IDs
A trailer is text anyone can type. The Commit trailers card reads each commit of the pull request and compares its Session-Id and AI co-author trailers with the sessions recorded on this machine. A Session-Id that no recorded session matches, or a co-author that names a different agent from the session the commit names, is flagged. So is a commit whose Repoops-Attribution line counts agent lines and which carries no trailer at all. A co-author with no Session-Id beside it reads not checked, because RepoOps never matches a commit to a session by time alone. A commit that matches names a recorded session whose tool agrees with its co-author; that does not prove the session wrote the commit, since trailers copied from another commit of the same session also match.
The same check runs when you commit. The prepare-commit-msg hook RepoOps installs compares the message with the session it stamps and prints a warning when they disagree, for example a Cursor co-author line in a Claude Code commit. The warning names the trailer and the session; the commit goes ahead and the message is not changed. Set REPOOPS_NO_TRAILER_CHECK=1 to turn it off.
Outside agent IDs sit beside the agent version ID of the session they belong to. GitHub's Agent-Logs-Url comes from the commit trailer. Microsoft Entra agent object IDs and SPIFFE IDs come from a .repoops/agent-ids.json file you commit, with one entry per agent version ID or tool, for example { "tool": "claude-code", "spiffeId": "spiffe://example.org/agents/claude" }. An Entra ID must be a GUID and a SPIFFE ID must parse, or it is dropped. Add "githubEnterprise": "your-enterprise" to the same file and RepoOps reads GitHub's enterprise audit log for the pull request with your own gh sign-in, and puts each agent_session_id GitHub recorded there on the record with the action and the person who started it. That needs an enterprise owner's sign-in; without one, the card says the log was not read. The card also says whether each session a trailer names wrote the change: it compares the files that session's own captured edits touched with the files the change touched, so a trailer copied from another commit of the same session no longer reads as proof.
Receipts from repoops.ai
The install's key proves that some install signed a version, but no one outside your machine can tell whose key it is. When the desktop app is signed in to a team, each version you sign is also sent to repoops.ai with the device token. The hosted service recomputes the record digest from the signed version, refuses it if anything beside the envelope disagrees, and issues a change.record receipt signed with its own Ed25519 key. The Receipt line on The record card names the receipt and has a Download receipt link; the download opens on repoops.ai for members of your team.
The receipt is a small JSON file in the receipt format promptcompute.ai, RepoOps and ContextFile share. Its body holds ids and hashes only: a hash of the team id, the change id, the version, the record digest and the previous one, the merge commit, the pull request as owner/repo#number, the session id, a hash of the first prompt, a hashed reviewer id, the id of the install key that signed the version, whether your team linked that key, which link that was and when it was made, and the time the install says it signed the version. A key that moved teams has a different link in each team's receipts, and a version the install says it signed before the link was made is marked by a signed time earlier than the link time. It never holds the prompt, the diff, a file path or anyone's name. Version 2 and later link to the receipt of the version before. A version gets one receipt: sending it again returns the same receipt, and a different record under the same version number is refused.
Anyone can check a receipt against the keys RepoOps publishes at https://repoops.ai/.well-known/receipt-keys.json, for example with npx @promptcompute/ledger-verify receipt receipt.json --keys https://repoops.ai/.well-known/receipt-keys.json. The key rotates every 90 days, and old keys stay listed with the date they stopped signing, so an old receipt still checks. An install that is not signed in to a team sends nothing and keeps its versions signed locally as before. A version signed while offline waits on this machine and goes with the next one you sign.
The install's key belongs to one team at a time. If you sign in to a different team on the same machine, repoops.ai refuses the key there until it is released, and the Receipt line says so with a Release and link here button. Choosing it signs a release with the key, tells the old team, links the key to your new team and sends the refused versions again. If this machine never linked the key itself, an owner or admin of the team that holds it can release it instead, from the Install keys list on the Team activity page, which shows each linked key with the device that linked it and the date. Each link or release signature counts once, so one copied from the network cannot be sent again. Each release is written to that team's audit log. Receipts the old team already holds stay valid: they still say the key was linked, which was true when they were signed.
When you sign the desktop app in to a team, it links its key to that team: it sends the public half with a signature over your team id, which only the holder of the private half can make. It then sends every version it signed before the sign-in that has no receipt yet, oldest first, so your earlier history gets receipts too, and each of those receipts says the key is linked. A key stays with the team that linked it, and repoops.ai refuses it for any other team.
Only repositories your team has authorized are sent. Before it sends anything, the desktop app reads the team's authorized repository list and matches each repository by its github.com owner/repo. A repository outside that list, or one with no github.com remote, keeps its versions on this machine, and the Receipt line reads not sent: this repository is not authorized by the team. That holds when you choose Sign on one of its changes too: the version is signed and kept here, and nothing goes to repoops.ai. If the list cannot be read, nothing is sent and the Receipt line says so. Once the team authorizes the repository, the next sign-in or Sign sends the versions it held back. repoops.ai checks the same list on its side, so an older desktop build that still sends every repository gets no receipt for one the team has not authorized: it reads the repository from the pull request link inside the signed record. The list is available on every plan that gets receipts.
Receipts issued before that check began are kept. The Team activity page lists every receipt whose repository is not in your list today, under Receipts for repositories outside your list, newest first. An owner or admin can revoke one and must give a reason. A revoked receipt is never deleted: it stays readable and its signature still verifies, but anyone who checks it with repoops.ai is told it is revoked, when and why, including someone outside your team who holds a copy and looks it up by its hash. A later push of the same version gets no new receipt. Each revocation is written to the team's audit log.
Change risk before merge
Before a pull request becomes a change record, Attribution's Change risk tab shows a card for each open one in the desktop app, read from GitHub with gh. It has four lines: how many earlier changes to the same files in the last 90 days an incident case joined (a file most changes touch is left out, and fewer than 5 earlier changes read as a count, not a rate), the revert history of the agent whose session names the pull request on the surface it touches, the change lane (contained, wide or hard to reverse, with the files that set it), and the latency budget of the routes its files serve. Each line links to its evidence, and a line with nothing behind it reads Not measured with the reason. The latency line stays Not measured for now, because service goal readings live with the hourly evidence job. With brain publishing on, the desktop app publishes the cards at most every ten minutes, and the same card appears in two more places: the hosted dashboard's Change risk tab, and a check named RepoOps change risk on the pull request in GitHub. The check's conclusion is always neutral. If no card has been published for the pull request yet, or the card read an older commit, the check says so; choose Re-run on it after the next publish to read the latest card. The card is advisory: it has no pass or fail, and nothing reads it to hold a merge. A fifth line, This pattern broke production before, appears only when the pull request repeats a past production incident case: it changes the line where that case's error was raised, changes the code serving the route its lesson's error signature names, or adds a line its lesson's guard matches. The line names the case, what broke and why it matched, and links to the case. When nothing matches, the line is absent rather than a reassurance.
The old address
The Change record subtab under Agent traces is gone. A saved change-record.html link redirects to the v2 change page with its lookup kept: id, pr, commit, session, deploy and repo.
Cost, egress and storage
No model call. The record is rebuilt from git, gh, the sessions on this machine, the production events and the incident cases RepoOps already holds each time it opens. Signed versions are kept in a file in the repository's ignored telemetry folder, beside the transparency log, and each export writes an audit row with no record content. Text from a production event is never copied into the record; it shows declared fields such as the route, the source and the deploy. The outbound calls are gh to GitHub for the pull request and, when the app is signed in to a team, one request to repoops.ai per signed version in a repository the team authorized, for its receipt. The record is built in the desktop app only; the hosted dashboard has no checkout to read the merge commit from, and stores the receipt, not the record.