openapi.json and verified against
production, not assumed from the spec.
The six rules
- One ticket = one session = one branch. The branch is the unit of isolation for writes; the session is the unit of isolation for execution. Never let two sessions share a branch.
- Sessions are disposable. Context comes from the ticket and the repo. Delete the session when the PR is open. Pausing exists, but you don’t design around it — you design around your tracker and git.
- Webhooks, not the event stream. Subscribe once, handle three event types, and let every request return immediately. See webhooks.md.
- Idempotency everywhere. Your tracker re-delivers. Gobare webhooks are
at-least-once and can arrive out of order. Every write carries an
Idempotency-Key; every handler you write is safe to run twice. See idempotency.md. - Gate on the two ceilings that actually bite a fleet. Session creation is rate-limited separately from everything else, and concurrent sessions have an organization-wide ceiling that a session holds until you delete it. See limits.md. Your dispatcher owns both.
- A ticket must fit inside the workspace lifetime. A workspace is
reclaimed after a fixed amount of active time; paused time doesn’t
count. A turn still running past it ends
failed. Split tickets that won’t fit — don’t discover it near the deadline.
Architecture
There are two systems of record and Gobare is neither. Your tracker knows what was asked and where it is. Git knows what changed. Gobare knows only how to run the work.One-time setup
Connect GitHub, in the Console, before namingenvironment.repo on any
session — a request that names it before the connection exists is refused up
front.
Save the fleet’s standing rules as an agent, so no per-ticket prompt has
to repeat them, and changing the rules is one call rather than a deploy:
approval_mode: "auto" for a fleet that lands PRs — the PR is the approval
gate. See saved-agents.mdx for what an agent does and does
not carry forward.
Subscribe your webhook endpoint to the three events this pattern needs:
Per-ticket flow
Tracker → dispatcher. When an issue enters your trigger state, your tracker’s webhook handler dedupes on issue id + transition, enqueues{issue_id, identifier, title, description, repo}, and returns. Do not
create the Gobare session inside that handler — the creation-rate ceiling and
the concurrency ceiling both need one place that knows how many are in
flight.
Worker → gate, then create the session with its opening message. Before
creating: count in-flight sessions — keep your own counter, or ask Gobare with
GET /v1/sessions?created_by_token=me&limit=100 — and hold the ticket if
you’re at the ceiling. The refusal for a full project never succeeds on
retry; only a delete frees a slot.
Then one call — the session and its first message together:
agent.idinherits the saved agent; any field can still be overridden per session.inputis all-or-nothing: if the message can’t be accepted, the session isn’t created either. Nothing to clean up on failure.metadatais yours, returned unchanged, never interpreted — put the tracker’s own id in it so every later webhook routes back without a lookup table:GET /v1/sessions?metadata=issue:ENG-123. See sessions.md for the field, and pagination.md for filtering by it.- The branch is reported, not chosen. The clone checks out the repo’s
default branch; the agent creates the ticket branch from it, per the
instructions above. Sending
environment.branchis refused — it is reported by the clone, not something you set. - A
201doesn’t mean the clone happened — that happens when the workspace comes up. Before concluding the agent ignored the instructions, readenvironment.repo.clone_erroron the session.
Getting the change out of the sandbox
The sandbox clones through your GitHub connection. Whether it can also push depends on what credential it holds, and the safe default is: it holds none. Recommended — you push, from your own infrastructure. The saved agent’s instructions above make it writegit format-patch … > /workspace/outputs/changes.patch. Anything under
/workspace/outputs is published as an artifact when the turn completes.
Your turn.completed handler fetches it, applies it to a fresh clone on
agent/eng-123, and pushes:
turn.completed → land it, open the PR, close the loop
- Verify the signature; dedupe on
(session_id, type, created_at). GET /v1/sessions/{id}formetadata.issue,metadata.branch.GET /v1/sessions/{id}/turns?limit=1for status, final message, artifacts.- If artifacts are
"pending": re-enqueue and come back — rare, publication can lag by a few seconds."partial"names what was dropped inartifacts_skipped. - Fetch
changes.patch, apply, pushagent/eng-123(above). - Open the PR: base
main, headagent/eng-123, body = the agent’s final message plus a link to the issue. - Tracker: move the issue to “In review”, attach the PR URL.
DELETE /v1/sessions/{id}.
turn.failed
GET /v1/sessions/{id}/turns?limit=1for the error.- Tracker: comment the error on the issue, move it to a failed state.
- Decide: retry with a new session on the same branch, or route to a human.
DELETE /v1/sessions/{id}.
environment.repo.clone_error too: a failed clone is the one
failure that isn’t the agent’s.
session.action_required
The agent stopped and is waiting — see required-actions.md
for the full shape. For this fleet, the move that pays off: a question
becomes a comment on the ticket. Post the agent’s question, move the issue
to a waiting state. When a human replies, send input.question_answer with
the reply and the turn resumes. The sandbox paused while it waited and cost
nothing — paused time doesn’t count against the workspace lifetime, and
nothing times a required action out.
The ticket is the conversation. The agent never talks to anyone directly.
The write-race question, answered
How do you handle multiple instances of the same agent racing on writes?Gobare doesn’t, and shouldn’t. Sessions are isolated machines; nothing races inside the runtime. The race is at the destination — your repository — and that is your consistency model to own. Git is already good at this; the pattern above just uses it:
- One session per branch. Two sessions never write the same ref.
- One pusher. With the recommended path above, every push comes from your
turn.completedworker. Serialise that worker per repository and there is no race left to have. - Merge is where conflicts surface — at PR time, in front of a reviewer or a merge queue. Visible, reversible, already tooled.
- Retry = new session, same branch. No half-written workspace to reason about.
- Two tickets touching the same files is a tracker problem, not a runtime
one. Your dispatcher can see every in-flight session
(
?created_by_token=me) with its branch inmetadata; hold a ticket whose paths overlap one already running.
Idempotency, concretely
turn_id and call_id are copied from the required action, and output for
a tool result must be a string — serialise JSON yourself. Never send a
thrown exception’s message as error; it reaches the model.
A crew, if you go there
parent_session_id on create joins sessions into a tree;
GET /v1/sessions?root= and GET /v1/events?root= address the whole tree at
once — see Crews. For tickets running in parallel you
don’t need it, but if one ticket ever fans out into its own reviewer session,
that’s the primitive. See
Let several agents work on one thing.
What this doesn’t cover
- Cross-ticket dependencies (B needs A’s branch). Sequence them in your dispatcher.
- A shared build cache across sessions. Not first-class today.
- Cost per ticket. The shape is compute-time, not tokens — model spend stays with your provider. See limits.md for what the sandbox itself costs against.
Next
- sessions.md — every field
POST /v1/sessionsaccepts - webhooks.md — verification, retry schedule, the traps
- limits.md — the two ceilings this fleet gates on, with the numbers
- idempotency.md — the key, scoped precisely
- required-actions.md — answering what an agent asks