How We Built Charlie, Part 15: What We Learned Building Charlie
What we learned about making AI engineering work owned, bounded, verifiable, and useful to a team.
How scoped execution environments turn plans into durable, reviewable code artifacts without becoming lifecycle authority.
How We Built Charlie continues from Chapter 9: changing requirements still need a controlled place to edit, test, and preserve artifacts.
“Fix the failed deploy” sounds like one instruction. For an engineering agent, it expands into a set of concrete requirements.
Charlie needs the current repository, including the branch and revision that produced the failure. He needs the project’s tools and enough environment configuration to run them. When work may overlap or conflict, the workflow must identify a base and isolate mutable edits in an appropriate branch or environment. He also needs access to CI and the systems that hold the evidence. Then he needs to leave behind something another engineer can inspect: a commit, a pull request, test output, or a precise explanation of why no safe patch exists.
A model can reason about a deploy failure without any of that. It cannot verify the repository state, reproduce the failure, or create a reviewable artifact. The practical unit of agency is therefore larger than the model call. Charlie combines a durable Task, a recoverable executor, and an attached devbox that gives tools a real place to operate.
The devbox is Charlie’s body for repository work. It supplies a filesystem, processes, Git state, and networked engineering tools. It does not own the task, decide whether work is still authorized, or prove the final change is correct. Keeping those boundaries separate is what makes real execution useful instead of merely impressive.
The implementation details below are simplified and omit private identifiers, credential names, operational constants, and infrastructure topology. They describe the contracts that matter to an engineer evaluating the system.

The Task authorizes work, the executor advances it, and the devbox gives that execution a repository-aware operating environment.
It is tempting to describe the sandbox as the agent runtime. That collapses several responsibilities into one word.
The Task Scheduler owns the lifecycle of the engineering objective. It records whether the Task is scheduled, running, canceled, timed out, succeeded, or failed. A devbox continuing to exist does not keep a terminal Task alive.
The executor owns one run’s progression. It fetches context, checks scheduler state, calls the model, dispatches tools, waits for results, and reports an outcome. It persists enough phase state to resume selected work after an interruption.
The devbox is an execution environment used by tools and context providers. It holds the checkout and processes. Its state can be useful across turns or related work, but it is subordinate to Task and executor policy.
| Boundary | Owns | Does not own |
|---|---|---|
| Scheduler | Task state, cancel state, timeout, final result | Files or command execution |
| Executor | Run phase, trans | Task lifecycle or provider artifact validity |
| Devbox | Checkout, files, tools, Git worktree, command runtime | User intent, result state, or semantic proof |
| Provider | External artifact state, such as a pull request or CI check | Charlie’s internal Task or executor state |
This division prevents several common mistakes. A process exiting is not automatically a failed Task. A branch existing does not mean the requested change is complete. A suspended devbox does not mean work is canceled. A successful command does not override a stop request that the executor observes at its next scheduler boundary.
The model also stays on the correct side of the boundary. It can request a command through an exposed tool. The executor resolves that request against the current run, capabilities, and devbox. The model does not receive an unrestricted connection to an arbitrary machine.
Context providers often need a checkout before the first model turn. Repository rules, skills, Git status, language versions, and project structure cannot be fetched accurately from an environment that has not attached the requested repository.
Charlie’s executor handles this during its context phase. At a high level, it resolves the Task, determines whether the selected agent has context providers, looks for a reusable devbox, and starts one when repository-backed providers need it. Only then does it run those providers.
async function prepareFirstTurn(task, execution, providers) {
let devboxId = execution.devboxId ?? task.devboxId;
if (!devboxId && task.repo && providers.length > 0) {
devboxId = await startRepoDevbox(task.repo);
await persistExecutionDevbox(execution.id, devboxId);
}
if (devboxId && task.repo) {
await refreshRepoAttachmentBestEffort(devboxId, task.repo);
}
return runContextProviders({ task, devboxId });
}
The real code has more failure handling, but the ordering is the important part. Repo-backed context should not race ahead of repo readiness. The executor also preserves a recovery fast path: if context and the expected transcript state were already persisted for the current turn, resuming the run does not depend on fetching the Task again or refreshing remote repository metadata first.
That tradeoff is deliberate. Freshness work is useful, but it should not make an otherwise recoverable executor state unrecoverable because another service is temporarily unavailable.
Creating a remote machine is only the beginning. Charlie needs to know that the environment is running, the intended repository is attached, and commands will execute from the expected root.
A start path conceptually performs these steps:
Each step prevents a different class of false confidence. A running VM with no checkout is not repo-ready. A checkout at the wrong root can make a command silently operate on an empty directory. A repository name in metadata is not enough if its remotes, branch, or revision do not match the requested work.
Charlie therefore treats repository attachment as evidence with degrees of confidence. Process-local registration is fast when the current client started the devbox. Persisted metadata can recover that information after the client changes. An environment hint or conventional path can help locate a legacy checkout. Those fallbacks are useful, but they do not turn missing Git evidence into certainty.
When the request names a repository and the environment appears attached to another one, the safe response is to reject or repair that mismatch rather than inject repository-scoped configuration into the wrong checkout.
Starting a clean environment for every turn would be wasteful. Keeping one environment forever would be unsafe and operationally brittle. Charlie reuses a devbox when the current execution or Task provides a suitable handle, then refreshes the repository-working context before relying on it.
Reuse is valuable because the checkout, dependency cache, build artifacts, and diagnostic files may already exist. It is especially useful when a worker continues narrowly scoped work in the same repository. The child Task can be seeded with the parent’s devbox handle as an optimization, and the child executor records that handle as its own active execution context.
That inheritance is not authority. The Task model explicitly treats the devbox handle as optional optimization data. A worker must still receive a self-contained assignment, appropriate capabilities, and repository scope. Sharing compute does not share the parent’s hidden conversation, decisions, or permissions.
Before reused compute supports context or tools, Charlie refreshes the attachment metadata on a best-effort basis. This catches changes such as a suspended environment resuming under a new client process. Prompt context is a separate decision: the fact that a devbox exists does not automatically dump its filesystem or process state into the model input.
Remote development environments have a lifecycle. A devbox may be running, suspended, starting, shutting down, or failed. Recovery policy depends on the state and on what the caller requested.
For selected operations, Charlie opts into suspended-devbox recovery. If a command encounters an inactive environment, the client reads the current state, requests a resume when appropriate, waits until the environment is running, and retries the command once. The retry is narrow because the initial command was not accepted as a completed operation.
A shutdown or failed devbox is different. It is treated as unresumable, and the caller needs a new environment. Repeatedly asking a terminal machine to resume would add delay without improving correctness.
requested command
|
v
running? -------- yes --------> execute
|
no
v
suspended? ------ yes --------> resume -> wait -> execute once
|
no
v
terminal/unusable ------------> start replacement or report failure
Automatic recovery is not universal. The command caller has to opt into resume behavior because some operations need stricter control over their environment. A long-running background command, an interactive diagnostic, and a read-only version check do not necessarily have the same recovery contract.

Reuse and resume reduce setup cost, while terminal states remain explicit replacement boundaries.
Repository work often needs authenticated access to Git hosting, issue trackers, CI, deployment systems, or diagnostic services. The useful design question is not which secret names exist. It is how access is scoped and composed.
Charlie constructs the devbox environment from several sources with deterministic precedence. Explicit environment values supplied for a particular invocation win over integration-derived values, which win over customer or repository configuration. Reserved keys are filtered so lower-trust configuration cannot replace values owned by the runtime.
const environment = mergeInOrder(
repositoryConfiguration,
integrationCredentials,
explicitInvocationValues,
);
const safeEnvironment = removeReservedOrDisallowedKeys(environment);
The simplified example leaves out an important guard: repository-scoped configuration is admitted only when the requested repository matches the resolved repository context. That prevents a reusable environment or malformed request from receiving configuration intended for another codebase.
The values are operational inputs, not model context. Tools can use them through the command environment without putting them in prompts. Known credential forms are selectively redacted from relevant telemetry and lifecycle errors, while command output remains available to the agent and caller. Evidence surfaces that may contain sensitive content stay restricted.
No mechanism makes arbitrary shell access risk-free. Scope, filtering, capability checks, short-lived access where supported, output redaction, and reviewable artifacts reduce the exposure. High-impact operations may still require stronger policy or human approval.
A useful engineering environment isolates mutable work that would otherwise interfere. The most obvious example is Git state. Two tasks changing the same checkout can race on the active branch, index, generated files, dependency installation, or test database.
Branch isolation follows the conflict profile of the work. Before overlapping or conflicting edits, the workflow identifies the base revision and prefers a unique branch, separate environment, or both where appropriate. Commands run from the repository root. Read-only exploration and clearly non-overlapping work can often reuse compute safely.
Isolation has limits. Two branches can still compete for remote provider state, external test fixtures, or the same deployment. A devbox boundary does not create a transaction across GitHub, CI, and a cloud provider. Those effects need their own idempotency, correlation, and read-back policies.
The environment also accumulates state. Generated artifacts and dependency caches may speed up later work, while stale build output can mislead a test. The executor and tools need to know which evidence comes from the current revision. A command report without its working directory and revision is weaker than it appears.
A devbox can disappear after the run. The useful result should survive it.
For code work, the strongest common artifacts are Git commits and pull requests. A commit SHA identifies exact content. A branch gives continuing work an address. A pull request adds provider-visible base, head, diff, review, and CI state. Test output supports a narrower claim about a command on a revision in an environment.
| Artifact | Supports | Does not prove |
|---|---|---|
| Branch | A named line of work exists | Its contents are correct or pushed |
| Commit SHA | Exact repository content can be retrieved | Tests passed or the change meets the request |
| Pull request URL | A provider exposes a change for review | CI passed, review accepted, or merge is safe |
| Test command | A named check was requested | It completed or ran against the intended revision |
| Test result | That command produced an observed outcome in one environment | Hidden cases, remote CI, or live use will agree |
| Provider read-back | The external system currently exposes the named artifact | The artifact is correct |
This is why Charlie verifies branch and commit identifiers after writing them, then reports stable links when available. The devbox handle is useful to another agent only while the environment can be reused. It should not appear as the primary completion evidence for a human reviewer.

Mutable execution state earns trust by producing durable, independently inspectable artifacts.
“Tests passed” is one of the easiest agent claims to overstate. A useful report includes the exact command, working directory, revision, and observed exit status. It distinguishes local checks from remote CI and notes important checks that did not run.
Suppose Charlie changes a deployment manifest and runs a unit test package. That result may validate a parser without validating the deployment. If the build uses generated output, a passing test against stale files may say even less. The agent should choose checks that match the consequence and state their limits plainly.
The devbox helps because it gives those checks a reproducible place to run. It does not make them exhaustive. Repository rules may require a focused test, a full typecheck, a build, visual checks, or CI. Some changes also require provider-side evidence after the local run, such as a deployment preview or a check attached to the pushed commit.
Failures are artifacts too. A consistent reproduction command, log excerpt, and revision can be more useful than a speculative patch. If an external dependency prevents verification, Charlie should preserve the blocker rather than convert incomplete testing into success.
Compute should not become durable merely because the Task was durable. Charlie’s standard start path allows idle suspension so environments can be reused without running indefinitely. Lifecycle services can also perform best-effort cleanup of temporary access material and abandoned resources.
Cleanup is intentionally separate from terminal Task recording. A Task can succeed while environment cleanup is still pending. Cleanup failure should be observed and retried according to its own contract, not rewrite a successful code result as if the patch disappeared. The reverse also holds: deleting a devbox cannot make an incomplete Task successful.
There is no useful public promise in a universal deletion interval. Different environment types and lifecycle services may evolve. The stable contract is that a devbox is operational, reusable within policy, and disposable. Durable evidence must live elsewhere.
This separation also improves incident response. An environment may be retained temporarily for diagnosis without changing Task truth. Another process can recover the branch or commit even if the original devbox is gone. If the only useful state lives on one machine, the system has produced a debugging session rather than an engineering artifact.
The devbox boundary is valuable because it makes several failure modes explicit.
Context and commands can describe the wrong directory or fail in misleading ways. Readiness ordering makes repository attachment a prerequisite for repo-backed providers.
Compute reuse can look like context reuse. Charlie keeps the worker assignment self-contained and treats the devbox handle as an optimization only.
Selected commands can resume and retry once. Terminal environments fail clearly and trigger replacement rather than endless resume attempts.
Repository matching, deterministic precedence, and reserved-key filtering keep scoped configuration attached to the intended work.
Branch and environment isolation are chosen according to edit overlap, not according to how many model calls exist.
Charlie reports commits, pull requests, test evidence, and provider read-back. A devbox ID remains an in-run coordination handle.
Charlie needs a real environment because engineering work is stateful. The repository has a revision. Tools have versions. Tests create processes and files. Git changes move through branches and commits. External systems return evidence that must be reconciled with local state.
The execution environment supplies that physical layer, while the surrounding contracts make it dependable. The scheduler owns whether work is active. The executor prepares the environment before context and tools rely on it. Repository readiness and scoped configuration reduce mismatches. Resume handles a recoverable lifecycle state without treating failed environments as healthy. Durable, reviewable artifacts carry the result beyond the machine that produced them.
Completing engineering work means leaving behind a reviewable record: the repository and revision changed, the checks that ran, the durable artifact created, and any remaining uncertainty.
Previous: How We Built Charlie, Part 9: Follow-Ups While Work Is Running. Next: How We Built Charlie, Part 11: Context Without Magic Memory. Browse the full How We Built Charlie series.