Utkrushta · Task Audit Pipeline

Task Audit — the end-to-end flow

What happens from the moment a position is created to the moment every one of its tasks has a verdict, a trace, and a cache entry — one endpoint call, one DAG, one ephemeral container, two audit legs, one solve.

deterministic check LLM / interpretive E2B sandbox persisted artifact

01The pipeline, stage by stage

1
Trigger — the audit endpoint fastapi_service/routers/v2/audit.py

A position is created (or an operator wants tasks verified). The backend calls one endpoint with the position's task ids — a batch, not one call per task.

Position created POST /v2/tasks/audit
{"task_ids": [id1 … id10]}
2
Cache gate — the content hash runs inside the endpoint, BEFORE any compute

For each id, the endpoint asks: "have we already audited this exact content?" It never trusts a timestamp (the tasks table has none) — it fingerprints the content itself.

Per task id — three steps, milliseconds
① fetch 5 fields
task_blob · criterias · pre_requisites · answer · template_id
② sha256 fingerprint
canonical JSON, keys sorted
③ lookup task_audits
WHERE task_id = X AND content_hash = H
Healthy row found (PASS-family only) → content byte-identical to the last audit → return that verdict instantly → cached[]  ·  No row / negative verdict / editedqueued[]  ·  Id not in tasks tableunknown[] (rejected up-front, never queued)
Measured: cache hit = 2.3 s, $0 · full audit = 2–17 min. A 10-task position where 9 are unchanged does 1 real audit, not 10.
3
Trigger the DAG — only for the queued ids shared/utils/airflow_utils.trigger_dag

If everything was cached, the DAG is never triggered — the endpoint returns immediately with the cached verdicts. Otherwise the queued ids are comma-joined into ONE DAG run (idempotent run_id, so retries can't double-fire).

endpoint— JWT → Airflow REST /dags/audit_task/dagRuns DAG run
conf = {"task_ids": "id2,id7"}
Response to the caller: { triggered, dag_run_id, task_ids: queued[], cached[], unknown[] } — instant answers for the cached, a ticket for the rest.
4
DAG → one ephemeral container airflow/dags/audit_task_dag.py

The DAG is a thin orchestrator — it owns no audit logic. It launches one throwaway container running the worker image, passes the ids as the container command, streams the container's logs into the Airflow task log, and waits.

Prod path
EcsRunTaskOperator → AWS Fargate task utkrusht-task-audit-{dev,prod}, deferrable + reattach, CloudWatch logs
Local path (AUDIT_EXEC_MODE=local)
docker run --rm utkrusht-task-audit:local via the mounted docker.sock — same image, same command, zero AWS
container command:runner.py --task-id id2,id7 --env prod --persist db
5
The worker image Dockerfile.verifier — everything the audit needs, nothing else (1.16 GB)
python 3.12-slim
runner.py + deps (openai · e2b · supabase · boto3)
node 22 + pi 0.80.3
the headless coding agent
pi provider config
~/.pi/agent/models.json → ollama = https://ollama.com/v1

Built grader-style: explicit COPYs only — never COPY . ., so secrets can't land in image layers. All secrets arrive at runtime (-e env locally / AWS Secrets Manager in prod): OLLAMA_API_KEY, OPENROUTER_API_KEY, E2B_API_KEY, GitHub tokens, Supabase keys, S3 creds.

6
runner.py — LEG 1 · INTEGRITY seconds, no sandbox — the two audit files

For each task id (serial loop, crash-isolated per task), the worker re-checks the cache (safety net), then runs two complementary audits:

File 1 · task_audit.py — DETERMINISTIC (the linter)
Pure Python rules. Same input → same output, every single run. No LLM, no network except the GitHub API. Checks:
title non-empty, not a slug short_overview exactly 3 items, no •/-/* glyphs question 120–1500 chars outcomes · hints · definitions present, non-empty pre_requisites 2–3 items, ≤120 chars criterias fields present · no duplicates · FK exists in competencies answer non-empty infra ↔ template consistency + FK exists in templates repo + gist reachable live GitHub API check
File 2 · llm_audit (in runner.py) — INTERPRETIVE (the judge)
The things a regex can't judge. The SKILL.md rule-books are passed verbatim as the system prompt (task-audit + task-repo-check) — the skill IS the spec, never re-authored in code. Model: glm-5.2, temperature 0.
input task_blob + README + starter source files + GIST files (fetched from GitHub; truncation is marked, never silent) judges prose quality · narrative shape (context→problem→ask) · instruction-stacking · markdown/setup leaks · nothing pre-solved · no answer leak findings[]
Real catch: "short_overview[1] opens with imperative 'Harden' — instruction-stacking" · "question contains unrendered Markdown backticks" — judged from the actual content, not a pattern match.
7
runner.py — LEG 2 · SOLVABILITY the expensive leg — can an agent actually solve this task?
Routing — by the task's infra flag (task-verify skill rule)
is_shared_infra_required = true
→ E2B sandbox (template's live services)
is_shared_infra_required = false
→ LocalExec: tests run right here in the worker container — no sandbox, no template needed (69% of the catalog)
E2B sandbox — the candidate's real environment (infra tasks)
deploy
sandbox from the task's template — live Postgres / Kafka / services
+ clone starter repo
→ sandbox /home/user/task AND a local checkout ex.work on the worker
Baseline — one test run on the CLEAN starter, before any solving
no tests collected
→ verdict UNVERIFIED — no oracle; task-quality defect. Solve ladder SKIPPED (~2 min, $0 — not 17 min, $1.09)
already green
→ verdict INVALID — nothing to solve
red
→ normal: proceed to solve
Pi headless — the chef / courier model
Pi never enters the sandbox. The chef (pi binary, on the worker) reads and edits the LOCAL checkout with its own read/edit/bash tools. The courier (sbtest.sh) syncs every changed file INTO the sandbox and runs pytest THERE — where the live services are. Local mode skips the courier: Pi runs the tests directly.
Pi edits ex.work
local, fast
— sbtest.sh syncs → sandbox runs tests
against live services
— results back → Pi iterates
Escalation ladder — cheap first, frontier only on failure
① kimi-k2.7-code
Ollama Cloud · $0 marginal
fail → ② glm-5.2
Ollama Cloud · $0 marginal
fail → ③ claude-sonnet-5
paid · the reference gate
A PASS is self-validating — the task's own tests really went green, so it's solvable no matter which model did it. Only a FAIL escalates: Sonnet separates "genuinely unsolvable task" from "cheap model too weak". Every attempt records error, the failing-test tail, and its sampling metadata (temperature / resolved model) — so "passed yesterday, failed today" is answerable.
verdicts → PASS · PASS_WITH_FINDINGS (solvable, row needs fixes) · FAIL (real effort, never green) · UNVERIFIED (no test oracle — solvable stored as unknown, not false) · INVALID (starter already green). Worker crashes are exit-codes, never verdicts — Airflow retries crashes, not outcomes.
8
Persist — the audit trail one row + heavy artifacts, per task
Supabase · task_audits (one row per run — full history)
verdict · findings[] · solvable (true/false/null) · solved_by · failure_reason · content_hash (stamps the cache for next time) · trace_url · detail{escalation ladder, sampling, baseline, solve_mode, solution_diff, integrity split} · cost_usd · duration_s
S3 · the heavy artifacts
trace.json — THE RECORDING: the full Pi transcript — every thinking block, every read/edit/bash tool call, every test output. Replayable forensics (real run: 59 entries, 33 tool calls, 107 KB). Failed attempts' transcripts kept too.
+ verdict.json snapshot · solution.diff (the exact code the agent changed)
Local runs write the same three files to solvability_runs/<task-id>/ instead.
9
The loop closes

The row just written contains the task's content_hash. The next time anyone triggers an audit for this task — tomorrow, or for a different position sharing the same task — stage 2 recomputes the hash, finds this row, and answers in 2 seconds for $0. Only a real content edit (new question, new answer, new criteria…) changes the hash and earns a fresh audit.

02The two audit files, side by side

task_audit.py — deterministicllm_audit — interpretive
NaturePure rules — counting, presence, format, ranges, duplicates, FK existence, live URL reachabilityJudgment — prose quality, narrative shape, leaks, "is the starter appropriately incomplete?"
Same input → same output?Always (that's the point)Near-deterministic (temperature 0) but fundamentally a model's judgment
Where the rules liveIn the code — each check is an explicit Python ruleIn the SKILL.md files, passed verbatim as the system prompt — editing the skill updates the auditor, no code change
InputThe Supabase task row (+ reference sets from competencies/templates tables)task row + README + starter source files + gist files fetched from GitHub
Cost / speed~free, <1 s (+2 GitHub API calls)one LLM call (~15–20 s, ~$0 on the Ollama flat plan)
Example catchpre_requisites: 6 items (max 3)short_overview[1] opens with imperative "Harden" — instruction-stacking
Why bothMechanical rules can't judge prose; an LLM shouldn't be trusted to count. Each does what the other can't — findings from both merge into one findings[].

03The content hash — how the cache gate knows nothing changed

The tasks table has no updated_at column — so instead of trusting a timestamp, we fingerprint the content itself. The hash is stored only on audit rows (the report), never on the task (the cake) — the current hash is recomputed fresh each time, which is free and can never go stale.

# the recipe — identical code in the endpoint AND the worker (byte-parity locked by twin unit tests) material = { task_blob, criterias, pre_requisites, answer, template_id } # the 5 audited fields — nothing else canonical = json.dumps(material, sort_keys=True) # sorted keys ⇒ same content always ⇒ same string content_hash = sha256(canonical) # ⇒ 64-char fingerprint # real example — task 4e3fb8bc (streamflix), live dev data: eb245974f8325f398d25b5469d440b8472473ee925ef838641d109d6b683cffa # the gate, per task id: SELECT verdict FROM task_audits WHERE task_id = :id AND content_hash = :fresh_hash AND verdict IN ('PASS','PASS_WITH_FINDINGS') → row found = unchanged since a HEALTHY audit → CACHED (2.3 s, $0) → no row = edited / never audited / last verdict negative → QUEUED (real audit)
EventFresh hashMatch in task_audits?Action
First ever auditeb2459…noneaudit → store row with eb2459…
Re-trigger, task untouchedeb2459…✓ matches (PASS-family)cached — no container, no sandbox, no LLM
Someone edits the question55de77…✗ differentre-audit → store row with 55de77…
Task previously FAILED / UNVERIFIEDunchanged✗ negative verdicts never cachedre-audit — a repo-side fix always gets a fresh look
Someone touches status / other non-content fieldeb2459… (unchanged)✓ matchescached — incidental touches never waste an audit
Known limit (deliberate, mitigated)
The hash covers the database row only — an edit made purely to the starter GitHub repo doesn't change it. Mitigation already in place: negative verdicts (FAIL / UNVERIFIED / INVALID) are never served from cache, so a repo-side fix always earns a fresh audit; only healthy PASS-family verdicts are reused. Folding the repo's commit SHA into the fingerprint is the planned full fix.

04What we can prove afterwards — logs & traces at every layer

LayerWhat's capturedWhere it lives
Endpointcache-gate decision per batch (requested / cached / queued / unknown counts)structured app logs
Airflowthe exact container command (secrets excluded — value-less -e passthrough) + the container's full stdout, streamed liveAirflow task log (CloudWatch in prod)
Worker phases[1/2] AUDIT findings count → [2/2] mode + baseline → per-model attempt: passed?, tool calls, the failing-test tail (the exact RED reason), errors (pi exit codes, timeouts, provider errors)container stderr → Airflow log
Verdictverdict + findings + escalation ladder + failure_reason + baseline + solve_mode + sampling (temperature / resolved model) + cost + duration + content_hashtask_audits row (queryable)
The recordingfull Pi transcript per attempt (winner AND failed attempts) — every thinking block, tool call, test output; plus the solution diffS3 trace*.json (trace_url on the row) + detail.solution_diff

05Measured numbers (from the real runs)

ScenarioWall timeCostOutcome
Cache hit (unchanged task)2.3 s$0prior verdict returned, no compute
Full audit, kimi solves (streamflix infra task)~2.5 min$0 marginal (Ollama flat)PASS_WITH_FINDINGS · 17–27 tool calls · trace + diff persisted
Full audit, escalates to Sonnet~6 min$0.31PASS_WITH_FINDINGS · 33 calls · reference gate did its job
Broken task (no tests) — BEFORE the baseline check~17 min$1.09FAIL after burning the full ladder — the motivation for the UNVERIFIED short-circuit
Same broken task — AFTER the baseline check~2 min$0UNVERIFIED — baseline detected "no tests" and skipped all three models entirely
Non-infra task (LocalExec, no sandbox)~4 min$0 marginalPASS_WITH_FINDINGS · solved by kimi in-container — unlocks the 341 template-less tasks (69% of catalog)
Mixed batch (cached + unknown + new)cached answered instantly · bogus id rejected as unknown · only the new id triggered the DAG

Stack: FastAPI (cache-gated endpoint + DAOs) · Airflow audit_task DAG (EcsRunTaskOperator / local docker) · Dockerfile.verifier (python + node + pi, 1.16 GB) · runner.py (task_audit.py + llm_audit + baseline + Pi solve, E2B or local) · task_audits (Supabase) + S3 traces · 19 unit tests incl. cross-repo hash-parity lock. All timings/costs measured on real dev runs, 2026-07-10.