# Bitbucket agents

A Bitbucket agent wakes from a verified repository event, reads the
current pull-request or commit state, and writes the result back to
Bitbucket. `bitbucketChannel()` gives the same hooks and normalized
payloads on Bitbucket Cloud and Data Center.

## Review a pull request

Creating a pull request, or pushing new commits to a Data Center pull
request, starts a review. The hook reads the current diff through the
repository-bound API, the model returns a verdict, and the completed
answer appears as a comment on the pull request.

```ts
// bot/channels/bitbucket.ts
import {
  bitbucketChannel,
  bitbucketChannelStateForEvent,
  defaultBitbucketAuth,
} from "@grok/agent-sdk/channels/bitbucket";
import {
  postPullRequestComment,
  setPushTriageStatus,
} from "../lib/bitbucket.js";

export default bitbucketChannel({
  repos: ["acme/widgets"],
  onPullRequest: async (ctx, pullRequest) => {
    if (pullRequest.draft) {
      return null;
    }
    const hasNewCommits =
      pullRequest.action === "updated" &&
      pullRequest.previousHeadSha !== undefined &&
      pullRequest.previousHeadSha !== pullRequest.headSha;
    if (pullRequest.action !== "created" && !hasNewCommits) {
      return null;
    }

    const diff = await ctx.bitbucket.api.getPullRequestDiff(pullRequest.id);
    return {
      // `auth` starts a model turn running as the event sender.
      auth: defaultBitbucketAuth(ctx),
      message: `Review ${ctx.repository.fullName}#${pullRequest.id}.`,
      context: [`<bitbucket_diff>${diff}</bitbucket_diff>`],
    };
  },
  events: {
    async "message.completed"(event, channel, ctx) {
      if (ctx.session.purpose === "eval") {
        return;
      }
      if (event.data.finishReason === "tool_call" || event.data.text === "") {
        return;
      }
      const pullRequestId = channel.state.pullRequestId;
      if (
        !channel.state.eventType?.startsWith("pullrequest:") ||
        pullRequestId === null
      ) {
        return;
      }
      await postPullRequestComment(
        channel.state,
        pullRequestId,
        event.data.text
      );
    },
  },
});
```

`postPullRequestComment` is a small wrapper around the normalized
Bitbucket API. Configure it with the same token and API base described
under [Connect](#connect). It takes the repository and PR ID from the
verified event state, so the model never chooses where its review is
posted.

## Answer a pull-request comment

Mentioning `@cursor-reviewer` in a PR comment resumes that pull
request's conversation, and the first example's reply sink posts the
answer back to the same PR. The author guard ignores comments written
by the agent, preventing each reply from starting another turn.

```ts
const AGENT_USERNAME = "cursor-reviewer";

onPullRequestComment: (ctx, comment) => {
  if (comment.author?.username === AGENT_USERNAME) {
    return null;
  }
  if (!comment.body.includes(`@${AGENT_USERNAME}`)) {
    return null;
  }
  return {
    auth: defaultBitbucketAuth(ctx),
    message: comment.body,
  };
},
```

Add this property to the same `bitbucketChannel()` declaration as the
review handler.

## Triage a push to `main`

A push to `main` immediately adds a running `cursor/push-triage` build
status to the new commit. The webhook is acknowledged while the model
investigates, then the status reports whether that triage turn
completed.

```ts
onPush: (ctx, push) => {
  const change = push.changes.find(
    candidate =>
      candidate.name === "main" && candidate.after !== undefined
  );
  if (change?.after === undefined) {
    return null;
  }
  const sha = change.after;

  return {
    // `task` runs this host sequence after the webhook is acknowledged.
    task: async ({ send }) => {
      await ctx.bitbucket.setBuildStatus({
        sha,
        key: "cursor/push-triage",
        state: "INPROGRESS",
        name: "Cursor push triage",
        description: "Investigating push to main",
      });

      await send(`Triage push ${sha} on ${ctx.repository.fullName}.`, {
        auth: defaultBitbucketAuth(ctx),
        title: `Bitbucket push ${sha}`,
        state: {
          ...bitbucketChannelStateForEvent({
            repository: ctx.repository,
            headSha: sha,
            eventType: ctx.eventType,
          }),
        },
      });
    },
  };
},
```

Extend the `events` map so the turn's terminal event concludes the
status through the same provider wrapper:

```ts
async "message.completed"(event, channel, ctx) {
  if (
    ctx.session.purpose !== "eval" &&
    channel.state.eventType === "repo:push" &&
    channel.state.headSha !== null &&
    event.data.finishReason !== "tool_call"
  ) {
    await setPushTriageStatus(
      channel.state,
      "SUCCESSFUL",
      "Push triage complete"
    );
    return;
  }

  // Continue with the pull-request reply branch from the first example.
},
async "turn.failed"(_event, channel, ctx) {
  if (
    ctx.session.purpose === "eval" ||
    channel.state.eventType !== "repo:push" ||
    channel.state.headSha === null
  ) {
    return;
  }
  await setPushTriageStatus(
    channel.state,
    "FAILED",
    "Push triage failed"
  );
},
```

This status reports the triage run, not the health of the pushed commit
itself.

## Events

Use `onPullRequest`, `onPullRequestComment`, or `onPush` for the events
shown above. Each hook returns one of three decisions:

| Return | What happens |
| --- | --- |
| `{ auth }` | Start a model turn as the Bitbucket event sender |
| `{ task }` | Acknowledge the webhook, then run host code that may also call `send` |
| `null` | Skip the event |

The [Channels reference](/docs/reference/channels.md) covers the remaining
hook and session behavior.

## Pull-request affinity

Every pull-request event and comment for the same repository and PR ID
resumes one agent session. A new PR, later commits, and a mention in the
discussion therefore share the same conversation across both Bitbucket
dialects.

## Call Bitbucket through `ctx.bitbucket`

Each hook receives an API client already bound to the repository and
dialect from the verified delivery. Use `ctx.bitbucket.api` for
pull-request reads and comments, `setBuildStatus()` for one status
update, or `createCheck()` when a status changes throughout longer
work.

The repository does not come from model output. Keep API calls on this
bound client so the event decides where reads and writes go.

## Connect

On Cursor-managed hosting, use the signed-in Cursor account and list
the repositories that may wake the agent:

```ts
export default bitbucketChannel({
  cursorAccount: {
    repos: ["acme/widgets"],
  },
});
```

`cursorAccount` covers event delivery and API calls made through
`ctx.bitbucket` inside hooks. The `postPullRequestComment` and
`setPushTriageStatus` wrappers are separate REST clients, so configure
their Bitbucket API credential too.

For direct webhooks, configure the repository hook secret, API token,
and allowed repositories. Data Center also needs its REST API base:

```ts
export default bitbucketChannel({
  repos: ["PLATFORM/api"],
  webhookSecret: { secretName: "BITBUCKET_WEBHOOK_SECRET" },
  token: () => process.env.BITBUCKET_TOKEN ?? "",
  apiBaseUrl: "https://bitbucket.example.com/rest/api/1.0",
});
```

Without `webhookSecret`, the direct production route stays
loopback-only. Setting it admits deliveries signed with the matching
repository-hook secret; `token` authorizes the API reads and writes in
the examples.

Name the secret rather than reading it. Locally the channel reads that
environment variable, and on Cursor hosting the platform verifies
`X-Hub-Signature` with the deployment secret of that name at ingestion,
so an unsigned or mis-signed delivery gets a `401` before any compute
starts. `architecture: "v2"` requires the named form and `agent-sdk validate`
rejects a literal or `() => process.env.X` getter there; set the value
with `agent-sdk secret set <slug> BITBUCKET_WEBHOOK_SECRET`.

Register the repository webhook at
`https://<your-host>/<slug>/v1/channels/bitbucket` with the same secret.
For Bitbucket Cloud, omit `apiBaseUrl`.

## Test locally

Replay lets you exercise the same hooks with a pull request you can
read before registering a live repository webhook.

```text
agent-sdk dev
agent-sdk bitbucket replay \
  https://bitbucket.example.com/projects/PLATFORM/repos/api/pull-requests/42 \
  --events '*'
```

Use `--dry-run --out fixtures/bitbucket` to save the payloads instead of
posting them. `BITBUCKET_TOKEN` needs read access to the pull request.

## Related

- [Channels reference](/docs/reference/channels.md): channel hooks,
  dispatch decisions, and session behavior
- [Webhooks and custom channels](/docs/guides/webhooks.md): direct webhook auth
  and reply delivery
- [Evals](/docs/evals.md): turn saved Bitbucket payloads into regression
  fixtures
