Best AI Coding Assistants: 12 Steps, 90 Min [2026] A 90-minute Python script using only the standard library can rank four AI coding assistants on speed, rework, cost per merged pull request and developer sentiment by reading a team's git history, pull request export and billing data, according to Shattered, which published the pilot method alongside its Cursor vs GitHub Copilot benchmark. The method runs for four weeks and warns that 6 to 20 pull requests per tool leaves small differences as noise, that developers picking tools per task biases results, and that mid-pilot model updates make a tool different at the end than at the start. Shattered cited Stack Overflow's 2025 developer survey press release reporting that 18% of developers used Cursor and 10% used Claude Code among the AI-enabled editors surveyed, and noted GitHub Copilot's changelog added seven models in seven weeks in 2026, from GPT-6 Astra on September 4 to GPT-6.1 Terra on October 26. The best AI coding assistants change faster than any review can keep up with, so a ranking you read today may describe a product that has since swapped its model. GitHub’s changelog shows what that looks like in practice. Copilot added GPT-6 Astra on September 4, GPT-6 Sol and GPT-6 Luna on September 22, GPT-6.1 Sol on September 29, Claude Haiku 5.5 on October 7, Gemini 4 Argon on October 13 and GPT-6.1 Terra on October 26, 2026. That is seven models in seven weeks inside one product. Adoption is not the open question anymore. Stack Overflow’s 2025 developer survey press release said, in its own words, “18% of developers reported using Cursor, while 10% reported using Claude Code among the AI-enabled editors included in the survey.” https://stackoverflow.co/company/press/archive/stack-overflow-2025-developer-survey/ The harder question for a team lead is which AI coding assistant earns its seat price on your code, with your reviewers, in your CI. Public leaderboards cannot answer that. Task benchmarks like the Cursor vs GitHub Copilot benchmark https://shattered.io/benchmark-cursor-vs-github-copilot-2026/ show how tools behave on a fixed set of prompts. A pilot measures something else: what happens to real pull requests when real developers use each tool for a month. A small Python project can run that pilot. It reads your git history, your pull request export and your billing numbers, then ranks four assistants on speed, rework, cost per merged PR and developer sentiment. Don't miss new tech stories on Google Add Shattered once in the Google app and our stories show up in your news suggestions. Add now https://www.google.com/preferences/source?hl=en&q=https%3A%2F%2Fshattered.io%2F Why a team pilot beats a benchmark for picking among the best AI coding assistants A benchmark controls everything except the thing you care about. It fixes the repository, the prompt and the grader, which makes results comparable across vendors. It also strips out the friction that decides whether a tool survives in a team: slow approvals, noisy suggestions, awkward review hand-offs and surprise bills. A pilot flips the trade. You give up tight control and gain realism. Each pull request carries a label saying which assistant helped, each commit carries a trailer, and the pilot script turns those marks into four numbers per tool. Those numbers come from your own work, so they already include your test suite, your code style and your reviewers’ patience. The method has limits. With 6 to 20 pull requests per tool, small differences are noise. Developers pick the tool for the task, which biases results. A tool that shipped a model update mid-pilot is a different tool at the end than at the start. The project handles these limits with minimum sample warnings, a rotation rule, a weights stress test and a model log. Treat the output as a decision aid, not a verdict. Three questions drive the design: - Does the assistant shorten the time from opening a pull request to merging it? - Does the merged work come back as a fix within a week? - What does one merged pull request cost once seats and usage are added together? Developer sentiment is the fourth input, kept at the lowest weight, because people rate tools they enjoy higher even when the numbers disagree. For a head-to-head view of two specific contenders before you commit pilot time, the GitHub Copilot vs Claude Code comparison https://shattered.io/github-copilot-vs-claude-code-2026/ covers seat pricing and feature differences in more detail. The whole build takes about 90 minutes. The pilot itself runs four weeks. Plan the calendar first, because the tooling is the easy part. Prerequisites and versions The project uses only the Python standard library. No pip installs, no virtual environment drama, no lockfile to drift. That choice keeps the pilot reproducible a year from now when you want to rerun it on a new quarter of data. | Component | Version used | Why it matters | |---|---|---| | Python | 3.12.3 3.11 is the minimum | tomllib for config parsing, modern type syntax | | Git | 2.43.0 | trailer formatting in git log, interpret-trailers in the hook | | GitHub CLI gh | current 2.x release | exports merged pull requests as JSON | | POSIX shell | any | runs the prepare-commit-msg hook | | Test repository | 4+ weeks of merged PRs | the subject of the pilot | | Assistants | latest release of each | install and sign in before day one | You also need a few non-software prerequisites. Get written agreement from the developers in the pilot, since the project reads commit trailers and PR labels that identify who used which tool. Make sure the repository has branch protection and CI, otherwise lead time measures nothing useful. Finally, decide who owns the cost export. Seat charges live in billing consoles, and usage charges sit in a different place for each vendor. Pin the tool versions on day one, whichever AI coding tools you test. Run each assistant’s version command, paste the output into a pilot log and note the default model it shows. Most of these CLIs update themselves, so a version string at the start and another at the end tells you whether the tool changed under you. The Claude Code documentation at code.claude.com https://code.claude.com/docs/en describes the CLI and its settings if you need a refresher on where model selection lives. The four assistants in the pilot, as of October 28, 2026 The pilot compares four products that cover the main ways developers use these tools today: a terminal agent from Anthropic, an agent and desktop app from OpenAI, a multi-model platform from GitHub and an agent-first editor from Anysphere. The table lists what the vendors’ own pages and changelogs confirm for this week. | Assistant | Where you use it | Latest confirmed change | Billing notes | |---|---|---|---| | Claude Code | Terminal, IDE extensions | Anthropic’s alignment team published its Auto Mode red-teaming write-up on Oct. 22 | Seats on Anthropic plans, with API credits on some tiers | | OpenAI Codex | CLI, desktop app, Codex Cloud | GPT-6.1 Terra began rolling out in ChatGPT Work and Codex on Oct. 26 | Included with ChatGPT Free through Education plans, Codex Cloud excluded on Free and Go | | GitHub Copilot | IDEs, Copilot CLI, Copilot app | GPT-6.1 Terra generally available Oct. 26, assisted permissions generally available Oct. 26 | Plans: Free, Student, Pro, Pro+, Max, Business, Enterprise, with 1 AI credit equal to $0.01 | | Cursor | Editor, web, iOS, CLI, SDK | Grok 4.5 shipped July 8, trained jointly with SpaceXAI | Composer 2.5 listed at $0.50 per million input tokens and $2.50 output | Two details from that table shape the pilot design. First, Codex usage counts against plan limits, credits or billing even when a cloud environment carries no separate VM charge, so a seat-only cost model understates it. Second, Copilot’s newest models are not offered on every plan. GitHub’s October changelog https://github.blog/changelog/label/copilot/ says Gemini 4 Argon is available to Pro+, Max, Business and Enterprise and bills at provider-list pricing under usage-based billing. A developer on a lower plan cannot reproduce a colleague’s results with the same model. Cursor’s pricing is in motion. According to forum posts, Cursor introduced “The X Subscription” on October 12, and users there report that third-party models such as Claude and GPT are billed at API prices under the new tiers. A community thread on Teams Premium https://forum.cursor.com/t/switched-from-pro-to-teams-premium-cursor-models-pool-is-smaller-and-more-expensive/174065/7 complains that the Cursor model pool is smaller and costs more. Those are user reports, not a vendor price sheet, so check the pricing page on the day you sign up and write the seat price into the ledger yourself. For the full list of what each plan costs, the Copilot pricing and AI credits breakdown https://shattered.io/github-copilot-pricing-ai-credits-claude-codex-agents-2026/ tracks the credit model, and the Cursor vs Claude Code entry-plan comparison https://shattered.io/cursor-vs-claude-code-2026/ covers the other side. If your team already leans toward a terminal agent, the usage split in Codex vs Claude Code https://shattered.io/codex-vs-claude-code-stack-overflow-65-5-vs-29-5-2026/ gives context on who is using which. Steps 1 to 3: Set the rules, scaffold the project, write the config Step 1: Define the pilot rules Write these down before anyone installs anything, and get sign-off from the people doing the work. 1. Duration: four weeks, starting on a Monday, with no tool changes in the final week. 2. Subjects: one repository, or two at most, with active development and required CI. 3. Assignment: rotate. Each developer uses each tool for roughly a week, in a different order, so no tool gets only the easiest tickets. 4. Minimum sample: 8 merged pull requests per tool. Below that, the report prints a warning and you extend the pilot. 5. Baseline: keep a human-only lane. Some pull requests carry no assistant label, and they become the comparison row. 6. Model log: record each tool’s version and default model at the start of each week. The rotation rule matters more than it looks. If your strongest developer holds one tool for four weeks, you measure the developer. Rotating spreads the skill effect across all four tools. Step 2: Scaffold the project Create a folder and the empty package. Everything lives in one directory so you can zip it and hand it to a colleague. mkdir assistant-pilot && cd assistant-pilot mkdir pilot tests data touch pilot/ init .py The final layout, once all steps are done, looks like this: assistant-pilot/ pilot.toml make fixture.py data/ prs.json, costs.csv, survey.csv pilot/ init , main , bootstrap, cli, config, gitmine, ledger, metrics, min sample, score tests/ test pilot.py Step 3: Write pilot.toml The config holds everything a reviewer might want to argue about: the date range, the trailer name, the fix window and the scoring weights. Putting weights in a file instead of code means the debate happens in a pull request on the config, not in a hallway. pilot repo = "fixture-repo" since = "2026-09-01" trailer = "Assisted-by" baseline = "human" fix window days = 7 weights speed = 0.25 quality = 0.35 cost = 0.25 survey = 0.15 files prs = "data/prs.json" costs = "data/costs.csv" survey = "data/survey.csv" The weights reflect a priority call. Quality counts most because rework is the expensive failure, speed and cost split the next tier, and developer sentiment gets the smallest slice. Change them to match your own priorities, but change them before you see results. Adjusting weights after seeing a ranking is how pilots end up confirming whatever the loudest person already believed. The loader refuses weights that do not add up to 1.0, which catches typos early. This is the config module: python import tomllib from dataclasses import dataclass from pathlib import Path @dataclass frozen=True class Config: repo: Path since: str trailer: str baseline: str fix window days: int weights: dict prs: Path costs: Path survey: Path def check weights w : total = sum w.values if abs total - 1.0 1e-9: raise ValueError f"weights must sum to 1.0, got {total:.3f}" def load path="pilot.toml" : raw = tomllib.loads Path path .read text p, w, f = raw "pilot" , raw "weights" , raw "files" check weights w return Config repo=Path p "repo" , since=p "since" , trailer=p "trailer" , baseline=p "baseline" , fix window days=int p "fix window days" , weights=w, prs=Path f "prs" , costs=Path f "costs" , survey=Path f "survey" , Steps 4 and 5: Tag every change with a trailer and a label The pilot is only as good as its tagging. Two marks identify assistant work: a git trailer on each commit and a label on each pull request. Both must be cheap enough that developers do not skip them. Step 4: Install the commit trailer hook A prepare-commit-msg hook stamps every commit with Assisted-by: tool based on a per-repository git setting. Developers switch tools by changing one setting, and the hook does the rest. Save this as .git/hooks/prepare-commit-msg and make it executable. bash /bin/sh tool="$ git config --get pilot.tool " -z "$tool" && exit 0 "$tool" = "human" && exit 0 git interpret-trailers --in-place --if-exists replace --trailer "Assisted-by: $tool" "$1" Run chmod +x .git/hooks/prepare-commit-msg and then set the tool for the week. The slugs below match what the report prints, so keep them lowercase and consistent. git config pilot.tool claude-code or: codex, copilot, cursor, human Test it before you trust it. Commit once with a tool set and once with human , then read the trailers back: bash $ git log --format='%s | % trailers:key=Assisted-by,valueonly,separator=%x2C ' fix: demo | feat: demo 1 | codex Hooks live inside .git and do not travel with a clone, so each developer installs their own. Put the script in the repository under scripts/ and add a one-line install note to your contributing guide. Developers who already follow the Claude Code tutorial https://shattered.io/claude-code-tutorial-13-steps-2026/ setup can add the hook to the same onboarding checklist. Commit subjects must also end with the pull request number in the form 12 . GitHub adds that suffix automatically on squash merges, which is how the report maps commits to pull requests. Fix commits link back with a trailer: Fixes: 12 . Agree on that convention in the rules, because the rework metric depends on it. Step 5: Label every pull request Labels carry the same information at the pull request level, where lead time is measured. Create one label per tool, using the ai: prefix the script expects. for t in claude-code codex copilot cursor; do gh label create "ai:$t" --color 5319e7 --description "Pilot: PR assisted by $t" done A pull request template with a checkbox list reminds authors to apply the right label. Labels also give you a free audit trail. If a PR has an ai:codex label but its commits carry Assisted-by: cursor , the author switched tools mid-PR, and you should look at that case by hand. The export you will pull in Step 8 includes labels, so no extra tooling is needed. Teams that also run a headless agent in CI can tag those runs the same way. The Cursor CLI in GitHub Actions guide https://shattered.io/cursor-cli-github-actions-headless-agent-2026/ shows how an unattended run fits into a workflow, and you would give that run its own label so automation does not blur the human numbers. Steps 6 and 7: Mine git history and measure rework Step 6: Read commits with git log The first module shells out to git log and asks git itself to extract the trailers. Parsing trailers with regular expressions over raw commit bodies breaks on wrapped lines and mixed-case keys. Git’s % trailers:key=...,valueonly placeholder does the job correctly. The format string joins five fields with an ASCII unit separator and starts each record with a record separator. Those control characters never appear in commit messages, which avoids the classic bug where a subject line containing a pipe or a tab corrupts the parse. python import re import subprocess from dataclasses import dataclass, field from datetime import datetime RS, US = "\x1e", "\x1f" PR REF = re.compile r"\ \d+ \ " FIX REF = re.compile r" ? \d+ " @dataclass class Commit: sha: str when: datetime subject: str tool: str pr: int | None = None fixes: list = field default factory=list def read commits repo, since, trailer, baseline : fmt = f"{RS}%H{US}%aI{US}%s{US}% trailers:key={trailer},valueonly,separator=%x2C " f"{US}% trailers:key=Fixes,valueonly,separator=%x2C " out = subprocess.run "git", "-C", str repo , "log", "--no-merges", f"--since={since}", f"--format={fmt}" , check=True, capture output=True, text=True, .stdout commits = for record in out.split RS 1: : sha, when, subject, tool, fixes = record.strip "\n" .split US m = PR REF.search subject commits.append Commit sha, datetime.fromisoformat when , subject, tool.strip .split "," 0 .strip .lower or baseline, int m.group 1 if m else None, int x for x in FIX REF.findall fixes , return sorted commits, key=lambda c: c.when Three details deserve attention. The code lowercases the tool name, so Assisted-by: Codex and assisted-by: codex collapse into one row. Commits with no trailer fall into the baseline bucket named in the config. And the PR number comes from the N suffix in the subject, which is why Step 4 insisted on that convention. Use --no-merges deliberately. Merge commits carry no authorship signal and would double count work already represented by the commits they merge. If your team merges with real merge commits instead of squashing, the trailer needs to live on the PR’s own commits, and the PR number comes from the merge commit message. In that case adjust PR REF to match your merge message format. Step 7: Measure rework Rework answers the question reviewers care about most: how often does merged work come back? The definition here is narrow and explicit. A pull request counts as reworked when a later commit carries Fixes: N pointing at it, and that commit lands within the configured window seven days by default after the original change. python def rework commits, window days : """Share of each tool's PRs that a later commit repairs via a 'Fixes: N' trailer.""" owner = {c.pr: c for c in commits if c.pr is not None} repaired = set for fix in commits: for n in fix.fixes: base = owner.get n if base and timedelta 0 < fix.when - base.when <= timedelta days=window days : repaired.add n total, hit = defaultdict int , defaultdict int for n, c in owner.items : total c.tool += 1 hit c.tool += n in repaired return {t: hit t / k for t, k in total.items }, dict total Save the function inside pilot/metrics.py with these imports at the top. The next step adds the second function to the same file. python import json import statistics from collections import defaultdict from datetime import datetime, timedelta The design choice to rely on explicit references rather than file overlap came from a failed first attempt. A first version flagged any later “fix” commit that touched the same files within seven days. On a repository with eight hot files, almost every fix overlapped with someone’s earlier change, and the human-only baseline showed 50% rework. Switching to explicit Fixes: N references dropped the same baseline to 10% and spread the four assistants between 7% and 36%, which follows the failure rates seeded into the synthetic data, small-sample wobble included. The lesson carries over to real repositories. Heuristics that guess at causality inflate every tool’s number and bury the differences you are trying to see. The unit tests include a case that pins this behavior: an unlinked fix commit must not count against anything. If your team will not write the trailer, rework is the one metric you should drop from the score rather than approximate. Step 8: Compute lead time from your pull request export Lead time is the gap between opening a pull request and merging it. It captures review waits, CI reruns and back-and-forth that git history alone hides. GitHub’s CLI exports exactly the fields the script needs. gh pr list --state merged --limit 200 \ --search "merged: =2026-09-01" \ --json number,title,createdAt,mergedAt,labels data/prs.json Run it from inside the repository, and raise --limit if the pilot produces more than 200 merged pull requests. The output is a JSON array. Each element looks like this: { "number": 2, "title": "change 2", "createdAt": "2026-09-22T22:31:30Z", "mergedAt": "2026-09-24T01:53:34Z", "labels": {"name": "ai:assistant-d"} } The second function in pilot/metrics.py groups pull requests by their ai: label and takes the median lead time in hours. A pull request with no ai: label lands in the baseline group. python def lead times prs path, baseline : prs = json.loads open prs path .read by tool = defaultdict list for pr in prs: tool = next l "name" .split ":", 1 1 for l in pr "labels" if l "name" .startswith "ai:" , baseline made = datetime.fromisoformat pr "createdAt" .replace "Z", "+00:00" merged = datetime.fromisoformat pr "mergedAt" .replace "Z", "+00:00" by tool tool .append merged - made .total seconds / 3600 return {t: statistics.median v for t, v in by tool.items }, \ {t: len v for t, v in by tool.items } The median is the right statistic here. A single pull request that waited over a holiday weekend would drag a mean upward by days, and with a dozen samples per tool that one outlier could flip the ranking. The median shrugs it off. Two cautions apply. Draft pull requests that sat open for a week before anyone asked for review inflate lead time for whoever opened them. If your team uses drafts heavily, export the reviews field instead and measure from the first review. And pull requests merged by bots dependency updates, for instance belong in the baseline only if humans would otherwise do that work. Filter them out in the export query when in doubt. Lead time rewards small pull requests. A tool that nudges developers into tiny, frequent changes will look fast. That can be a real benefit, but check the size of the changes before crediting the tool for speed. Step 9: Build the cost ledger and the developer survey Cost per merged pull request needs two inputs: what you paid and how many PRs merged. The ledger module sums seat and usage charges per tool, and Step 11 divides by the PR count from Step 8. python import csv from collections import defaultdict def totals costs path : spend = defaultdict float with open costs path, newline="" as fh: for row in csv.DictReader fh : spend row "tool" += float row "seat usd" + float row "usage usd" return dict spend def survey means path : scores = defaultdict list with open path, newline="" as fh: for row in csv.DictReader fh : scores row "tool" .append float row "rating" return {t: sum v / len v for t, v in scores.items } The expected file, data/costs.csv , has one row per tool per week. tool,week,seat usd,usage usd assistant-a,1,5.0,10.9 assistant-a,2,5.0,14.66 assistant-a,3,5.0,14.94 assistant-a,4,5.0,16.8 Where the numbers come from differs by vendor, and this is the most error-prone chore in the whole pilot. Seat charges appear on the invoice or billing page for each plan. Usage charges hide in other places. Copilot meters usage in GitHub AI Credits, where one credit equals $0.01, and each plan includes a monthly allowance. Codex usage counts against plan limits and credits. Claude Code usage depends on whether your developers sign in with a plan or an API key. Cursor’s billing is changing, so read the current dashboard. Convert everything to dollars per week and write one row per tool. If a vendor bills in credits, multiply by the credit price on the invoice. If a seat is shared across your other work, allocate only the share used on the pilot repository. Honest allocation matters more than precision. Write the allocation rule at the top of your pilot log so the reviewer who reads the report in six months can reproduce it. The survey file is simpler: one row per developer per tool with a rating from 1 to 5, collected in the last week. tool,dev,rating assistant-a,dev1,3 assistant-a,dev2,5 Keep the survey to a single question, such as “How much would you miss this tool if it disappeared tomorrow?” Longer surveys get abandoned, and a single clear question produces cleaner data. The module averages ratings per tool. Because sentiment carries only 15% of the default score, a loved but slow tool cannot win on affection alone. Security reviewers will ask what leaves the building during a pilot. Settle that with your security team before you start, and read the overview of AI assistant security and data exposure https://shattered.io/ai-assistant-security-copilots-and-your-data/ to prepare for the questions about prompts, telemetry and retention. Step 10: Score and rank the assistants Four metrics live on four different scales: hours, percentages, dollars and a 1-to-5 rating. The scorer converts each one to a 0-to-1 value with min-max normalization across the tools in the pilot, flips the direction where lower is better, and combines them with the weights from the config. | Dimension | Default weight | Metric | Better when | |---|---|---|---| | Speed | 0.25 | Median hours from PR open to merge | Lower | | Quality | 0.35 | Share of PRs repaired by a Fixes: trailer within 7 days | Lower | | Cost | 0.25 | Seat plus usage dollars divided by merged PRs | Lower | | Survey | 0.15 | Mean developer rating, 1 to 5 | Higher | The scorer is short enough to read in one sitting. python def normalise values, lower is better : lo, hi = min values.values , max values.values if hi == lo: return {k: 1.0 for k in values} span = hi - lo return {k: hi - v / span if lower is better else v - lo / span for k, v in values.items } def rank metrics, weights : parts = { "speed": normalise metrics "lead hours" , True , "quality": normalise metrics "rework" , True , "cost": normalise metrics "cost per pr" , True , "survey": normalise metrics "survey" , False , } tools = metrics "lead hours" .keys scored = {t: round 100 sum weights k parts k t for k in weights , 1 for t in tools} return sorted scored.items , key=lambda kv: kv 1 , reverse=True , parts Min-max scaling has a quirk you should know about before you trust the output. The best tool on each metric gets 1.0 and the worst gets 0.0, no matter how small the gap between them. If two tools differ by 30 minutes of median lead time, the scaled speed score still spreads them across the full range. That is why Step 12 stress-tests the weights and why the report prints the raw numbers beside the score. A tie on any metric returns 1.0 for everyone, which avoids a divide-by-zero error and gives that metric no influence on the ranking. The minimum sample guard sits in its own two-line module. It returns the tools with fewer merged PRs than the floor, and the CLI prints a warning instead of silently ranking a tool on four data points. python def too small counts, floor : return sorted t for t, n in counts.items if n < floor Step 11: Wire up the CLI, generate test data and run the pilot The command-line module ties the pieces together. The collect function gathers every metric into one dictionary, and main filters to tools that have both PR data and cost data, applies the optional weights override, prints the table and shows the human-only baseline underneath. python import argparse import sys from dataclasses import replace from . import config, gitmine, ledger, metrics, score from .min sample import too small def collect cfg : commits = gitmine.read commits cfg.repo, cfg.since, cfg.trailer, cfg.baseline rw, commit counts = metrics.rework commits, cfg.fix window days lead, pr counts = metrics.lead times cfg.prs, cfg.baseline spend = ledger.totals cfg.costs cost per pr = {t: spend t / pr counts t for t in spend if t in pr counts} return { "rework": rw, "commits": commit counts, "lead hours": lead, "prs": pr counts, "spend": spend, "cost per pr": cost per pr, "survey": ledger.survey means cfg.survey , } def main argv=None : ap = argparse.ArgumentParser prog="pilot" ap.add argument "--config", default="pilot.toml" ap.add argument "--min-prs", type=int, default=8 ap.add argument "--weights", help="override, e.g. speed=0.1,quality=0.5,cost=0.3,survey=0.1" args = ap.parse args argv cfg = config.load args.config if args.weights: override = {k: float v for k, v in kv.split "=" for kv in args.weights.split "," } config.check weights override cfg = replace cfg, weights=override m = collect cfg tools = t for t in m "lead hours" if t = cfg.baseline and t in m "cost per pr" small = too small {t: m "prs" t for t in tools}, args.min prs if small: print f"WARNING: fewer than {args.min prs} merged PRs for: {', '.join small }", file=sys.stderr view = {k: {t: v t for t in tools} for k, v in m.items if k in "rework", "lead hours", "cost per pr", "survey" } ranking, = score.rank view, cfg.weights print f"{'tool':<14}{'PRs': 5}{'lead h': 9}{'rework': 9}{'$/PR': 8}{'survey': 8}{'score': 8}" for tool, pts in ranking: print f"{tool:<14}{m 'prs' tool : 5}{m 'lead hours' tool : 9.1f}" f"{m 'rework' tool : 8.0%} {m 'cost per pr' tool : 8.2f}" f"{m 'survey' tool : 8.1f}{pts: 8.1f}" b = cfg.baseline print f"\nbaseline {b} : {m 'prs' .get b, 0 } PRs, " f"median {m 'lead hours' .get b, float 'nan' :.1f} h, " f"rework {m 'rework' .get b, float 'nan' :.0%}" return 0 if name == " main ": raise SystemExit main Add a two-line pilot/ main .py so python3 -m pilot works. python from .cli import main raise SystemExit main You cannot run a four-week pilot to test the tooling, so the project ships a fixture generator. It builds a throwaway git repository with dated commits, trailers and fix references, plus matching PR, cost and survey files. Every number is random but seeded, so reruns produce identical files. The tool names are deliberately generic assistant-a through assistant-d so nobody mistakes the output for a real result. """Builds a synthetic repo plus PR, cost and survey exports. All numbers are random but seeded.""" import csv import json import os import random import shutil import subprocess from datetime import datetime, timedelta, timezone from pathlib import Path rng = random.Random 2026 ROOT = Path "fixture-repo" shutil.rmtree ROOT, ignore errors=True Path "data" .mkdir exist ok=True ROOT.mkdir PROFILE = { tool: PR count, median lead hours, chance of a follow-up fix, weekly seat, weekly usage "human": 20, 30.0, 0.25, 0.0, 0.0 , "assistant-a": 14, 18.0, 0.20, 5.0, 14.0 , "assistant-b": 12, 22.0, 0.12, 5.0, 31.0 , "assistant-c": 11, 16.0, 0.35, 2.3, 6.0 , "assistant-d": 6, 20.0, 0.18, 9.0, 4.0 , } FILES = f"src/mod{i}.py" for i in range 1, 9 def git args, when=None : env = dict os.environ, GIT AUTHOR NAME="Dev", GIT AUTHOR EMAIL=" email protected ", GIT COMMITTER NAME="Dev", GIT COMMITTER EMAIL=" email protected " if when: env "GIT AUTHOR DATE" = env "GIT COMMITTER DATE" = when.isoformat subprocess.run "git", "-C", str ROOT , args , check=True, env=env, capture output=True def commit subject, tool, files, when, n, fixes=None : for f in files: p = ROOT / f p.parent.mkdir parents=True, exist ok=True with p.open "a" as fh: fh.write "".join f"line {n}.{i}\n" for i in range rng.randint 3, 40 git "add", "-A" trailers = f"Assisted-by: {tool}" if tool = "human" else + \ f"Fixes: {fixes}" if fixes else msg = "commit", "-q", "-m", subject if trailers: msg += "-m", "\n".join trailers git msg, when=when git "init", "-q", "-b", "main" start = datetime 2026, 9, 1, 9, tzinfo=timezone.utc jobs = t for t, p in PROFILE.items for in range p 0 rng.shuffle jobs prs, n = , 0 for i, tool in enumerate jobs, 1 : n += 1 opened = start + timedelta hours=rng.uniform 0, 24 45 lead = max 2.0, rng.gauss PROFILE tool 1 , PROFILE tool 1 0.3 merged = opened + timedelta hours=lead files = rng.sample FILES, rng.randint 1, 3 commit f"feat: change {i} {i} ", tool, files, merged, n if rng.random < PROFILE tool 2 : n += 1 fix at = merged + timedelta days=rng.uniform 0.5, 5 commit f"fix: follow-up for change {i}", "human", files :1 , fix at, n, fixes=i prs.append {"number": i, "title": f"change {i}", "createdAt": opened.strftime "%Y-%m-%dT%H:%M:%SZ" , "mergedAt": merged.strftime "%Y-%m-%dT%H:%M:%SZ" , "labels": if tool == "human" else {"name": f"ai:{tool}"} } Path "data/prs.json" .write text json.dumps prs, indent=1 with open "data/costs.csv", "w", newline="" as fh: w = csv.writer fh w.writerow "tool", "week", "seat usd", "usage usd" for tool, p in PROFILE.items : if tool == "human": continue for week in range 1, 8 : w.writerow tool, week, p 3 , round p 4 rng.uniform 0.7, 1.3 , 2 with open "data/survey.csv", "w", newline="" as fh: w = csv.writer fh w.writerow "tool", "dev", "rating" base = {"assistant-a": 4, "assistant-b": 4, "assistant-c": 3, "assistant-d": 3} for tool, b in base.items : for dev in range 1, 6 : w.writerow tool, f"dev{dev}", min 5, max 1, b + rng.choice -1, 0, 0, 1 print f"fixture ready: {len prs } PRs" Run the generator and the report: bash $ python3 make fixture.py fixture ready: 63 PRs $ python3 -m pilot WARNING: fewer than 8 merged PRs for: assistant-d tool PRs lead h rework $/PR survey score assistant-a 14 16.5 7% 9.97 3.6 73.6 assistant-c 11 11.8 36% 5.38 3.2 50.0 assistant-b 12 21.6 8% 21.95 4.0 48.6 assistant-d 6 18.8 17% 15.62 3.4 44.0 baseline human : 20 PRs, median 31.6 h, rework 10% Read the output in layers. The warning fires first because assistant-d merged only six pull requests, below the floor of eight, so its row is a placeholder and not evidence. The table sorts by score. Assistant-a leads because it combines a fast merge time, low rework and the second-lowest cost per PR. Assistant-c is the cheapest and fastest but fixes 36% of its pull requests afterward, and that quality penalty drags it from first place on speed to second overall. Assistant-b is the best loved and the most expensive. The baseline row shows what the humans did alone, so every assistant can be judged against doing nothing. Now run the tests. They use only unittest , so there is nothing to install. bash $ python3 -m unittest discover -s tests -v test weights must sum to one test pilot.ConfigTests.test weights must sum to one ... ok test fix after window is ignored test pilot.ReworkTests.test fix after window is ignored ... ok test fix inside window counts test pilot.ReworkTests.test fix inside window counts ... ok test unlinked fix is not guessed test pilot.ReworkTests.test unlinked fix is not guessed ... ok test normalise lower is better test pilot.ScoreTests.test normalise lower is better ... ok test rank orders by weighted sum test pilot.ScoreTests.test rank orders by weighted sum ... ok test tie does not divide by zero test pilot.ScoreTests.test tie does not divide by zero ... ok Ran 7 tests in 0.000s OK The test file covers the three places where a silent error would mislead a decision: the normalization direction, the fix window edges and the refusal to guess at unlinked fixes. python import unittest from datetime import datetime, timedelta from pilot import config, metrics, score from pilot.gitmine import Commit T0 = datetime 2026, 9, 1, 9 def c sha, days, subject, tool, pr=None, fixes= : return Commit sha, T0 + timedelta days=days , subject, tool, pr, list fixes class ScoreTests unittest.TestCase : def test normalise lower is better self : out = score.normalise {"a": 10, "b": 20, "c": 30}, lower is better=True self.assertEqual out, {"a": 1.0, "b": 0.5, "c": 0.0} def test tie does not divide by zero self : self.assertEqual score.normalise {"a": 5, "b": 5}, True , {"a": 1.0, "b": 1.0} def test rank orders by weighted sum self : m = {"lead hours": {"x": 10, "y": 20}, "rework": {"x": 0.1, "y": 0.1}, "cost per pr": {"x": 5, "y": 5}, "survey": {"x": 3, "y": 5}} w = {"speed": 0.6, "quality": 0.0, "cost": 0.0, "survey": 0.4} ranking, = score.rank m, w self.assertEqual t for t, in ranking , "x", "y" self.assertEqual dict ranking , {"x": 60.0, "y": 40.0} class ReworkTests unittest.TestCase : def test fix inside window counts self : commits = c "1", 0, "feat 1 ", "a", 1 , c "2", 3, "fix thing", "human", None, 1 rate, total = metrics.rework commits, 7 self.assertEqual rate "a" , total "a" , 1.0, 1 def test fix after window is ignored self : commits = c "1", 0, "feat 1 ", "a", 1 , c "2", 9, "fix thing", "human", None, 1 self.assertEqual metrics.rework commits, 7 0 "a" , 0.0 def test unlinked fix is not guessed self : commits = c "1", 0, "feat 1 ", "a", 1 , c "2", 1, "fix thing", "human" self.assertEqual metrics.rework commits, 7 0 "a" , 0.0 class ConfigTests unittest.TestCase : def test weights must sum to one self : with self.assertRaises ValueError : config.check weights {"speed": 0.5, "quality": 0.6} if name == " main ": unittest.main High rework on a single tool often points at a review gap more than at the tool itself. Teams that see it should consider adding automated review in front of human review, and the AI code review tools comparison https://shattered.io/ai-code-review-tools-bugbot-vs-codex-2026/ lists options with their trade-offs. Step 12: Stress-test the weights and make the call A ranking that flips when you nudge the weights is not a ranking. Run the same data through several weight profiles and see which conclusions survive. The CLI takes an override for exactly this. bash $ python3 -m pilot --weights speed=0.6,quality=0.1,cost=0.2,survey=0.1 $ python3 -m pilot --weights speed=0.1,quality=0.2,cost=0.6,survey=0.1 $ python3 -m pilot --weights speed=0.1,quality=0.6,cost=0.2,survey=0.1 The four profiles produced this on the synthetic data: | Profile speed/quality/cost/survey | 1st | 2nd | 3rd | 4th | |---|---|---|---|---| | Default 0.25/0.35/0.25/0.15 | Not listed | assistant-c 50.0 | Not listed | assistant-d 44.0 | | Speed-heavy 0.6/0.1/0.2/0.1 | assistant-c 80.0 | Not listed | assistant-d 34.0 | Not listed | | Cost-heavy 0.1/0.2/0.6/0.1 | Not listed | assistant-c 70.0 | Not listed | Not listed | | Quality-heavy 0.1/0.6/0.2/0.1 | Not listed | Not listed | Not listed | assistant-c 30.0 | Assistant-a wins three of four profiles and comes second in the fourth, so the conclusion survives. Assistant-c wins only when speed dominates and collapses to last when quality dominates. That pattern tells you to avoid weighing it on one dimension. Assistant-b finishes last in two profiles because it is the slowest to merge and the most expensive per PR, so a negotiated price would help it only on the cost axis. Real pilots produce this kind of structure, and the stress test is how you read it. Add a statistical sanity check before you sign anything. The bootstrap helper in the advanced tips resamples each tool’s lead times and reports a 90% interval for the median. On the fixture, the intervals for assistants a, b, c and d overlap heavily, while all four sit clear of the human baseline. The honest reading: every assistant beat the baseline on speed, and the order among them is not settled by speed alone. Turn the numbers into a decision about which of the best AI coding assistants earns seats, using rules written in advance: | Situation | Action | Reason | |---|---|---| | One tool wins 3 of 4 weight profiles with 8+ PRs | Buy seats for the whole team | The result is stable under reasonable priorities | | Two tools trade first place by profile | Split by role or task type and re-pilot | The weights, not the data, decide the winner | | A tool has fewer PRs than the floor | Extend the pilot two weeks | Too few samples to rank | | A tool is fastest but has the highest rework | Pair it with automated review before adopting | Speed gains vanish if work returns as fixes | | No tool beats the human baseline on rework | Keep the pilot small and revisit next quarter | The assistants add cost without quality gain | Whatever you choose, schedule a re-run. Models change every few weeks, and a tool that lost in October can win in December. Keep the project in the repository, rerun it each quarter on fresh data and compare reports. More coverage of coding tools and the editors they live in sits in the software and developer tools section https://shattered.io/category/software/ . Common pitfalls that skew an AI coding assistant pilot Most failed pilots fail on measurement design, not on tooling. Teams comparing the best AI coding assistants tend to repeat the same mistakes. 1. Letting developers choose the tool per task. People reach for the assistant they trust on the easy ticket and the human lane on the hard one. Rotate assignments on a schedule, so each tool sees a mix of work. 2. Skipping the baseline. Without a human-only row, a 16-hour median lead time means nothing. The baseline turns “fast” into “faster than we were”. 3. Changing the weights after seeing results. Weights encode priorities. Set them in the first week and commit the file, so any later change goes through review. 4. Ranking on a thin sample. Six pull requests cannot support a verdict. Keep the minimum-sample warning on, and extend the pilot instead of ranking noise. 5. Counting seat price only. Usage charges can match or exceed the seat fee. Copilot’s newest models bill at provider-list pricing, Codex usage counts against plan limits, and Cursor’s third-party model billing is shifting. Put every charge in the ledger. 6. Ignoring mid-pilot model changes. A default model swap is a new tool. Log versions weekly and flag any week where the default changed. 7. Mixing PR size with speed. Smaller pull requests merge faster. Compare changed lines per PR across tools before you credit a tool for lead time. 8. Measuring only developers who volunteer. Enthusiasts produce cheerful surveys and good numbers. Include at least one skeptic per tool. Troubleshooting Each item below lists the symptom, the likely cause and the fix. 1. ValueError: weights must sum to 1.0. The four weights in the config or the override add up to something else, and the message prints the actual total. Adjust them until they total exactly 1.0. 2. ModuleNotFoundError: No module named ‘tomllib’. You are on Python 3.10 or older. The standard library added tomllib in 3.11, so upgrade the interpreter. 3. Every commit lands in the human bucket. The hook never ran. Check that it is executable, that it sits at .git/hooks/prepare-commit-msg without a file extension, that git config --get pilot.tool prints a value, and that core.hooksPath does not point somewhere else. 4. The report is empty or misses early weeks. A shallow clone drops history. Run git fetch --unshallow , and confirm the since date in the config predates the pilot start. 5. Trailers vanish after merge. Squash merges rewrite messages. Check your repository’s squash message setting on a test PR, and fall back on PR labels, which survive squashing, when commit trailers do not. 6. gh: command not found or authentication errors. Install the GitHub CLI and run gh auth login , then confirm with gh auth status . Enterprise servers need the --hostname flag. 7. The PR export has fewer rows than expected. gh pr list returns 30 results by default. Raise --limit , and confirm the --search date filter matches your pilot start. 8. A tool is missing from the table. The slug in costs.csv does not match the label. claude code and claude-code are different keys, and the CLI drops tools that lack either cost or PR data. Standardize on lowercase with hyphens. 9. Rework shows 0% for everyone. Either commit subjects lack the N suffix or developers write Closes N instead of Fixes: N . Pick one convention, and add a CI check that rejects a “fix:” commit with no reference. 10. Every score reads 100.0. A tie on each metric normalizes to 1.0. That happens when you test with one tool, or when all tools have identical data. Add at least two tools with different numbers. 11. fromisoformat raises ValueError on a timestamp. GitHub returns a trailing Z that older Pythons reject. The code already swaps Z for +00:00, so check whether a custom export uses another format. 12. Cost per PR shows a huge number. The tool merged almost no pull requests, so a small count divides a full month’s spend. Treat it as a sample size warning, not a price. Advanced tips Once the basic run works, five additions make the pilot harder to argue with. Add confidence intervals. Resampling a tool’s lead times shows how much the median could move. Save the helper as pilot/bootstrap.py . It is short and uses only the standard library. python import random import statistics def median ci samples, rounds=2000, seed=7 : rng = random.Random seed meds = sorted statistics.median rng.choices samples, k=len samples for in range rounds return meds int rounds 0.05 , meds int rounds 0.95 On the fixture it printed these 90% intervals for median lead time: assistant-a n=14 median 16.5 h 90% interval 14.1 to 20.0 assistant-b n=12 median 21.6 h 90% interval 14.4 to 25.6 assistant-c n=11 median 11.8 h 90% interval 9.0 to 19.7 assistant-d n=6 median 18.8 h 90% interval 14.4 to 23.9 human n=20 median 31.6 h 90% interval 28.1 to 32.8 The assistants overlap one another and sit apart from the baseline, which is exactly the claim the data supports. Slice by task type. Add labels such as bug , feature and refactor , then group the metrics by tool and label. A tool that struggles on refactors may shine on bug fixes, and a split purchase is sometimes the right answer. Log the model. Create a models.log with one line per tool per week holding the version string and the default model. Most CLIs print both. When a result looks odd, the log tells you whether the tool changed that week. Measure review effort. Export the reviews field from gh pr list and count review rounds per pull request. A tool that merges quickly but needs three review cycles moves cost from the author to the reviewer. Add a zero-cost control. If seat prices dominate your cost numbers, add a self-hosted lane. The Aider local assistant setup https://shattered.io/aider-local-ai-coding-assistant-ollama-git-2026/ shows how to run an open-source tool against a local model. Give it its own trailer and label, and the same report scores it next to the paid tools. FAQ What are the best AI coding assistants in 2026? The answer depends on your code and your team, which is why a measured pilot beats a ranking. Claude Code, OpenAI Codex, GitHub Copilot and Cursor are the four most common contenders, and each made product or model announcements in October 2026. Run the pilot on your repository and let the numbers decide. How long should an AI coding assistant pilot run? Four weeks is a practical minimum. That period usually yields 8 or more merged pull requests per tool when each developer rotates through all of them. Extend the pilot if any tool falls below the minimum sample. Can I run the pilot without GitHub? Yes. The git history side works on any host. Replace the gh export with a script that writes the same JSON shape number, title, createdAt, mergedAt, labels from the GitLab or Bitbucket API, and the rest of the project runs unchanged. Why does the project use trailers instead of author names? Developers switch tools during a week, and author names say nothing about which assistant helped. A trailer follows the commit itself, and git can read it natively without parsing the message body. Does a higher score mean the tool writes better code? No. The score blends speed, rework, cost and sentiment, which describe how a tool performs inside your workflow. It does not grade code quality directly. Use rework as the closest proxy and read the diffs of a few merged pull requests yourself. How do I handle tools that change pricing mid-pilot? Record the new price from the week it takes effect, and note the change in the pilot log. Cursor, for example, reportedly moved to new subscription tiers on October 12, so a pilot spanning that date should split its ledger rows at that point. Should the survey carry more weight? Raise it only if adoption is your main risk. A tool nobody wants to use produces no value even when the numbers look good. Otherwise keep it at 15% or lower, since sentiment tracks enjoyment as much as results. How often should I rerun the pilot? Quarterly is reasonable, and sooner when a vendor swaps its default model or changes pricing. Rerunning is cheap once the hook and labels are in place, since the project reads the same files each time.