B4.run
Menu

Site

Essay · 23 min read

Build a code-fixing agent you can read

Follow a B4.run agent from a failing CLI test through a Docker workspace, independent checks, and approval of the exact change.

September 15, 2026

Learn how to build a code-fixing agent with TypeScript and B4.run.

Updated September 2026: The source links now point to commit b4084553 from September 20, and the published blueprint uses B4.run 0.10.0.

If you've written a Node.js application that reads files, runs a command and checks the result, you already know most of the pieces we'll use. The new part is letting a language model decide which operation to perform next.

In this article we'll follow an agent that fixes a small bug in a command-line application. We'll give it the source code and a failing test, let it make a change, and review the result before exporting it. Let's start with a few concepts, then work through the code.

What is an agent?Copy link to section: What is an agent?

For this application, an agent is a language model running in a loop with access to functions that can do work.

A model can generate text, including TypeScript. To change a file or run a test, it needs the application to do it. We expose those operations as tools.

A tool is a function the model can ask the application to call. For example, our readFile tool reads a file and returns its contents. A tool call includes the function's name and input, such as the path of the file to read.

Here is roughly how it works:

  1. We send the model our instructions and the user's request.
  2. The model requests a tool call, such as reading src/cli.ts.
  3. The runtime checks whether the call is allowed and executes the tool.
  4. The result is added to the conversation and sent back to the model.
  5. The model can request another tool or respond to the user.

That last step lets the agent work through a problem. It can read a file, see a failed test, inspect another file and then choose an edit. We don't have to predict every file it will need.

We do need to decide what it can do and how we'll check its work. A model that says "fixed" hasn't proven that our application works.

GoalsCopy link to section: Goals

Our code-fixing agent will:

  1. Reproduce a failing test before editing.
  2. Inspect and change the permitted TypeScript source.
  3. Run the test again.
  4. Verify the proposed change using a fresh copy of the project and additional tests.
  5. Wait for a person to approve exporting the change.

We'll use B4.run, a TypeScript framework for building agent applications, for the tool runtime, workspace lifecycle and approval handling. We'll write the application-specific rules ourselves.

Source codeCopy link to section: Source code

You can follow along with the completed example. The source links throughout this article point to that version, so you don't need to copy each code block. First we'll walk through how it works. At the end, we'll start the application and send it a request through its HTTP API.

The bug we're going to fixCopy link to section: The bug we're going to fix

Our sample is a small command-line application built with Commander, a library for parsing command names, arguments and options. It contains a real bug from an earlier B4.run fix.

The following command should run memory consolidation without changing any files:

bash
node --import tsx src/cli.ts memory consolidate --dry-run

Here, memory is the command, consolidate is its subcommand and --dry-run is an option for that subcommand. The --import tsx argument lets Node run the TypeScript entry point.

Instead, the command fails because the parser rejects --dry-run before our handler runs. Let's look at the registration in src/cli.ts:

ts
import { Command } from "commander"
import { runMemoryCommand } from "./memory.js"
 
const program = new Command().name("fixture")
program
  .command("memory [subcommand] [args...]")
  .description("Manage memory")
  .option("--cwd <path>", "App directory")
  .action(async (subcommand: string, args: string[], options: { cwd?: string }) => {
    await runMemoryCommand(subcommand, args, options)
  })
 
await program.parseAsync(process.argv)

Source: sample/project/src/cli.ts.

A few things to note:

  • We create a Command instance and name the program fixture.
  • The memory command accepts a subcommand and additional arguments.
  • The command defines --cwd, which selects the application's directory.
  • The action passes the parsed values to runMemoryCommand.

The problem occurs before that action receives the arguments, so testing runMemoryCommand directly would skip the parser and miss the defect. Our test starts the actual CLI process instead:

ts
import assert from "node:assert/strict"
import { spawnSync } from "node:child_process"
import test from "node:test"
 
test("documented dry-run flag reaches the handler", () => {
  const result = spawnSync(process.execPath, ["--import", "tsx", "src/cli.ts", "memory", "consolidate", "--dry-run"], { encoding: "utf8" })
  assert.equal(result.status, 0, result.stderr)
  assert.deepEqual(JSON.parse(result.stdout), { action: "consolidate", dryRun: true })
})

Source: sample/project/test/cli.test.ts.

First, spawnSync runs the command and waits for it to finish. Then we assert that its exit status is zero, meaning success. Finally, we parse the output and check that the handler reported dryRun: true.

We'll call this the visible test because the agent can read it in the project. The agent also needs to know which files it may change and which behavior it must preserve. We put those instructions in TASK.md:

md
# Repair CLI flag forwarding
 
The documented command `node --import tsx src/cli.ts memory consolidate --dry-run`
fails before the handler runs. Reproduce the failure with `npm test`, repair
the CLI registration, and verify it. Preserve memory-level `--cwd`, value-taking
`prune --cap`, and rejection of invalid arguments. Change only `src/cli.ts`.
Do not change tests, dependency versions, configuration, or the handler.

Source: sample/task.md.

That tells the agent it may edit one file: src/cli.ts. A repair must also preserve --cwd, numeric --cap values, rejection of invalid arguments and dry-run behavior.

Before we build the agent, let's look at the known fix so we understand what we're asking it to find. The reference patch adds two Commander settings:

diff
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,9 +1,10 @@
 import { Command } from "commander"
 import { runMemoryCommand } from "./memory.js"
 
-const program = new Command().name("fixture")
+const program = new Command().name("fixture").enablePositionalOptions()
 program
   .command("memory [subcommand] [args...]")
+  .passThroughOptions()
   .description("Manage memory")
   .option("--cwd <path>", "App directory")
   .action(async (subcommand: string, args: string[], options: { cwd?: string }) => {

Source: sample/reference.patch.

The parent enables positional options, and the memory command passes through subcommand arguments for the handler to interpret. The handler can then receive --dry-run and validate the arguments it understands.

We'll use this known repair later to test the application's plumbing. It stays outside the files the agent can see, so a real model has to inspect the broken source and choose an edit itself.

Define the agentCopy link to section: Define the agent

Next, let's open src/app/fix/index.ts.

B4.run uses the application directory to discover routes. This file defines the /fix agent, and the tools directory beside it holds that agent's own functions. We define the agent by passing a configuration object to agent():

ts
import { agent } from "@b4run/sdk"
 
export default agent({
  model: process.env.B4_CODE_FIXER_MODEL ?? "gpt-5-mini",
  recursionLimit: 60,
  description: "Repairs a failing test and verifies the change.",
  tools: { approve: ["exportForReview"] },
  systemPrompt: `You fix a focused code defect in the workspace project.
Read TASK.md and the verify-change skill. Reproduce the failure with npm test
before editing. Inspect the relevant source and make the smallest correct fix.
Use readFile, listDir, writeFile, and runBash. Use the commands documented in
TASK.md for diagnostics; inspect files rather than inventing other shell commands. Never change tests, configuration,
dependencies, or files outside the source paths specified in TASK.md.
Run npm test after the change and verify the task's preservation requirements,
not just its failing example. Explain what changed and what actually passed.
Then call prepareReview({}) to independently verify and inspect the exact candidate.
Call exportForReview({candidate}) with the complete candidate returned by prepareReview
to request runtime approval. Calling this tool
pauses BEFORE export and presents the human approval gate. Do not substitute
a prose confirmation question for the tool call; the runtime owns approval.
Do not claim success if a command failed or a test was not run.`,
})

Source: src/app/fix/index.ts.

Let's break down the configuration:

  1. model selects the language model. We default to gpt-5-mini and let an environment variable override it.
  2. recursionLimit bounds the runtime's execution steps across repeated model and tool interactions.
  3. description explains what this agent does.
  4. tools.approve names a tool that requires a person's approval before execution.
  5. systemPrompt supplies the instructions the model receives while working.

The prompt names four general-purpose tools: readFile, listDir, writeFile and runBash. They let the agent inspect files, make an edit and run npm test. It also names two tools from our application, prepareReview and exportForReview, which handle review and export.

The prompt tells the model to reproduce the failure before editing, but that is not a hard-coded sequence of calls. The model still chooses its next action. We'll check its submitted files in application code and measure its tool-call order in an evaluation later.

Here is how the files are organized:

text
src/app/fix/
  index.ts
  plan.md
  skills/verify-change/SKILL.md
  tools/prepareReview.ts
  tools/exportForReview.ts
  evals/repair.eval.ts
  evals/scoring.ts

The plan lists six steps: read and reproduce, inspect the cause, edit source, test and explain, prepare the candidate, and request export approval.

A skill holds instructions for a particular task. Here, verify-change explains how to check a repair. It is a Markdown file with a name, description and instructions:

md
---
name: verify-change
description: Reproduce and verify a focused code repair.
---
 
Read TASK.md. Run the documented failing command and inspect its output before
editing. Follow the actual entry point through the source; don't stop at a
handler test if the failure occurs at a parser or adapter boundary.
 
Change only the permitted source files. Preserve tests and validation behavior.
Run the documented tests again and check the task's preservation requirements.
For validation changes, consider both accepted and rejected inputs with the
documented diagnostics. Report the commands, actual results, and remaining failures.
After verification, call prepareReview({}) and inspect its diff and checks.
Pass the complete returned candidate to exportForReview({candidate}) for approval. The
runtime pauses before exporting; a prose approval question does not raise that gate.

Source: src/app/fix/skills/verify-change/SKILL.md.

The instruction to follow the entry point matters, because calling a handler directly would miss a bug in the CLI parser that runs before it.

Give the agent a workspaceCopy link to section: Give the agent a workspace

The agent needs a directory where it can read and write files. We'll call it the workspace.

Each conversation starts with its own copy of the broken project. The model can make changes without touching the sample in our repository, and we can compare those changes with the original files.

B4.run calls a conversation a thread. The workspace belongs to that thread, so later tool calls in the same conversation can see earlier edits.

Let's declare the files that go into a new workspace:

ts
export function projectWorkspace(id: string): WorkspaceDefinition {
  const manifest = projectManifest(id)
  return {
    source: {
      directory: "sample/project",
      include: [...manifest.allowedSourcePaths, ...manifest.immutablePaths],
      files: [
        { path: "TASK.md", file: "sample/task.md" },
        { path: ".gitignore", text: "node_modules/\n" },
        { path: "project.json", text: JSON.stringify({ id: manifest.id }) },
      ],
    },
    environmentLinks: [
      { path: "node_modules", target: `/opt/fixtures/${manifest.id}/node_modules` },
    ],
    baseline: "git",
  }
}

Source: src/project/workspace.ts.

Let's quickly review:

  • source.directory identifies the project to copy.
  • source.include selects the files from that project. The manifest is a JSON file listing the source files and other information about our sample.
  • source.files adds TASK.md, a Git ignore file and a small JSON file that identifies the project.
  • environmentLinks supplies a link to the prepared node_modules directory.
  • baseline: "git" initializes a Git baseline for inspecting changes.

The sample manifest lists src/cli.ts as editable and the handler, test, package files and license as immutable. Those lists decide the captured source and what candidate inspection accepts. They do not make files in the editing workspace read-only.

B4.run captures the starting bytes, initializes the workspace and handles reconnection and deletion. baseline: "git" lets the model inspect a diff while working. The review tool reads the original bytes through ctx.workspace.readInitialFile, so editing Git metadata can't change what the review compares against.

Configure the sandboxCopy link to section: Configure the sandbox

A sandbox is the controlled environment where file and shell operations run. The dockerSandbox provider runs them inside a Docker container. For example, runBash runs npm test there, not in our repository on the host.

The app's b4.config.ts connects the provider to the workspace declaration. Here is its sandbox property:

ts
  sandbox: {
    ...sandboxPolicy,
    provider: dockerSandbox({ scope: "code-fixer-local", image: sandboxImage }),
    workspace: projectWorkspace(task),
  },

Source: b4.config.ts.

The provider selects Docker and the prepared image. Its scope identifies this application's Docker resources, so use a distinct scope for a separate installation. The workspace property supplies the declaration we just reviewed.

We also provide a policy that controls network access and resources:

ts
export const sandboxPolicy = {
  network: { mode: "deny" as const },
  env: { npm_config_cache: "/tmp/npm-cache", npm_config_update_notifier: "false" },
  resources: { memoryMb: 1024, cpus: 1, timeoutMs: 120_000 },
}
 

Source: src/project/workspace.ts.

Let's quickly review:

  • network denies network access during execution.
  • env configures npm to use a temporary cache and disables its update notifier.
  • resources limits execution to one CPU, 1 GiB of memory and a 120-second command timeout.

The container can't download packages while the agent works, so we prepare the project's dependencies in the image first. We'll run that step when we start the app.

The B4.run server on the host still calls the language model. Denying network access to the sandbox doesn't block that call, and the host's API key never reaches the container.

The full config also allows reads of the prepared dependency directory and shell commands including npm test, Node diagnostics and Git inspection. Docker isolation, command permissions and rejection of out-of-scope source edits are separate controls, and the app needs all three.

Create a tool to prepare the changeCopy link to section: Create a tool to prepare the change

At this point, the model may have edited src/cli.ts and run a passing test. We need to turn those edits into something a person can review.

We'll call the proposed change a candidate. It contains the new file contents and information identifying the workspace and original source, as data our application can validate and pass around.

The agent calls prepareReview({}) to create that candidate. Here is the whole tool in src/app/fix/tools/prepareReview.ts:

ts
import type { B4ToolContext } from "@b4run/sdk"
import { inspectCandidate } from "../../../review/inspect.js"
import { renderReviewDiff } from "../../../review/patch.js"
import { verifyChanges } from "../../../review/verifier.js"
 
/** Verify the source repair independently and return the exact candidate for review. */
export default async function prepareReview(_input: Record<string, never>, ctx: B4ToolContext) {
  // Compare the workspace with its captured source and reject edits outside the allowed files.
  const { manifest, candidate, baseline, initial } = await inspectCandidate(ctx)
 
  // Apply these exact changes in a fresh workspace and run both test suites.
  const verification = await verifyChanges(manifest.id, candidate.changes, ctx.signal, initial)
  if (!verification.passed) throw new Error("Independent verification failed")
 
  // Give the agent a readable diff and the exact candidate it must submit for approval.
  return {
    task: manifest.id,
    candidate,
    diff: renderReviewDiff(baseline, candidate.changes),
    verification,
  }
}

Source: src/app/fix/tools/prepareReview.ts.

Let's walk through it:

  1. _input is an empty input object. The function reads the workspace itself, so the model doesn't supply any file contents.
  2. ctx is a B4ToolContext supplied by the runtime. It gives the tool access to the workspace, filesystem operations and cancellation signal.
  3. inspectCandidate(ctx) compares the current files with the original files and returns the proposed changes.
  4. verifyChanges(...) applies those changes to a separate workspace and runs the checks we'll discuss next.
  5. If verification fails, the tool throws an error. Otherwise, it returns the candidate, a diff and the verification results.

The model reads that object as the tool result, and a client can show it to the person reviewing the change.

Inspect the changed filesCopy link to section: Inspect the changed files

The candidate inspector first reads the original project identity and files from B4.run's captured source. It then reads the current workspace through this shared B4.run API:

ts
  const { files: current } = await inspectWorkspace(ctx.fs, {
    signal: ctx.signal,
    maxEntries: 1000,
    maxFileBytes: 2 * 1024 * 1024,
    maxTotalBytes: 2 * 1024 * 1024,
    excludeRootDirectories: [".git"],
    expectedRootSymlinks: { node_modules: `/opt/fixtures/${manifest.id}/node_modules` },
  })

Source: src/review/inspect.ts.

inspectWorkspace reads a bounded set of text files. The options limit the number of entries and the bytes it reads. We exclude the root Git directory and require node_modules to stay the prepared dependency link.

The result's files property holds the current file contents. Inspection also rejects executable files, unexpected links, and binary or invalid UTF-8 content, which keeps the example focused on a small source change.

The app's collectChanges helper compares that inventory with the original. It rejects added or deleted files, changes outside src/cli.ts and more than 1 MiB of changed-file content. An empty repair also fails.

Identify the change we are reviewingCopy link to section: Identify the change we are reviewing

The candidate contains the complete changed-file contents, workspace ID and initial source digest. A digest is a value calculated from some data that tells us whether the data has changed. candidateDigest sorts the changed paths and hashes these fields with SHA-256, which pins down the bytes under review. The diff is generated from the same validated contents, with three lines of surrounding context.

Verify the change with a fresh project copyCopy link to section: Verify the change with a fresh project copy

We could stop after the agent runs a passing test, but that trusts the same directory it has been editing. Instead, we'll create another workspace from the original source and apply only the accepted candidate files. That gives us the original tests and configuration with the proposed source change.

verifyChanges uses B4.run's withWorkspace to create a disposable Docker workspace from the captured source and image identity. It applies the candidate's changed files, then:

  1. Inspects the workspace and runs the original visible test.
  2. Inspects it again and rejects persistent file changes made during that test.
  3. Copies the host-owned independent checks into the verifier.
  4. Runs those checks and again rejects persistent file changes.

The first suite is the visible test we saw earlier. The second checks behavior the repair must preserve, such as rejecting invalid arguments. These extra tests live outside the editing workspace and are copied into the verifier after the visible test runs.

The app specifies the test files and assertion names in sample/checks.json:

json
{
  "visible": {
    "file": "test/cli.test.ts",
    "assertions": [
      "documented dry-run flag reaches the handler"
    ]
  },
  "independent": {
    "file": "checks/independent.test.ts",
    "assertions": [
      "forwards cap and memory-level cwd",
      "rejects unknown and incomplete arguments",
      "dry-run preserves memory state and creates no files"
    ]
  }
}

Source: sample/checks.json.

The independent tests make those names concrete:

CheckInput and required result
Forward argumentsmemory --cwd /workspace/app prune --cap 17 returns the expected action, directory, and numeric cap.
Reject invalid argumentsUnknown flags, a missing cap, and a nonnumeric cap exit unsuccessfully.
Preserve a dry runCreate memory.json, run consolidation with --dry-run, then require unchanged contents and no additional files.

The verifier reads structured Node test-runner events. Exit code zero isn't enough: every expected assertion must occur exactly once and pass, with no skip or todo. Output printed by submitted code is recorded as stdout and never counts as a passing assertion.

Of course, these checks have limits. Both suites run in the same verifier container, and our file comparisons catch changes left behind after a suite finishes, not every transient change while code runs. The tests establish the behaviors listed above. They don't prove that any submitted program is correct.

Require approval before exportingCopy link to section: Require approval before exporting

Now we have a proposed change and its check results, so we can ask a person to review it before the application exports it. Recall the tools.approve property in our agent configuration:

ts
tools: { approve: ["exportForReview"] },

When the model requests exportForReview({candidate}), B4.run pauses before running the tool and returns a pending approval request. A client can display that request, let the person decide and send the decision back to resume the run.

Why not just let the model ask? A model asking "May I export this?" in a text response doesn't stop a function from running. The runtime's approval handling is what pauses the tool call.

After approval, the tool starts with these checks:

ts
  const candidate = validateCandidate(input.candidate)
  const inspected = await inspectCandidate(ctx)
  if (candidate.receiptDigest !== inspected.candidate.receiptDigest)
    throw new Error("Workspace changed since review; prepare and approve a new candidate")
  const verification = await verifyChanges(
    inspected.manifest.id,
    candidate.changes,
    ctx.signal,
    inspected.initial,
  )
  if (!verification.passed) throw new Error("Independent verification failed")

Source: src/app/fix/tools/exportForReview.ts.

Let's walk through what happens after approval:

  1. validateCandidate checks the input structure and recomputes its digest.
  2. inspectCandidate reads the workspace again.
  3. We compare the new digest with the approved candidate's digest. If someone edited the workspace while approval was pending, the tool throws an error.
  4. We verify the candidate again in a fresh workspace.

Why check again? Imagine approving a diff, then exporting a different edit made a moment later. The approval would no longer describe the exported files. Comparing candidates makes that mismatch an error, and the agent must prepare the new change for approval.

Only then does the export tool write .b4/code-fixer/review-outbox/<digest>.json. The receipt contains the task, candidate and diff. The write uses flag: "wx", and an existing file is accepted only if its contents match exactly, so retrying the same approved export leaves the same receipt. Export creates no commit, push or pull request.

Evaluate the agentCopy link to section: Evaluate the agent

Our tests check the repaired application. We also want to check how the agent went about the task. Did it reproduce the failure before editing? Did it request approval with the candidate it had prepared?

An evaluation, or eval, runs an agent on an input and scores its behavior or result. Here, our scores come from tool calls, test results and the pending approval request.

The eval definition lives beside the route:

ts
import { defineEval, gate } from "@b4run/evals"
import { repairScorers } from "./scoring.js"
 
export const taskInput =
  "Read TASK.md, reproduce the failure, repair the permitted source, verify the preservation requirements, call prepareReview, and then exportForReview with its exact candidate to request runtime approval."
 
/** Run with b4 eval --live; test:sandbox also exercises the same gates offline. */
export default defineEval({
  name: "code-fixer repair workflow",
  dataset: [{ name: "configured project", input: taskInput }],
  scorers: repairScorers,
  gate: gate.perScorer(),
})

Source: src/app/fix/evals/repair.eval.ts.

The scoring implementation defines six criteria. In the definition above, dataset supplies the input to run, scorers supplies the functions that examine the result and gate decides whether the evaluation passes.

Each scorer has a threshold of 1. With gate.perScorer(), every criterion must pass, so a successful test can't make up for a missing approval request:

CriterionEvidence required
reproducedA recognized npm test command failed before the first writeFile.
verifiedA recognized npm test command passed after the first writeFile.
visibleThe latest successful preparation reports passing visible tests.
independentThat preparation reports passing independent checks.
scopeIts validated candidate contains changed files. Preparation already enforced the allowed paths.
approvalThe run has one pending export-tool interrupt, no export result, and the export call carries the same validated candidate digest as preparation.

The first two scores establish command results and order. They don't establish a correct diagnosis, and the post-edit score doesn't require testing after the last edit. Candidate verification tests the submitted files separately. The approval score establishes a pause before export, and the Docker integration tests exercise acceptance and denial.

Run these from the repository root after setup:

bash
pnpm --filter @b4-example/code-fixer-server test
pnpm --filter @b4-example/code-fixer-server test:sandbox
pnpm --filter @b4-example/code-fixer-server eval --live

Each command answers a different question:

  • test runs the application's unit tests.
  • test:sandbox runs Docker integration tests. They replay the known repair through the real tools and check verification, approval and denial without paid model calls.
  • eval --live asks the configured model to solve the task and scores the run. This uses your model API key.

Replay is useful when changing the application: it checks that the tools and approval handling still work with a known repair. Live evaluation measures whether the model can find a repair on its own, which replay can't tell us.

Offline b4 eval requires recorded fixtures. This example uses Docker tests for replay because candidate identities change between runs.

Run it and inspect the resultCopy link to section: Run it and inspect the result

Use Node 24+, pnpm, Git and a running Docker daemon. In a checkout of the repository version linked above, run from the root:

bash
pnpm install --frozen-lockfile
pnpm build
pnpm --filter @b4-example/code-fixer-server sandbox:prepare
pnpm --filter @b4-example/code-fixer-server check
pnpm --filter @b4-example/code-fixer-server dev --port 3001

Set OPENAI_API_KEY on the host or in the server's local .env before starting the development server. Rerun image preparation only when dependencies change. In another terminal, create a thread and start the agent:

bash
THREAD=$(curl -fsS http://127.0.0.1:3001/threads \
  -H 'Content-Type: application/json' -d '{}' | \
  node -e 'let s=""; for await (const c of process.stdin) s+=c; console.log(JSON.parse(s).thread_id)')
 
curl -N --fail-with-body http://127.0.0.1:3001/threads/$THREAD/runs/stream \
  -H 'Content-Type: application/json' \
  -d '{"route":"/fix#agent","input":{"messages":[{"role":"user","content":"Read TASK.md, reproduce and repair the defect, verify the change, and request approval to export the exact candidate."}]}}'

The first request creates a thread and saves its ID in the THREAD shell variable. The second request starts a run of /fix#agent in that thread. Its input.messages array contains the user's request.

The -N option lets curl display the streamed response as it arrives. You'll see tool calls, their results and the prepared diff. When the agent requests export, the runtime pauses. Read the pending approval request with:

bash
curl -fsS http://127.0.0.1:3001/threads/$THREAD/pending_interrupts

Inspect the operation. For the export request, copy its interruptId into the resume body below to approve once. Use "deny" instead of "once" to decline. If multiple requests are pending, the resume array must answer each exactly once.

bash
curl -N --fail-with-body http://127.0.0.1:3001/threads/$THREAD/resume \
  -H 'Content-Type: application/json' \
  -d '{"route":"/fix#agent","resume":[{"interruptId":"PASTE_INTERRUPT_ID","status":"resolved","payload":"once"}]}'

An approved export writes its receipt under the server app's .b4/code-fixer/review-outbox/. When finished, delete the thread and managed workspace:

bash
curl --fail-with-body -X DELETE http://127.0.0.1:3001/threads/$THREAD

These commands use the ordinary B4.run HTTP runtime, not an evaluation runner. The example README also covers built-server execution and adapting the sample.

Installing from a blueprintCopy link to section: Installing from a blueprint

You may also see b4 add code-fixer in the documentation. That command prints an installation guide for a coding agent to apply. You (or your coding agent) still install and start the application.

When this post came out, the published guide used an earlier example with B4.run 0.8.32. It now installs the example on B4.run 0.10.0. To follow the code in this article, use the linked repository example and the checkout instructions above.

What belongs in the framework?Copy link to section: What belongs in the framework?

Building an example like this is a good way to find work that belongs in the framework.

Every application that edits files needs to know where the files live, what they contained at the start and how to release its resources when finished. B4.run handles source capture and the workspace lifecycle. It also handles the approval pause and resume.

The code-fixer moved another reusable operation into B4.run: inspectWorkspace. The app used to maintain its own file walker and snapshot program. Now it passes limits and expected links to the shared inspection function. You can see that change in PR #670.

The application still decides which source files can change, what a correct repair must do and what gets exported. Those decisions depend on the project we are repairing, so they stay visible in the example's TypeScript code.

ConclusionCopy link to section: Conclusion

In conclusion, we've followed a code-fixing agent from a failing CLI command to a change ready for review. Along the way, we defined the agent's instructions, supplied file and shell tools, configured a Docker workspace, checked the proposed source independently and required approval before export.

If you want to adapt the example, start with a small project and a failure you can reproduce. Write down which files may change and which behavior must remain unchanged. Then turn those requirements into tests the review tool can run.

The model chooses how to investigate the problem. Your application decides what it can do and what evidence you need before accepting its work.

Build your own agent.

Start a project, or follow the code-fixer agent from its first failing test to a verified patch.