Inspiration

A bug is usually fixed where it was reported. The flaw behind it, such as dates formatted in the viewer's timezone, one-cent rounding errors or a missing idempotency key, lives on in every other place that shares the pattern, and returns as a new ticket a month later. The knowledge of why the first one happened sits in a closed ticket and in someone's head. We wanted the ticket history to work as a team memory: fix a bug once, everywhere.

What it does

When a bug is filed or triaged, Bug-Class Memory:

  1. Compares it with the resolved bugs on your Jira site and ranks the similar ones.
  2. If several earlier fixes share a bug class (for example "UTC date truncation"), says so, with the root causes, the patterns to search the code for and the files touched before.
  3. After the fix, records the root cause, class, pattern and files on the issue with one call, so the next bug of that class is found faster.

It is deliberately cautious. A class is only called "likely" when the evidence is strong. A single stray shared word shows up as a weak match, with no class and no checklist, and nothing is posted on a bug without a strong match.

It works in five places, on one engine:

  • Issue panel (jira:issuePanel): the likely class, similar fixes, search patterns, a checklist, and a Record this fix form.
  • New-bug hint (trigger on avi:jira:created:issue): nobody has to remember to open the panel. When a Bug is created and a class is likely, the app comments within seconds with the class, the earlier fixes, what to search for and the files touched before. Weak matches, other issue types and resolved bugs get nothing.
  • Project page (jira:projectPage): which kinds of bug keep coming back in a project, how many of its resolved bugs have a fix record, and which open bugs already match a class.
  • Rovo agent (rovo:agent with a rovo:skill): Bug-Class Triage answers "is this the same kind of bug as one we fixed before?", records a fix on request, and backfills: it reads closed bugs that have no record, proposes a root cause and class for each with the sentence it relied on, and records nothing until a person confirms. Rovo asks the human to confirm before every write.
  • MCP server (rovo:mcp): find-similar-fixes, list-unrecorded-bugs and record-fix are exposed so a coding agent that already has your repository can call them while it fixes the bug: triage, search the code for the returned patterns, fix every sibling site, then report the fix back. Jira cannot read your repository, which is exactly why this route exists. The same Agent Skills file that the Rovo agent uses is published in the repository for Claude Code and other agents.

In the video, a coding agent triages a real bug, finds the two files that still have the flaw, fixes them, leaves alone the files fixed before, and records the fix on the Jira ticket.

How this differs from asking Rovo. Rovo can already search tickets, and we tested it: on a bug with no record of its own, the default Rovo chat found the right class too, but it took about three minutes and offered generic search patterns instead of the ones recorded from the team's own fixes. Bug-Class Memory answers in under a second from a structured record, says "no close match" when there is none, comments on a bug the moment it is filed, shows the project-level picture, and lets the developer's coding agent do the code search. The records it writes also make Rovo's own answers better.

How we built it, and how it meets the Forge requirement

Bug-Class Memory is a Forge app that runs entirely on Atlassian's platform:

Forge module Use
jira:issuePanel (UI Kit) The triage panel and the Record-fix form
jira:projectPage (UI Kit) Recurring classes, record coverage and open bugs per project
trigger The hint on a newly created Bug
rovo:agent Bug-Class Triage, with three actions as tools
rovo:skill The Agent Skills file that teaches the triage, search, fix, record loop and the backfill
rovo:mcp The same three tools for external coding agents
action x3 find-similar-fixes (read), list-unrecorded-bugs (read), record-fix (write)
function The resolver, the action handlers and the trigger handler

Scopes: read:jira-work and write:jira-work only. The app adds comments and a label: when a fix is recorded, and a hint on a new Bug with a likely class.

  • Jira is the store. A fix record is a few labelled lines in a comment (Root cause, Class, Fix, Files, Pattern) plus a bugclass-... label. There is no Forge storage, no external server and no copy of your data. The records are readable by people, and bugs resolved before the app existed are read too if their comments use the same labels, or after the backfill.
  • One engine. The panel, the project page, the hint, the agent and the MCP server call the same functions, so behaviour is identical everywhere. Calls run as the invoking user, so Jira permissions decide what is visible. The one exception is the new-bug trigger, which has no user and runs as the app. Errors come back as data, so the model can tell the user what to correct.
  • The hint is never a record. It quotes the root causes of earlier bugs, so it carries a marker and the record parser skips it. Unit tests cover a hint next to a real record in both orders.
  • Ranking. An IDF-weighted cosine over word sets, so rare shared words (cent, retry) count more than common ones. Matches are grouped by class, so repeated evidence beats one slightly better match. A class needs a score of at least 0.18: on the demo data, junk bugs peak near 0.10 and real repeats start at 0.26. The project page uses the same ranking and threshold as the panel.
  • "Resolved" means status category Done, not the resolution field, because many workflows never set one.
  • 37 unit tests cover the record format, the ranking, the backfill, the hint and the project overview. The app passes forge lint, and the Forge CLI reports it as eligible for Runs on Atlassian.

Challenges we ran into

  • A manifest cannot mix Bitbucket workspace scopes with Jira site scopes, and Forge storage is per installation. Our first design (a Bitbucket trigger writes, Jira reads) was impossible. We pivoted to a Jira-only app and used the developer's own coding agent, through MCP, for the code side.
  • Precision. Our first version called a "likely class" on junk bugs that shared one word. We added a calibrated threshold and a confidence result (strong, weak, none), and the panel now says "No close match" instead of inventing a class.
  • Making the agent safe to write. Recording a fix writes to Jira, so the Rovo agent asks the user to confirm first, backfill proposals are shown with the sentence they rely on, and every tool returns structured errors the model can act on.
  • Events have no user. The new-bug trigger cannot run as the user, so we proved it can read, search and comment as the app on a team-managed project before building on it, and kept every other path running as the user.
  • A hint that quotes other bugs can corrupt the memory. We made it unreadable as a record and kept it out of the backfill's evidence.

Accomplishments we're proud of

  • One engine behind five surfaces, all verified end to end on a live site, including all three ways of recording a fix.
  • A data model that needs no database, no server and no data copy: the memory lives in the tickets people already write.
  • A tool that is honest about uncertainty, and about how it compares to what Rovo already does.

What we learned

Forge's constraints shaped a better design: the memory belongs where the team already writes, and the agent that has the code should do the code search. rovo:mcp turns a Jira app into a tool for any coding agent, and an Agent Skills file lets the same instructions serve Rovo and a coding agent.

What's next

  • A per-project switch for the new-bug hint.
  • Semantic ranking alongside the lexical one, for bugs described in very different words.
  • Reading Bitbucket pull requests (a separate app, because of the scope rule above) to link fixes to the code they changed.
  • Marketplace listing.

Built With

Share this project:

Updates

Submission history