agents: add simplification before fanout verification (#3892)
diff --git a/.claude/agents/code-simplifier.md b/.claude/agents/code-simplifier.md
new file mode 100644
index 0000000..455fe65
--- /dev/null
+++ b/.claude/agents/code-simplifier.md
@@ -0,0 +1,17 @@
+---
+name: code-simplifier
+description: Run bundled /simplify on assigned changes before verification.
+tools: Bash, Read, Edit, Write, Grep, Glob, Skill, Agent
+model: opus
+effort: xhigh
+---
+
+Invoke bundled `/simplify` once through `Skill`, passing the task, writer notes, and assigned scope. Require preservation of unrelated work and input contracts; do not stage, commit, or push.
+
+## Output contract
+
+Your final message is parsed by a program. Return ONLY this JSON — no prose, no code fences:
+
+{"changed": false, "files": [], "summary": "No useful simplification found."}
+
+Set `changed` only when files were edited; `files` uses repo-relative paths; `summary` covers fixes, skips, and checks the caller must rerun.
diff --git a/.claude/workflows/fanout-dev.js b/.claude/workflows/fanout-dev.js
index c1458a7..a37c7c1 100644
--- a/.claude/workflows/fanout-dev.js
+++ b/.claude/workflows/fanout-dev.js
@@ -1,9 +1,10 @@
 export const meta = {
   name: 'fanout-dev',
-  description: 'Implement one described change across many ports/file-sets: one code-writer worker per item, independent builder verification, optional review',
+  description: 'Implement one described change across many ports/file-sets: writers, one combined simplification pass, independent builder verification, optional review',
   whenToUse: 'Applying a fix or pattern across multiple TinyUSB ports (e.g. the same DCD bug in several drivers)',
   phases: [
-    { title: 'Implement', detail: 'code-writer per item (opus xhigh)' },
+    { title: 'Implement', detail: 'code-writer per item' },
+    { title: 'Simplify', detail: 'one pass after all shared-checkout writers finish' },
     { title: 'Verify', detail: 'builder single-example check' },
     { title: 'Review', detail: 'optional code-verifier pass' },
   ],
@@ -17,7 +18,7 @@
 const boardFor = (item) =>
   typeof args.board === 'string' ? args.board : (args.board && args.board[item]) || null
 const short = (s) => s.replace(/\/+$/, '').split('/').slice(-2).join('/')
-if (args.worktree) log('worktree mode: independent builder verification and review skipped (workers verify inside their own worktrees)')
+if (args.worktree) log('worktree mode: combined simplification, independent builder verification and review deferred until integration (workers verify inside their own worktrees)')
 
 const DEV = {
   type: 'object', additionalProperties: false,
@@ -61,7 +62,16 @@
   },
 }
 
-const results = await pipeline(
+const SIMPLIFY = {
+  type: 'object', additionalProperties: false,
+  required: ['changed', 'files', 'summary'],
+  properties: {
+    changed: { type: 'boolean' }, files: { type: 'array', items: { type: 'string' } },
+    summary: { type: 'string' },
+  },
+}
+
+const devs = await pipeline(
   args.items,
 
   item => agent(
@@ -75,12 +85,40 @@
       ...(args.worktree ? { isolation: 'worktree' } : {}),
     },
   ),
+)
 
-  (dev, item) => {
+const summarize = (rows, buildClean) => {
+  const dropped = args.items.length - rows.length
+  if (dropped > 0) log(`${dropped} item(s) dropped (worker died)`)
+  log(`${rows.length}/${args.items.length} items completed; ${rows.filter(buildClean).length} build-clean`)
+  return rows
+}
+
+// worktree mode: edits live in each worker's own worktree; neither an independent
+// verifier nor one combined simplifier in the shared tree can see them — trust
+// dev.buildOk and leave both to integration.
+if (args.worktree) return summarize(devs.filter(Boolean), r => r.buildOk)
+
+const live = args.items.filter((_, index) => devs[index])
+if (live.length === 0) return summarize([], r => r.verifyBuild === true)
+
+const simplification = await agent(
+  `Simplify the completed changes for this task:\n${args.task}\n\n` +
+  `Assigned scopes: ${JSON.stringify(live)}. Touch nothing outside them.\n` +
+  `Writer notes: ${JSON.stringify(devs.filter(Boolean).map(dev => ({ item: dev.item, notes: dev.notes })))}\n` +
+  'All writers have finished. Inspect staged and unstaged changes and task-owned untracked files. ' +
+  'Make one behavior-preserving pass; no changes is success. Independent builds and optional review follow.',
+  { label: 'simplify', phase: 'Simplify', agentType: 'code-simplifier', schema: SIMPLIFY },
+)
+if (!simplification) throw new Error('simplifier failed — inspect possible partial edits before retrying')
+log(`simplify: ${simplification.changed ? simplification.files.join(', ') : 'no changes'} — ${simplification.summary}`)
+
+const results = await pipeline(
+  args.items,
+
+  (item, _item, index) => {
+    const dev = devs[index]
     if (!dev) return null
-    // worktree mode: edits live in the worker's own worktree; an independent
-    // verifier in the shared tree cannot see them — trust dev.buildOk.
-    if (args.worktree) return dev
     return agent(
       `Build the single example device/cdc_msc for board ${dev.board}. Use a unique build dir (mktemp -d) to avoid collisions with parallel builds.`,
       { label: `verify:${short(item)}`, phase: 'Verify', agentType: 'builder', schema: BUILD },
@@ -92,9 +130,9 @@
   },
 
   (r, item) => {
-    if (!r || !args.review || args.worktree) return r
+    if (!r || !args.review) return r
     return workflow('code-verify', {
-      prompt: `Review the uncommitted change in ${item} (inspect with: git diff -- ${item}) against this task:\n${args.task}\n` +
+      prompt: `Review the uncommitted change in ${item} (inspect staged and unstaged changes with git diff HEAD -- ${item}, and read task-owned untracked files) against this task:\n${args.task}\n` +
       'Dimension: does the diff correctly and completely implement the task with no unintended side effects? Coverage-first findings.',
       label: `review:${short(item)}`,
       schema: FINDINGS,
@@ -106,8 +144,4 @@
   },
 )
 
-const done = results.filter(Boolean)
-const dropped = args.items.length - done.length
-if (dropped > 0) log(`${dropped} item(s) dropped (worker died)`)
-log(`${done.length}/${args.items.length} items completed; ${done.filter(r => r.buildOk && r.verifyBuild !== false).length} build-clean`)
-return done
+return summarize(results.filter(Boolean), r => r.verifyBuild === true)
diff --git a/.claude/workflows/test/test-code-verify.mjs b/.claude/workflows/test/test-code-verify.mjs
index d5f63ef..1a8a46c 100644
--- a/.claude/workflows/test/test-code-verify.mjs
+++ b/.claude/workflows/test/test-code-verify.mjs
@@ -293,5 +293,86 @@
   assert.deepEqual(seen[0].args.boards, ['test'])
 })
 
+await check('fanout simplifies once after all writers and before verification', async () => {
+  const src = readFileSync(new URL('../fanout-dev.js', import.meta.url), 'utf8').replace(/^export /m, '')
+  const fn = new AsyncFunction(
+    'args', 'agent', 'pipeline', 'parallel', 'phase', 'log', 'workflow', 'budget', src)
+  const pipeline = (items, ...stages) => Promise.all(items.map(async (item, index) => {
+    let value = item
+    for (const stage of stages) value = await stage(value, item, index)
+    return value
+  }))
+  const runFanout = async ({ worktree = false, deadWriter = false, deadSimplifier = false,
+    deadBuilder = false } = {}) => {
+    const events = []
+    const logs = []
+    const scopes = []
+    const agent = async (prompt, options) => {
+      if (options.agentType === 'code-writer') {
+        const item = options.label.slice(4)
+        if (item === 'a') await new Promise(resolve => setImmediate(resolve))
+        events.push(`wrote:${item}`)
+        if (deadWriter && item === 'a') return null
+        assert.equal(options.isolation, worktree ? 'worktree' : undefined)
+        return { item, board: item, buildOk: true, diffstat: '', notes: `note:${item}` }
+      }
+      if (options.agentType === 'code-simplifier') {
+        assert.deepEqual([...events].sort(), ['wrote:a', 'wrote:b'])
+        scopes.push(prompt.match(/Assigned scopes: (\[[^\]]*\])/)[1])
+        // writer results reach the simplifier as notes only, not build metadata
+        assert.match(prompt, /Writer notes: .*note:b/)
+        assert.doesNotMatch(prompt, /buildOk/)
+        events.push('simplified')
+        return deadSimplifier ? null : { changed: true, files: ['b/file.c'], summary: 'tidied' }
+      }
+      assert.equal(options.agentType, 'builder')
+      assert.equal(events.filter(e => e === 'simplified').length, 1)
+      events.push(options.label)
+      return deadBuilder ? null : { pass: true }
+    }
+    const workflow = async (name, args) => {
+      assert.equal(name, 'code-verify')
+      assert.ok(events.includes(args.label.replace('review:', 'verify:')))
+      assert.match(args.prompt, /git diff HEAD/)
+      events.push(args.label)
+      return { findings: [] }
+    }
+    const result = await fn({ task: 'fix', items: ['a', 'b'], review: true, worktree },
+      agent, pipeline, null, () => {}, message => logs.push(message), workflow, null)
+    return { result, events, logs, scopes }
+  }
+
+  const { result, events, logs, scopes } = await runFanout()
+  assert.equal(events.filter(e => e === 'simplified').length, 1)
+  assert.deepEqual(scopes, ['["a","b"]'])
+  assert.equal(result.length, 2)
+  for (const row of result) {
+    assert.equal(row.verifyBuild, true)
+    assert.deepEqual(row.review, [])
+    // the run-level simplification is logged once, not stamped on every row
+    assert.equal('simplification' in row, false)
+  }
+  assert.ok(logs.some(message => /^simplify: b\/file\.c — tidied$/.test(message)))
+  assert.match(logs.at(-1), /2\/2 items completed; 2 build-clean/)
+
+  // a dead writer drops its own item; the survivors are still simplified and verified
+  const partial = await runFanout({ deadWriter: true })
+  assert.deepEqual(partial.scopes, ['["b"]'])
+  assert.deepEqual(partial.result.map(row => row.item), ['b'])
+  assert.ok(partial.logs.some(message => /1 item\(s\) dropped/.test(message)))
+  assert.match(partial.logs.at(-1), /1\/2 items completed; 1 build-clean/)
+
+  await assert.rejects(runFanout({ deadSimplifier: true }), /simplifier failed/)
+
+  const isolated = await runFanout({ worktree: true })
+  assert.deepEqual(isolated.events.sort(), ['wrote:a', 'wrote:b'])
+  assert.match(isolated.logs[0], /deferred until integration/)
+  assert.match(isolated.logs.at(-1), /2\/2 items completed; 2 build-clean/)
+
+  const unverified = await runFanout({ deadBuilder: true })
+  assert.ok(unverified.result.every(row => row.verifyBuild === null))
+  assert.match(unverified.logs.at(-1), /0 build-clean/)
+})
+
 console.log(failed ? `\n${failed} FAILED` : '\nall checks passed')
 process.exit(failed ? 1 : 0)
diff --git a/.codex/agents/code-simplifier.toml b/.codex/agents/code-simplifier.toml
new file mode 100644
index 0000000..e32fab7
--- /dev/null
+++ b/.codex/agents/code-simplifier.toml
@@ -0,0 +1,43 @@
+name = "code-simplifier"
+description = "Review changed code for reuse, simplification, efficiency, and altitude cleanup, then apply the fixes."
+model = "gpt-5.6-sol"
+model_reasoning_effort = "xhigh"
+developer_instructions = """
+Improve the quality of the changed code. Correctness review belongs to the subsequent verification stage.
+
+Work within the assigned task-owned changes. Read the original requirements and writer notes, preserve unrelated work, and never stage, commit, push, or invoke Claude. Do not infer narrower input contracts from test coverage; skip changes whose equivalence is uncertain.
+
+## Phase 0 — Gather the diff
+
+Run `git diff @{upstream}...HEAD` (or compare against the repository's main/master branch, then `git diff HEAD~1` if no base branch exists) to get the unified diff. If there are uncommitted changes, or the range diff is empty, also run `git diff HEAD` and include task-owned working-tree changes. Read task-owned untracked files directly. If a PR number, branch, or file path was supplied, review that target instead. Restrict the resulting review scope to the assigned scopes.
+
+## Phase 1 — Review (4 independent cleanup agents)
+
+Launch four read-only review agents concurrently through the available agent-spawning tool. Give each the diff, task context, the constraints above, and one angle below; they must not edit files or spawn more agents. Each returns findings with `file`, `line`, a one-line `summary`, and the concrete cost: what is duplicated, wasted, or harder to maintain.
+
+### Reuse
+
+Flag new code that reimplements something the codebase already has. Search shared utilities and code adjacent to the change, and name the existing helper to call instead.
+
+### Simplification
+
+Flag unnecessary complexity the diff adds: redundant or derivable state, copy-paste with slight variation, deep nesting, and dead code left behind. Name the simpler form that does the same job.
+
+### Efficiency
+
+Flag wasted work the diff introduces: redundant computation or repeated I/O, independent operations run sequentially, and blocking work added to startup or hot paths. Also inspect long-lived closures or captured environments that retain unnecessary enclosing state; prefer an object that retains only the needed fields. Name the cheaper alternative.
+
+### Altitude
+
+Check that each change fixes the root cause at the right depth rather than patching a symptom. Special cases layered on shared infrastructure can indicate a fix belongs deeper. Prefer the simpler, more general change to the underlying mechanism over adding special cases, and name that change.
+
+## Phase 2 — Apply the fixes
+
+Wait for all four reviews to complete, deduplicate findings that point at the same line or mechanism, and apply the remaining fixes directly. Skip findings whose fixes would change intended behavior, exceed the assigned scope, require changes well outside the reviewed diff, or are false positives. Note skips rather than arguing with them. If a reviewer fails, report the incomplete coverage in the summary rather than claiming all four reviews completed.
+
+Return ONLY this JSON — no prose, no code fences:
+
+{"changed": false, "files": [], "summary": "No useful simplification found."}
+
+Set `changed` only when you edited files; `files` uses repository-relative paths; `summary` covers fixes, skips, and checks the caller must rerun. Final verification belongs to the caller.
+"""
diff --git a/CLAUDE.md b/CLAUDE.md
index ec43f47..9536e2d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -24,6 +24,7 @@
 
 - Keep `CLAUDE.md` and `.claude/{agents,skills,workflows}` canonical; preserve `AGENTS.md -> CLAUDE.md` and `.agents -> .claude`.
 - Use `.codex/agents/<role>.toml` to load `.claude/agents/<role>.md`; keep adapters thin and never duplicate role bodies.
+- Exception: `code-simplifier` wraps bundled `/simplify` in Claude; Codex keeps its standalone equivalent in TOML.
 - Use `/codex:review` for independent read-only review, `/codex:adversarial-review` to challenge a design, and `/codex:rescue` for bounded implementation or diagnosis.
 - Concurrent writers need separate worktrees; otherwise yield the worktree until delegated edits finish.
 - Keep orchestration in `.claude/workflows/`. Use `code-verify` with `provider: 'codex'` (default), `'claude'`, or `'both'`; `validate`/`full-check` use `reviewProvider`. Keep workflow nesting to one level and the Codex subprocess in `.claude/agents/codex-code-verifier.md`.