SpedySpedy Docs

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 Aufruf

1. 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:

OptionWirkung
allowedBoardIdsBegrenzt das Token auf bestimmte Projekte — unabhängig von den Mitgliedschaften des Agenten. Leer = alle Projekte, in denen der Agent Mitglied ist.
readOnlyNur lesende Tools. Keine Schreibzugriffe.
denyDeleteSperrt jedes destruktive Tool. Für Agenten-Tokens standardmäßig an.
clientLabelEin 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.

FilterVerengt auf
claimable: trueAlles, was dieser Loop gerade nehmen darf
assignedToMe: trueVerengt 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: true setzt 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
ticketdisplayId (AG-12) oder id
outcomedone · blocked · needs_review · partial
summaryWas du getan hast, als Fließtext
changesWas sich geändert hat, ein Punkt pro Zeile
openPointsWas offen ist — das liest ein Mensch zuerst
prUrl, branchWo der Code liegt
statusId / statusKeyWohin das Ticket soll
stopTimerDeinen laufenden Timer auf diesem Ticket stoppen (Standard true)
releaseClaimTicket wieder freigeben (Standard true)
isSecretAls 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:

EventFeuert, wenn
ticket.assignedJemand übergibt ein Ticket an den Agenten — der Weckruf
ticket.claimedEin Claim wurde genommen (von wem auch immer)
ticket.claim_expiredEin Claim ist unfreigegeben abgelaufen; das Ticket ist wieder frei
ticket.reportedEin Loop hat einen Lauf beendet (outcome, summary, prUrl)
pr.merged / pipeline.updatedDie 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

BenachrichtigungFeuert, wennWer sie bekommt
TICKET_CLAIM_EXPIREDEin Claim ist unfreigegeben abgelaufen und wurde abgeräumt — das Ticket ist wieder frei, und vermutlich ist etwas gestorbenVerantwortliche(r) des Tickets und Besitzer des Agenten
AGENT_RUN_REPORTEDEin Lauf meldet blocked oder needs_review — die beiden Ergebnisse, die ausdrücklich nach einem Menschen fragenVerantwortliche(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

ToolCapabilityZweck
tickets_claimtickets:updateExklusives, ablaufendes Lease nehmen (oder verlängern)
tickets_heartbeattickets:updateLease verlängern; belebt kein abgelaufenes wieder
tickets_releasetickets:updateTicket zurückgeben
tickets_reporttickets:update + comments:createKommentar, PR verknüpfen, Status, Timer, Freigabe
tickets_list / tickets_searchtickets:readFilter claimable / assignedToMe / claimedBy
work-context_gettickets:readWas Aufmerksamkeit braucht, plus „meine Claims"
tickets_delivery_tracetickets:readWas auf diesem Ticket schon passiert ist, in Reihenfolge
users_meAkteur, 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/overview

Jede 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 …/claim mit einem denyDelete-Token. Agenten-Tokens sperren Löschungen standardmäßig, und dies ist ein DELETE. 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 mit 403 PAT_DENY_DELETE abgelehnt; einem anderen Loop ein Ticket wegzunehmen ist ein administrativer Akt.
  • PATCH …/status ist keine Abkürzung an der Richtlinie vorbei. Ein Agent, der ein Ticket in einen Menschen vorbehaltenen Status bewegt, bekommt 403 AGENT_APPROVAL_REQUIRED und 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.