Skip to content

🤖 feat: mutation gate (make mutate-check, Quality / Mutation) on unclebob/mutator - #7148

Merged
ThomasK33 merged 17 commits into
gauntlet/dry-gatefrom
gauntlet/mutation-gate
Oct 22, 2026
Merged

ThomasK33 merged 17 commits into
gauntlet/dry-gatefrom
gauntlet/mutation-gate

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a mutation gate. make mutate-check (and a warn-only Quality / Mutation CI job) makes small changes ("mutants") in the functions a PR changed, such as === to !==, > to >= or ?? to ||. It then lists the mutants that no unit test catches. The mutation sites come from Uncle Bob's mutator. Xum runs the mutants itself.

Stack 2 of 3, on #7147.

Background

A mutant that survives means no test checks that behavior. The first run found a real gap: parseGoalTurnCapInput had no direct test, and all 5 of its mutants survived. #7151 adds that test.

Implementation

  • Why not the upstream runner: the mutator command fails on this repo with ENAMETOOLONG, because its import scan reads a string in a source file as a path. It also follows only relative imports, so with Xum's @/ imports a mutant can run against the original file. scripts/mutate/mutate_report.py uses mutator's discover_raw/apply_site with UTF-8 byte offsets and runs them in its own workers.
  • Workers: each worker owns a private copy of the tracked files, with node_modules linked and src/version.ts generated. The real checkout is never touched.
  • Tests per mutant: the colocated test runs first, then the nearest other unit-lane tests that import the file, at most 10. Each test file runs in its own bun test process. Several Xum test files in one process break each other on shared temp config. A failing file or a timeout (10× the file's baseline, at least 2 s) kills the mutant. The runner kills the whole process group.
  • Baselines: each test file must pass alone first. A file that fails is dropped and listed, and the run exits 3.
  • Crash handling: a crash in one mutant becomes one error row. If the crash left the worker's copy mutated, the runner retires that worker.
  • CI: the job waits for Test / Unit and reuses its LCOV, so sites on lines no unit test ran are skipped. The job stops at 300 mutants or 15 minutes. The deadline also cuts a running test, and such a mutant reports not-run. Required does not list the job.

Validation

On the real repo:

  • I touched parseGoalTurnCapInput. Exactly its 5 mutants survived, with exit 1.
  • With a test added, all 5 were killed, with exit 0.
  • With a broken test, the report said BASELINE FAILED, with exit 3.
  • I touched agentMessageEnvelope.ts, which 10 unit tests import. Its 4 mutants were killed in 54 s at a host load average of about 200.

The end-to-end tests in throwaway repos run real bun processes, including an @/ import kill. I confirmed that the crash tests and the survivor test fail when the code under test is broken.

Risks

Low for merges, because the job only warns and Required does not wait for it. The job adds runner minutes on every PR that changes src/ or CI config.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $12.44

…ob/mutator

Runs mutator's mutation sites in the changed functions against the Bun
unit tests that import the file (colocated first, nearest next, at most
10), each test file in its own process, in private copies of the tracked
files. Warn-only. CI reuses the Test / Unit LCOV to skip uncovered sites
and stops at 300 mutants or 15 minutes.
@ThomasK33
ThomasK33 added this pull request to stack #7150 October 22, 2026 06:40
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T16:10:34.238293Z 3273eb5 New commits
🔒 Security Review ✅ Completed 2026-10-22T16:06:38.663892Z 3273eb5 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ThomasK33 ThomasK33 changed the title gauntlet/mutation gate 🤖 feat: mutation gate (make mutate-check, Quality / Mutation) on unclebob/mutator Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

🟢 e2e stories (Haiku): 8 passed

21 agent steps · 7 replayed from cache · 24 model calls · 196.0k tokens (74% cached)
Spend: $0.01 of the $1.00 cap (proxy ledger)

All 8 tests in 8 files
Test Agent Time
🟢 chatRestart.e2e.ts · 1 passed 2 steps · 4 calls 24.9s
🟢 a chat survives a restart of the server 2 steps · 4 calls 24.9s
🟢 compaction.e2e.ts · 1 passed 3 steps · 7 calls 44.9s
🟢 /compact replaces the old chat with a summary 3 steps · 7 calls 44.9s
🟢 pluginTrustRestart.e2e.ts · 1 passed 4 steps · 2 calls 11.3s
🟢 a trusted plugin hook stays trusted after the server restarts 4 steps · 2 calls 11.3s
🟢 settingsRoundTrip.e2e.ts · 1 passed 3 steps · 4 calls 29.6s
🟢 a chat survives trips through Settings 3 steps · 4 calls 29.6s
🟢 sidebarPerWorkspace.e2e.ts · 1 passed 4 steps · 1 call 4.6s
🟢 a new workspace starts with its right sidebar collapsed while another keeps it open 4 steps · 1 call 4.6s
🟢 smoke.e2e.ts · 1 passed 2 steps · 1 call 2.8s
🟢 the agent opens the playground workspace 2 steps · 1 call 2.8s
🟢 streamErrors.e2e.ts · 1 passed 1 step · 1 call 7.7s
🟢 stream errors show in the chat, and the chat recovers 1 step · 1 call 7.7s
🟢 todoNudge.e2e.ts · 1 passed 2 steps · 4 calls 25.9s
🟢 the agent finishes open TODO items after the turn-end nudge 2 steps · 4 calls 25.9s

e2e 0.20.0 · 2m 35s · web · run artifacts

🟢 e2e Electron stories (Haiku): 3 passed

3 agent steps · 3 model calls · 8.0k tokens (32% cached)
Spend: $0.00 of the $1.00 cap (proxy ledger)

All 3 tests in 3 files
Test Agent Time
🟢 electron/deepLink.e2e.ts · 1 passed 1 step · 1 call 5.8s
🟢 a deep link of a second launch starts a chat in the running app 1 step · 1 call 5.8s
🟢 electron/menuBar.e2e.ts · 1 passed 1 step · 1 call 6.4s
🟢 the workspace menu bar is the desktop title bar 1 step · 1 call 6.4s
🟢 electron/smoke.e2e.ts · 1 passed 1 step · 1 call 6.3s
🟢 Xum Electron opens the playground workspace 1 step · 1 call 6.3s

e2e 0.20.0 · 21.1s · electron · run artifacts

🟢 e2e agent repros (Haiku): no tests selected

Spend: $0.00 of the $1.00 cap (proxy ledger)

e2e 0.20.0 · 6.4s · 2 targets (web, phone) · run artifacts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93f248ff2a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Makefile
Comment thread scripts/mutate/ci.ts Outdated
Comment thread scripts/mutate/ci.ts
Comment thread scripts/mutate/mutate_report.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1011048724

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/mutate/mutate_report.py Outdated
Comment thread scripts/mutate/ci.ts
Comment thread scripts/mutate/mutate_report.py Outdated
Comment thread scripts/mutate/mutate_report.py
… baseline deadline)

Review round 2 on #7148:
- The import graph parses each test with tree-sitter, so a type-only
  import, a comment or a string fixture is no importer (1,523 of 7,842
  edges on the repo were phantoms).
- A site that a no-coverage test (test-unit-ci.sh --list-no-coverage)
  can reach is not skipped as uncovered.
- The mutant cap applies before baselines, and a baseline gets only the
  time left before the run's deadline.
- node_modules is a directory link, and a timeout kills bun on Windows.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b575414ad2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/mutate/mutate_report.py
Comment thread scripts/mutate/mutate_report.py
Comment thread scripts/mutate/ci.ts
…ummary)

Review round 3 on #7148:
- mutate-check refuses Windows up front with a tool error: run_test kills
  process groups and the workers run bash helpers. This replaces the
  partial Windows fallbacks from round 2.
- The CI summary lists each error row with its path, line and reason.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 991dfe6641

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/mutate/mutate_report.py
…workers

Review round 4 on #7148: the None slot of a retired worker let an idle
thread mark the pending sites as errors faster than the healthy workers
ran them. Now None enters the queue only when the last worker retires.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 72be907090

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/mutate/mutate_report.py Outdated
Comment thread scripts/mutate/mutate_report.py
@ThomasK33

Copy link
Copy Markdown
Member Author

The maintainer approved one more review round past the cap (Option A). The next push fixes the open #7147 finding (a function rewritten line for line keeps its DRY base form) and merges it up the stack. A finding in this round stops the loop.

@ThomasK33

Copy link
Copy Markdown
Member Author

The maintainer approved one manual review request past the cap. A finding stops the loop.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: e3f76266d3

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e3f76266d3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/mutate/mutate_report.py
@ThomasK33

Copy link
Copy Markdown
Member Author

Review loop stopped. The approved manual round on e3f7626 found one new P2: with full CI coverage, a site whose anchor line has zero hits is skipped as uncovered, even where Bun 1.4.3 reports zero hits falsely. The CRAP gate already flags that case as suspect, but the mutation gate does not reuse that check. The mutation gate only warns by default, and Required does not wait for it. I did not fix it, because a finding in this round ends the loop. The thread stays open for the maintainer's decision.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

# Conflicts:
#	docs/AGENTS.md
#	src/node/services/agentSkills/builtInSkillContent.generated.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3273eb5692

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/mutate/mutate_report.py
@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 22, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Oct 22, 2026
@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit e139933 Oct 22, 2026
78 of 103 checks passed
@ThomasK33
ThomasK33 deleted the gauntlet/mutation-gate branch October 22, 2026 17:05
asm pushed a commit to asm/mux that referenced this pull request Oct 22, 2026
…oder#7147)

## Summary

Adds a DRY gate next to the CRAP gate. `make dry-check` (and a new
warn-only `Quality / DRY` CI job) reports near-duplicate functions that
a change **creates**. It compares each changed function with every
production function in `src/`, using structural fingerprints from Uncle
Bob's [dryer](https://github.com/unclebob/dryer). A pair that already
matched on main stays quiet.

Stack 1 of 3. coder#7148 adds the mutation gate, and coder#7149 adds accept
markers and an opt-in block mode.

## Background

Uncle Bob's "gauntlet" runs three tools: crapper (complexity ×
coverage), dryer (duplication) and mutator (mutation testing). Xum
already runs Crapper (`scripts/crap/`). This PR adds dryer, and coder#7148
adds mutator.

## Implementation

- `scripts/dry/dryer_report.py` (uv script, pins dryer `66ff6d2`, plus
the Crapper and grammar pins that CRAP uses). Upstream `dryer` compares
every function with every other one, and that run did not finish in 15
minutes on `src/` (about 14,000 functions). This script compares only
the changed functions with all the others. Extraction takes about 19 s
of CPU, plus about 0.13 s per changed function.
- **New-pair rule:** a pair counts only when the diff made it. The
script finds each changed function's base version through git's `-U0`
hunks, not by name. A function made only of added lines has no base
version. Matching by name hid a new copy that shared a name with an old
match, for example two `render` methods. Regression test: `a new copy
that shares a name with an old match is a finding`.
- `scripts/crap/changes.ts`: the diff code moves out of `crap/check.ts`
so both gates agree on what "changed" means. CRAP behavior does not
change: its 61 script tests pass unchanged.
- `Makefile`: `dry-check`, `test-dry-scripts` (in `static-check-full`),
and a shared `tree-sitter-grammars` download step with retries.
- `.github/workflows/pr.yml`: `Quality / DRY`. It needs only `changes`,
so it starts at once. Every step is soft. `Required` does not list it.
- `.xum/skills/gauntlet/SKILL.md` and one AGENTS.md line: how to read
and repair findings.

## Validation

On the real repo:
- I copied `centsBucket` as `feeBucket` in `workspaceGoalService.ts`.
`make dry-check` reported 2 warnings at score 1.00, with exit 1.
- I edited only `centsBucket`. The check passed, because that pair is
old.

I confirmed that the new-pair test and the same-name test fail on the
old rule and pass on the new one. A 200-function sample at the 0.82
threshold found that 7.5% of functions have a near-twin.

## Risks

Low. The gate only warns and lives in `scripts/`. The CRAP refactor is
the one shared path, and its tests cover it.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking:
`high` • Cost: `$12.44`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high
costs=12.44 -->
asm pushed a commit to asm/mux that referenced this pull request Oct 22, 2026
…oder#7149)

## Summary

Adds accept markers and an opt-in block mode to the DRY and mutation
gates.

- `// dry-duplicate-ok: <reason>` in a changed function, or in the
comment lines right above it, accepts a duplicate pair.
- `// mutation-equivalent: <reason>` on a mutant's line, or right above
it, accepts a surviving mutant as equivalent.
- `DRY_MODE=block` and `MUTATION_MODE=block` (repo variables, default
warn) make unaccepted findings fail the job.

Stack 3 of 3, on coder#7148.

## Background

Some findings are correct but on purpose. Two parsers can share a shape
and still need to change apart, and a mutant like `m[2] ?? ""` to `m[2]
|| ""` is equivalent when `m[2]` is never `""`. Without a way to say so
next to the code, neither gate can block.

## Implementation

- `scripts/crap/markers.py`: one shared parser. A marker needs a reason.
A DRY marker counts only on a changed side, because a marker on an old
function would excuse every future copy of it. A mutation marker affects
only survivors: a killed mutant stays killed.
- **DRY block mode:** `Quality / DRY` gets a Verdict step and joins
`Required`, filtered by `DRY_MODE`, the same way `crap` is filtered by
`CRAP_MODE`. The job starts at once and takes a few minutes, so it adds
no time to `Required`.
- **Mutation block mode:** fails `Quality / Mutation` but stays out of
`Required`. The job starts after Test / Unit, which is the last job that
`Required` waits for (about 9.5 min). Listing it would add its whole
run, up to 15 minutes, to every PR. To gate merges on it later, add it
to `Required`'s `needs` and accept that wait.
- A tool error, a timeout, a baseline failure, an `error` row or a
mutant that did not run never blocks.

## Validation

- I marked the real equivalent mutant at `budgetParser.ts:42`. `make
mutate-check` reported `PASS, 7 killed, 1 equivalent`, with the reason.
- End-to-end tests cover each case: a DRY marker accepts both pairs, a
marker on the old side excuses nothing, and a mutation marker turns a
survivor into `equivalent`.
- I ran the `Required` jq filter by hand: `dry` drops out in warn mode
and counts in block mode.

## Risks

None until a maintainer sets a mode variable to `block`. Rollback is
setting it back to `warn`.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking:
`high` • Cost: `$12.44`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high
costs=12.44 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant