Browser-Spiel Battle Drones: 98 Prozent des Codes stammen von Claude
Simon Willison hat ein Command-&-Conquer-artiges Strategiespiel über 12 Versionen und 321 Commits grösstenteils mit Claude Code entwickelt und gibt offen zu, fast keinen Code selbst geprüft zu haben. Das öffentliche Repository gilt als interessantes Beispiel für KI-geschriebene Software.

Am 20. Oktober veröffentlichte Simon Willison Battle Drones, ein browserbasiertes RTS im Stil von Command & Conquer. Er baute es über 12 größere Versionen und 321 Commits hinweg auf, überwiegend mit Claude Code, und sagt offen dazu: „Ich habe fast keinen einzigen Teil des Codes reviewt." Das Repository ist öffentlich, was daraus den besten offenen Datensatz macht, den ich für eine Frage kenne, die sich jedes Team stellen sollte, das Coding-Agents einsetzt: Wie viel unseres Codes hat der Agent geschrieben, und was ist danach mit diesem Code passiert?
Daher habe ich ein kleines, ausschließlich auf der Python-Standardbibliothek basierendes Skript geschrieben, das diese Frage aus dem git log beantwortet, das Repository geklont und das Skript darauf ausgeführt. Im Folgenden findet man das Skript, die tatsächliche Ausgabe und meine Einschätzung, was das für Teams bedeutet.
Mein Team ist klein, und Agents schreiben inzwischen einen echten Anteil unserer Commits. „Ist der Agent produktiv?" ist die falsche Frage. Nützlich sind die operativen Fragen:
Nichts davon benötigt ein Vendor-Dashboard. Agents hinterlassen bereits Spuren in git.
Claude Code fügt einen Co-Authored-By: Claude... <noreply@anthropic.com> Trailer hinzu, und Claude Code im Web ergänzt zudem eine Claude-Session: Zeile mit einer Session-URL. Copilots Coding-Agent committet als copilot-swe-agent[bot] oder über eine +Copilot@users.noreply.github.com Adresse. Cursor, Codex, Devin und Aider haben ihre eigenen Muster. Das Skript prüft je Agent einen regulären Ausdruck gegen Author, Committer und die vollständige Commit-Message. Alles andere zählt als menschlich. Merge-Commits und CI-Bots werden übersprungen.
Anschließend berechnet es in einem einzigen Durchlauf über die gesamte Patch-Historie (git log -p mit null Kontextzeilen), ältester Commit zuerst:
#!/usr/bin/env python3 """ agentaudit.py: how much of a git repo did coding agents write? Usage: python3 agentaudit.py [REPO] [--days 14] [--top 8] (stdlib only) """ import argparse, collections, datetime as dt, re, subprocess # One regex per agent, matched against "author / committer / full message". AGENTS = { " Claude ": r " noreply@anthropic\.com|co-authored-by:[^\n]*claude|claude-session: " r " |claude\.ai/code/session|generated with \[claude code\] ", " Codex ": r " co-authored-by:[^\n]*codex|chatgpt-codex-connector|codex@openai\.com ", " Copilot ": r " copilot-swe-agent|\+copilot@users\.noreply|co-authored-by:[^\n]*copilot ", " Cursor ": r " cursoragent@cursor\.com|co-authored-by:[^\n]*cursor ", " Devin ": r " devin-ai-integration ", " Aider ": r " \(aider\)|co-authored-by:[^\n]*aider ", } AGENTS = { k: re. compile ( v, re.
I | re. M ) for k, v in AGENTS. items ()} BOT = re. compile ( r " \[bot\]|actions@users\.noreply|github-actions ", re. I ) # CI, not agents FIX = re. compile ( r " ^(revert|fixup!|hotfix)|\b(fix(es|ed)?|bug|regression|broke|broken|oops)\b ", re.
I ) TRAILER = re. compile ( r " ^co-authored-by:\s*([^<\n]+?)\s*< ", re. I | re. M ) # Generated files inflate line counts; skip them (override with --skip).
SKIP = r " (^|/)(dist|build|vendor|node_modules|docs/assets)/|\.lock$|-lock\.json$|\.min\.|\.map$ " FMT = " %x00%H%x1f%an <%ae>%x1f%cn <%ce>%x1f%at%x1f%P%x1f%B%x1e " def who ( author, committer, msg ): text = f " { author } \n { committer } \n { msg } " return next (( name for name, rx in AGENTS. items () if rx. search ( text )), None ) def audit ( repo, days, skip_rx ): skip_rx = re. compile ( skip_rx ) out = subprocess. run ([ " git ", " -C ", repo, " log ", " --reverse ", " -p ", " -M ", " --unified=0 ", " --no-color ", " --format= " + FMT ], capture_output = True, text = True, errors = " replace ", check = True ). stdout window = days * 86400 s = collections. defaultdict ( collections. Counter ) # stat name -> {"agent"/"human": n} per_agent, models = collections. Counter (), collections.
Counter () files, day = collections. defaultdict ( collections. Counter ), collections. defaultdict ( collections. Counter ) added = collections. defaultdict ( lambda: collections. defaultdict ( collections. deque )) last_touch = {} # path -> (ts, cls) for chunk in out. split ( " \x00 " )[ 1:]: head, patch = chunk. split ( " \x1e ", 1 ) sha, author, committer, ts, parents, msg = head. split ( " \x1f " ) ts = int ( ts ) if len ( parents. split ()) > 1: s [ " merges " ][ " all " ] += 1 continue agent = who ( author, committer, msg ) if not agent and BOT. search ( author ): s [ " merges " ][ " bots " ] += 1 continue cls = " agent " if agent else " human " if agent: per_agent [ agent ] += 1 models. update ( t for t in TRAILER. findall ( msg ) if AGENTS [ agent ]. search ( f " co-authored-by: { t } " )) s [ " commits " ][ cls ] += 1 if not agent and re. search ( r " \(#\d+\)$ ", msg. split ( " \n ", 1 )[ 0 ]. strip ()): s [ " squash " ][ " human " ] += 1 # squash-merged PR: author unknown d = dt. datetime. fromtimestamp ( ts, dt. timezone. utc ). strftime ( " %Y-%m-%d " ) day [ d ][ cls ] += 1 path, in_hunk, skip, touched = None, False, False, set () for line in patch. split ( " \n " ): if line. startswith ( " diff --git " ): m = re. match ( r " diff --git a/(.*) b/(.*) ", line ) old, path = ( m. group ( 1 ), m. group ( 2 )) if m else ( None, line ) if old and old!= path and old in added: # follow renames added [ path ] = added. pop ( old ) skip = bool ( skip_rx. search ( path )) if not skip: touched. add ( path ) in_hunk = False elif line. startswith ( " @@ " ): in_hunk = not skip elif in_hunk and line [: 1 ] in ( " + ", " - " ): content = line [ 1:]. strip () s [ " lines " ][ cls ] += 1 files [ path ][ cls ] += 1 day [ d ][ cls + " _lines " ] += 1 if line [ 0 ] == " + ": s [ " added " ][ cls ] += 1 if len ( content ) > 2: # skip "}", "", ");" added [ path ][ content ]. append (( ts, cls )) elif added [ path ][ content ]: t0, c0 = added [ path ][ content ]. popleft () # oldest matching add if ts - t0 <= window: s [ " churn " ][ c0 ] += 1 if FIX. search ( msg. split ( " \n ", 1 )[ 0 ]): s [ " fixes " ][ cls ] += 1 hit = { last_touch [ p ][ 1 ] for p in touched if p in last_touch and ts - last_touch [ p ][ 0 ] <= window } s [ " fix_after " ]. update ( hit ) for p in touched: last_touch [ p ] = ( ts, cls ) return s, per_agent, models, files, day def pct ( a, b ): return f " { 100 * a / b: 5.1 f } % " if b else " n/a " def report ( repo, days, top, skip_rx ): s, per_agent, models, files, day = audit ( repo, days, skip_rx ) c, l = s [ " commits " ], s [ " lines " ] nc, nl = sum ( c. values ()), sum ( l. values ()) print ( f " repo: { repo } window: { days } days merges skipped: { s [ ' merges ' ][ ' all ' ] } bot commits skipped: { s [ ' merges ' ][ ' bots ' ] } " ) print ( f " commits agent { c [ ' agent ' ]: > 6 } { pct ( c [ ' agent ' ], nc ) } human { c [ ' human ' ]: > 6 } { pct ( c [ ' human ' ], nc ) } " ) print ( f " human commits that look like squash-merged PRs (true author unknown): " f " { s [ ' squash ' ][ ' human ' ] } " ) print ( f " lines agent { l [ ' agent ' ]: > 6 } { pct ( l [ ' agent ' ], nl ) } human { l [ ' human ' ]: > 6 } { pct ( l [ ' human ' ], nl ) } " ) print ( " agents " + ", ". join ( f " { k } { v } " for k, v in per_agent. most_common ())) print ( " models " + ", ". join ( f " { k } { v } " for k, v in models. most_common ( 5 ))) for cls in ( " agent ", " human " ): print ( f " churn { cls }: { s [ ' churn ' ][ cls ] } of { s [ ' added ' ][ cls ] } added lines deleted within " f " { days } d ( { pct ( s [ ' churn ' ][ cls ], s [ ' added ' ][ cls ]). strip () } ) " ) print ( f " fixes fix/revert commits: { sum ( s [ ' fixes ' ]. values ()) } (by agent { s [ ' fixes ' ][ ' agent ' ] }, " f " by human { s [ ' fixes ' ][ ' human ' ] } ) " ) for cls in ( " agent ", " human " ): print ( f " touching files last changed by { cls } work <= { days } d earlier: " f " { s [ ' fix_after ' ][ cls ] } ( { s [ ' fix_after ' ][ cls ] / max ( c [ cls ], 1 ) * 100:.
1 f } per 100 { cls } commits) " ) print ( f " \n top { top } files by agent-changed lines: " ) for p, n in sorted ( files. items (), key = lambda kv: - kv [ 1 ][ " agent " ])[: top ]: print ( f " { n [ ' agent ' ]: > 6 } { pct ( n [ ' agent ' ], n [ ' agent ' ] + n [ ' human ' ]) } agent { p } " ) print ( " \n day commits A/H lines A/H agent share of lines " ) for d in sorted ( day ): n = day [ d ] share = n [ " agent_lines " ] / max ( n [ " agent_lines " ] + n [ " human_lines " ], 1 ) print ( f " { d } { n [ ' agent ' ]: > 4 } / { n [ ' human ' ]: < 4 } { n [ ' agent_lines ' ]: > 7 } / { n [ ' human_lines ' ]: < 6 } " f " { ' # ' * round ( share * 20 ): < 20 } " ) if __name__ == " __main__ ": ap = argparse. ArgumentParser ( description = __doc__ ) ap. add_argument ( " repo ", nargs = "? ", default = ".
" ) ap. add_argument ( " --days ", type = int, default = 14 ) ap. add_argument ( " --top ", type = int, default = 8 ) ap. add_argument ( " --skip ", default = SKIP, help = " regex of paths to ignore " ) a = ap. parse_args () report ( a. repo, a. days, a. top, a. skip ) Enter fullscreen mode Exit fullscreen mode Die Benutzung sieht genau so aus, und auf Battle Drones dauerte sie etwa 4 Sekunden:
python3 agentaudit.py pfad/zum/repo --days 14 --top 8 Vollbildmodus aktivieren Vollbildmodus beenden Battle Drones: die tatsächliche Ausgabe Ich habe das Repository bei Commit 39d6f58 (20. Oktober) geklont. Der Klon enthält 334 Commits, davon 48 Merges, also etwas mehr als die 321, die in Simons Beitrag gezählt wurden. Die Datumsangaben sind UTC.
repo:. window: 14 days merges skipped: 48 bot commits skipped: 0 commits agent 267 93.4% human 19 6.6% human commits that look like squash-merged PRs (true author unknown): 0 lines agent 170299 98.1% human 3258 1.9% agents Claude 267 models Claude Opus 5.5 229, Claude Fable 5 28, Claude 1 churn agent: 12824 of 153755 added lines deleted within 14d (8.3%) churn human: 362 of 2875 added lines deleted within 14d (12.6%) fixes fix/revert commits: 39 (by agent 39, by human 0) touching files last changed by agent work <= 14d earlier: 38 (14.2 per 100 agent commits) touching files last changed by human work <= 14d earlier: 0 (0.0 per 100 human commits) top 8 files by agent-changed lines: 6362 100.0% agent src/ui/style.css 4640 100.0% agent DESIGN.md 3874 100.0% agent src/game/GameScene.ts 2747 100.0% agent src/ui/hud.ts 2680 99.8% agent SOUND-DUEL.md 2444 100.0% agent src/game/three/terrain.ts 2365 100.0% agent src/sim/ai/commander.ts 2339 100.0% agent public/assets/sprites/game.json day commits A/H lines A/H agent share of lines 2026-07-17 28/1 6504/2 #################### 2026-09-28 3/0 13953/0 #################### 2026-10-07 23/0 33545/0 #################### 2026-10-08 5/1 9589/581 ################### 2026-10-09 21/2 10002/473 ################### 2026-10-10 40/5 11759/683 ################### 2026-10-11 18/0 10034/0 #################### 2026-10-12 65/0 16785/0 #################### 2026-10-13 10/0 3075/0 #################### 2026-10-14 6/7 3934/1210 ############### 2026-10-15 9/0 2832/0 #################### 2026-10-16 3/0 2677/0 #################### 2026-10-19 15/0 33969/0 #################### 2026-10-20 20/3 11597/309 ################### 2026-10-21 1/0 44/0 #################### Vollbildmodus aktivieren Vollbildmodus beenden Was ich daraus lese:
98,1 Prozent der geänderten Zeilen stammen vom Agenten. Die Historie bestätigt das Motto "überwiegend mit Claude Code": 267 von 286 Nicht-Merge-Commits tragen ein Claude-Signal. 229 nennen Claude Opus 5.5 im Trailer und 28 nennen Claude Fable 5, was zu seiner Darstellung passt, dass er v1 im Juli mit einem früheren Modell begonnen hatte.
Generierte Dateien verzerren alles. Mein erster Lauf hatte keine Skip-Liste. Der Agent-Anteil an den Zeilen lag bei 95,4 Prozent, und fünf der acht meistgenannten "Agent-Dateien" waren Build-Ausgaben unter docs/assets/ sowie die package-lock.json. Wenn man den Review-Aufwand misst, sollte man alles ausklammern, das niemand reviewt.
Squash-Merges haben Commits fast versteckt. Simon ist Autor von 28 Nicht-Merge-Commits. Neun davon sind squash-mergegte Agent-PRs mit Betreffs wie "Shift+click adds or removes a unit from the selection (#6)". Acht dieser neun haben gar keinen Co-Authored-By-Trailer und werden nur erkannt, weil die Message einen Claude-Code-Session-Link enthält. Entfernt man dieses eine Signal, steigt die Zahl der menschlichen Commits von 19 auf 27.
Der Churn ist moderat. 8,3 Prozent der vom Agenten hinzugefügten Zeilen wurden innerhalb von 14 Tagen wieder gelöscht. Der Wert für Menschen liegt mit 12,6 Prozent höher, basiert aber auf nur 2.875 Zeilen, daher würde ich den Vergleich nicht überbewerten. Für ein Spiel, das über 12 Versionen hinweg mit nächtlichen Playtest-Läufen neu austariert wurde, hatte ich mehr erwartet.
Jeder Fix-Commit kam vom Agenten. Es gibt 39 Fix-artige Commits, alle vom Agenten verfasst, und 38 davon betreffen Dateien, die Agent-Arbeit in den vorangegangenen zwei Wochen geändert hatte. Die meisten Betreffs lesen sich wie "Fixes from the final review" oder "Sound duel: fix the final review's three major findings". Die Fix-Schleife besteht hier daraus, dass Agenten Agenten-Code reviewen und reparieren. Bei einem Spiel funktioniert das. Es ist die erste Zahl, die ich sehen wollen würde, bevor man denselben Workflow an einen Abrechnungsdienst heranlässt.
Die Hotspots liegen dort, wo man sie erwartet. src/ui/style.css, GameScene.ts, hud.ts und die commander.ts der KI führen die Liste an. Zwei der Top fünf sind Design-Dokumente (DESIGN.md, SOUND-DUEL.md): Der Agent schreibt und pflegt seine eigenen Spezifikationen, eine Angewohnheit, die man sich abschauen sollte.
Zum Vergleich habe ich das Skript auf simonw/tools ausgeführt, seine Sammlung von Single-File-HTML-Tools, bei Commit befbcec. Der Anfang der Ausgabe:
repo:. window: 14 days merges skipped: 6 bot commits skipped: 483 commits agent 226 24.5% human 698 75.5% human commits that look like squash-merged PRs (true author unknown): 48 lines agent 72709 9.1% human 727389 90.9% agents Claude 214, Copilot 12 models Claude 155, copilot-swe-agent[bot] 9, Claude Fable 5.1 6, Claude Opus 5.5 4, claude[bot] 3 churn agent: 1420 of 68797 added lines deleted within 14d (2.1%) churn human: 4639 of 716876 added lines deleted within 14d (0.6%) fixes fix/revert commits: 74 (by agent 40, by human 34) touching files last changed by agent work <= 14d earlier: 18 (8.0 per 100 agent commits) touching files last changed by human work <= 14d earlier: 42 (6.0 per 100 human commits) top 6 files by agent-changed lines: 5244 100.0% agent grok-mermaid/src/mermaid.rs 2929 100.0% agent chirp-pong.html 2343 100.0% agent video-compressor.html 2080 100.0% agent bluesky-bot-check.html 1614 100.0% agent latex.html 1613 100.0% agent sqlite-query-explainer.html Vollbildmodus aktivieren Vollbildmodus beenden Hier werden nur 24,5 Prozent der Commits und 9,1 Prozent der Zeilen Agenten zugeschrieben (Claude 214, Copilot 12). Agenten-Code unterliegt einem höheren Churn als menschlicher Code (2,1 gegenüber 0,6 Prozent) und seine Dateien ziehen etwas mehr nachfolgende Fix-Commits nach sich (8,0 gegenüber 6,0 pro 100 Commits). Doch die Kategorie "human" ist nicht das, was sie zu sein scheint.
Von den 698 Commits, die das Skript als menschlich einstuft, verweisen 112 auf eine geteilte Claude-Konversation und 48 auf eine ChatGPT-Konversation, und 48 sehen wie squash-mergegte PRs aus. Code, der in einem Chat-Fenster geschrieben und von Hand eingefügt wurde, sieht für Git exakt wie menschlicher Code aus. Git zeichnet auf, wer committet hat, nicht wer geschrieben hat.
Die meisten dieser Probleme löst man per Richtlinie, nicht per Code: Den Trailer verpflichtend vorschreiben, ihn durch Squash-Merges hindurch bewahren und etwas wie dies wöchentlich in der CI ausführen, damit der Trend sichtbar wird, bevor er zur Überraschung wird.
Bei Axionry bauen wir LLM-Anwendungen, Agenten, RAG-Pipelines und MCP-Tooling, und jeder Build wird mit Observability sowie Dashboards für die Kosten pro Request ausgeliefert. Ein Git-Audit wie dieser ist das entwicklungsseitige Gegenstück zu diesen Dashboards: Produktionsmetriken zeigen, was ein Agent im Betrieb kostet, und dies zeigt, was Agenten-Code an Review- und Pflegeaufwand kostet. Wer ein KI-Feature plant und zuerst eine grobe Zahl braucht, kann unseren KI-Produkt-Kostenschätzer ausprobieren.
Das Skript steht oben, 140 Zeilen, ohne Abhängigkeiten. Richtet es auf euer eigenes Repository und seht nach, wie hoch euer echter Agenten-Anteil ist.
Quellen: Simon Willison, "Battle Drones", simonw/battledrones auf GitHub, simonw/tools auf GitHub, AI Weekly Berichterstattung.
Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.
Original (Englisch) anzeigen
On 20 October Simon Willison published Battle Drones, a browser RTS in the Command & Conquer mould. He built it over 12 major versions and 321 commits, mostly with Claude Code, and he is candid about it: "I've reviewed almost none of the code." The repo is public, which makes it the best open dataset I have seen for a question every team adopting coding agents should ask: how much of our codebase did the agent write, and what happened to that code afterwards?
So I wrote a small, stdlib-only Python script that answers it from git log, cloned the repo and ran it. Below are the script, the real output and what I think it means for teams.
My team is small and agents now write a real share of our commits. "Is the agent productive?" is the wrong question. The useful ones are operational:
None of this needs a vendor dashboard. Agents already leave fingerprints in git.
Claude Code adds a Co-Authored-By: Claude... <noreply@anthropic.com> trailer, and Claude Code on the web also adds a Claude-Session: line with a session URL. Copilot's coding agent commits as copilot-swe-agent[bot] or a +Copilot@users.noreply.github.com address. Cursor, Codex, Devin and Aider have their own patterns. The script matches one regex per agent against author, committer and the full message. Everything else counts as human. Merge commits and CI bots are skipped.
Then, in one pass over the full patch history ( git log -p with zero context lines), oldest commit first, it computes:
#!/usr/bin/env python3 """ agentaudit.py: how much of a git repo did coding agents write? Usage: python3 agentaudit.py [REPO] [--days 14] [--top 8] (stdlib only) """ import argparse, collections, datetime as dt, re, subprocess # One regex per agent, matched against "author / committer / full message". AGENTS = { " Claude ": r " noreply@anthropic\.com|co-authored-by:[^\n]*claude|claude-session: " r " |claude\.ai/code/session|generated with \[claude code\] ", " Codex ": r " co-authored-by:[^\n]*codex|chatgpt-codex-connector|codex@openai\.com ", " Copilot ": r " copilot-swe-agent|\+copilot@users\.noreply|co-authored-by:[^\n]*copilot ", " Cursor ": r " cursoragent@cursor\.com|co-authored-by:[^\n]*cursor ", " Devin ": r " devin-ai-integration ", " Aider ": r " \(aider\)|co-authored-by:[^\n]*aider ", } AGENTS = { k: re. compile ( v, re.
I | re. M ) for k, v in AGENTS. items ()} BOT = re. compile ( r " \[bot\]|actions@users\.noreply|github-actions ", re. I ) # CI, not agents FIX = re. compile ( r " ^(revert|fixup!|hotfix)|\b(fix(es|ed)?|bug|regression|broke|broken|oops)\b ", re.
I ) TRAILER = re. compile ( r " ^co-authored-by:\s*([^<\n]+?)\s*< ", re. I | re. M ) # Generated files inflate line counts; skip them (override with --skip).
SKIP = r " (^|/)(dist|build|vendor|node_modules|docs/assets)/|\.lock$|-lock\.json$|\.min\.|\.map$ " FMT = " %x00%H%x1f%an <%ae>%x1f%cn <%ce>%x1f%at%x1f%P%x1f%B%x1e " def who ( author, committer, msg ): text = f " { author } \n { committer } \n { msg } " return next (( name for name, rx in AGENTS. items () if rx. search ( text )), None ) def audit ( repo, days, skip_rx ): skip_rx = re. compile ( skip_rx ) out = subprocess. run ([ " git ", " -C ", repo, " log ", " --reverse ", " -p ", " -M ", " --unified=0 ", " --no-color ", " --format= " + FMT ], capture_output = True, text = True, errors = " replace ", check = True ). stdout window = days * 86400 s = collections. defaultdict ( collections. Counter ) # stat name -> {"agent"/"human": n} per_agent, models = collections. Counter (), collections.
Counter () files, day = collections. defaultdict ( collections. Counter ), collections. defaultdict ( collections. Counter ) added = collections. defaultdict ( lambda: collections. defaultdict ( collections. deque )) last_touch = {} # path -> (ts, cls) for chunk in out. split ( " \x00 " )[ 1:]: head, patch = chunk. split ( " \x1e ", 1 ) sha, author, committer, ts, parents, msg = head. split ( " \x1f " ) ts = int ( ts ) if len ( parents. split ()) > 1: s [ " merges " ][ " all " ] += 1 continue agent = who ( author, committer, msg ) if not agent and BOT. search ( author ): s [ " merges " ][ " bots " ] += 1 continue cls = " agent " if agent else " human " if agent: per_agent [ agent ] += 1 models. update ( t for t in TRAILER. findall ( msg ) if AGENTS [ agent ]. search ( f " co-authored-by: { t } " )) s [ " commits " ][ cls ] += 1 if not agent and re. search ( r " \(#\d+\)$ ", msg. split ( " \n ", 1 )[ 0 ]. strip ()): s [ " squash " ][ " human " ] += 1 # squash-merged PR: author unknown d = dt. datetime. fromtimestamp ( ts, dt. timezone. utc ). strftime ( " %Y-%m-%d " ) day [ d ][ cls ] += 1 path, in_hunk, skip, touched = None, False, False, set () for line in patch. split ( " \n " ): if line. startswith ( " diff --git " ): m = re. match ( r " diff --git a/(.*) b/(.*) ", line ) old, path = ( m. group ( 1 ), m. group ( 2 )) if m else ( None, line ) if old and old!= path and old in added: # follow renames added [ path ] = added. pop ( old ) skip = bool ( skip_rx. search ( path )) if not skip: touched. add ( path ) in_hunk = False elif line. startswith ( " @@ " ): in_hunk = not skip elif in_hunk and line [: 1 ] in ( " + ", " - " ): content = line [ 1:]. strip () s [ " lines " ][ cls ] += 1 files [ path ][ cls ] += 1 day [ d ][ cls + " _lines " ] += 1 if line [ 0 ] == " + ": s [ " added " ][ cls ] += 1 if len ( content ) > 2: # skip "}", "", ");" added [ path ][ content ]. append (( ts, cls )) elif added [ path ][ content ]: t0, c0 = added [ path ][ content ]. popleft () # oldest matching add if ts - t0 <= window: s [ " churn " ][ c0 ] += 1 if FIX. search ( msg. split ( " \n ", 1 )[ 0 ]): s [ " fixes " ][ cls ] += 1 hit = { last_touch [ p ][ 1 ] for p in touched if p in last_touch and ts - last_touch [ p ][ 0 ] <= window } s [ " fix_after " ]. update ( hit ) for p in touched: last_touch [ p ] = ( ts, cls ) return s, per_agent, models, files, day def pct ( a, b ): return f " { 100 * a / b: 5.1 f } % " if b else " n/a " def report ( repo, days, top, skip_rx ): s, per_agent, models, files, day = audit ( repo, days, skip_rx ) c, l = s [ " commits " ], s [ " lines " ] nc, nl = sum ( c. values ()), sum ( l. values ()) print ( f " repo: { repo } window: { days } days merges skipped: { s [ ' merges ' ][ ' all ' ] } bot commits skipped: { s [ ' merges ' ][ ' bots ' ] } " ) print ( f " commits agent { c [ ' agent ' ]: > 6 } { pct ( c [ ' agent ' ], nc ) } human { c [ ' human ' ]: > 6 } { pct ( c [ ' human ' ], nc ) } " ) print ( f " human commits that look like squash-merged PRs (true author unknown): " f " { s [ ' squash ' ][ ' human ' ] } " ) print ( f " lines agent { l [ ' agent ' ]: > 6 } { pct ( l [ ' agent ' ], nl ) } human { l [ ' human ' ]: > 6 } { pct ( l [ ' human ' ], nl ) } " ) print ( " agents " + ", ". join ( f " { k } { v } " for k, v in per_agent. most_common ())) print ( " models " + ", ". join ( f " { k } { v } " for k, v in models. most_common ( 5 ))) for cls in ( " agent ", " human " ): print ( f " churn { cls }: { s [ ' churn ' ][ cls ] } of { s [ ' added ' ][ cls ] } added lines deleted within " f " { days } d ( { pct ( s [ ' churn ' ][ cls ], s [ ' added ' ][ cls ]). strip () } ) " ) print ( f " fixes fix/revert commits: { sum ( s [ ' fixes ' ]. values ()) } (by agent { s [ ' fixes ' ][ ' agent ' ] }, " f " by human { s [ ' fixes ' ][ ' human ' ] } ) " ) for cls in ( " agent ", " human " ): print ( f " touching files last changed by { cls } work <= { days } d earlier: " f " { s [ ' fix_after ' ][ cls ] } ( { s [ ' fix_after ' ][ cls ] / max ( c [ cls ], 1 ) * 100:.
1 f } per 100 { cls } commits) " ) print ( f " \n top { top } files by agent-changed lines: " ) for p, n in sorted ( files. items (), key = lambda kv: - kv [ 1 ][ " agent " ])[: top ]: print ( f " { n [ ' agent ' ]: > 6 } { pct ( n [ ' agent ' ], n [ ' agent ' ] + n [ ' human ' ]) } agent { p } " ) print ( " \n day commits A/H lines A/H agent share of lines " ) for d in sorted ( day ): n = day [ d ] share = n [ " agent_lines " ] / max ( n [ " agent_lines " ] + n [ " human_lines " ], 1 ) print ( f " { d } { n [ ' agent ' ]: > 4 } / { n [ ' human ' ]: < 4 } { n [ ' agent_lines ' ]: > 7 } / { n [ ' human_lines ' ]: < 6 } " f " { ' # ' * round ( share * 20 ): < 20 } " ) if __name__ == " __main__ ": ap = argparse. ArgumentParser ( description = __doc__ ) ap. add_argument ( " repo ", nargs = "? ", default = ".
" ) ap. add_argument ( " --days ", type = int, default = 14 ) ap. add_argument ( " --top ", type = int, default = 8 ) ap. add_argument ( " --skip ", default = SKIP, help = " regex of paths to ignore " ) a = ap. parse_args () report ( a. repo, a. days, a. top, a. skip ) Enter fullscreen mode Exit fullscreen mode Usage is just this, and it took about 4 seconds on Battle Drones:
python3 agentaudit.py path/to/repo --days 14 --top 8 Enter fullscreen mode Exit fullscreen mode Battle Drones: the real output I cloned at commit 39d6f58 (20 Oct). The clone has 334 commits, 48 of them merges, so slightly more than the 321 counted in Simon's post. Dates are UTC.
repo:. window: 14 days merges skipped: 48 bot commits skipped: 0 commits agent 267 93.4% human 19 6.6% human commits that look like squash-merged PRs (true author unknown): 0 lines agent 170299 98.1% human 3258 1.9% agents Claude 267 models Claude Opus 5.5 229, Claude Fable 5 28, Claude 1 churn agent: 12824 of 153755 added lines deleted within 14d (8.3%) churn human: 362 of 2875 added lines deleted within 14d (12.6%) fixes fix/revert commits: 39 (by agent 39, by human 0) touching files last changed by agent work <= 14d earlier: 38 (14.2 per 100 agent commits) touching files last changed by human work <= 14d earlier: 0 (0.0 per 100 human commits) top 8 files by agent-changed lines: 6362 100.0% agent src/ui/style.css 4640 100.0% agent DESIGN.md 3874 100.0% agent src/game/GameScene.ts 2747 100.0% agent src/ui/hud.ts 2680 99.8% agent SOUND-DUEL.md 2444 100.0% agent src/game/three/terrain.ts 2365 100.0% agent src/sim/ai/commander.ts 2339 100.0% agent public/assets/sprites/game.json day commits A/H lines A/H agent share of lines 2026-07-17 28/1 6504/2 #################### 2026-09-28 3/0 13953/0 #################### 2026-10-07 23/0 33545/0 #################### 2026-10-08 5/1 9589/581 ################### 2026-10-09 21/2 10002/473 ################### 2026-10-10 40/5 11759/683 ################### 2026-10-11 18/0 10034/0 #################### 2026-10-12 65/0 16785/0 #################### 2026-10-13 10/0 3075/0 #################### 2026-10-14 6/7 3934/1210 ############### 2026-10-15 9/0 2832/0 #################### 2026-10-16 3/0 2677/0 #################### 2026-10-19 15/0 33969/0 #################### 2026-10-20 20/3 11597/309 ################### 2026-10-21 1/0 44/0 #################### Enter fullscreen mode Exit fullscreen mode What I read from it:
98.1% of changed lines are agent-written. The history backs up "mostly by Claude Code": 267 of 286 non-merge commits carry a Claude signal. 229 name Claude Opus 5.5 in the trailer and 28 name Claude Fable 5, which matches his account of starting v1 on an earlier model in July.
Generated files distort everything. My first run had no skip list. Agent share of lines came out at 95.4%, and five of the top eight "agent files" were build output under docs/assets/ plus package-lock.json. If you are measuring review load, exclude what nobody reviews.
Squash merges nearly hid commits. Simon is the author on 28 non-merge commits. Nine of them are squash-merged agent PRs, with subjects like "Shift+click adds or removes a unit from the selection (#6)". Eight of those nine have no Co-Authored-By trailer at all and are caught only because the message keeps a Claude Code session link. Remove that one signal and the human commit count goes from 19 to 27.
Churn is moderate. 8.3% of agent-added lines were deleted within 14 days. The human figure is higher at 12.6%, but on only 2,875 lines, so I would not read anything into the comparison. For a game rebalanced across 12 versions with overnight playtest runs, I expected more.
Every fix commit came from the agent. There are 39 fix-style commits, all agent-authored, and 38 of them touch files that agent work changed in the previous two weeks. Most subjects read like "Fixes from the final review" or "Sound duel: fix the final review's three major findings". The fix loop here is agents reviewing and repairing agent code. That works for a game. It is the first number I would want visible before letting the same workflow near a billing service.
Hotspots are where you would expect. src/ui/style.css, GameScene.ts, hud.ts and the AI's commander.ts lead. Two of the top five are design docs ( DESIGN.md, SOUND-DUEL.md ): the agent writes and maintains its own specs, which is a habit worth copying.
For contrast I ran it on simonw/tools, his collection of single-file HTML tools, at commit befbcec. Top of the output:
repo:. window: 14 days merges skipped: 6 bot commits skipped: 483 commits agent 226 24.5% human 698 75.5% human commits that look like squash-merged PRs (true author unknown): 48 lines agent 72709 9.1% human 727389 90.9% agents Claude 214, Copilot 12 models Claude 155, copilot-swe-agent[bot] 9, Claude Fable 5.1 6, Claude Opus 5.5 4, claude[bot] 3 churn agent: 1420 of 68797 added lines deleted within 14d (2.1%) churn human: 4639 of 716876 added lines deleted within 14d (0.6%) fixes fix/revert commits: 74 (by agent 40, by human 34) touching files last changed by agent work <= 14d earlier: 18 (8.0 per 100 agent commits) touching files last changed by human work <= 14d earlier: 42 (6.0 per 100 human commits) top 6 files by agent-changed lines: 5244 100.0% agent grok-mermaid/src/mermaid.rs 2929 100.0% agent chirp-pong.html 2343 100.0% agent video-compressor.html 2080 100.0% agent bluesky-bot-check.html 1614 100.0% agent latex.html 1613 100.0% agent sqlite-query-explainer.html Enter fullscreen mode Exit fullscreen mode Here only 24.5% of commits and 9.1% of lines are attributed to agents (Claude 214, Copilot 12). Agent code churns more than human code (2.1% vs 0.6%) and its files attract slightly more follow-up fix commits (8.0 vs 6.0 per 100 commits). But the "human" bucket is not what it looks like.
Of the 698 commits the script calls human, 112 link to a shared Claude conversation and 48 to a ChatGPT one, and 48 look like squash-merged PRs. Code written in a chat window and pasted in by hand looks exactly like human code to git. Git records who committed, not who wrote.
Most of these are solved by policy, not code: require the trailer, keep it through squash merges, and run something like this weekly in CI so the trend is visible before it becomes a surprise.
At Axionry we build LLM apps, agents, RAG pipelines and MCP tooling, and every build ships with observability and cost-per-request dashboards. A git audit like this is the development-side twin of those dashboards: production metrics tell you what an agent costs to run, this tells you what agent-written code costs to review and keep. If you are scoping an AI feature and want a rough number first, try our AI product cost estimator.
The script is above, 140 lines, no dependencies. Point it at your own repo and see what your real agent share is.
Sources: Simon Willison, "Battle Drones", simonw/battledrones on GitHub, simonw/tools on GitHub, AI Weekly coverage.