Agent Loops
Binde deinen eigenen Coding-Agenten an Spedy an — er claimt Tickets, arbeitet und meldet über MCP zurück.
Spedy betreibt keine Agenten. Das machst du. Ein Loop auf deinem Rechner (oder in eurer CI) fragt Spedy über MCP mit einem eigenen Zugriffstoken ab, claimt ein Ticket, damit es niemand sonst anfasst, arbeitet und meldet zurück. Spedy ist die Schicht, in der diese Arbeit ankommt: Ticket, Kommentar, Pull Request, Zeit, Zuordnung.
Spedy ruft nie bei deinem Agenten an. Steht dein Loop still, passiert nichts — genau diese Eigenschaft macht es unbedenklich, ihn laufen zu lassen.
Der Loop von Anfang bis Ende
users_me → wer bin ich, was darf ich, wie viele Claims halte ich
tickets_list claimable: true → was kann ich mir nehmen
tickets_claim → exklusives, ablaufendes Lease auf EIN Ticket
timers_start → (optional) Zeit erfassen
… arbeiten …
tickets_heartbeat → Lease am Leben halten
tickets_report → Kommentar, PR, Status, Timer, Freigabe — in einem Aufruf1. Agent anlegen
Einstellungen → Agenten → Agent anlegen.
Ein Agent ist ein Nutzer mit isAgent = true. Er hat überhaupt kein Login — kein Passwort, kein OAuth, keine Einladung — und authentifiziert sich ausschließlich mit einem Personal Access Token. Er ist sitzplatzfrei (Agenten zählen nicht in die abgerechneten Sitzplätze) und bekommt keine E-Mails. Überall, wo ein Mitglied auftaucht — Zuweisungs-Picker, Kommentare, Zeitfreigabe — taucht auch ein Agent auf, als Agent gekennzeichnet.
Jeder Agent gehört einem menschlichen Besitzer. Der Besitzer darf Einstellungen und Tokens des Agenten auch ohne agents:manage verwalten; den Besitzer zu wechseln erfordert das Admin-Recht.
2. Projekte freigeben
Ein Agent erreicht ein Projekt genauso wie ein Mensch: über die Projekt-Mitgliedschaft. Füge ihn unter Projekt → Mitglieder hinzu. Ein Agent ohne Mitgliedschaft sieht nichts.
3. Eingeschränktes Token erstellen
Einstellungen → Agenten → <Agent> → Zugriffstoken → Token erstellen. Das Klartext-Token wird genau einmal angezeigt.
Schränke es so eng ein, wie die Aufgabe es zulässt:
| Option | Wirkung |
|---|---|
allowedBoardIds | Begrenzt das Token auf bestimmte Projekte — unabhängig von den Mitgliedschaften des Agenten. Leer = alle Projekte, in denen der Agent Mitglied ist. |
readOnly | Nur lesende Tools. Keine Schreibzugriffe. |
denyDelete | Sperrt jedes destruktive Tool. Für Agenten-Tokens standardmäßig an. |
clientLabel | Ein Name für diesen Loop (z. B. nightly-refactor auf ci-1). Er erscheint als Client auf Kommentaren, Zeiteinträgen und Statuswechseln — und schlägt das, was der MCP-Client über sich selbst behauptet. |
Die Durchsetzung ist fail-closed: Ein auf zwei Projekte beschränktes Token erreicht kein drittes, auch wenn der Agent dort Mitglied ist.
4. MCP-Client auf Spedy zeigen lassen
{
"mcpServers": {
"spedy": {
"type": "http",
"url": "https://<eure-org>.spedy.ai/api/v1/mcp",
"headers": { "Authorization": "Bearer pat_…" }
}
}
}Rufe zuerst users_me auf. Es liefert Akteur, Besitzer, Token-Scope, den wirksamen MCP-Zeitfaktor, das Wochenlimit und wie viele Claims der Agent von wie vielen erlaubten hält — alles, was ein Loop braucht, um zu entscheiden, ob er überhaupt anfängt.
5. Arbeit finden
tickets_list und tickets_search kennen claimable: true: Tickets, auf denen kein aktives Claim liegt (frei, oder das Lease ist bereits abgelaufen) und die nicht in einem finalen Status sind.
| Filter | Verengt auf |
|---|---|
claimable: true | Alles, was dieser Loop gerade nehmen darf |
assignedToMe: true | Verengt claimable auf Tickets, die diesem Akteur bereits zugewiesen sind — die „ein Mensch hat mir das übergeben"-Warteschlange, und der Filter der Wahl, wenn der Agent nur bewusst übergebene Arbeit anfassen soll |
claimedBy: "me" | Was dieser Loop schon hält |
Dieselben drei Filter gibt es in der Weboberfläche („Nur claimbar") und auf GET /issues, auf einer gemeinsamen Implementierung — Mensch und Loop sehen immer dieselbe Warteschlange.
6. Claimen
tickets_claim { ticket, leaseSeconds?, assign? } nimmt ein exklusives, ablaufendes Lease.
- Exklusiv. Von zwei Loops, die im selben Moment claimen, gewinnt genau einer. Der andere bekommt einen klaren Konflikt mit dem aktuellen Halter und dem Ablaufzeitpunkt und kann warten oder weiterziehen.
- Ablaufend. Das Lease läuft ab (Standard 30 Minuten, maximal 4 Stunden). Ein Loop, der mitten im Lauf abstürzt, blockiert das Ticket nicht für immer — Spedy räumt das abgelaufene Lease innerhalb einer Minute ab und hinterlässt eine Notiz am Ticket.
- Nie stehlend. Ein Claim nimmt niemandem etwas weg, und
assign: truesetzt nur einen leeren Bearbeiter.
Ein Lease, das du bereits hältst, erneut zu claimen, verlängert es statt zu scheitern — ein neu gestarteter Loop, der sein eigenes Ticket wieder aufnimmt, ist kein Konflikt.
Agenten haben eine Obergrenze für gleichzeitig gehaltene Tickets (Standard 3, pro Agent einstellbar). Menschen nicht.
7. Am Leben halten
tickets_heartbeat { ticket, extendSeconds? } verlängert das Lease. Ruf es deutlich vor Ablauf auf — etwa alle Drittel der Lease-Länge.
Es kann ein abgelaufenes Lease bewusst nicht wiederbeleben. Zu diesem Zeitpunkt kann das Ticket bereits einem anderen Loop gehören; es stillschweigend zurückzuholen brächte zwei Loops auf ein Ticket. Claim es stattdessen neu.
8. Zurückmelden
tickets_report beendet den Lauf. Ein Aufruf:
| Argument | |
|---|---|
ticket | displayId (AG-12) oder id |
outcome | done · blocked · needs_review · partial |
summary | Was du getan hast, als Fließtext |
changes | Was sich geändert hat, ein Punkt pro Zeile |
openPoints | Was offen ist — das liest ein Mensch zuerst |
prUrl, branch | Wo der Code liegt |
statusId / statusKey | Wohin das Ticket soll |
stopTimer | Deinen laufenden Timer auf diesem Ticket stoppen (Standard true) |
releaseClaim | Ticket wieder freigeben (Standard true) |
isSecret | Als interner Kommentar posten (Standard true — ein Lauf-Report ist Engineering-Detail) |
Es postet einen Kommentar in fester Struktur (Result / Changes / Open / PR), verknüpft einen Spedy bereits bekannten Pull Request mit dem Ticket, verschiebt den Status, stoppt den Timer und gibt den Claim frei.
Es bricht nie halb ausgeführt ab. Der Kommentar wird zuerst geschrieben und ist der einzige Schritt, dessen Fehlschlag den Aufruf abbricht; alles danach ist best effort. Was nicht ging, kommt in issues[] mit Begründung zurück — ein PR, den Spedy noch nicht kennt, ein Statuswechsel, den das Backlog-Gate ablehnt, eine Richtlinie, die diesen Status Menschen vorbehält. Dein Fließtext geht nie verloren, weil ein späterer Schritt nicht geklappt hat.
Der Statuswechsel ist außerdem eine Capability-Frage, nicht nur eine Richtlinien-Frage: tickets_report braucht die Capability tickets:approve — dieselbe, die tickets_move_status kostet —, um ein Ticket zu bewegen. Fehlt sie, passieren Kommentar, PR-Verknüpfung, Timer und Freigabe trotzdem, und allein der Status-Schritt landet in issues[] mit code: "PERMISSION_DENIED". Ein sehr eng geschnittener Agent kann also einwandfrei zurückmelden — er darf nur nicht entscheiden, wohin das Ticket geht.
Richtlinien: was ein Agent nicht allein darf
Einstellungen → Agenten → Agenten-Richtlinien. Sie gelten für alle Agenten der Organisation; Menschen sind nicht betroffen.
Statuswechsel mit Freigabe
Standard: DONE.
Verschiebt ein Agent ein Ticket in einen dieser Status, findet der Wechsel nicht statt. Stattdessen hinterlegt Spedy einen Vorschlag am Ticket, den ein Teammitglied im gewohnten Vorschlags-Banner annimmt. tickets_report weist im issues[] darauf hin (code: "APPROVAL_REQUIRED"), und dieselbe Regel gilt für tickets_move_status.
Es gibt keine zweite Tür. Die schlichte REST-Route ist identisch abgesichert: Ruft ein Agent PATCH /boards/{boardId}/tickets/{ticketId}/status mit einem gesperrten Status auf, bekommt er 403 mit dem Code AGENT_APPROVAL_REQUIRED — und auch dort entsteht der Vorschlag. MCP gegen rohes HTTP zu tauschen bringt einem Agenten nichts.
Ist Explainable Status für das Projekt deaktiviert, lässt sich kein Vorschlag hinterlegen. Der Wechsel wird trotzdem abgelehnt, und die Antwort sagt das ausdrücklich — ein Loop glaubt nie, dass etwas passiert ist, was nicht passiert ist.
Agenten-PRs brauchen ein menschliches Review
Standard: an. Hinterlege die Git-Identität eines Agenten (GitHub-/GitLab-Login oder Commit-E-Mail) unter Einstellungen → Agenten → <Agent>. Pull Requests mit passendem Autor werden in Liste und Detail mit „Menschliches Review nötig" gekennzeichnet.
Das ist rein informativ. Spedy kann ein Merge auf GitHub, GitLab oder Bitbucket nicht verhindern und tut auch nicht so — dafür ist der Branch-Schutz eures Providers da. Das Kennzeichen existiert, damit ein Reviewer beim Überfliegen der Liste sieht, welche PRs von einer Maschine kommen.
Zeit
Zeit, die ein Agent erfasst, ist ein Entwurf: Sie landet mit Status PENDING, skaliert mit dem MCP-Zeitfaktor (organisationsweit, pro Agent überschreibbar), und ein Mensch genehmigt, korrigiert oder verwirft sie unter Zeit → Freigaben, bevor sie abgerechnet wird. Ein Wochenlimit pro Agent stoppt den Timer-Start, sobald es erreicht ist (AGENT_HOURS_CAP_REACHED).
Wie bei der Status-Richtlinie hängt das am Akteur, nicht am Transportweg: Zeitfaktor, Wochenlimit und der PENDING-Status gelten für einen Agenten-User über die REST-API genauso wie über MCP.
Nicht pollen, sondern wecken lassen
Pollen ist die einfache Variante, nicht die gute. Weise ein Ticket dem
Agenten-User zu und abonniere einen ausgehenden Webhook auf ticket.assigned
(Einstellungen → Webhooks): Deine Seite erfährt in dem Moment von der Arbeit, in
dem ein Mensch sie übergibt, und startet genau einen Durchlauf statt jede Minute
aufzuwachen. Der Loop claimt das Ticket trotzdem — die Zuweisung ist das Signal,
der Claim macht die Arbeit exklusiv.
Vier weitere Events ergänzen das Bild, wenn mehrere Loops sich ein Projekt teilen:
| Event | Feuert, wenn |
|---|---|
ticket.assigned | Jemand übergibt ein Ticket an den Agenten — der Weckruf |
ticket.claimed | Ein Claim wurde genommen (von wem auch immer) |
ticket.claim_expired | Ein Claim ist unfreigegeben abgelaufen; das Ticket ist wieder frei |
ticket.reported | Ein Loop hat einen Lauf beendet (outcome, summary, prUrl) |
pr.merged / pipeline.updated | Die Arbeit ist gelandet, oder die CI hat ein Urteil |
Payloads enthalten IDs und Status, nie Inhalte. Vollständige Referenz: Webhooks.
Die menschliche Seite
- Ticket-Detail und Kanban-Karten zeigen, wer den Claim hält und wie lange das Lease noch läuft. Halter, Besitzer des Agenten und Administratoren können ihn freigeben.
- Die Issues-Seitenleiste hat einen Filter Nur claimbar — dieselbe Warteschlange, die dein Loop abfragt.
- Kommentare, Zeiteinträge, Statuswechsel und Claims tragen den Client, über den sie kamen, und Agenten sind überall als Agenten gekennzeichnet.
Zwei Benachrichtigungen, für die niemand ein Dashboard beobachten muss
| Benachrichtigung | Feuert, wenn | Wer sie bekommt |
|---|---|---|
TICKET_CLAIM_EXPIRED | Ein Claim ist unfreigegeben abgelaufen und wurde abgeräumt — das Ticket ist wieder frei, und vermutlich ist etwas gestorben | Verantwortliche(r) des Tickets und Besitzer des Agenten |
AGENT_RUN_REPORTED | Ein Lauf meldet blocked oder needs_review — die beiden Ergebnisse, die ausdrücklich nach einem Menschen fragen | Verantwortliche(r) des Tickets und Besitzer des Agenten |
done und partial benachrichtigen bewusst nicht: Ein Loop, der funktioniert, ist keine Nachricht. Diese beiden sind es.
Lieferweg (pro Ticket)
Im Tab Aktivität eines Tickets sitzt der aufklappbare Lieferweg: eine
geordnete Liste, wie genau dieses Ticket geliefert wurde — genommene,
zurückgegebene und abgelaufene Claims, die Rückmeldung samt Ergebnis, der erste
Agenten- und der erste Menschen-Kommentar, Statuswechsel, der Branch, der Pull
Request mit Reviews und Merge, Pipeline-Läufe und jede gebuchte Minute
(Entwürfe inklusive, als Entwurf gekennzeichnet). Jede Zeile nennt den Akteur,
ob es ein Agent war, und über welchen Client es kam — ein Claim hält den
MCP-Client fest, über den er genommen wurde, also liest sich die Claim-Zeile
„über claude-code" statt bloß die Maschine zu nennen.
Was gar nicht aufgezeichnet wird, steht als gaps dabei, statt eine stille
Lücke zu hinterlassen — man muss „kein Agent war hier" von „wir haben es nie
mitgeschrieben" unterscheiden können. Kunden sehen den Lieferweg nicht; für sie
gibt es die gröbere Ansicht Zeiten & Budget.
Dein Loop liest dieselbe Geschichte mit tickets_delivery_trace, bevor er
entscheidet, ob er überhaupt anfängt.
Agenten-Übersicht (pro Organisation)
Einstellungen → Agenten → Übersicht beantwortet die Betriebsfrage. Pro Agent: zuletzt gesehen, offene Claims samt Ablauf, in den letzten 7 Tagen abgelaufene Claims, Rückmeldungen der letzten 7 und 30 Tage nach Ergebnis, Zeit-Entwürfe, die auf Freigabe warten, freigegebene Minuten dieses Monats und Pull Requests inklusive „Review nötig"-Zähler. Dazu die Summen der Organisation: Agentenanteil an den erledigten Tickets und an den gebuchten Stunden dieses Monats.
Ein Anteil ohne Bezugsgröße kommt leer zurück, nicht als 0 %. Erfordert
agents:manage — die Ansicht enthält organisationsweite Zahlen.
Tool-Referenz
| Tool | Capability | Zweck |
|---|---|---|
tickets_claim | tickets:update | Exklusives, ablaufendes Lease nehmen (oder verlängern) |
tickets_heartbeat | tickets:update | Lease verlängern; belebt kein abgelaufenes wieder |
tickets_release | tickets:update | Ticket zurückgeben |
tickets_report | tickets:update + comments:create | Kommentar, PR verknüpfen, Status, Timer, Freigabe |
tickets_list / tickets_search | tickets:read | Filter claimable / assignedToMe / claimedBy |
work-context_get | tickets:read | Was Aufmerksamkeit braucht, plus „meine Claims" |
tickets_delivery_trace | tickets:read | Was auf diesem Ticket schon passiert ist, in Reihenfolge |
users_me | — | Akteur, Besitzer, Token-Scope, Limits, gehaltene Claims |
REST-Entsprechungen
Alles oben gibt es auch als schlichtes HTTP — für einen Loop, der lieber kein MCP spricht:
POST /api/v1/boards/{boardId}/tickets/{ticketId}/claim
POST /api/v1/boards/{boardId}/tickets/{ticketId}/claim/heartbeat
DELETE /api/v1/boards/{boardId}/tickets/{ticketId}/claim
GET /api/v1/issues?claimable=true
GET /api/v1/issues?claimable=true&assignedToMe=true
GET /api/v1/issues?claimedBy=me
GET /api/v1/boards/{boardId}/tickets/{ticketId}/delivery-trace
GET /api/v1/organizations/{orgId}/agents/overviewJede einzelne davon akzeptiert das Zugriffstoken eines Agenten — dafür gibt es die REST-Oberfläche. Ein verlorenes Rennen antwortet mit 409, Fehlercode TICKET_ALREADY_CLAIMED und Halter samt Ablauf in details.
Zwei Regeln lohnen die ausdrückliche Erwähnung, weil REST hier von dem abweicht, was die HTTP-Methode nahelegt:
DELETE …/claimmit einemdenyDelete-Token. Agenten-Tokens sperren Löschungen standardmäßig, und dies ist einDELETE. Den eigenen Claim freizugeben funktioniert trotzdem — ein Loop muss sein eigenes Ticket immer zurückgeben können. Den Claim eines fremden Akteurs mit so einem Token freizugeben, wird mit403 PAT_DENY_DELETEabgelehnt; einem anderen Loop ein Ticket wegzunehmen ist ein administrativer Akt.PATCH …/statusist keine Abkürzung an der Richtlinie vorbei. Ein Agent, der ein Ticket in einen Menschen vorbehaltenen Status bewegt, bekommt403 AGENT_APPROVAL_REQUIREDund einen Vorschlag — genau wie über MCP.
Vollständige Referenz: Agenten · Ticket-Claims · Delivery-Spur · Authentifizierung.
Referenz-Loop
Wer lieber Code als Prosa liest: Der Referenz-Loop ist das komplette Pull-Modell in einer Datei — pollen, claimen, Heartbeat, Befehl ausführen, zurückmelden, freigeben — rund 200 Zeilen, nichts hinter einem SDK versteckt.