Task Audit —
running our skills in the cloud
We already have SKILL.md files that check whether a generated task is any good. Today someone runs them by hand, one task at a time. This pipeline runs the same skills automatically, in a throwaway cloud container, for every task in a position.
01What this is
Our tasks are generated by AI, so some of them are broken. The question might be too short, a link might be dead, or the task might simply be impossible to do. We only find out when a candidate complains.
We already know how to check this by hand: open a coding agent on your laptop, point it at a SKILL.md file, and let it go through the task. It works, but it takes about twenty minutes per task and nobody has time to do it for every task.
This pipeline is that same check, done by a machine. When a recruiter creates a position, every task in it is sent off to a small cloud container. Inside, an AI agent reads the task, tries to actually do it, and something independent checks whether the work is right. The team gets one Discord message saying which tasks are fine and which need fixing.
Before — by hand
Someone opens a coding agent on their laptop
Points it at the SKILL.md file
Waits ~20 minutes
Does this for one task, when they remember
Now — automatic
A position is created
The same SKILL.md runs inside a container
All tasks in the position, in one go
Results land in the database and Discord
02The flow
Four steps from "position created" to "results in Discord".
A bit more on each step
- The API call. We send it the list of task IDs. Before doing anything expensive, it checks which tasks have already been checked and haven't changed since — those are skipped. If a task was edited (even just its README on GitHub), it gets checked again.
- Airflow. Think of it as the scheduler. It doesn't do any of the checking itself — its only job is to start the container, watch it, show us the logs, and start it again if the machine crashes. We cap it at two containers at a time so we don't flood anything.
- Fargate. AWS gives us a fresh machine, runs our container on it, and deletes it when the container exits. Nothing is left behind, so every run starts clean.
03Inside the container
The Dockerfile is just a list of what to install so the machine can do the job on its own. Five things matter:
# 1. Python, git and curl — the basics
FROM python:3.12-slim
# 2. Node, then the "pi" coding agent — this is the thing that will do the task
RUN install node ... && npm i -g pi-coding-agent
# 3. Tell pi where to get its AI from: Ollama Cloud, over the internet.
# (On a laptop this points at localhost — there's no such thing in a container,
# so we point it straight at ollama.com. This one line trips people up.)
RUN write /root/.pi/agent/models.json → baseUrl: https://ollama.com/v1
# 4. Our own code and our SKILL.md files
COPY runner.py scripts/ rules/ /app/
# 5. When the container starts, run our script
ENTRYPOINT ["python", "runner.py"]
Notice what is not in there: no passwords, no API keys. Those are handed to the container by AWS when it starts. The image itself is safe to store.
The last line is the important one. Airflow doesn't send code to the container — the code is already inside. Airflow only passes the arguments: which tasks to check, and whether this is dev or prod.
The file that actually runs — runner.py
Think of it as the foreman. The Dockerfile builds the workshop and hires the workers;
runner.py is the one holding the clipboard, walking the job from start to finish and
writing down the result. It's the file named in that last line, so it starts when the container
starts — and when it's done, the container dies with it.
It reads the arguments it was given (which tasks, dev or prod, where to save results), then works through the tasks one at a time. If one task blows up, the rest still get done.
For each task, in order:
- Check whether anything about the task has changed since last time. If not, reuse the old result and skip the rest.
- Run the plain checks — fields filled in, links working, the environment it points to actually exists. Ordinary code, no AI: these are facts, and an AI would just make them up.
- Send the task's content to an AI along with our SKILL.md rules, for the judgement calls — is the answer leaked in the starter code, does the question read badly.
- Decide where the work happens: a full cloud sandbox for tasks that need live services, or right inside the container for simpler ones.
- Run the task's tests on the untouched starter first. That shows whether there's genuinely something to solve, and how the work will be judged later.
- Start the Pi agent and let it attempt the task — trying a second model if the first genuinely fails, and handling the awkward cases (the agent did nothing at all, or it hung).
- Grade the result, after putting the original tests back in case the agent touched them.
- Walk the candidate's guided tour to check nothing is broken, then save the row, upload the files, and move on.
Once the whole batch is done, it posts the single Discord message and exits.
What it deliberately does not do is any of the specialist work — it only decides what happens next, and in what order:
| Job | Who does it |
|---|---|
| Talking to the sandbox | courier.py |
| Running the agent | pi_solve.py |
| The plain field checks | task_audit.py |
| Saving to the database and S3 | persist.py |
| Writing the Discord message | notify.py |
runner.py reports success is about us, not about the tasks. A task
that fails its check is still a successful run — the container did its job and recorded a real
answer. It only reports failure when something on our side broke, which is the signal for Airflow
to try again. That's what stops a genuinely bad task from being retried forever.04How it actually solves the task
This is the part people usually want to see. Our script starts the pi agent as a
normal command, the same way you would in a terminal:
pi -p "<the task, written out>" --provider ollama --model kimi-k2.7-code --api-key ...
After that we don't drive it. Pi runs on its own in a loop:
- It sends the task and the files it has read to Ollama Cloud.
- The AI answers with something to do — read this file, change that line, run this command.
- Pi does it, locally, in the copy of the code we downloaded.
- It sends the result back and asks what's next. Repeat until it thinks it's done.
Two details worth mentioning:
- The agent gets the same instructions a candidate gets — the actual task description, not a vague "go fix this". We also tell it to run things and look at the real output before changing anything, so we know its findings are real and not guessed.
- We check the work, not the agent. When it finishes, our script takes over: it puts the original tests back (in case the agent touched them) and checks the result itself. If the task has no tests, a separate AI reads the change against what the task asked for and the answer we have on file. Either way, the agent doing the work never decides whether it passed.
05How a SKILL.md file runs in the cloud
This is the whole point, so it's worth being clear about it.
A SKILL.md file is just instructions written in plain English — "check the title is properly written", "make sure the answer isn't leaked in the starter code", "the question should be between 120 and 1500 characters". When you run a skill on your laptop, your coding agent reads that file and follows it.
In the container it's the same idea, with one difference: instead of a person opening the agent,
our script does it. The SKILL.md files are copied into the image (rules/ in the
Dockerfile above), and at the right moment the script reads the file and hands it to an AI as its
instructions — word for word, nothing rewritten.
On your laptop
You open the coding agent
You point it at SKILL.md
It follows the instructions on one task
You read the result on screen
In the container
The script starts automatically
It reads the same SKILL.md from rules/
It follows the instructions on every task
The result is saved and posted to Discord
Which skills go where
We have three SKILL.md files in the image, and they're used at different moments:
| Skill file | When it runs |
|---|---|
task-audit | While checking the task's details — the field-by-field rules |
task-repo-check | Same moment — the rules for the starter code and the gist |
task-summary | At the end — how to write the report a human will read |
Where these three came from
Upstream we don't have three separate skills like this. We have one bigger skill,
task-verify, with task-solvability underneath it. That didn't
get copied across as it was — it got split up, because different parts of it needed to become
different kinds of thing:
| Part of the original skill | What it became here |
|---|---|
| The step-by-step procedure — download the code, run it, let an agent try, check the result | Code. It's the eight steps under Inside the container (section 03) — that's
runner.py |
| How to write up the result for a human | A rule file — task-summary. Still a judgement call, so it stays as
instructions for an AI |
| Its sandbox helper | Code — courier.py |
task-audit and task-repo-check were already skills in their own right,
and they were copied in unchanged.
The rule of thumb behind the split: a fixed sequence of steps belongs in code; a judgement call belongs in a SKILL.md. Steps don't need an AI to follow them, and code can't make a judgement call.
06What you get back
Every task ends with a verdict. These are the exact values saved in the
verdict column and shown in the Discord message:
| Verdict | What it means |
|---|---|
PASS | Good to give to a candidate. |
PASS_WITH_FINDINGS | It works, but some details need tidying. The findings list says exactly what. |
FAIL | The agent genuinely tried and the task couldn't be done — or the guided walkthrough is broken. |
INVALID | The starter code already passes, so there's nothing to actually do. |
ERROR | Our own machine broke. Not a verdict on the task — it just runs again. |
Only PASS and PASS_WITH_FINDINGS are
remembered. A task that failed always gets checked again next time, so a fix is never hidden behind
an old result.
All of it lands in a task_audits row (kept forever, so you can see a task go from
failing to fixed), the full record goes to S3, and the team gets one Discord message per position
listing what needs attention and how to fix each thing.
07What's left to set up
The code is done and has been tested end to end on dev by running the container locally. What's missing is the AWS side — nothing has been created there yet.
- Create the AWS pieces — somewhere to store the image, the container definition for dev and prod, and permission for our CI to push images.
- Add the keys to that container definition: Ollama, Anthropic (the fallback), GitHub, E2B, AWS, Supabase, and the new Discord webhook.
- Copy the database changes to prod. They're all applied on dev. One of them matters more than the rest: without it the container can't read the template list, and the check that catches broken tasks stays switched off.
- Call the endpoint when a position is created.
- Fix six tasks already live in prod that this audit found — they point at environments that don't exist, so they won't even start for a real candidate.
One thing to expect on the first run: everything gets checked once, because none of it has been checked before. After that, only tasks that actually changed are re-checked.