-
-
CSV Mender: file an issue and walk away. 6 of 7 test runs zero-touch, median 14 min 45 s from issue filed to issue closed.
-
The loop: GitLab Duo flow, checks and GitLab Duo Code Review, merge:gate CI job, deploy and smoke test, notify:issue, automatic rollback.
-
Issue #8 filed; the GitLab Duo flow runs the project's parser, labels it import::header_alias and names the fix.
-
Names replaced before anything is committed. Amounts unchanged.
-
Merge request !6 opened by the flow; GitLab Duo Code Review approves with no comments.
-
Main pipeline 2984075566 after the gate merge: staging, production and Cloud Run deployed and smoke-tested, 5 min 14 s.
-
notify:issue posts "Fix shipped" and closes #8, 20 min 42 s after filing, no clicks.
-
Same bank file before and after the fix: a classified error, then a clean import that reconciles.
Verify it yourself in 2 minutes
The claim: a user files an issue, and the fix reaches production and the issue is closed with no human clicks in between.
The numbers: 7 runs, 6 of 7 zero-touch (shipped and closed with nothing clicked after filing), median lead time 14 min 45 s from issue filed to issue closed. Full table: https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/blob/main/README.md#results-real-runs-public-links
Three zero-touch runs; every link opens logged out. Times Europe/Berlin (UTC+2 on 23 and 24 Oct).
- Run 06, 23 Oct: a seeded bad instruction, refused. Filed 08:43:16, closed 08:54:21, 11 min 5 s. Issue https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/work_items/7 ; merge request https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/merge_requests/5 ;
merge:gatejob https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/jobs/17406732415 ;mainpipeline after merge https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/pipelines/2979871097 ;notify:issuejob https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/jobs/17406736568 - Run 05, 24 Oct: header row repeated on every page. Filed 16:59:33, closed 17:11:35, 12 min 2 s. Issue https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/work_items/10 ; merge request https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/merge_requests/8 ;
merge:gatejob https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/jobs/17438972548 ;mainpipeline after merge https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/pipelines/2984729462 ;notify:issuejob https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/jobs/17438975415 - Run 04, 24 Oct: balance rows with an empty Amount. Filed 09:31:08, closed 09:44:27, 13 min 19 s. Issue https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/work_items/9 ; merge request https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/merge_requests/7 ;
merge:gatejob https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/jobs/17435660175 ;mainpipeline after merge https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/pipelines/2984096966 ;notify:issuejob https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/jobs/17435661073
What to look for: on the issue, the flow's triage comment and its merge request link, then the "Fix shipped" note and the close. On the merge request, the GitLab Duo Code Review approval and the note "Merged automatically by the merge:gate CI job". In the main pipeline, deploy:production and smoke:production passed with no manual play.
Inspiration
I wanted the loop after the code is written to run by itself: file an issue, walk away, and the next time you look the fix is in production and the issue is closed. I picked a first domain where "correct" can be checked by code, because an agent loop is only as trustworthy as the checks around it.
Banks change their CSV exports without warning: a semicolon instead of a comma, day and month swapped, negatives written as (12.50) or 12.50-. Each time, an importer breaks, the fix is manual, the regression test is easy to skip, and the customer's real file, full of names and account numbers, is the obvious thing to paste into the repo.
The code change itself is usually small. Everything around it is the slow and risky part: reproducing the bug, getting a safe test file, testing, scanning, deploying and telling the user it is fixed. That is the post-code work this hackathon is about, so I built a loop for exactly that.
What it does
CSV Mender is a hands-off loop: it turns a bug report into a tested fix in production, with nobody clicking anything on the issue or the merge request. Its first domain is bank CSV imports. It is a Path A (Start Fresh), Hands-off entry.
- Report. A user opens an issue from the "Import failure" template and pastes a few rows. Filing it starts my custom GitLab Duo flow.
- Triage. The triage agent writes the sample to a git-ignored
raw/folder and runs the project's own normalizer on it. The label (for exampleimport::sign_convention) comes from the error category the code prints, not from a guess. It then comments with the cause and line number, without quoting the raw rows. - Keep raw data out of git. A deterministic scrubber (
scripts/scrub_fixture.py) turns the sample into a fixture that still reproduces the bug: delimiter, quoting, encoding, dates and amounts stay; names, account numbers, IBANs and e-mails are replaced. Only the scrubbed file is committed, and CI runs a PII pattern check on every committed CSV. - Fix, test first. The fixer agent commits the scrubbed fixture with its expected output first, so the pipeline goes red, then makes the smallest fix in
normalizer/with a unit test and opens a merge request with root cause, fix, evidence and privacy sections. - Verify, secure, package. Every branch pipeline runs ruff, 451 pytest tests with coverage (including 19 synthetic bank formats and property-based tests), the fixture privacy gate, SAST, secret detection, a Docker build that is health-checked before it is pushed to the GitLab registry, and container scanning of that image. GitLab Duo Code Review reviews the merge request.
- Review loop and merge gate. If Duo requests changes, the scheduled
merge:gateCI job (every 15 minutes during bugs 01 and 02, every 5 minutes since) posts one note asking the flow to apply them; the flow pushes on the same branch and Duo reviews again (at most 2 rounds).merge:gatemerges only an openimport-fix/merge request with an allowlisted label, a green pipeline on the current head, Duo's approval and GitLab's "mergeable", pinned to the checked commit. Since 22 Oct it also refuses a fix that touches CI, flow or agent definitions,scripts/, dependency files or an existing test fixture, so a fix cannot pass by changing what the tests expect (not yet exercised live). It merges with my personal access token, used only in this project, and every merge commit and merge request note says it was the gate. - Release. That merge commit deploys to
staging, is smoke-tested, then deploys to production on its own, and the same build is deployed to Google Cloud Run. Any other commit onmainkeeps a manual production job. Kill switches:AUTO_MERGEturns merging off,AUTO_DEPLOY_PRODUCTION=falseputs production back behind the manual job. Pushing av*tag creates a GitLab release with notes generated from the commit history. - Monitor. The smoke test checks that the deployed build id matches the pipeline's commit and uploads three corpus files, comparing the output exactly with the expected files. A second smoke test (
smoke:cloudrun) runs the same checks against the Google Cloud Run service. After production, a job posts "Fix shipped" on the original issue and closes it. If the production deploy or smoke test fails,rollback:productionredeploys the last good build and the issue stays open; a deliberate drill on 25 Oct ran it live and production was back on the last good build within about a minute (https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/blob/main/README.md#rollback-drill).
Bug 02 ran this way on 21 Oct 2026 (Berlin time): issue #3 filed about 17:41; the flow opened merge request !2 with a green pipeline (353 tests); Duo asked for one change; the gate asked the flow, the flow pushed a fix, Duo approved; the gate merged at about 18:36; the pipeline deployed version 0.1.5 to production, smoke-tested it, and closed the issue at about 18:42. Nobody clicked anything on the issue or the merge request; while it ran I changed two CI/CD variables (added my account to the gate's authors, because the flow opens merge requests as the trigger user, and switched the gate from dry run to live).
What runs today. Every lifecycle stage ran end to end in the bug 02 run in the public project. Govern is the gate's checks, the 2-round review cap, the two kill switches, the flow's service account acting with at most Developer rights, written rules for agents in AGENTS.md, the gate's path check and a CODEOWNERS file for the control-plane paths; I did not set up tool governance rules or code owner approval. The README maps each lifecycle stage to the file or job and marks what is planned.
Privacy, honestly. The flow's agents read the issue, so pasted sample rows are sent to the GitLab Duo model. The scrubber only guarantees that raw rows never land in git. If you want a sample to stay on your machine, use the scrubber locally, or the MCP server below, before posting. Every file in this demo is synthetic.
Deploys, honestly. Staging and production run inside GitLab CI: each deploy and smoke job starts the image from the project's registry as a CI service container and tests it over the job network, recorded under Operate > Environments. Every merge to main also deploys the same build to Google Cloud Run, and its own smoke test checks the build id and the corpus files there. The Cloud Run service is public: https://csv-mender-879903213333.europe-west6.run.app (/health and /version.json show the version and build that are live).
How I built it
- The app is a small, deterministic Python package (
normalizer/) with a CLI and a FastAPI web service. It detects encoding (UTF-8 with or without BOM, UTF-16, UTF-32 byte order marks, cp1252), delimiter, header aliases in English, German and French, date format per column, decimal separator per column and sign conventions, and converts everything todate, description, amount, balance. Every failure is aNormalizeErrorwith a category that doubles as the issue label. No AI runs inside the app. - Reconciliation checks row counts, control totals, the running balance (which does not depend on how amounts were parsed, so it catches sign and decimal mistakes) and an optional expected total.
- Tests: 451 pytest tests. 19 synthetic bank exports generated by
scripts/make_fixtures.py, whose expected outputs are written independently of the parser; unit tests for each step, the scrubber, the smoke test, the release notes, the issue notifier and repository checks; and robustness tests, including Hypothesis property tests for date parsing. Each fixed issue adds its scrubbed fixture undertests/fixtures/regressions/. - Robustness fixes, each with its own regression test: malformed digit grouping such as
12,34.00is rejected with anumber_formaterror instead of being misread; very long fields no longer cause a false delimiter error; delimiter detection splits the sample only on real CSV line breaks; UTF-32 files with a byte order mark are no longer misread as UTF-16; a file with two columns for the same money role is rejected instead of silently dropping one; and descriptions that start with=,+,-or@are escaped in CSV output so a spreadsheet does not run them as formulas. - GitLab Duo Agent Platform:
- a custom flow (
flows/import_fix_flow.yaml, flow registry v1, checked against the AI Catalog flow schema) with a triage AgentComponent, a router that only continues onfixable, a fixer AgentComponent, a reporter OneOffComponent, and a reviser that applies GitLab Duo review comments on the same branch; - its runtime config in
.gitlab/duo/agent-config.yml(setup script and a network allowlist limited to PyPI); AGENTS.mdwith the rules every agent must follow; the foundational Fix CI/CD Pipeline Flow, enabled in the project as a fallback for red pipelines, is meant to read it (it has not run yet);- a custom chat agent, Import Doctor (
agents/import_doctor.md), created in the project for cases the flow does not fix (not yet used in a recorded session); - GitLab Duo Code Review on every flow merge request, with re-review after each push;
- a small MCP server (
scripts/mcp_server.py, standard library only) registered in.gitlab/duo/mcp.json, so the tools run the real normalizer, scrubber and privacy check on the maintainer's machine; tested locally but not yet used from Duo Chat.
- a custom flow (
- GitLab CI/CD (
.gitlab-ci.yml): lint, test, build, security, deploy-staging, smoke-staging, deploy-production (automatic for gate merges, manual otherwise), smoke-production (plusdeploy:cloudrunandsmoke:cloudrunfor Google Cloud Run), release (tags only), notify and gate (scheduled) stages, using GitLab's SAST, Secret Detection and Container Scanning templates, the container registry, environments with deployment tiers, resource groups, JUnit and coverage reports, and the release keyword. - The merge gate and the issue summary use my personal access token (used only in this project), because the CI job token cannot merge or write notes.
- Tools. Built on GitLab Duo Agent Platform. Inside the product, the Duo flow writes the fixture, test, fix and review changes (its commits are authored by its service account), and GitLab Duo Code Review decides whether the gate may merge. The app itself contains no AI.
Challenges I ran into
- Keeping customer data out of git while keeping the bug reproducible. If you anonymize too much, the bug disappears; too little, and you leak data. The scrubber keeps the structure (delimiter, quoting, line endings, dates, amounts, encoding) and replaces everything else with pseudonyms that depend only on the order of first appearance. A test proves the scrubbed file parses to exactly the same dates, amounts and balances as the original. It is pattern based, so the merge request still needs a careful review. It also does not stop the agents from seeing the pasted rows; it only keeps them out of the repository.
- Prompt injection. The pasted CSV is untrusted text that an agent reads. The prompts treat it as data and ask the agent to flag instructions found in it, the fixer is told not to touch CI, Duo config or the scrubber, and nothing merges without a green pipeline and a GitLab Duo approval. The instructions are not a technical lock; the gate checks are.
- Permissions and identities. Flow triggers ignore notes by bots, so the gate posts its "please address the review" note with my token, and the flow opens merge requests as the user who triggered it, so the gate's author list has to include my account. The CI job token cannot merge or write notes, so the gate and the summary use my personal access token (used only in this project), and every automatic merge says so in its commit message and note.
- Agent tools have sharp edges. The flow's file tools refuse git-ignored paths, and
run_commandtakes one program per call, so the prompts write the raw sample withrun_commandand run every command on its own. - Non-deterministic agents. Anything that can be code is code: the error category and label, the scrubber, the expected outputs of the corpus, the smoke test. The agents are left with the parts that need judgment: reading the issue, writing the fix and explaining it.
- Proving the right build is live. Production runs in CI and on Google Cloud Run. Both smoke tests check that the build id answering is the pipeline's own commit before they upload the corpus files, so a stale deploy fails the pipeline instead of passing quietly.
Accomplishments that I'm proud of
- One loop from issue to production with nobody clicking anything on the issue or the merge request. In the batch of 7 runs on 22 to 24 Oct, 6 shipped and closed with zero touches after filing, median lead time 14 min 45 s; one of them included a GitLab Duo review round that the flow applied on its own.
- A seeded bad instruction (run 06, issue #7 asked for debits to be read as money in) was not followed: the fix kept debits as money out and the statement reconciled.
- A rollback drill on 25 Oct: a production smoke test failed on purpose and
rollback:productionput the last good build back in production and re-ran its smoke test, in under a minute. - Test first is part of the flow, not a good intention: the red pipeline for the new fixture is in the history before the fix.
- Raw samples stay out of git: they go to a git-ignored folder, only scrubbed fixtures are committed, and CI blocks any committed CSV that looks like personal data.
- 451 tests at 93 % line coverage, including property tests, plus six robustness fixes, each pinned by a test. A pipeline with eleven stages and three GitLab security scanners on every branch pipeline.
- The README maps every lifecycle stage to a real file or CI job, and says plainly which parts are still planned.
What I learned
- Agents are most reliable when they call deterministic tools and report what those tools say, instead of reasoning about the data themselves.
- "Privacy" needs a precise sentence: keeping data out of git and keeping data away from a model are different promises.
- GitLab's composite identity and job token scopes are a real safety net, but you have to design the workflow around them on purpose.
- A reconciliation check that does not depend on the parser (the running balance) catches more bugs than more parser tests, and property tests find the inputs you would not think of.
What's next for CSV Mender
- Scrub before the model sees anything: let the reporter run the scrubber locally (CLI or MCP) and paste only the scrubbed rows, or scrub in a deterministic step before the triage agent.
- Real governance: tool governance rules for the flow, code owner approval on the control-plane paths, and a merge-request-only
main. - A faster gate: a 5-minute schedule now, merge request events later.
- A second domain for the same loop, wherever a deterministic classifier and an oracle exist (the README lists what carries over and what does not).
- A hosted staging target (staging still runs only inside CI), and reading attached files instead of pasted rows once flows can.
Try it
- Source (MIT): https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase
- Pipelines: https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/pipelines
- Verify block with three zero-touch runs: https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/blob/main/README.md#verify-it-yourself-in-2-minutes
- Rollback drill: https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/blob/main/README.md#rollback-drill
- Hands-off run (bug 02): issue https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/work_items/3, merge request https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/merge_requests/2, main pipeline https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43461547/showcase/-/pipelines/2972961925
- Live on Google Cloud Run: https://csv-mender-879903213333.europe-west6.run.app (
/healthand/version.jsonshow the deployed build) - Run it locally:
pip install -r requirements-dev.txt && python -m pytest -q, thenuvicorn normalizer.web:appand uploaddemo/trailing_minus_export.csv.
Log in or sign up for Devpost to join the conversation.