Proof of Done

Merged isn’t done.

After every deploy, a GitLab Duo flow reads the issues that just shipped, turns each acceptance criterion into steps, and walks them in production with a real browser, photographing every step.

What holds is closed with the photos. What doesn’t is reopened with the failing frame and a fix merge request. Nobody in the middle.

#7 Show a floor plan on each room's page. Shot in production 7 Oct 2026, 23:14 IST, roll 27.

Every step held in production.

Closed with these frames attached. See the whole roll

So far

27 rolls shot in production460 frames7 checks that failed in production right after a merge2 regressions found by the nightly re-check

One issue, start to finish

Tests passed and the merge request merged, so #7 closed. In production it didn’t work. This is everything that happened next, with links to each record.

Roll 12, after the deploy: The “Floor plan of the Quiet Room” img is broken: https://carrel-194851097912.us-central1.run.app/assets/rooms/quiet.jpg answered 404.
Roll 12, after the deploy: The “Floor plan of the Quiet Room” img is broken: https://carrel-194851097912.us-central1.run.app/assets/rooms/quiet.jpg answered 404.
Roll 15, after the fix: every step held.
Roll 15, after the fix: every step held.

#7 Show a floor plan on each room's page. The criterion: “Each room's page shows a floor plan of that room”

  1. 1A personmerges the change that ships #7
  2. 2GitLab CItests, scans, packages, releases and deploys it to Cloud Run
  3. 3Examiner agentreads #7's acceptance criteria and walks them in production
  4. 4Clerk agentreopens #7 with the failing frame
  5. 5Fixer agentopens a fix merge request: small, labelled, with a test
  6. 6Guard jobchecks the fix against policy and merges it when the pipeline is green
  7. 7Re-check jobreplays the same check in production, no model involved
  8. 8Re-check jobcloses #7 with the new frames

Commits 23f6ce6 then 5f5cdc8. In this loop the only human step was the first merge.

The latest roll, frame by frame

One strip per acceptance criterion. A circle marks the frame that proves it held; a cross marks the step that failed. Hover a frame for the loupe; open it to step through.

Roll 27After deploy

Commit
4bf2314
Environment
production
Shot
7 Oct 2026, 23:14 IST
Result
2 held, 0 failed, 0 for a person

#7 Show a floor plan on each room's page

Held in production

Each room's page shows a floor plan of that room

Held. 8 steps, all as written.

  1. Start at the rooms list
  2. Follow the Quiet Room link from the grid
  3. Address contains “/rooms/quiet”
  4. The Quiet Room floor plan image must be present and its file must load
  5. Return to rooms list to navigate to Maker Lab
  6. Follow the Maker Lab link from the grid
  7. Address contains “/rooms/maker”
  8. The Maker Lab floor plan image must be present and its file must load

The plan matches the room: the Quiet Room's plan says silent study

Held. 3 steps, all as written.

  1. Open the Quiet Room page directly
  2. The floor plan image must load — if the CSP or path is broken the image won't appear
  3. The Quiet Room page must say 'Silent study' — this is the room's note that matches the floor plan content

Nine stages, one loop

Life after code is everything between a merge and a feature that keeps working. Each stage below is a real job or agent in this project.

  1. PlanAcceptance criteria written on the issue become the contract the deploy is held to.Issue template
  2. CreateWhen production fails a criterion, the fixer agent writes the fix and a test, on a proof/fix branch.fixer agent
  3. VerifyUnit tests for the app, the runner, the board and the policy; then the real verification, in production.carrel:test, runner:test, examiner
  4. PackageThe runner image the flow runs in, and the app and board images, built per commit.runner:image, carrel:image
  5. SecureSAST and secret detection on every pipeline; a fix with high findings can't merge itself.sast, secret_detection, guard
  6. ReleaseA GitLab release per deploy that lists what shipped and links to its proof, never claiming done.release:notes
  7. ConfigureProduction configuration lives in service.yaml, so a missing flag is a reviewed one-line fix.deploy:carrel
  8. MonitorEvery night the light table replays each proven check; a regression reopens its issue.light-table (scheduled)
  9. GovernA fix merges itself only inside policy; every roll is hash-chained; agents use a fixed step vocabulary.guard, the chain

Done stays proven

Every night the light table replays the saved check of every proven issue, with no model involved. If one stops working, its issue reopens with tonight’s frame.

See every check on the light table

Autonomy, on a short leash

Hands-off doesn’t mean unchecked. The agents write plans in a fixed vocabulary, a deterministic runner decides every verdict, and a fix merges itself only inside a written policy.

What the agents may write

goto
open a page on the app's own address
click, fill, select, press
use the page like a visitor
see, absent, see_text, not_see_text, url
observe, and fail if it isn't so; text checks can be scoped to one element
image_loads
a picture is there and its file actually arrived
remember
carry a value forward, like a booking code
download, file_contains, calendar_event
check what the visitor takes away

The runner rejects any plan with another step, before a browser starts.

What they can’t do

  • run its own JavaScript in the page
  • leave the app's origin, or call any other site
  • mark a criterion held: only the runner's assertions do
  • edit the pipeline, the flow, the runner or the checks in a fix
  • merge a fix outside policy, or close an issue before production holds

When a fix merges itself

Files changed3 at most
Lines changed40 at most
Protected pathsuntouched
Scan findingsno high or critical
Pipelinegreen

Outside any of these, the guard says why and leaves it for a person. The policy is code.

When production rolls itself back

Regressed issues last held onone commit
Production since thenchanged
Age of the deploy7 days at most
Deploys undonethe last one only

Found by the nightly light table, moved by a keyless job, written on the issue. The policy is code.

What a roll costs the planet

Checking production after every deploy is extra compute, so we measure it. A typical roll here is 11 frames; the estimate below uses the measured browser time of the latest deploy roll.

0.060grams CO2e per roll, grid average (location-based)

0.041grams after Google’s carbon-free energy share (market-based)

0.0036kilograms a month at 60 rolls (deploys plus nightly)

Measured: the browser time of a typical roll here, 1.6 s, plus 40 s of job start-up. Modelled with the Green Software Foundation’s SCI (energy times grid intensity, per roll) and Cloud Carbon Footprint’s coefficients: 0.71 to 4.26 W per vCPU at 50% load, 0.000392 kWh per GB-hour, PUE 1.1.

The runner job: 2 vCPU and 8 GB on GitLab’s hosted runners in us-east1 (579 g/kWh, 31% carbon-free). The app: Cloud Run in us-central1 (432 g/kWh, 88% carbon-free). Google’s 2025 region data.

Not included: the agents’ model inference, which GitLab doesn’t report per flow. Replays (after a fix, and nightly) use no model at all: the saved plan is the check.

Put it on your project

Proof of Done is a GitLab Duo flow, a runner image and a pipeline. Three steps, and your next deploy gets its first roll.

  1. Write acceptance criteria the way you already do

    ## Acceptance criteria
    - Add to calendar downloads an event that
      starts at the booked time, Pune time
    - My phone reminds me 15 minutes before

    Anything a browser can observe gets checked. Anything it can’t is marked for a person, never passed.

  2. Tell it where production is

    // .proof/config.json
    { "app_url": "https://your-app.run.app",
      "deploy_job": "deploy:production",
      "board_url": "https://proof-….run.app" }

    Add agent-config.yml so flow jobs run in the runner image.

  3. Enable the flow with one trigger

    AI > Flows > Proof of Done > Enable
    Trigger: Pipeline events, Run when: Passed

    From then on, every passing deploy on the default branch is proved, and every fix is re-checked.