Skip to content

Link Hackbot's Bugzilla comments to their run with front matter - #7433

Open
kyoshino wants to merge 2 commits into
mozilla:masterfrom
kyoshino:claude/hackbot-feedback-system-66a0ee
Open

kyoshino wants to merge 2 commits into
mozilla:masterfrom
kyoshino:claude/hackbot-feedback-system-66a0ee

Conversation

@kyoshino

@kyoshino kyoshino commented Nov 9, 2026

Copy link
Copy Markdown

For Bug 2086124.

Important

Deploy this only once mozilla/bmo#2819 is in production and BMO's hackbot_user param names the account hackbot-api posts as. Until then, Bugzilla shows the block as plain text at the top of every Hackbot comment.

hackbot-api now puts a front matter block at the top of every Bugzilla comment it posts:

---
hackbot:
  run: <run ID>
  action: <action ID>
  agent: <agent name>
  url: https://hackbot.moz.tools/runs/<run ID>
---

BMO's Hackbot extension hides the block from readers, bugmail, webhooks and Pulse, and shows the agent and a link to the run in the comment header instead. REST raw_text keeps it. This is what native feedback (bug 2086122) will attach to.

  • The block is added at apply time in bugzilla_handler.py, so a model can't write or change it, and the Hackbot UI and Slack never show it.
  • ApplyContext carries the action ID (RunAction.ref). When a field update and a comment are merged into one PUT, it carries the comment's ID.
  • The block is left out when the action has no ID (summaries recorded before action IDs) or a value isn't an identifier, because BMO would show it as text.
  • Agent text posted without our block starts on a new line when it begins with ---, so BMO can't read a block the model wrote as metadata. That covers the case above, a comment inside update_bug's changes, attachment comments and new-bug descriptions.
  • The feedback footer is unchanged; that change belongs to bug 2086122.

Testing

  • hackbot-api tests pass (424), including new ones for the block, the merged-comment ID, and agent-written blocks.
  • Pre-commit hooks pass on the changed files.
  • On a local BMO with the extension, a comment that starts with a newline keeps it, and the block below it isn't read as metadata.

🤖 Generated with Claude Code

hackbot-api now puts a front matter block at the top of every Bugzilla
comment it posts: the run, the action, the agent and a link to the run in
the Hackbot UI. BMO's Hackbot extension hides the block and shows the agent
and the link in the comment header instead (bug 2086124).

The block is added at apply time, so a model can't write or change it, and
the Hackbot UI and Slack never show it. ApplyContext carries the action ID;
a field update merged with a comment carries the comment's ID. The block is
left out when the action has no ID (older summaries) or a value isn't an
identifier, since BMO would show it as text.

Agent text posted without our block (those comments, a comment inside
update_bug's changes, attachment comments and new-bug descriptions) starts
on a new line when it begins with a rule, so BMO can't read a block the
model wrote as metadata.
@kyoshino
kyoshino requested a review from a team as a code owner November 9, 2026 06:01
@kyoshino

kyoshino commented Nov 9, 2026

Copy link
Copy Markdown
Author

It seems that the test failure is related to GitHub’s downtime, in the same way as the pull request to BMO. Please rerun the test once the issue has been resolved.

@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.

Thank you! It looks great. I left some simple comments.

for member in member_rows
]
# The group carries at most one comment, which keeps its own ID.
comment_id = next(

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.

Suggested change
comment_id = next(
comment_action_id = next(

params = resolve_placeholders(row.params, results_by_ref)
outcome = await _dispatch(
run, row.type, params, attachments, hackbot_client
run, row.type, params, attachments, hackbot_client, row.ref

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.

We should pass the id instead of the ref:

Suggested change
run, row.type, params, attachments, hackbot_client, row.ref
run, row.type, params, attachments, hackbot_client, row.id

]
# The group carries at most one comment, which keeps its own ID.
comment_id = next(
(m.ref for m in member_rows if m.type == "bugzilla.add_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.

params: dict,
attachments: list[dict],
hackbot_client: HackbotClient,
action_id: str | None,

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.

If we use id instead of ref that would be int instead of str and it will never be None.

Comment on lines +131 to +138
body["comment"] = _with_front_matter(params["comment"], ctx)
body["comment_tags"] = [ctx.agent]
elif isinstance(body.get("comment"), dict) and isinstance(
body["comment"].get("body"), str
):
# A comment the agent put in `changes` itself, which is also posted
# from the Hackbot account.
body["comment"] = _with_front_matter(body["comment"], ctx)

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.

We could simplify it be checking for body.get("comment") after if params.get("comment"):

if params.get("comment"):
    body["comment"] = params["comment"]
    body["comment_tags"] = [ctx.agent]

if body.get("comment"):
    body["comment"] = _with_front_matter(body["comment"], ctx)

return "\n".join(["---", "hackbot:", *lines, f" url: {url}", "---", "", ""])


def _unparsed(text: str) -> str:

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.

Suggested change
def _unparsed(text: str) -> str:
def _escape_front_matter(text: str) -> str:

fields = {"run": ctx.run_id, "action": ctx.action_id, "agent": ctx.agent}
invalid = [key for key, value in fields.items() if not _IDENTIFIER.fullmatch(value)]
if invalid:
log.warning(

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.

So we get a single about this in Sentry:

Suggested change
log.warning(
log.error(

Alternatively, we could raise an exception to fail loudly.

Comment on lines +61 to +66
# BMO's Hackbot extension reads this block off the top of comments from the
# Hackbot account, hides it, and shows the agent and a link to the run in the
# comment header instead. It reads only this shape: `run`, `action` and `agent`
# must be identifiers and `url` an https URL. Anything else shows as plain text,
# so a block that would not parse is left out rather than posted.
_IDENTIFIER = re.compile(r"[A-Za-z0-9._-]+")

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.

Do we really need this? Could we make it dynamic so it accepts whatever we pass in? Or does it solve a technical limitation?

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