Cargotracker guardrails experiment: results for Devoxx slides

Summary

With one control run and two treatment runs, the results support a case study, not a statistical comparison. Five claims hold up against the data:

  1. Behavioral defects still reached code review in both arms. Control: 1 of 1 caught by Copilot Code Review. Treatment: 3 of 4 caught by review, 1 caught earlier by a CI unit test.
  2. Guardrails add their own failure class. The treatment's test-inventory check failed 4 times across the two runs. These are obligations the guardrail creates, so the control has no equivalent.
  3. Agents never produce formatted code. Spotless runs in both arms and failed in every run (3, 2 and 4 times). Two treatment violations came from fix commits made during review.
  4. Some failure modes repeat exactly, in every run, in both arms. The same 16-line duplicated dialog code, out-of-scope Maven Wrapper files, and the same invalid gh flag combinations appear in all three runs.
  5. The treatment costs more. Mean AIU is 25% higher (1,101 vs 881) and recorded session time is 86% higher (4h 14m vs 2h 16m).

Every open item is now resolved, and none changed these claims.

What was compared

Both arms share the project layout, Spotless formatting and a Maven build that runs tests; the treatment adds deeper CI guardrails on top, plus a stricter test section in its plan and 20 production files already reformatted and fixed for its gates (see Caveats).

Experimental runAgentic runs, reportArmRepository/branchRan (UTC)Active timeSessions
control-run-0101, reportControldd-3072973-cargotracker-control-01Oct 3, 00:42 to 14:4013h 58m elapsed, including a 10h 49m idle gap10
treatment-run-0301 02, reportTreatmentdd-3072707-tricket-out-cargotracker-run-03Oct 2, 14:14 to 21:115h 31m, plus a 1h 27m gap before #6 was retried11
treatment-run-0401 02, reportTreatmentdd-3072797-tricked-out-cargotracker-run-04Oct 2, 19:02 to Oct 3, 01:295h 23m, plus a 1h 05m gap before #610

CI checks in both arms: formatting (Spotless) and a Maven build. Tests run by default (skipTests=false), the code lives in demo/, and the compiler targets Java 17 via maven.compiler.release.

Added in the treatment only: a source-gates check, plus 43 more CI steps. The ones that can fail a build:

  • Test inventory, build contract, and the Java 17 / Java EE 7 contract
  • Unit and Arquillian test-count assertions
  • SpotBugs
  • A dependency security gate
  • OpenTelemetry acceptance with negative controls
  • A bounded performance comparison

Held constant in all three runs: Shepherd 1.0.4, Copilot CLI 1.0.91, model gpt-5.6-sol at reasoning level medium, lesson propagation off, and the same three skill name hashes. Skill content lengths match exactly (27,427, 25,715 and 4,423 characters); content hashes were not captured.

Where each defect was first caught

In every run, at least 67% of defects were caught before code review. The treatment's extra gates moved one behavioral defect earlier.

Most defects were caught before code review in both arms Stacked bars per run showing product defects by the gate that first detected them. Most defects were caught before code review in both arms Product defects per run, by the gate that first detected them, in pipeline order CI: formatting CI: build contract CI: unit tests Shepherd stage-30 gate Code review (CCRA) Control run-01 Control run-01, CI: formatting: 33 Control run-01, Shepherd stage-30 gate: 22 Control run-01, Code review (CCRA): 11 5 of 6 before review (83%) Treatment run-03 Treatment run-03, CI: formatting: 22 Treatment run-03, CI: build contract: 33 Treatment run-03, Shepherd stage-30 gate: 55 Treatment run-03, Code review (CCRA): 11 10 of 11 before review (91%) Treatment run-04 Treatment run-04, CI: formatting: 44 Treatment run-04, CI: build contract: 11 Treatment run-04, CI: unit tests: 11 Treatment run-04, Shepherd stage-30 gate: 22 Treatment run-04, Code review (CCRA): 44 8 of 12 before review (67%)
Evaluator 0.4.4 defects.csv for each run, plus two scope violations classified by hand and two double-counted requests removed.

Behavioral defects are the comparison that matters. Copilot Code Review found the control's single behavioral defect, treatment run-03's single one, and two of treatment run-04's three. The third, in run-04 task #4, failed a CI unit test first. That CI step exists only in the treatment.

Two of run-04's formatting failures happened after review: the fix commits for review comments were not formatted.

Defect classes across arms

Only behavioral defects, formatting and out-of-scope changes can be compared between arms; test-inventory failures exist only because the treatment checks for them.

Defect classComparable across arms?Control run-01Treatment run-03Treatment run-04
Behavioral (code does the wrong thing)Yes113
Formatting (Spotless or review)Yes, both arms run Spotless326†
Out-of-scope file changesYes, caught by the same Shepherd gate2*41*
Missing requirement or evidenceYes, caught by the same Shepherd gate011
Test inventory not updatedNo, treatment-only check031
Total61112

* Includes one scope violation the evaluator left unclassified and I classified by hand from its excerpt (see Method). † Includes two review findings on run-04's issue #3 that its post-mortem describes as unrelated formatting; the evaluator had labeled them behavioral.

The out-of-scope changes are the same in every run: the agent adds demo/mvnw, demo/mvnw.cmd and the two .mvn/wrapper files. That happens in both arms, so it comes from the project setup, not from the guardrails.

The two treatment runs differ widely: run-04 had three behavioral defects to run-03's one. With one control run, that spread is larger than any difference between the arms.

Cost to converge

The treatment used 25% more AIU and 28% more input tokens, and its sessions ran 86% longer. The likely cause is waiting on a CI pipeline with 43 more steps; the data does not break session time down by wait type.

MeasureControl run-01Treatment run-03Treatment run-04Treatment mean vs control
AIU880.61,051.11,151.4+25%
Premium requests101110+5%
Input tokens (incl. cache)10.47M11.88M14.93M+28%
Output tokens111,826133,291132,627+19%
Recorded session time2h 16m4h 13m4h 14m+86%
Model calls230255284+17%
Tool calls337399405+19%
Nonzero tool exits222035+25%
CI runs (distinct head SHAs)81011+31%
Code review rounds667+8%
Fix commits after review115+200%
Stage-30 change requests353+33%
Task attempts that failed01 (#6)1 (#5)

Elapsed wall-clock time is not comparable. The control sat idle for 10h 49m between stage 30 and stage 40 of task #4 while its Dev Box slept, and both treatment runs were split into two directories with a gap of 1h 05m to 1h 27m.

The tokens cover local Shepherd sessions only. The remote coding agent and the review agent run on GitHub, and their costs are not in the artifacts.

Symptom scorecard

Five of the eight measured symptoms appeared, and only token cost differed clearly between arms.

Symptom (H2)What the runs showDifference between arms
Non-determinismDefect totals vary from 6 to 12 per run. The code itself barely varies: PR #7 is +116/−82 lines in two runs and +112/−82 in the third.None visible; the two treatment runs differ more than either differs from the control
Hallucinated APIs and dependenciesZero in product code. Tooling misuse occurs in every run (8, 6, 7 events), mostly the same invalid gh flag combinations, plus one capability hallucination: run-04's shepherd decided a working gh feature was unsupportedNone
DRY violationsThe same 16-line block in ChangeArrivalDeadlineDateDialog.java (lines 19–34) is copied from ChangeDestinationDialog.java in all three runsNone: no treatment gate runs a duplicate-code check
Token costSee Cost to convergeTreatment +25% AIU, +28% input tokens
Unreviewable codeEach PR touches 2–4 files and at most 303 added linesNone visible per PR. The treatment's campaign totals include harness repair commits, so they are not compared
Failure to convergeTwo failed attempts, both in the treatment: run-03 task #6, where the coding agent stopped pushing fixes, and run-04 task #5, where the shepherd stopped on a false tooling conclusion. No review-cap, timeout or idle-kill eventsToo few events to compare
Test tamperingControl: 4 test files changed, 36 assertions added, none removed or disabledNone in either arm. Treatment PRs added 76 assertions and removed none; agents edited the gate files 4 times, only to register new tests
Stale and deprecated APIsZero new deprecation or removal warnings in any run (JDK 17, -Xlint:deprecation,removal)None

Scope creep was on the literature-only list, but the runs measured it: out-of-scope Maven Wrapper files appeared in all three runs, and Shepherd's stage-30 gate caught every case.

Caveats to disclose on stage

The sample is one control run and two treatment runs, so every difference above is anecdotal.

  • The arms differ by deep guardrails only. Formatting and a test-running build exist in both arms by design, because both repos derive from a project that already had these basics. The experiment measures what the extra 43 CI steps add on top of them, not guardrails versus none.
  • The runs overlapped. Treatment run-03 and run-04 ran concurrently for about 2h 09m. Control run-01 and the resumed part of run-04 both started at 00:42:08 UTC on Oct 3 and overlapped for 47m. All three used the same GitHub account.
  • The control's idle gap broke its prompt cache. The control's uncached input was 298,285 tokens; each treatment run had under 1,000. Most likely the cache expired while the control's Dev Box slept for 10h 49m, which inflates the control's cost slightly and makes the treatment's cost increase look smaller.
  • The treatment also got a stricter plan and different starting code. Compared against control-run-01's start (325b7e1), the treatment's plan requires running BookingServiceTest under Open Liberty with all five tests passing; the control's plan only requires the tests to compile. Shepherd's stage-30 gate checks these criteria, so the treatment gate asked for more evidence. The treatment plan also says to keep the Java 7 source level, contradicting its own POM (release 17). Twenty production files also differ at the start, all from four treatment commits: Spotless reformatting, SpotBugs and compiler fixes (serialVersionUID fields, a per-call SimpleDateFormat instead of a shared static one, an unused field and getter removed), an access log for observability, and removal of the GlassFish descriptor. No feature behavior changed. The files include ChangeDestinationDialog.java and BookingBackingBean.java, which the feature touches. Both treatment runs used the identical plan. By design, the stricter plan is part of the guardrail package, so results describe the package as a whole rather than the CI checks alone.
  • The evaluator could not classify every event. It left 7, 10 and 23 events unclassified in the three runs. I reviewed them by hand: two were scope violations, now counted; the rest are check-status polls, skipped jobs, and agent tool or script errors.
  • Both treatment runs stopped once and needed manual help to finish. Run-03's last task merged only after the maintainer ran its acceptance tests by hand and added the evidence to the PR. Run-04 stopped when the shepherd wrongly concluded GitHub CLI couldn't request a Copilot review. Harness repair commits went onto the base branch between the two directories of each run. They didn't change the guardrails, but they add about 130,000 lines to the treatment's start-to-final diff, so campaign-level diff and test-integrity numbers for the treatment are left out.

Resolved checks

All items are resolved; none changed the summary claims.

  • The mid-campaign base-branch changes were harness repairs, and the agents didn't tamper with tests. Each treatment run has one repair commit (run-03 f586a46, run-04 c94b49f), named after the interrupted campaign directory. Neither touches a workflow, CI script, POM, config or source file; together they account for about 130,000 lines in the evaluator's start-to-final totals, so the treatment's campaign diff size and its "test execution reduced" flag are not agent results. Checked per PR, all ten treatment PRs delete no test files, disable nothing, add no skip flags, and add 76 assertions while removing none. Four PRs edited the guardrail files, but only to register new test classes and raise the expected counts (active tests 8→9→10; unit tests 27→34). None removed a check or lowered a count.
  • The 20 production files are the treatment's own gate fixes. All changes come from four treatment commits: Spotless reformatting, SpotBugs and compiler fixes, an observability access log, and making Open Liberty the only runtime. Run-04 started from identical production code.
  • Control task #5 had no convergence failure. Every SHEPHERD FAILED line in that transcript is part of a script's text, either the skill's instructions or a command's own failure branch (line 1115). None is command output. The command that contains the matched line printed its normal result, and the session's four nonzero exits (lines 1296, 1643, 1838 and 2535) belong to other commands. The evaluator matched the string inside the command rather than in its output, so the control had zero failed attempts.
  • Run-04's unit-test detection counts as a guardrail detection. In the treatment, unit tests run in CI only in treatment-only steps (Run focused unit tests, Assert unit test count); the control's tests run inside its own Build with Maven step, which the treatment doesn't have.
  • Why the runs paused. The control's 10h 49m gap during task #4 happened because the Dev Box running it went to sleep. Run-03 stopped when the coding agent pushed no remediation; run-04 stopped when the shepherd wrongly concluded GitHub CLI couldn't request a Copilot review. Both were restarted by hand; see Caveats.
  • Control task #2 had one stage-30 request, not two; the control total is 6. Shepherd posted two change requests on PR #7. At 00:58:55 UTC it requested a fix for the Maven Wrapper scope violation plus a wrong test reload path (one request, two items). At 01:05:13 UTC it requested a formatting fix after CI failed. The evaluator's "missing requirement" defect at 01:05:07 is that formatting request, mislabeled and double-counted. Corrected: one formatting defect and one scope violation with two items.
  • Run-04 had one missing-requirement defect, not two; its total is 12. Task #3 had three requests: the Maven Wrapper scope violation, a formatting fix and a test-inventory fix. The evaluator's "missing requirement" at 20:01:48 UTC is the inventory request counted a second time. Task #5's request is genuine: the plan requires the Update action to refresh the Administration view after a successful close, and CCA's first version didn't.

Method

All three runs were scored by shepherd-task-evaluator 0.4.4 at commit f76ebf2, from a clean worktree, on October 4.

  • Sources: each run's findings.json, defects.csv, summary.csv and unclassified.md. Treatment runs use the combined output, which merges the two campaign directories into one record.
  • Manual adjudication: two unclassified stage-30 change requests had headings the evaluator didn't recognize ("Scope violation: four disallowed Maven wrapper files" in the control, "Focused diff gate failure: unrelated Maven Wrapper files" in run-04). I counted both as scope violations. I also removed two evaluator defects that were other requests counted a second time: control #2's "missing requirement" (its formatting request) and run-04 #3's (its test-inventory request). Comparing with the runs' post-mortems led to three more corrections: run-04 issue #3's two review findings are counted as formatting rather than behavioral (based on the post-mortem's description, not yet checked against PR #8's comments), review rounds come from the post-mortems' per-task tables, and run-04's task #5 stop counts as a failed attempt. Every other count is the evaluator's own.
  • Tokens: the last cumulative OTEL export per metric series. Input tokens include cache reads and writes.
  • Time to fix: one treatment defect (run-04, #5) has a fix timestamped 57 seconds before its detection. It is flagged as an anomaly and left out of timing claims.
  • Raw transcripts: I checked treatment run-03 against its raw transcripts in an earlier review. For the control and run-04, I checked the evaluator's outputs plus the transcript passages and git history cited under Resolved checks.