# Why osFoundry makes Claude Code name the commit when it asks to rerun a GitNest check

> Source: <https://dev.to/bbt_c84feb0f535/why-osfoundry-makes-claude-code-name-the-commit-when-it-asks-to-rerun-a-gitnest-check-36e1>
> Published: 2026-11-11 10:11:57+00:00

This is the tool Claude Code calls when it wants a failed GitNest check run again, as [osFoundry](https://osfoundry.io)'s live MCP endpoint lists it in `tools/list` (no token needed for the catalogue):

```
{
  "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"]}]
}
```

Its read-side partner takes two arguments: `list_gitnest_checks {repository_id, change_set_id}`, "bounded check status for the current composed commit of a change set". Two more reads feed the decision: `list_gitnest_saved_results` (limit 1–50) and `read_gitnest_work` (`kind` is `tasks`, `changes` or `runs`). All three are `osfoundry:read` with `readOnlyHint: true`.

Six required fields to rerun one check is a lot. A CI button needs a job ID. The extra two, a version and a commit, are what this post is about. No `claude` binary is installed on this Mac, so `checks_sim.py` plays the client. The code and its full output are in `code/`.

Before an agent proposes anything, it has to read the check list correctly. osFoundry's GitNest UI summarises a commit's check runs with one small pure function (`summarise` in `check-runs.ts`). Its order of tests is the rule:

`none`, never `passed`.` failure`, `timed_out` or `cancelled` is `failed`.` success` keeps it `running`, including a status the client doesn't recognise.
The script ports that rule and compares it with two shortcuts a model writes on its own:

```
== 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
```

"Nothing failed, so it's green" is wrong in 5 of 7 cases. It calls an unexamined commit green, a queued check green and a timeout green. The ratio check (`passed == total`) is closer, but on zero checks 0 == 0 and it says green too. That's the case the GitNest code comments call the most misleading one, since a commit with no checks hasn't been verified at all. The UI renders it as "No checks have run against this commit", with no tick.

If you prompt Claude Code to answer "is it green?", give it these four rules in `CLAUDE.md`. The tool returns statuses, not a verdict.

Because the person approving it is looking at a specific commit, and the change set can move before they click.

A change set in GitNest is composed from saved results into one commit. Recompose it, after a new saved result or a moved base, and it gets a new version and a new composed commit, and its checks start over. A rerun proposal waits for a human. If it only said "rerun e2e on cs_4e81a0", approval would rerun e2e on whatever the change set is by then, which may be a commit the owner never saw.

`checks_sim.py` models that wait. Each trial is one proposal. The agent reads version 3, commit A, with e2e failed. The owner's review time is log-normal with a median of 8 minutes. That's an assumption, not a measurement. During the wait, the change set is recomposed as a Poisson process, on average every 45 minutes, and after a recompose the checks take 4 minutes to run. In 10% of trials a second Claude Code session read the same failure and filed its own proposal with its own key, and the owner approves both. Three approval-time guards, 10,000 trials each:

```
== 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
```

With no guard, 1,411 approvals reran a check on a commit the owner hadn't reviewed, and 1,764 queued a check that was already queued or running. Comparing version and commit at approval drops the unreviewed runs to 0 and refuses 2,435 stale cards instead. The 740 duplicates left are the second sessions: same version, same commit, but the first approval already requeued the check. The third guard, "is this check still finished?", catches those. That's the full set the tool description lists: "Approval rechecks repository access, version, commit and check state."

The recompose rate is the input I'm least sure of, so here's the no-guard row at three rates:

```
== 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
```

Even a change set that moves every three hours gives 452 wrong-commit runs per 10,000. The guard costs nothing on the happy path: the reviewed-commit count is 7,789 in all three rows.

In this model they always move together, so either alone would score the same. They answer different questions, though. The version is the change set's own counter, and it moves when its contents change. The commit is what the checks actually ran on, and it's what the card prints as "Reviewed commit: …". Asking the client to send both means the server can refuse on either, and the owner reads a commit hash on the card, not an internal counter. The schema enforces the version as an integer ≥ 1. The commit is a free string, so its check happens on the server.

The script validates arguments against the live `inputSchema` with `jsonschema`:

```
== 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
```

Two rejections are design, not hygiene. `check_name` is a string, not a list, so "rerun everything" is one card per check and the owner decides each one. And `additionalProperties: false` means a model that invents `"force": true` gets an error instead of a silently ignored flag.

The idempotency key should come from what the proposal is about, not from the attempt. The script uses `"rerun-" + sha256(change_set_id, version, oid, check_name)[:32]`:

```
key retry same: True   key after recompose same: False   len 38
```

A retry after a lost response sends the same key and doesn't make a second card. A recompose changes the key, so a proposal for the new commit is a new card, which is right.

```
== 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
```

The 13 GitNest tools are about a quarter of the catalogue's tokens (counted with tiktoken `cl100k_base`, so treat them as approximate). Ten unauthenticated calls each from this Mac in Japan to the live endpoint:

``` bash
$ 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
```

The 401 carries `error_description="Connect osFoundry to continue"` and a `resource_metadata` URL, which is what sends Claude Code into its OAuth flow. `initialize` advertises no push, so nothing tells the client the retry was approved or that the check finished. Waiting for a check is a runner's work on your machine plus a human's click, minutes rather than milliseconds. The cheap pattern is to end the turn after proposing and call `list_gitnest_checks` once when the user says they've approved.

Not osFoundry. GitNest's runner panel says checks "run on your machine, not ours". A registered runner polls with its own token (`OSF_RUNNER_TOKEN=… osfoundry-runner --api https://api.osfoundry.io`), and without one checks "stay queued". Approving a retry only requeues a check. Claude Code never holds the runner token, and the read tools leave out local bindings and credentials. The agent can see that e2e failed. It can't make anything execute without you and your runner.

The change-set review page with the checks panel is on the [GitNest page](https://osfoundry.io/platform/gitnest). For the protocol itself, see the [MCP glossary entry](https://osfoundry.io/glossary/mcp).
