Personal Project
Unshackle
A Roslyn-powered dotnet tool that migrates .NET code away from libraries that went commercial. Every line was written by AI agents; my job was to design the system that keeps them honest.
Challenge
FluentAssertions, MediatR and AutoMapper went commercial. Migrating by hand is slow, and a careless automated rewrite can silently break code at run time.
My contribution
I did not write the code. I designed the process: architecture, rules, review gates, the agent loop and the escalation ladder, and I made the calls the agents could not.
Approach
Small, provable steps. Each rewrite pattern is planned, built with proofs, verified by an independent session and documented with diagrams before it lands on main.
Outcome
A working tool for three libraries, measured on 36 open-source repos it had never seen: the build never stayed broken and no test regressed.
AI at scale, in numbers
Speed is easy with AI. The hard part is trust. Everything below exists so that a rewrite made by an agent is never accepted on its word alone.
How the AI worked: overview
The whole system on one page. I set the guardrails; the orchestrator hands tasks to worker agents; open choices go to the council; nothing reaches main without passing the gates; and the open-source corpus feeds new bugs back into the queue.
Guardrails: clear tasks, clear rules
The biggest lesson: AI output is only as good as the instructions around it. Agents do excellent work on a sharp task with explicit rules, and drift on a vague one. So most of my effort went into the guardrails, not the prompts.
- Clear tasksEvery unit of work is a task file with a goal, requirements and acceptance criteria. An agent never starts from a vague prompt.
- Clear rulesCLAUDE.md, code standards and a workflow document state the architecture, naming, packages and what an agent may and may not do.
- Enforced, not hoped forWarnings are errors, style is checked at build time, and hooks block an agent from stopping while docs or proofs are missing.
The rule: never guess
A pattern either proves a rewrite is equivalent, or it reports the case as Manual with the reason. It never throws and never half-migrates: MediatR migrates all or nothing, because a half-migrated app breaks at run time. This one rule turns a confident but fallible AI into a tool you can run on real code.
Proof: MediatR handler, before
public class CommandHandler : IRequestHandler<Command>
{
public async Task Handle(Command request, CancellationToken cancellationToken)
{
if (request.Id < 0)
{
return;
}
await Task.Delay(request.Id, cancellationToken);
}
}Proof: Mediator handler, after
public class CommandHandler : IRequestHandler<Command>
{
public async ValueTask<Unit> Handle(Command request, CancellationToken cancellationToken)
{
if (request.Id < 0)
{
return Unit.Value;
}
await Task.Delay(request.Id, cancellationToken);
return Unit.Value;
}
}The agent loop
The main session acts as orchestrator. Every 15 minutes it starts a background agent on the next pattern; agents run in parallel, and each step leaves a file behind so any session or model can take over.
- QueueEvery pattern is a row in a queue. A cron tick in the main session claims the next open row.
- ResearchA background agent writes a task file first: the analysis, the edge cases and the plan (KingmaDoc).
- BuildThe agent implements the pattern plus proofs: before.cs compiles against the old library, after.cs against the new one.
- VerifyThe main session never trusts "done": it runs build, tests and the sample solution in a clean worktree.
- ModelA second agent updates the C4 and UML diagrams from the real code, so the docs never drift.
- ShipOnly green commits are pushed to main, and CI must turn green before the next step.
Right model for the job
Not every task deserves the biggest model. Work starts on the cheapest tier that can do it, and only a failure moves it up. The ladder ends with a human, not an infinite loop.
- SonnetDefault for patterns: implementation that needs judgment.
- OpusThe foundation, and the second attempt when a pattern failed once.
- FableThird and last attempt. If that fails too, it stops and a human decides.
A council instead of a coin flip
The council is what made fully autonomous (AFK) work possible. Without it, every open design question stops the loop until I am back. With it, the work keeps going and the decision stays traceable.
When an agent hits a choice that the code, a task or an earlier decision does not settle, it does not decide alone. Three sub-agents with different personalities vote, and rounds continue until all three accept one rule. Every decision is recorded with date, votes and rule, and I can overrule any of them.
- The GuardianCan this silently change behaviour in the user's code?
- The PragmatistHow much hand work does the developer have left?
- The ArchitectCan a user predict this rule from the report?
Architecture the agents had to follow
Strict boundaries make AI output reviewable: Interface → Core ← DataAccess, and Core references no other project. Inside Core, a Fixer runs one IFix per library, each made of small IPattern classes with Find and Rewrite. Warnings are errors, code style is enforced, and a hook blocks an agent from stopping while a pattern's plan, verification or README row is missing.
C4 Context: the developer runs Unshackle on their own solution; it depends on MSBuild and git, and refuses to fix a dirty working tree.
C4 Container: Interface (the CLI and composition root), Core (fixes and patterns, pure Roslyn) and DataAccess (loading, saving, reports).
Validation in two layers
Fully autonomous only works when validation is stronger than the agent. First the change has to pass the unit tests and proofs; only then does it meet the real world, open-source projects nobody tuned the tool for.
- Unit testsEach pattern is tested in isolation against real library assemblies, with NSubstitute mocks only for our own interfaces.
- Proofsbefore.cs must compile against the old library, after.cs against the new one, or a manual.txt states why it is not rewritten.
- Sample solutionA sample app full of old FluentAssertions, MediatR and AutoMapper code is migrated and its tests must still pass.
- Open-source corpusReal repos the tool never saw: their own test suites run before and after the fix. Any regression is a bug.
Measured, not claimed
The real test: open-source solutions that were never used to develop the tool, with their own test suites run before and after the fix.
| Fix | Repos | Fully | Partly | Stopped | Test regressions |
|---|---|---|---|---|---|
| FluentAssertions → Shouldly | 12 | 5 | 7 | 0 | 0 |
| MediatR → Mediator | 12 | 6 | 1 | 5 | 0 |
| AutoMapper → mappings | 12 | 1 | 9 | 2 | 0 |
The first run of this round found four bugs that broke the build. The build caught all of them, each got a proof and a fix, and the affected repos were measured again. That is the point: AI makes mistakes, the process makes sure they surface.