Skip to content

Add the gecko-triage agent for Gecko bug triage - #7239

Open
alastor0325 wants to merge 28 commits into
mozilla:masterfrom
alastor0325:gecko-triage/pr
Open

alastor0325 wants to merge 28 commits into
mozilla:masterfrom
alastor0325:gecko-triage/pr

Conversation

@alastor0325

Copy link
Copy Markdown
Member

Background

The Firefox media team triages its new bugs with the /triage skill of fx-bug-toolkit. fx-bug-toolkit is a Claude Code plugin. The media team has used this skill for weekly triage for a while.

For each bug, the skill does these steps:

  • It reads the bug and finds the data that is missing.
  • It looks for duplicates.
  • It reads the attached profiles and logs.
  • It writes a draft comment and the field changes.

This PR brings that workflow into Hackbot as the gecko-triage agent. The agent does not call /triage. Its own rules (rules/ and teams/<team>/rules/) and its code now hold the triage steps. The agent still uses four helper skills of fx-bug-toolkit, at version 0.7.0, in their Hackbot mode:

  • analyze-profile reads Firefox Profiler links.
  • check-log reads media logs.
  • spec-check checks web spec behavior.
  • bug-start investigates the cause in the code.

The agent records its comment and its field changes as actions.

We've been runing the agent in this PR locally every day in the past week o media bugs, and a person reviewed each result before it was posted to refine the PR to current shape. The gecko-triage dashboard shows these results.

One agent can triage for each Gecko team. A team adds its components and its rules in teams/<team>/. The media team is the first team. A run with no team uses only the shared rules, in the component of the bug.

Architecture

Each box names its main file. The table below lists all the files for each box. The agent's files are in agents/gecko-triage/hackbot_agents/gecko_triage/.

flowchart LR
    ui[Hackbot UI or API<br/>bug_id, team]
    api[hackbot-api<br/>app/agents.py]
    subgraph job[Cloud Run job]
        direction TB
        pre[Preflight<br/>preflight.py]
        agent[gecko-triage agent<br/>agent.py]
        subs[Subagents<br/>agent.py, dupes.py, toolkit.py]
        hooks[Record-time hooks<br/>hooks.py, closing.py]
        post[Post-run checks<br/>targets.py]
        broker[Broker sidecar<br/>broker.py]
        pre --> agent
        agent --> subs
        agent -- comment and field changes --> hooks
        hooks --> post
        agent -. reads .-> broker
        subs -. reads .-> broker
    end
    bmo[(Bugzilla)]
    person{{A person approves<br/>in the Hackbot UI}}
    ui --> api --> job
    broker -. read-only key .-> bmo
    post -- recorded actions --> api
    api --> person -- apply --> bmo
Loading
Box Files What it does
hackbot-api services/hackbot-api/app/agents.py, app/schemas.py Registers the agent with auto_apply_actions=False, and checks the run inputs bug_id and team.
Preflight preflight.py, state.py Reads the bug and stops the run before the model starts when the rules say to leave the bug alone.
gecko-triage agent __main__.py, agent.py, rules/, teams/<team>/, config.py, teamconfig.py Runs the model with the triage rules, and checks the final result.
Subagents agent.py (the make_* functions), dupes.py, toolkit.py, attachments.py Read profiles, logs, specs, code and attachments, search for duplicates, and verify a close. toolkit.py loads the pinned fx-bug-toolkit skills.
Record-time hooks hooks.py, closing.py, authors.py Refuse a write that breaks a rule, at the moment the agent records it.
Post-run checks targets.py After the session, read the other bugs that a write or the comment names, and drop or hold a write that is not safe.
Broker sidecar broker.py, visibility.py Holds the Bugzilla key, and serves only public bugs, comments and attachments.
  • Reads. The agent holds no Bugzilla key. The broker sidecar holds it. The broker drops restricted bugs, private comments and private attachments.
  • Writes. The agent records one comment and the field changes that its rules allow. The record-time hooks refuse a write that breaks a rule. Examples are S1, P4 or P5, a needinfo on a person who is not on the bug or on the team's expert list, and a close with no check. The post-run checks then read the bugs that a write names.
  • Approval. Nothing reaches Bugzilla until a person approves the run in the Hackbot UI. hackbot-api registers gecko-triage with auto_apply_actions=False.
  • Closes. Every close (INVALID, DUPLICATE, INCOMPLETE) needs a confirm from close_verifier. This subagent starts with a fresh context, uses only read tools, and tries to prove the close wrong.
  • Model access. On Hackbot, the agent uses Workload Identity Federation. It needs no API key.
  • Starting runs. This PR adds no automatic trigger. A person starts a run from the Hackbot UI or API. A BugBot rule for daily runs is a separate change.

Workflow

Each run triages one bug. rules/workflow.md gives the steps.

  1. Preflight. Code, not the model, does this step. The run stops at no model cost when one of these is true:
    • The bug is restricted.
    • The bug is not in the team's components.
    • The bug is a meta bug.
    • The bug is closed.
    • A developer filed the bug and set its severity.
    • The agent wrote the newest comment, and nothing changed after it.
  2. Fetch. The agent reads the full bug: all the comments, the attachments and the open needinfos.
  3. Inventory. The agent records the data that the bug has. Subagents read the diagnostics with fx-bug-toolkit skills:
    • profile_analyst reads Firefox Profiler links.
    • log_analyst reads media logs.
    • spec_checker checks web spec behavior.
    • bug_investigator reads the code.
  4. Classify. The agent gives the bug its type and its main symptom (issue_type).
  5. Duplicates and meta bugs. duplicate_hunter searches for duplicates. Code runs some of the searches, and the subagent runs a concept search. The agent then links the correct meta bug.
  6. Gaps. The agent compares the data on the bug with the data that the bug type needs.
  7. Branch. The agent selects the decision. It records one comment and the field changes. Before a close, close_verifier must confirm the close.

Decisions

Decision What the run records
needs_info A comment that asks for the missing data, and a needinfo on the person who can give it. Severity and priority too.
analyzed The cause or a fix direction. The comment asks for no data. Severity, priority, meta bugs and the regression fields.
duplicate RESOLVED DUPLICATE, after close_verifier confirms it.
invalid RESOLVED INVALID (or WONTFIX), after close_verifier confirms it. Never when the reported input can cause a failure.
incomplete RESOLVED INCOMPLETE, after close_verifier confirms it. The reporter did not answer a needinfo for 14 days.
wrong_component A move to the component that owns the code, and a needinfo on a developer of that team.
stalled The stalled keyword. The a cause. The bug stays open.
watching Nothing. Nothing new occurred since the last request.
skipped Nothing. The preflight stopp decision, not the model.

@alastor0325
alastor0325 requested a review from a team as a code owner October 19, 2026 22:24

@suhaibmujahid suhaibmujahid left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a quick, relatively to the PR size ;) and I have some generic comments.

@padenot will take a look as well.

Comment thread .github/CODEOWNERS Resolved
Comment thread agents/gecko-triage/docs/bugzilla.md Resolved
Comment thread agents/cloudbuild.yaml Resolved
@alastor0325

Copy link
Copy Markdown
Member Author

I submitted another PR to start intergrating some of my skills into firefox-aidev-plugins, so we will eventually remove my own fx-bug-toolkit repo reference.

@alastor0325

Copy link
Copy Markdown
Member Author

I had a short discussion with Paul today. I’m thinking about which parts could become shared tools or move in-tree to benefit other tools, as I’m also working on another PR to let the bugfix agent support more customized implementation workflows. I’ll share another update as this progresses.

@alastor0325

Copy link
Copy Markdown
Member Author

I rebased this PR on the newest master and kept the 3 commits. The big changes since the last push:

  • agent-tools: a new get_bug_history tool, comment paging in get_bug_comments, and a request timeout. Each request runs in a worker thread.
  • Broker: it now serves the agent-tools Bugzilla tools with no public-only filter, as you suggested. Access control is for the proxy in Add Bugzilla proxy and scoped token flow #6740. I closed Add a shared Bugzilla broker lib with a public-only mode #7338.
  • Security bugs: when the agent finds a likely security bug in a public bug, it adds the bug to the product's security group and posts no public analysis.
  • Limits: turn and cost limits come from shared/limits.toml. Each run records its steps per session.
  • In-tree docs: a team can add Firefox tree docs to its rules. The media team now uses dom/media/docs/Diagnostics.md (bug 2086288).

zcorpan added a commit to zcorpan/fx-bug-toolkit that referenced this pull request Nov 3, 2026
Notes for mozilla/bugbug#7239: where the two differ in scope, team
configuration, enforcement, and security model, plus open questions.
zcorpan added a commit to zcorpan/fx-bug-toolkit that referenced this pull request Nov 3, 2026
Notes for mozilla/bugbug#7239: where the two differ in scope, team
configuration, enforcement, and security model, plus open questions.
zcorpan added a commit to zcorpan/fx-bug-toolkit that referenced this pull request Nov 3, 2026
Notes for mozilla/bugbug#7239: where the two differ in scope, team
configuration, enforcement, and security model, plus open questions.
zcorpan added a commit to zcorpan/fx-bug-toolkit that referenced this pull request Nov 3, 2026
Notes for mozilla/bugbug#7239: where the two differ in scope, team
configuration, enforcement, and security model, plus open questions.
zcorpan added a commit to zcorpan/fx-bug-toolkit that referenced this pull request Nov 3, 2026
Notes for mozilla/bugbug#7239: where the two differ in scope, team
configuration, enforcement, and security model, plus open questions.
zcorpan added a commit to zcorpan/fx-bug-toolkit that referenced this pull request Nov 3, 2026
Notes for mozilla/bugbug#7239: where the two differ in scope, team
configuration, enforcement, and security model, plus open questions.
zcorpan added a commit to zcorpan/fx-bug-toolkit that referenced this pull request Nov 4, 2026
Notes for mozilla/bugbug#7239: where the two differ in scope, team
configuration, enforcement, and security model, plus open questions.
zcorpan added a commit to zcorpan/fx-bug-toolkit that referenced this pull request Nov 4, 2026
Notes for mozilla/bugbug#7239: where the two differ in scope, team
configuration, enforcement, and security model, plus open questions.
zcorpan added a commit to zcorpan/fx-bug-toolkit that referenced this pull request Nov 4, 2026
Notes for mozilla/bugbug#7239: where the two differ in scope, team
configuration, enforcement, and security model, plus open questions.
zcorpan added a commit to zcorpan/fx-bug-toolkit that referenced this pull request Nov 4, 2026
Notes for mozilla/bugbug#7239: where the two differ in scope, team
configuration, enforcement, and security model, plus open questions.
zcorpan added a commit to zcorpan/fx-bug-toolkit that referenced this pull request Nov 4, 2026
Notes for mozilla/bugbug#7239: where the two differ in scope, team
configuration, enforcement, and security model, plus open questions.
get_bug_history returns the change history of a bug. get_bug_comments reads a
long thread in pages: start is a comment number, and the result gives the total
and the next start. Each Bugzilla request runs in a worker thread, so it does
not block the event loop. BugzillaContext takes an optional request timeout, and its
connection pool keeps a connection for each worker thread.

hackbot-runtime exports COMMENT_FOOTER_SEPARATOR. An agent's hooks use it to
tell the agent's text from the runtime's trailer.
gecko-triage triages one Gecko bug per run, for one team or, with no team, with the
shared rules only. Each run reads the whole bug, all comments included, and decides
its status, what is missing and the next step. It reads the diagnostics reporters
attach (profiles, logs, CI logs, crash reports, archives, memory reports and video
frames, plus specs) through fx-bug-toolkit skills and its own tools, and it looks at
the code before it asks a reporter for more. It records one comment and the field
changes its rules allow. Record-time hooks limit every write, and a post-run check
reads the bugs a write names. A broker sidecar holds the Bugzilla key and serves the
agent-tools Bugzilla tools. A restricted bug is skipped. An independent, read-only
verifier must confirm every close before the hooks accept it. A run with no team never
applies by itself. A team can name docs from the Firefox tree to add to its rules. The
media team is the first team.
The agent starts human-gated: auto_apply_actions is False and each run must vouch for
itself. A run needs a bug id and a known team. The UI trigger form lets a person choose
the team, or no team for a run with the shared rules only.
Wording only, no change of behavior. The shared prompts lose their media words, the log
report asks for the decoded error codes, a source link may cite a range of lines, and
the system prompt says that a later change by a person starts a new run. The link rule
says what to link, and the docstrings of agent.py and targets.py are shorter and plain.
The image notes say which builds webspec-index has. The agent description now reads
"Each team can add its own rules" in hackbot-api, the docs and the README.
The user prompt wraps the bug's fields and comments in <bug_fields> and <bug_comments>
tags. Their < is escaped, so bug text cannot close a tag, and the other parts of the
user prompt have any forged tag removed. One regex lists the prompt tags, and a rule
file must contain none. The main agent gives close_verifier its draft comment inside
<draft_comment> tags, and the verifier prompt expects them.
Bugzilla notifies a person that a comment mentions as @nick, and sends nothing for a
:nick. The rules now say so, and a request to a person with no real name opens with
@nick. The name check accepts @nick at the start of a request, and refuses an @nick in
backticks, which may notify nobody. A needinfo is still the way to ask for action.
The test with the RDD or GPU sandbox off is media specific, so it leaves the shared
diagnostics rules and goes to the media team's. The team rule points to the steps in the
in-tree Diagnostics.md, and keeps the fact that the GPU variable works only on Windows
and macOS.
The media team now has Core :: Web Speech (dom/media/webspeech/) in its components, a
Web Speech row in its bug types, and Web Speech in the web-api issue type.
hidden_env, which blanks the secrets in the CLI's environment, and make_undumpable,
which stops other processes of the same user from reading the agent's environment, move
out of agent.py into their own module. Their tests move to test_isolation.py. The
behavior does not change.
The prompts are now Jinja templates, prompts/*.md.j2. The text that differs between a
run with a team and a run with no team moves from parts.toml into the templates, as
if blocks, and parts.toml is gone. The templates also format the keywords, the link
hosts, the resolutions, the issue types and the needinfo experts, in place of helper
functions in agent.py. The subagent descriptions move to prompts/descriptions.j2, and
the bug investigator's Searchfox revision sentence moves into its template. A missing
value fails the render.

A snapshot test renders every prompt and subagent description, with and with no team,
and compares them with a saved copy. The model reads the same text as before.
A pinned cargo-binstall, checked by SHA-256 for amd64 and arm64, installs webspec-index
0.14.0 from its release build and never builds it from source. It replaces the download
by hand, which had a build for amd64 only, so the image now builds on arm64 too.
The image installs socorro-cli 0.6.0 with cargo-binstall, beside webspec-index. The
crash rule now has the investigator run socorro-cli crash on a crash id, in place of
curl on the ProcessedCrash API, and socorro-cli bugs to find the bugs filed for the same
signature. A crash id needs no bp- prefix for socorro-cli.
On amd64, the image installs treeherder-cli 0.3.1 with cargo-binstall. It has no arm64
Linux build, so an arm64 image goes without it. The CI rule now names it: the failed
jobs and crash stacks of a push, how often an intermittent happened before, and the pass
and fail history of a test manifest. The Treeherder failures-by-bug query stays.
The media team now lists dom/media/docs/Specs.md beside Diagnostics.md in tree_docs, so
the system prompt carries the spec of each media area. When the team's in-tree docs name
the spec of an area, the main agent gives it to spec_checker. The configuration doc says
how every team splits its knowledge: the facts in the docs it owns in the Firefox tree,
and the triage decisions in its rule files.
The Bugzilla proxy controls which comments and attachments a key can read, so the agent
no longer filters them by their privacy mark. render_bug keeps every comment that the
read returns, and fetch_attachment no longer refuses an attachment marked private: its
anonymous read fails on one it cannot get. visibility.py is gone.
render_bug, fit_comments and the JSON escaping are not specific to gecko-triage, so they
move to agent_tools.bugzilla_prompt with their tests. The renderer takes the comment cap,
the phrase that says where the data came from, and the test for an automation comment as
arguments. gecko-triage keeps a thin render_bug that passes its cap and its bot test: the
agent's own account never counts as automation, and the bot aliases do. The prompt text
does not change, as the snapshot test shows.
The 223 local pilot runs from 2026-10-15 to 2026-11-05 that used the model ran with no
limit. A turn is one model call in a session, and each tool result starts a new one; for
the main session this count is the SDK's num_turns in every run. Each new limit is about
1.2 to 1.5 times the most turns seen, so a real investigation does not stop and a session
that loops does. A session type with few sessions keeps 30.

| Session          | Sessions | Median | p95 | Most | Old limit | Over old limit | New limit |
| ---------------- | -------: | -----: | --: | ---: | --------: | -------------: | --------: |
| main             |      223 |     12 |  21 |   26 |        60 |              0 |        40 |
| bug_investigator |      144 |     35 |  71 |  132 |        50 |             38 |       160 |
| profile_analyst  |       61 |     24 |  37 |   51 |        30 |             15 |        60 |
| duplicate_hunter |      199 |      6 |  14 |   18 |        16 |              3 |        24 |
| close_verifier   |       27 |      6 |  23 |   36 |        25 |              1 |        45 |
| log_analyst      |       34 |     12 |  20 |   34 |        30 |              1 |        40 |
| investigator     |       13 |      6 |  10 |   11 |        30 |              0 |        30 |
| spec_checker     |        4 |     10 |  11 |   11 |        30 |              0 |        30 |

"Over old limit" is the number of sessions that used more turns than the old limit, so
the old limits would have stopped them early.

The cost of a run: median $1.73, p95 $4.32, most $9.60. 6 runs cost more than the old $5
limit, so max_usd goes to 12.

The README said that parallel tool calls in one reply are one turn. The transcripts show
that each tool result is a turn, so it now says that.
A model that reads an earlier comment of its agent can copy the runtime's feedback
footer into its new text. add_comment then added the footer again, so the comment had
it twice. add_comment now removes any copies at the end of the text with without_footer
before it adds one. COMMENT_FOOTER and COMMENT_TRAILER are public, so an agent can find
the footer in a comment.
The model copied the footer of the agent's earlier comment from the bug data in the user
prompt. render_bug now leaves the footer out of each comment that has it, so the prompt
has no footer to copy. The get_bug_comments tool still returns the footer; add_comment
then keeps one. split_runtime_footer now uses without_footer, so the agent has one rule
for where the footer starts.
profiler-cli 0.12.0 cannot read the profile format of new Firefox Profiler captures, so
the agent could not read new profiles. 0.13.0 reads them. It also needs Node 24, which
the image already copies from the official Node image.
The run-level inputs that every agent shares, such as require_review, now come from
AgentInputs (mozilla#6730), as for the other agents.
gecko-triage is human-gated like the other agents that the test already covers, so the
applier test needs no gecko-triage line.
Only gecko-triage uses render_bug and the comment trimmer, so bugzilla_prompt.py and its
tests move back to the agent. A later PR can extract them when another agent needs them.
The read_from parameter, which only this agent passed, becomes the fixed text.
The runtime footer goes away once the Bugzilla integration is ready, so the agent no
longer depends on the runtime's footer code, and hackbot-runtime is as on master. The
comment rules now say that Hackbot adds its own feedback line, so the model copies none.
render_bug no longer filters the footer out of the bug's comments. The record-time
checks keep their own copy of the line that comes before the footer.
…riage broker

agent-tools drops its worker threads, connection pool and request timeout: an
async-native Bugzilla client replaces bugsy there soon. The broker keeps the same
behavior on its own. It mounts the 60-second timeout on the bugsy session that it makes,
and it wraps each agent-tools Bugzilla tool so that each call runs in a worker thread.
So one slow read does not hold the reads of the other subagents, in one process.
…mes none

With no team input, the run reads the bug's product and component through the broker
first, and takes the one team whose team.toml lists that component. No such team, or
more than one, gives a run with the shared rules only, as before. A team input still
wins. config reads TEAM at import, so __main__ is now a short entry point that sets
TEAM before it imports the agent, and the run itself is in run.py.
The agent picks its team from the bug's component, so the form needs no team field and
no gecko-triage code beyond its entry in the agent list.
The sessions that analyze the bug (bug_investigator, profile_analyst, log_analyst and
close_verifier) keep high. The main session, which plans and drafts, and the search
sessions (investigator, spec_checker, duplicate_hunter) use medium. The new [effort]
table sits beside [turns] in shared/limits.toml, so one file holds every per-session
setting. An EFFORT run input still overrides the main session only.
@alastor0325

Copy link
Copy Markdown
Member Author

Thanks for the review. I rebased the PR on the current master. The changes for your
comments are in separate commits after the earlier ones:

  • The inputs inherit AgentInputs.
  • The agent picks its team from the bug's component when the input names none, so the
    UI has no team field and no gecko-triage code.
  • The applier test and hackbot-runtime are as on master.
  • The bug renderer is back in the agent.
  • The Bugzilla request timeout and the parallel reads are in the gecko-triage broker,
    not in agent-tools.

One more change, not from the review: shared/limits.toml now also sets the effort of
each session. The sessions that analyze the bug use high. The main session, which
plans and drafts, and the search sessions use medium.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants