RepoOps and SalesAsCode push what they already know. CommitOps never pulls.
CommitOps works without either product. A feed only adds what the sibling already knows: RepoOps sends the measured agent spend and the roadmap items for a change, SalesAsCode sends deals and the commitments it found. Each sibling pushes as an agent principal, under the same rules as any other agent. The input and output schemas are in openapi.json, generated from the same code the server runs; where this page and the code disagree, the code wins.
An owner of the CommitOps company connects the feed on /connect, or with the connect_sibling tool. That makes:
- an agent principal named
RepoOpsorSalesAsCode, operated by that owner; - a source of its own (provider
repoopsorsalesascode), so disconnecting the feed deletes what it sent, as for any source; - a key, shown once. The sibling stores it and sends it as
Authorization: Bearer co_<org>_<prefix>_<secret>.
The key acts as that agent and nothing else. The RepoOps key carries propose and link_work; the SalesAsCode key carries propose. Neither can keep a candidate, and the database refuses an agent keeping an assertion it made itself.
Every call is POST https://commitops.ai/api/v1/tools/<name> with a JSON body, or the same tool over MCP. Errors come back as { "error": { "code", "message", "details" } } with a 4xx status. Retry a 5xx or a 429 with backoff; do not retry a 4xx unchanged.
invalid_input- 400
plan_limit- 402
forbidden- 403
not_found- 404
conflict- 409
guard_failed- 422
Measured agent spend for one change
{
"work": {
"provider": "github",
"key": "acme/app#42",
"url": "https://github.com/acme/app/pull/42",
"title": "Add SAML login",
"state": "merged",
"merged_at": "2026-10-15T12:00:00Z"
},
"cost_cents": 1234,
"attribution": "branch",
"roadmap_slugs": ["saml-sso"]
}work.provider- github or gitlab.
work.key- owner/repo#number for GitHub, group/project!number for GitLab.
work.state- open, merged or closed (closed without merging). merged needs merged_at.
cost_cents- Integer cents: the metered spend of the agent sessions tied to this change. From a float in dollars, send Math.round(cost_usd * 100).
attribution- branch when the sessions ran on this change's own branch, file_overlap when they were matched by touched files.
confidence- Required with file_overlap. Only strong is stored; likely is refused with 400.
roadmap_slugs- Optional. Same as record_work_slugs.
What CommitOps does with it
- Stores the figure on the change with
cost_basismeasured, who reported it, when, and the attribution. Reporting again replaces the figure. - A measured figure replaces a modeled estimate on the same change; the two are never added. A change that already carries a time-tracked cost is refused with 409, because measured agent spend and tracked human time measure different things.
- No screen sorts by cost. Measured and modeled cost show on separate lines.
- If CommitOps has not seen the change yet, it mirrors it from
workunder the feed’s source. When the GitHub or GitLab webhook later mirrors the same key, the forge’s copy wins for title, state and dates; the cost and slugs stay.
What RepoOps must not send
- A figure from a model or an estimate. Measured means metered.
- Spend it cannot tie to a change. Unattributed spend stays unattributed; never spread it across changes.
- A
likelyfile overlap match.
The roadmap items one change belongs to
{
"work": {
"provider": "github",
"key": "acme/app#42",
"url": "https://github.com/acme/app/pull/42",
"title": "Add SAML login",
"state": "merged",
"merged_at": "2026-10-15T12:00:00Z"
},
"roadmap_slugs": ["saml-sso"]
}A slug is lower case letters, digits and hyphens, starting with a letter or digit, at most 64 characters, at most 20 per change. Send a slug the company also sets on its promises with set_roadmap_slug.
Match pass 2 links any kept promise with the same slug to the change at confidence 0.90, the same as a roadmap:saml-sso label. The list replaces what RepoOps sent before for that change; send [] to withdraw. Slugs from the change’s own labels stay. The answer lists the promise refs linked.
One company and one deal, in the CRM shape
{
"account": { "external_id": "acct_7", "name": "Britelane", "domains": ["britelane.com"] },
"deal": {
"external_id": "opp_7",
"name": "Britelane, seats",
"amount_cents": 1200000,
"currency": "USD",
"stage": "stage 2",
"status": "open",
"close_date": "2026-12-15",
"renewal_at": null,
"owner": "Dana Whitfield"
}
}account.external_id- SalesAsCode's own id for the company, stable across calls.
account.domains- Website domains. A URL is reduced to its host; personal mail domains are dropped.
deal.amount_cents- Integer cents, or null when unknown. Never a guess.
deal.currency- ISO 4217, or null when unknown. Null is stored as USD, the default every CRM connector uses.
deal.status- open, won or lost.
The company links to the account CommitOps already has with exactly that domain. A domain that matches two accounts links to none and the deal is not stored (account: "ambiguous"), because a wrong link is worse than a missing one. Otherwise the company becomes an account. The deal is stored with source crm, so it counts wherever a blocking promise waits on it, and totals count distinct deals. Sending the same deal again updates it.
A commitment SalesAsCode found
The existing tool, unchanged. From the SalesAsCode agent it is filed under the feed’s own source.
{
"quote": "we can have the audit log export live before your renewal",
"message_text": "Priya: when does export land?\nDana: we can have the audit log export live before your renewal.",
"account": "Britelane",
"strength": "firm",
"due_phrase": "before your renewal",
"speaker": "Dana Whitfield",
"said_at": "2026-10-14T16:20:00Z",
"locator": { "recording_id": "gong:1234", "offset_ms": 61000 }
}quotemust be an exact substring ofmessage_text, whitespace aside, or the call is refused with 422guard_failedand nothing is written. Case and punctuation count.message_textis the whole message or call turn the quote came from, not the excerpt alone. A message_text equal to the quote passes the guard and proves nothing, so send the full text whenever you have it.said_atis when it was said, not when SalesAsCode read it.locatorpoints back:{message_id}for mail,{recording_id, offset_ms}for a call. Up to eight fields.- The promise is a candidate. A person keeps or discards it. The SalesAsCode key cannot keep.
Retrying the same quote with the same locator returns the first row with duplicate: true.
CommitOps never asks a sibling for customer content beyond the quote and the text it came from, and never for code, diffs, prompts, session transcripts, notes, drafts or sequences. Nothing a sibling sends goes to a log, an analytics event or a model’s training. What the product does with customer words: security.
An owner disconnects the feed on /connect. In one transaction: every deal it sent, every promise it proposed (kept or not) that has no quote from anywhere else, and every change it mirrored are deleted; the measured costs and slugs it put on changes GitHub or GitLab own are cleared, and a link pass 2 made on one of those slugs comes off. Its key is revoked. The agent principal stays, as every principal does, so the history of what it did keeps its author; an owner can retire it on /settings.
Back to the agent docs.