Patina

Your codebase ages one commit at a time. Patina shows you where.

Inspiration

There's a moment every developer knows. You open a file to fix a one-line bug. You find a 200-line function with MD5 password hashing, five levels of nested ifs, imports nobody's used in months, and a comment that says // TODO: fix this dated 2019.

You think: someone should clean this up.

Nobody does.

This is technical debt. Not the dramatic kind — not a security breach or a production outage. The quiet kind. The kind that makes every feature take 3x longer than it should. The kind that makes onboarding a new developer take weeks instead of days. The kind that compounds silently until one day a junior dev touches the wrong file and the whole thing collapses.

The tools that exist to catch it — SonarQube, CodeClimate, ESLint — are powerful. They're also this:

  1. Deploy a server (or pay for a cloud subscription)
  2. Write a config file
  3. Add a scanner to your CI pipeline
  4. Configure quality gates for your language
  5. Set up branch analysis
  6. Train your team to check the dashboard
  7. Maintain the server forever

For a 500-person engineering org, that's fine. For the other 99% of teams? That's why the debt never gets tracked.

We asked a different question: what if tracking technical debt was as easy as leaving a comment?

What Patina Does

Type one comment on any merge request or issue:

@patina scan this repo

Patina reads every changed file, analyzes it across four dimensions, and responds with a full technical debt report — complete with a debt score, a letter grade, and GitLab issues automatically created for every finding.

No server. No config. No CI changes. No dashboard. No plugins. No setup at all.

Here's a real scan from our test codebase:

🔍 Patina Report
━━━━━━━━━━━━━━━━━━━━━━
Debt score: 72/100
Debt ratio: ~24% (Grade C)
Estimated remediation: ~4 days

🔴 Critical (fix before merge): 4 issues
• MD5 password hashing — src/app.js:43 → #12
• SQL injection via string concatenation — src/app.js:53 → #13
• Hardcoded default password 'default123' — src/app.js:63 → #14
• Deprecated new Buffer() usage — src/app.js:48 → #15

🟡 Moderate (schedule soon): 3 issues
• God object UserManager (11 methods) — src/app.js:52 → #16
• Deep nesting (5 levels) — src/app.js:14 → #17
• Duplicated function logic — src/app.js:30 → #18

🟢 Low (track): 4 issues
• Unused imports (stream, events) — src/app.js:6-7
• Unreachable code after return — src/app.js:27
• Commented-out code block (7 lines) — src/app.js:66-72
• Unused variables — src/app.js:75-76

View all issues → label: ~technical-debt

Every critical and moderate finding becomes a real GitLab issue — with the file location, an explanation of why it's debt, and a suggested fix with actual code. Low findings get rolled into one summary issue. Everything is tracked, labeled, and assignable.

The debt doesn't disappear into a dashboard nobody checks. It goes into the same issue board your team already uses every day.

What Makes This Different

Every other tool in the market — approaches code quality the same way: scan the code, match patterns, output a report.

Patina does something different. It reasons.

SonarQube will tell you: Method has 10 parameters (max allowed: 7). Remediation: 10 min.

Patina will tell you:

God object with long parameter listsrc/auth/auth.js:54

authenticateUser(username, password, ip, userAgent, geoLocation, deviceId, mfaCode, rememberMe, captchaToken, referrer) — 10 parameters makes this function impossible to call correctly without reading the source. Combined with AuthenticationManager having 20+ methods and 11 constructor dependencies, this is a god object that should be decomposed.

Fix: Extract an AuthRequest object: authenticateUser({ username, password, ...options }). Split AuthenticationManager into SessionManager, PasswordService, and MfaService.

That's not pattern matching. That's architectural reasoning. It understands that the parameter count isn't the problem — the problem is that this class does too many things, and the long parameter list is a symptom.

Capability SonarQube Patina
"Function is too long" ✅ Counts lines ✅ Explains why it matters
"These two functions do the same thing differently" ❌ Only detects copied text ✅ Detects duplicated logic
"This class has too many responsibilities" ✅ Suggests how to split it
"MD5 for passwords — use bcrypt" ❌ Not a linter's job ✅ With code example
Creates GitLab issues automatically ❌ Reports to dashboard ✅ In your existing workflow
Setup time Hours to days Zero
Works on any language Needs plugins ✅ Out of the box

We're not replacing SonarQube. We're the senior engineer who reviews your code after the linter runs.

How We Built It

Patina runs entirely on the GitLab Duo Agent Platform. No external servers, no API keys, no infrastructure of any kind.

The project ships with 2 agents and 6 flows:

Flow Speed Creates Issues Use Case
Patina (full scan) ~2 min ✅ Yes Pre-merge deep analysis
Commit Assistant (quick) ~1 min ❌ No Fast feedback during dev
Commit Lint ~30 sec ❌ No Lightweight repo check
Commit Summary ~1 min ❌ No Score synthesis
Conflict Resolver ~2 min ✅ Yes Dependency upgrade guides
Hello World ~10 sec ❌ No Setup verification

We also ship a .debt-scanner-ignore file for suppression — skip vendored code, specific rules, or specific packages without touching any configuration.

The Journey

We didn't start here.

Version 1: The 6-agent chain. Our original architecture was beautiful on paper — six specialist agents in sequence, each an expert in one debt category:

Context Gatherer → Code Smell Agent → Dead Code Agent
→ Dependency Agent → Deprecated API Agent → Synthesizer

Each agent passed its findings to the next. The Synthesizer deduplicated, scored, and created issues. It was elegant. It also didn't work — the platform's session timeout killed the chain before it could complete.

Version 2: The 3-agent compromise. We consolidated the four scanners into one analyzer agent. Gather → Analyze → Report. Three agents, three handoffs. Still timed out.

Version 3: The single agent. We put everything into one agent with a comprehensive prompt. It was supposed to be a temporary hack. It turned out to be better than the chain — the LLM can cross-reference findings across categories in ways the chain couldn't. A god object with deprecated API usage and outdated dependencies is one interconnected problem, not three separate findings.

The lesson: Multi-agent architectures are elegant in theory. In practice, a single agent with a well-crafted prompt often outperforms a chain of specialists — especially when the task requires cross-referencing.

Other Challenges

Schema validation whack-a-mole. The ai-catalog validator rejected trigger_types in agent definitions (only valid in flows), and optional: true on inputs produced a cryptic tool_name is missing error that pointed to the wrong line. We debugged by diffing against flows that passed validation.

The catalog sync race condition. Every push to main triggered the catalog sync bot, which also pushed to main, which meant our next push was always rejected. Solution: git pull --rebase origin main before every push. Simple, but it cost us an hour to figure out.

What We're Proud Of

Simple to use. End to end. On real code. You mention it, it scans, it creates issues, it posts a report.

Zero config is real. It's not "easy to set up." It's "nothing to set up." No server, no CI changes, no dashboard, no plugins, no API keys. Just a comment.

Issues, not just comments. This is the difference between a report and a workflow. Findings become tracked, labeled, assignable GitLab issues in the same board your team already uses. The debt doesn't disappear into a dashboard nobody checks.

What We Learned

LLMs are better reviewers than rule engines. "These two functions do the same thing but are written differently" is trivial for an LLM and impossible for a regex. Architectural reasoning — "this class has too many responsibilities" — requires understanding intent, not counting lines.

LLMs are not linters The right answer isn't LLM or rules — it's both. Use SonarQube for deterministic checks. Use Patina for the things rules can't express.

Simplicity is a feature.We require one comment. That's not a limitation — it's the entire point.

What's Next

  • Debt tracking over time. Store scan results and show trends:
  • Team debt dashboard. Aggregate debt scores across all repos in a group. Show which repos are accumulating debt fastest.
  • Quality gate mode. Optional CI integration that blocks merges above a configurable debt score threshold.
  • Comparative mode. Run Patina alongside SonarQube on the same codebase and show what each catches that the other misses.

Built With

  • GitLab Duo Agent Platform — all flows and agents run natively
  • Claude via GitLab Duo — no API key needed
  • GitLab Toolsget_merge_request, list_merge_request_diffs, get_repository_file, gitlab_blob_search, grep, create_issue, create_merge_request_note

Your codebase ages one commit at a time. Patina shows you where. 🔍

note: it creates merge requests as issues.

Built With

  • gitlab
Share this project:

Updates