Inspiration
It is 4:47 PM on a Friday. The pipeline goes red.
Someone sighs, opens the job log, scrolls past a few thousand lines of output, finds the one line that matters, writes a one-line fix, waits for CI, merges it, and writes it up. Total time: a lot. Total glamour: none. Weekend plans: pending.
Code generation is basically solved. What happens after the code is written is still a very manual job, and it is exactly the kind of job nobody puts on their résumé.
So we asked two questions:
- If an AI agent can write the fix, why does a human still have to carry it across the finish line?
- How do we let a robot do that without trusting the robot blindly?
The second one is the fun one. Aegis is our answer: the agent proposes, but the pipeline decides. The robot gets a keyboard. CI keeps the veto.
What it does
Aegis is a tiny on-call engineer that never sleeps, never sighs, and never asks "who broke main?" in the team chat. After a developer pushes a bad change, no human has to act:
- Detect. The
verifyjob fails in GitLab CI. Red lights. Mild panic (from the humans, not from Aegis). - Diagnose. A GitLab Duo Agent Platform trigger fires on "pipeline failed" and starts the Fix CI/CD Pipeline flow. Nobody clicks a button. The flow reads the failed job log, finds the culprit, and posts the exact fix on the merge request.
- Apply. Aegis's own
autofixjob, which has been patiently waiting, grabs that suggestion, commits it, and switches on merge when pipeline succeeds. - Verify. The full pipeline runs again on the fixed code: unit tests, dependency audit, and SAST. Trust, but verify. Mostly verify.
- Merge, or not. GitLab merges only if every check passes. A red pipeline blocks the merge, no matter how confident the robot sounds.
- Record. The governance stage files an audit issue, because even robots should leave paperwork.
In our recorded run, a deliberately planted bug (the home route always returned HTTP 500, even when perfectly healthy) went from failed pipeline to merged fix in about five minutes, with zero human action after the push. We timed it. We also made coffee. There was time.
The whole safety philosophy fits on a napkin. A change merges only when all of its checks succeed:
$$\text{merge} = \bigwedge_{i=1}^{n} \text{check}_i$$
If even one check says no, the answer is no. A confident "I fixed it, trust me" from an AI is not a test result.
The robot's leash
Giving an AI commit access sounds like the opening scene of a disaster movie, so we gave it a short leash:
- Runs only on branches named
autofix/.... Yourmainbranch is off limits. - Applies suggestions only from the Duo fix service account. Strangers' suggestions get ignored.
- Stops after two applied fixes, so it can never get stuck in an infinite "fix, break, fix, break" loop.
- Merges only through GitLab's merge-when-pipeline-succeeds. No forcing, no shortcuts, no "trust me, bro".
- The selection logic is unit tested, and the whole project has 10 passing tests.
DevSecOps stages touched
plan · create · verify · secure · govern run in the pipeline today. Cloud Run canary deployment and rollback scripts are in the repository for the release stage.
How we built it
Aegis is a small Python service plus the automation around it.
The sample service. A Flask app (served by Gunicorn in a Docker image) exposes GET / and GET /health, plus token-protected POST /trigger-incident and POST /clear-incident endpoints that simulate an unhealthy state for demos.
The pipeline. .gitlab-ci.yml defines six stages (plan, create, verify, secure, release, govern), runs unit tests, pip-audit, and GitLab SAST, and creates an audit issue in govern.
The agent loop. Two pieces work together:
- A Duo Agent Platform trigger that runs the Fix CI/CD Pipeline flow whenever a pipeline fails.
- Our
agent/auto_fix_merge.py, run from a child pipeline, which waits for Duo's suggestion, applies it through the GitLab API, and enables auto-merge.
developer push
|
v
verify FAILS --> Duo trigger (pipeline failed)
| |
| v
| Fix CI/CD Pipeline flow
| posts suggestion on the MR
v |
child pipeline: autofix |
waits for suggestion <--+
|
v
apply suggestion --> new commit --> full pipeline reruns
|
all checks pass? |
yes --> GitLab merges
no --> nothing merges
The key function selects only the suggestions we trust:
def split_suggestions(discussions, author):
"""Return (pending, applied) suggestions posted by the Duo fix service account."""
pending, applied = [], []
for discussion in discussions:
for note in discussion.get("notes", []):
if note.get("system") or note.get("author", {}).get("username") != author:
continue
for suggestion in note.get("suggestions") or []:
if suggestion.get("applied"):
applied.append(suggestion)
elif suggestion.get("appliable"):
pending.append(suggestion)
return pending, applied
Challenges we ran into
- Our first pipeline never started. GitLab's SAST template puts its job in a stage called
test, which our six-stage pipeline did not define, so the whole pipeline was rejected with zero jobs. We moved the SAST job into oursecurestage. - A deadlock we almost built. Duo's trigger fires when a pipeline finishes as failed. If the job that waits for Duo's fix ran inside that same pipeline, the pipeline could never finish and the trigger would never fire. We solved it by running the waiting job in a child pipeline started without waiting on it.
- An invalid child pipeline. Our first child pipeline contained only a
.postjob, which GitLab will not create. Giving it a real stage fixed it. - Credentials and branch protection. The job needs an API token, but a protected CI variable is only available on protected branches, and
autofix/branches are not protected. We used a short-lived, masked, unprotected token and limited the job toautofix/branches. - Trusting the agent. Duo told us in an earlier run that it could not run the tests in its own sandbox. That reinforced our design: the project's CI, not the agent, is the authority that decides whether a fix is good.
Accomplishments that we're proud of
- A fully hands-off loop that we recorded end to end: failure, Duo diagnosis, applied fix, passing CI, and an automatic merge.
- CI stays in charge. The agent can propose and apply, but it cannot force a merge.
- It is safe by construction: scoped to
autofix/branches, restricted to one trusted author, capped at two fixes. - It uses GitLab Duo Agent Platform natively (flow plus trigger) rather than just describing it.
- A clean, public repository under the MIT license, with 10 passing unit tests and a green pipeline.
What we learned
- Autonomy and safety are not opposites. The more power we gave the agent, the more valuable a strict, boring gate (CI) became.
- Event ordering matters. A trigger that fires on pipeline completion changes how you must structure the jobs that depend on it.
- Make the agent's output machine-readable. Duo posts its fix as a structured suggestion, which is exactly what lets another job apply it safely.
- Be precise about what is automated. Separating "the agent diagnosed it" from "CI verified it" makes the system easier to trust and easier to explain.
What's next for Aegis
- Release with a canary. Deploy to Google Cloud Run with a 10% canary, check
/health, and roll back automatically. The scripts are in the repository; the live deployment is the next step. - Security findings as input. Route SAST and dependency-audit findings into the same fix loop, using Duo's SAST flows.
- Real incident signals. Connect production alerts, not only CI failures, to the loop.
- Smarter gating. Add latency and error-rate checks to the canary instead of a single health response.
- MCP integration. Finish wiring the included GitLab Duo MCP client so other agents can plug into the same workflow.
- Better evidence. A dashboard of how many failures were fixed automatically, and how long each took.

Log in or sign up for Devpost to join the conversation.