Das ist das Tool, das Claude Code aufruft, wenn ein fehlgeschlagener GitNest-Check noch einmal laufen soll, so wie der Live-MCP-Endpoint von osFoundry es in tools/list auflistet (für den Katalog braucht man kein Token):
{ "name": "propose_gitnest_check_rerun", "description": "Ask the repository owner to retry one finished check on a specific composed change-set version. Approval rechecks repository access, version, commit and check state.", "inputSchema": { "type": "object", "properties": { "repository_id": {"type": "string"}, "change_set_id": {"type": "string"}, "check_name": {"type": "string", "maxLength": 200}, "expected_change_set_version": {"type": "integer", "minimum": 1}, "expected_composition_oid": {"type": "string"}, "idempotency_key": {"type": "string", "minLength": 8, "maxLength": 128} }, "required": ["repository_id", "change_set_id", "check_name", "expected_change_set_version", "expected_composition_oid", "idempotency_key"], "additionalProperties": false }, "annotations": {"readOnlyHint": false, "openWorldHint": false, "destructiveHint": true}, "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}] } Das lesende Gegenstück nimmt zwei Argumente: list_gitnest_checks {repository_id, change_set_id}, "bounded check status for the current composed commit of a change set". Zwei weitere Lese-Tools liefern die Grundlage für die Entscheidung: list_gitnest_saved_results (limit 1–50) und read_gitnest_work (kind ist tasks, changes oder runs). Alle drei sind osfoundry:read mit readOnlyHint: true.
Sechs Pflichtfelder, um einen einzigen Check neu zu starten, sind viel. Ein CI-Button braucht eine Job-ID. Um die zwei zusätzlichen, eine Version und einen Commit, geht es in diesem Post. Auf diesem Mac ist kein claude-Binary installiert, deshalb spielt checks_sim.py den Client. Code und vollständige Ausgabe liegen in code/.
Was heißt "grün", wenn ein Agent Checks liest?
Bevor ein Agent irgendetwas vorschlägt, muss er die Check-Liste richtig lesen. Die GitNest-UI von osFoundry fasst die Check-Runs eines Commits mit einer kleinen reinen Funktion zusammen (summarise in check-runs.ts). Die Reihenfolge ihrer Prüfungen ist die Regel:
- Gar keine Runs ist
none, niemalspassed. - Jedes
failure,timed_outodercancelledistfailed. - Alles, was noch nicht
successist, hält den Zustand aufrunning, auch ein Status, den der Client nicht kennt. - Erst dann ist es
passed.
Das Skript portiert diese Regel und vergleicht sie mit zwei Abkürzungen, die ein Modell von sich aus schreibt:
== verdict == case summarise no-failure ratio no checks none green green all passed passed 2 green green one queued running 1/2 green red one failed failed: e2e red red one timed out failed: e2e green red one cancelled failed: e2e green red unknown status running 1/2 green red shortcut wrong: no-failure 5/7, ratio 1/7 "Nichts ist fehlgeschlagen, also grün" liegt in 5 von 7 Fällen falsch. Es nennt einen ungeprüften Commit grün, einen Check in der Warteschlange grün und einen Timeout grün. Die Quoten-Prüfung (passed == total) kommt näher heran, aber bei null Checks ist 0 == 0, und sie sagt ebenfalls grün. Genau diesen Fall nennen die Kommentare im GitNest-Code den irreführendsten, denn ein Commit ohne Checks ist überhaupt nicht verifiziert. Die UI zeigt ihn als "No checks have run against this commit" an, ohne Haken.
Wenn Claude Code die Frage "ist es grün?" beantworten soll, gib ihm diese vier Regeln in CLAUDE.md mit. Das Tool liefert Status, kein Urteil.
Warum trägt ein Rerun eine Version und einen Commit?
Weil die Person, die freigibt, auf einen bestimmten Commit schaut, und sich das Change Set bewegen kann, bevor sie klickt.
Ein Change Set wird in GitNest aus gespeicherten Ergebnissen zu einem einzigen Commit zusammengesetzt. Wird es neu zusammengesetzt, nach einem neuen gespeicherten Ergebnis oder einer verschobenen Basis, bekommt es eine neue Version und einen neuen zusammengesetzten Commit, und seine Checks beginnen von vorn. Ein Rerun-Vorschlag wartet auf einen Menschen. Würde er nur "rerun e2e on cs_4e81a0" sagen, würde die Freigabe e2e auf dem laufen lassen, was das Change Set bis dahin ist, womöglich ein Commit, den der Owner nie gesehen hat.
checks_sim.py modelliert dieses Warten. Jeder Durchlauf ist ein Vorschlag. Der Agent liest Version 3, Commit A, mit fehlgeschlagenem e2e. Die Review-Zeit des Owners ist log-normalverteilt mit einem Median von 8 Minuten. Das ist eine Annahme, keine Messung. Während des Wartens wird das Change Set als Poisson-Prozess neu zusammengesetzt, im Schnitt alle 45 Minuten, und danach brauchen die Checks 4 Minuten. In 10 % der Durchläufe hat eine zweite Claude-Code-Session denselben Fehler gelesen und einen eigenen Vorschlag mit eigenem Key eingereicht, und der Owner gibt beide frei. Drei Prüfungen zum Freigabezeitpunkt, je 10.000 Durchläufe:
== approval-time guards (n=10000, review median 8 min, recompose mean every 45 min, 10% second session) == guard ran on reviewed commit ran on unreviewed commit duplicate run refused (stale) refused (not finished) none 7789 1411 1764 0 0 version+oid 7789 0 740 2435 0 full 7789 0 0 2435 740 Ohne Prüfung haben 1.411 Freigaben einen Check auf einem Commit neu gestartet, den der Owner nicht reviewt hatte, und 1.764 haben einen Check eingereiht, der schon in der Warteschlange stand oder lief. Ein Abgleich von Version und Commit bei der Freigabe drückt die ungeprüften Runs auf 0 und lehnt stattdessen 2.435 veraltete Karten ab. Die 740 verbleibenden Duplikate sind die zweiten Sessions: gleiche Version, gleicher Commit, aber die erste Freigabe hatte den Check schon wieder eingereiht. Die dritte Prüfung, "ist dieser Check noch abgeschlossen?", fängt sie ab. Das ist genau der Satz, den die Tool-Beschreibung aufzählt: "Approval rechecks repository access, version, commit and check state."
Die Recompose-Rate ist der unsicherste Eingabewert, deshalb hier die Zeile ohne Prüfung bei drei Raten:
== sensitivity: unreviewed-commit runs with guard 'none' == recompose every 15 min unreviewed 2449 duplicates 3128 recompose every 45 min unreviewed 1411 duplicates 1764 recompose every 180 min unreviewed 452 duplicates 1208 Selbst ein Change Set, das sich alle drei Stunden bewegt, ergibt 452 Runs auf dem falschen Commit pro 10.000. Auf dem Happy Path kostet die Prüfung nichts: Die Zahl auf dem reviewten Commit ist in allen drei Zeilen 7.789.
Warum Version und Commit zusammen?
In diesem Modell bewegen sie sich immer gemeinsam, also würde jeder Wert allein gleich abschneiden. Sie beantworten aber unterschiedliche Fragen. Die Version ist der eigene Zähler des Change Sets und ändert sich, wenn sich sein Inhalt ändert. Der Commit ist das, worauf die Checks tatsächlich gelaufen sind, und genau das zeigt die Karte als "Reviewed commit: …". Wenn der Client beides schicken muss, kann der Server bei jedem der beiden ablehnen, und der Owner liest auf der Karte einen Commit-Hash statt eines internen Zählers. Das Schema erzwingt die Version als Integer ≥ 1. Der Commit ist ein freier String, seine Prüfung passiert also auf dem Server.
Was fängt das Schema schon vor dem Aufruf ab?
Das Skript validiert Argumente mit jsonschema gegen das Live-inputSchema:
== propose_gitnest_check_rerun arguments == ok PASS no oid REJECT 'expected_composition_oid' is a required property version 0 REJECT 0 is less than the minimum of 1 version as string REJECT '3' is not of type 'integer' two checks REJECT ['e2e', 'lint'] is not of type 'string' check 201 chars REJECT 'ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc key 7 chars REJECT 'abcdefg' is too short extra 'force' REJECT Additional properties are not allowed ('force' was unexpected) required: repository_id, change_set_id, check_name, expected_change_set_version, expected_composition_oid, idempotency_key Zwei der Ablehnungen sind Design, keine Hygiene. check_name ist ein String, keine Liste, also bedeutet "alles neu starten" eine Karte pro Check, und der Owner entscheidet jede einzeln. Und additionalProperties: false sorgt dafür, dass ein Modell, das sich "force": true ausdenkt, einen Fehler bekommt statt eines stillschweigend ignorierten Flags.
Der Idempotency-Key sollte sich daraus ableiten, worum es im Vorschlag geht, nicht aus dem Versuch. Das Skript nutzt "rerun-" + sha256(change_set_id, version, oid, check_name)[:32]:
key retry same: True key after recompose same: False len 38 Ein Retry nach einer verlorenen Antwort schickt denselben Key und erzeugt keine zweite Karte. Ein Recompose ändert den Key, also ist ein Vorschlag für den neuen Commit eine neue Karte, und das ist richtig so.
Wie schwer sind diese Tools und wie weit weg ist der Server?
== tools/list == 59 tools, 48140 B via json.dumps, 12722 tok (cl100k) list_gitnest_checks 612 B 158 tok readOnly=True destructive=False scope=osfoundry:read list_gitnest_saved_results 707 B 182 tok readOnly=True destructive=False scope=osfoundry:read read_gitnest_work 685 B 189 tok readOnly=True destructive=False scope=osfoundry:read propose_gitnest_check_rerun 1011 B 268 tok readOnly=False destructive=True scope=osfoundry:write all 13 gitnest tools 11227 B 3009 tok Die 13 GitNest-Tools machen etwa ein Viertel der Tokens im Katalog aus (gezählt mit tiktoken cl100k_base, also nur als Näherung zu verstehen). Je zehn nicht authentifizierte Aufrufe von diesem Mac in Japan an den Live-Endpoint:
$ python3 probe.py initialize HTTP 200 308 B median 566 ms min 326 max 807 capabilities: {"tools": {"listChanged": false}} tools/list HTTP 200 45368 B median 722 ms min 420 max 1406 tools/call list_gitnest_checks HTTP 401 337 B median 552 ms min 409 max 903 Der 401 enthält error_description="Connect osFoundry to continue" und eine resource_metadata-URL, und genau das schickt Claude Code in seinen OAuth-Flow. initialize kündigt kein Push an, also teilt dem Client nichts mit, dass der Retry freigegeben wurde oder der Check fertig ist. Auf einen Check zu warten heißt: Arbeit eines Runners auf deinem Rechner plus ein Klick eines Menschen, Minuten statt Millisekunden. Das günstige Muster ist, den Turn nach dem Vorschlag zu beenden und list_gitnest_checks einmal aufzurufen, wenn der Nutzer sagt, dass er freigegeben hat.
Wer führt den Check eigentlich aus?
Nicht osFoundry. Das Runner-Panel von GitNest sagt, Checks "run on your machine, not ours". Ein registrierter Runner pollt mit seinem eigenen Token (OSF_RUNNER_TOKEN=… osfoundry-runner --api https://api.osfoundry.io), und ohne ihn bleiben Checks "stay queued". Einen Retry freizugeben reiht einen Check nur wieder ein. Claude Code hält nie das Runner-Token, und die Lese-Tools lassen lokale Bindings und Credentials weg. Der Agent sieht, dass e2e fehlgeschlagen ist. Ausführen lassen kann er nichts, ohne dich und deinen Runner.
Wenn du das selbst verdrahtest
- Schreib die Regel für das Urteil in die Instruktionen des Clients, nicht nur in deine UI. "Keine Checks" und "unbekannter Status" sind genau die Stellen, an denen Agenten grün melden.
- Lass einen Rerun den Commit nennen, gegen den reviewt wurde, und vergleiche zum Freigabezeitpunkt, nicht beim Vorschlag.
- Prüf bei der Freigabe auch den Zustand des Checks selbst. Version und Commit erkennen keine zwei Vorschläge für denselben Fehler.
- Nimm einen Check pro Vorschlag. Eine Liste macht aus dem Retry eines flaky Tests einen pauschalen Rerun, den niemand gewählt hat.
- Leite den Idempotency-Key aus (Change Set, Version, Commit, Check) ab, damit Retries zusammenfallen und Recomposes nicht.
- Halte die Ausführung von der Plattform und vom Agenten fern: Der Runner hält das Token, der Mensch den Button.
Die Review-Seite eines Change Sets mit dem Check-Panel findest du auf der GitNest-Seite. Zum Protokoll selbst siehe den Glossar-Eintrag zu MCP.
