Skafu: turning a human delivery framework into an agentic one
What inspired it
Skafu started as an attempt to automate a framework I did not invent.
The 120x framework describes itself as "The Solo Software Factory" — a way for one operator, or a very small team, to ship production software at a scale that would normally need a department. The parts I want to credit specifically, because they are the parts skafu inherited wholesale, are:
- an Architect / Builder split, where the person who decides what is being built and the person who builds it are deliberately separated;
- a blueprint as the artefact that carries the decision from one to the other;
- and a validation posture summarised roughly as if it can't beat manual, it doesn't ship — the bar is not "the code runs", it is "this is better than the thing it replaces".
I am describing 120x only as far as I could verify it from their public material. It is their framework, it is worth reading in their words, and this post is not a summary of it. Go and look: https://120x.ai/.
What matters here is the shape of the idea. 120x is a human process. A person holds the architect role. A person holds the builder role. The discipline lives in the operator's head and habits, and the framework's value is that it tells that person what to do next.
Skafu asks a narrower question:
If the roles are real enough to be named, and the handoffs between them are real enough to be written down — can the roles be agents, and the handoffs be files?
The problem
Anyone who has pointed a coding agent at a real repository knows the failure mode. It is not that the agent cannot write code. It writes plenty. The failure is that a single long-running agent, given a large goal, will:
- drift from the thing you asked for, without ever announcing that it drifted;
- report success in the same confident register whether it succeeded or not;
- lose the thread when its context fills, and start again from a worse position;
- and leave you no way to tell, afterwards, which parts were verified and which were merely asserted.
The usual response is to supervise it more closely. But supervision does not scale, and it collapses back into the thing you were trying to avoid: a human in the loop on every step, which is just pair programming with extra latency.
The interesting constraint is this: you cannot trust one agent's account of its own work. Not because it lies, but because a single actor grading its own output has no independent evidence to offer you.
120x already solves the human version of this problem by separating the roles. The architect's plan is a document the builder must work against, and the gap between plan and result is visible to a third party. That separation is what makes the work auditable.
So skafu's problem statement is:
Build a pipeline where separated roles are automated agents, every handoff is a durable artefact, and no single agent can mark its own work as done.
Everything else in the system follows from that sentence.
From a human framework to an agentic one

Skafu keeps the architect and the builder, and adds two roles that exist purely to break the self-grading problem:
| Role | Answers | Cannot |
|---|---|---|
| Architect | What are we building, and is this plan acceptable? | Write source code |
| Builder | Here is the implementation, and here is what I actually changed | Approve its own plan or verdict |
| QA | Does this meet the acceptance criteria, with reproduced evidence? | Change the code it is judging |
| Reviewer | Is this genuinely safe to close, against the original intent? | Be overridden by any agent |
| Owner (human) | I accept this trade-off, and here is my name on it | — |
The addition that earns its keep is the reviewer. QA checks the sprint against its own acceptance criteria. The reviewer checks the sprint against what was originally approved — which is a different question, and occasionally a much less comfortable one. A sprint can pass every acceptance criterion as written and still have delivered a fraction of the approved scope, because the criteria were worded narrowly. QA has no mandate to notice that. The reviewer does.
The sprint machine
A sprint is not a prompt. It is a walk through thirteen states — plus blocked,
which is terminal and reserved for a human. Nine of those states dispatch an agent,
each with exactly one acting role and one prompt template; the rest are decisions
the orchestrator makes on its own.

Two properties of this machine matter more than the state count.
The loop is bounded. qa-failed → repair plan → approval → reverification → qa-failed is a cycle, and cycles in an automated system are how you wake up to a
five-figure bill. Skafu bounds it three ways: a hard iteration ceiling
(max_qa_iterations), a no-progress detector that fingerprints what failed and
stops when the same failures repeat, and a stuck-loop cap for a step that runs
without advancing. When a bound trips, the sprint goes to blocked — which is
terminal, and deliberately requires a human.
Every transition is a row. State lives in Postgres, and every move writes a
sprint_executions record with the actor, the from-state and the to-state. The
board is not a status field someone remembered to update; it is a ledger you can
query afterwards to reconstruct exactly what happened and who did it.
Artefacts are the handoff
The numbered files are the entire integration contract between agents. No agent talks to another agent. They talk to a folder.

Because the handoff is a file, three useful things fall out for free.
It is inspectable. When something goes wrong, the reason is on disk in the sprint folder, not in a log that has rotated away or a context window that has closed.
It is checkable. The builder declares which files it changed; the orchestrator compares that declaration against the real working tree. Agreement is expected. Disagreement stops the sprint before the reviewer ever sees it — the point being that an undisclosed change is a different kind of problem from a wrong one.
It is resumable. An agent that dies mid-sprint has not destroyed the sprint. The artefacts up to that point still stand, and the next dispatch reads them.
The guardrail stack
Separation of roles gets you honest reporting. It does not, on its own, get you safety. Skafu layers mechanical guards underneath the agents, on the principle that a guard which depends on an agent choosing to respect it is not a guard.

The write allowlist is the one I would keep if I could keep only one. Each role
gets a set of path fragments it may write to, enforced as a pre-tool hook rather
than as an instruction in a prompt. The architect owns planning/ and the
requirements, blueprint and acceptance files; the builder owns the source tree and
its own plan and results. So the builder does not get to quietly rewrite the
acceptance criteria it is about to be judged against — role ownership is checked by
the runtime, not left to the agent's good manners.
It is worth being precise about how strong that guarantee is, because it is easy to
overstate. The hook gates the editing tools — Write, Edit, MultiEdit,
NotebookEdit — and it does not gate Bash. An agent that really wants to write
outside its lane can still reach for shell redirection, which lands the same edit
with none of the review the hook implies. In practice the allowlist has been very
effective at keeping roles in their lane, and the observed failure mode when a
legitimate path is missing from the list is not a breach but a stall: the agent
loses its turn and gets pushed toward workarounds. That is a strong argument for
keeping the list current — which is why projects can extend it declaratively via a
.skafu.yaml rather than waiting on a code change.
The owner waiver is the other one. Agents cannot grant themselves authority, and — importantly — an agent cannot be persuaded by prose in a file that the owner approved something. Owner authority exists only as an attributed, timestamped, classified record in the database, scoped to a single sprint unless explicitly made project-wide. If a trade-off is accepted, the acceptance has a name on it and a reason attached, and it appears in the audit trail forever.
What the agentic code actually looks like
The implementation is a Flask backend with an APScheduler tick, driving the Claude Agent SDK. The snippets below are illustrative — simplified from the real thing to show the shape.
The tick. Autorun is a scheduled sweep, not a long-lived process. Every minute it asks which projects have work and drives them, up to a concurrency cap.
def autorun_tick():
settings = SkafuSettings.get()
if not settings.autorun_enabled:
return
candidates = [p for p in active_projects()
if p.state not in ("closed", "blocked")]
for project in candidates[:settings.max_concurrent_projects]:
advance(project) # one step, then return
advance() takes one step and returns. A crash costs you a step, not a sprint.
The step. Resolve state → pick the actor → run it → decide the next state.
def advance(project):
state = project.active_sprint_state
actor = ACTOR_FOR[state] # architect | builder | qa | reviewer
if actor is ORCHESTRATOR:
return transition(project, decide_without_agent(project))
output = dispatch(actor, state, project) # ← the agent call
persist_artifact(output) # 01.. 07.. 08.. on disk
nxt = state_machine.next(state, artifacts_of(project))
nxt = apply_progress_caps(state, nxt, output)
nxt = apply_quality_gates(state, nxt)
nxt = apply_owner_waivers(state, nxt)
transition(project, nxt)
Note the ordering. The state machine proposes; the caps, gates and waivers dispose.
An agent returning PASS is an input to that decision, not the decision itself.
The dispatch. Each role is a thin driver over the SDK, differing only in system prompt, allowed tools, and write allowlist.
async def run_agent(role, state, project):
session_id = str(uuid4())
park_session_marker(project, state, role, session_id) # BEFORE the run
options = ClaudeAgentOptions(
system_prompt = load_prompt(PROMPT_FOR[state]), # per-state template
cwd = project.workspace_path,
model = model_for(role),
max_turns = turns_for(state),
session_id = session_id,
hooks = build_hooks(project, role, budget), # ← the allowlist
)
async for msg in query(prompt=build_prompt(state, project), options=options):
record(msg) # stream to the live run-log
budget.observe(msg) # track context growth
return parse_output(role)
The hook. The allowlist is a PreToolUse hook. It sees the path before the
write happens and can deny it.
def build_hooks(project, role, budget):
allowed = ALLOWLIST[role] + project_extra_paths(project, role)
def before_write(tool, path):
if budget.exhausted and not is_completion_artifact(path):
budget.request_checkpoint()
return deny("Context budget reached — write your checkpoint and stop.")
if not any(fragment in path for fragment in allowed):
return deny(f"{path} is not writable by the {role}.")
return allow()
return {"PreToolUse": [before_write]}
That second branch is the guardrail. The first is the more interesting one.
Winding down instead of falling over. When a run approaches its context budget, the hook stops granting ordinary writes and tells the agent to write a checkpoint — a handoff describing what it did, what it learned, and what remains. The sprint then re-dispatches the same state, and the next run starts from that summary rather than from nothing.
class ContextBudget:
def observe(self, msg):
self.tokens = context_tokens(msg)
@property
def exhausted(self):
return self.requested or self.tokens >= self.limit
The agent is not truncated mid-thought. It is asked to land the plane.
Surviving the machine going away. Skafu runs on spot capacity, so runs get
killed. The session id is minted before the process starts and parked on disk,
keyed by (project, sprint, state, role) rather than by run id. Any later dispatch
of the same step finds it and resumes the transcript.
marker = read_session_marker(project, state, role)
if marker:
options.resume = marker # continue the killed run's conversation
else:
options.session_id = mint() # cold start, but parked before we begin
The ordering is the whole trick: park first, run second. A marker written after the run starts protects nothing during startup, which is exactly when a reclaim tends to land.
Lessons learnt
Separating the roles is the thing that works. Every genuinely valuable catch in this system came from a role looking at another role's output with a different question in hand. The reviewer catching a sprint that passed all ten of its acceptance criteria but delivered a fraction of the approved scope is not a clever algorithm — it is just a second reader with a different brief. If you take one idea from 120x into an agentic system, take that one.
Agents that refuse to invent work are worth more than agents that always produce something. The best behaviour I have seen in this pipeline was a builder that returned "there is no failure here to repair" and an architect that declined to manufacture a root cause, when the orchestrator had opened a repair cycle it should not have. Both were right, both refused to fill the silence with plausible output, and the bug was in the orchestration rather than in either of them. Prompts that reward honest refusal pay for themselves.
Make the guard mechanical, or it isn't a guard. Anything enforced only by an instruction in a prompt is a suggestion. The write allowlist works because it is a hook that returns "denied" before the tool runs. The gates work because the engine reads the quality result itself rather than trusting the agent's summary of it.
The corollary, learned the harder way: a mechanical guard is only as complete as its coverage. The allowlist gates the editing tools but not the shell, so it constrains where an agent routinely writes rather than where it possibly can. Knowing exactly which of your guards are walls and which are strong fences is worth more than believing they are all walls.
A gate has to hand its findings to whoever fixes them. An early version could fail a sprint on a quality gate and route it into a repair loop that had nothing to repair — the gate knew what was wrong, and the loop's input file did not exist. The fix was small and the principle is general: whatever stops the work must also describe the work, in the place the next actor is going to look.
Design for the process disappearing. Spot reclaims turned out to be a good forcing function. Once you assume the machine can vanish between any two instructions, you write the marker before the run, keep transitions atomic, take one step per tick, and put the evidence on disk rather than in memory. The system got simpler under that assumption, not more complicated.
Human authority should be scarce, explicit and permanent. Skafu's owner waivers are deliberately awkward: they require a classification, a written reason, and they are scoped to one sprint by default. That awkwardness is the feature. A trade-off you had to name and justify is a trade-off you can find again six weeks later, which is the difference between a decision and a shortcut.
Bounded loops beat clever loops. Most of the operational safety in this system comes from very unglamorous ceilings — max iterations, a no-progress fingerprint, a stuck-step cap. None of them are smart. All of them have, at some point, been the thing standing between an automated pipeline and an unbounded spend.
Where it stands
Skafu currently runs several projects concurrently against real repositories, driving them sprint by sprint through the machine above: architect authors, builder implements, QA verifies with reproduced evidence, reviewer audits against the original intent, owner decides anything the machine cannot. Sprints close on their own. The ones that stop, stop with their reasoning written down.
The framework underneath it is still 120x's idea: separate the person who decides from the person who builds, carry the decision in a blueprint, and hold the bar at better than manual. Skafu's contribution is narrower — it takes those roles seriously enough to give each one an agent, a prompt, a write boundary and a file to hand to the next one.
Which turns out to be most of what an operating system for delivery actually is.