{
"meta": {
"agent": "planner",
"task_id": "546",
"title": "BJ-6: BitbucketAdapter + webhook route",
"created_at": "2026-04-23T17:15:00Z"
},
"inputs": [
{
"name": "issue",
"type": "link",
"ref": "AgentSDE/agent-core#546",
"notes": "Wave 2, depends on #540 (BJ-0)"
},
{
"name": "epic",
"type": "link",
"ref": "AgentSDE/agent-core#539",
"notes": "Atlassian integration EPIC"
},
{
"name": "interface",
"type": "file",
"ref": "src/webhook/adapters/platform-adapter.interface.ts"
},
{
"name": "reference-adapter",
"type": "file",
"ref": "src/webhook/adapters/github.adapter.ts"
},
{
"name": "controller",
"type": "file",
"ref": "src/webhook/webhook.controller.ts"
},
{
"name": "module",
"type": "file",
"ref": "src/webhook/webhook.module.ts"
},
{
"name": "dispatch-event",
"type": "file",
"ref": "src/webhook/dto/dispatch-event.dto.ts"
}
],
"outputs": [
{
"name": "plan",
"type": "spec",
"format": "md",
"content": "See plan.md"
}
],
"files": [
{
"path": "src/webhook/adapters/bitbucket.adapter.ts",
"action": "create",
"reason": "New PlatformAdapter for Bitbucket webhook normalization"
},
{
"path": "src/webhook/adapters/bitbucket.adapter.spec.ts",
"action": "create",
"reason": "Unit tests for all event types, auth paths, bot filter"
},
{
"path": "src/webhook/adapters/platform-adapter.interface.ts",
"action": "modify",
"reason": "Add optional context arg to verifySignature for non-header auth (query, ip)"
},
{
"path": "src/webhook/adapters/github.adapter.ts",
"action": "modify",
"reason": "Accept optional context arg to match updated interface (no behavior change)"
},
{
"path": "src/webhook/webhook.controller.ts",
"action": "modify",
"reason": "Add POST /webhooks/bitbucket handler with query + ip context"
},
{
"path": "src/webhook/webhook.module.ts",
"action": "modify",
"reason": "Register BitbucketAdapter in onModuleInit()"
},
{
"path": "src/webhook/dto/dispatch-event.dto.ts",
"action": "modify",
"reason": "Add pr_updated, approved, changes_requested, push to DispatchEventType"
},
{
"path": "src/config/config.schema.ts",
"action": "modify",
"reason": "Declare BITBUCKET_WEBHOOK_SECRET + BITBUCKET_WEBHOOK_IP_ALLOWLIST env vars"
},
{
"path": ".env.example",
"action": "modify",
"reason": "Document new Bitbucket env vars"
},
{
"path": "CLAUDE.md",
"action": "modify",
"reason": "Add the new env vars to the environment table per repo convention"
}
],
"steps": [
{
"id": "S1",
"summary": "Extend PlatformAdapter.verifySignature with optional context arg; update GitHubAdapter to match signature (no behavior change).",
"acceptance": [
"platform-adapter.interface.ts declares optional context?: { clientIp?: string; query?: Record<string,string> }",
"GitHubAdapter compiles with new signature and all existing unit tests still pass"
],
"depends_on": []
},
{
"id": "S2",
"summary": "Extend DispatchEventType union with 'pr_updated' | 'approved' | 'changes_requested' | 'push'.",
"acceptance": [
"DispatchEventType union includes the four new values",
"tsc --noEmit passes across the repo"
],
"depends_on": []
},
{
"id": "S3",
"summary": "Implement BitbucketAdapter with platform='bitbucket', verifySignature (query secret + IP allowlist), normalize for 7 event types, bot self-filter + /agentsde directive on pullrequest:comment_created.",
"acceptance": [
"Class implements PlatformAdapter with platform='bitbucket'",
"verifySignature throws UnauthorizedException on bad secret or IP outside allowlist; no-op on success",
"normalize maps all 7 X-Event-Key values to correct DispatchEvent types",
"Unknown X-Event-Key returns { event: null, reason }",
"Comment events from bot user are filtered out; /agentsde (and legacy /agent) emit directive events"
],
"depends_on": [
"S1",
"S2"
]
},
{
"id": "S4",
"summary": "Register BitbucketAdapter in WebhookModule (providers + onModuleInit) and add POST /webhooks/bitbucket handler in WebhookController that passes req.ip + req.query to verifySignature.",
"acceptance": [
"WebhookModule.onModuleInit registers both GitHubAdapter and BitbucketAdapter",
"POST /webhooks/bitbucket returns 401 when ?secret is missing/wrong",
"Valid payload returns 200 and enqueues via DispatchService",
"WebhookDeliveryEntity is persisted with source='bitbucket'"
],
"depends_on": [
"S3"
]
},
{
"id": "S5",
"summary": "Add BITBUCKET_WEBHOOK_SECRET + BITBUCKET_WEBHOOK_IP_ALLOWLIST to config.schema.ts (optional), .env.example, and CLAUDE.md env table.",
"acceptance": [
"Joi schema declares both vars as optional strings",
".env.example contains both entries with inline comments",
"CLAUDE.md environment variables table lists both new vars"
],
"depends_on": [
"S4"
]
},
{
"id": "S6",
"summary": "Write unit tests in bitbucket.adapter.spec.ts covering all auth + event paths.",
"acceptance": [
"verifySignature: valid secret, bad secret, missing secret, allowlist hit, allowlist miss, no allowlist configured",
"normalize: one test per X-Event-Key (7 total) + unknown key + bot self-filter + /agentsde directive on PR comment",
"npm run test passes with the new spec file"
],
"depends_on": [
"S3"
]
}
],
"risks": [
{
"risk": "Extending DispatchEventType may break downstream exhaustive switches in dispatch/phase services.",
"mitigation": "Run npm run build; add default/no-op branches where new event types are not yet handled — they will be wired in later BJ-* tickets."
},
{
"risk": "IP allowlist implementation complexity (CIDR parsing).",
"mitigation": "Accept only comma-separated plain IPv4/IPv6 strings for v1; CIDR support deferred unless the issue explicitly requires it."
},
{
"risk": "Interface change could conflict with in-flight BJ-0 (#540).",
"mitigation": "Keep context arg strictly optional; coordinate rebase of #540 if needed — both target rc/atlassian-integration."
}
],
"assumptions": [
"Unknown X-Event-Key values return 200 with { event: null } — same pattern as GitHubAdapter.",
"IP allowlist matches raw strings (no CIDR) for v1 unless subsequent review requires otherwise.",
"BITBUCKET_WEBHOOK_SECRET remains optional in Joi schema — route returns 401 via verifySignature when the env var is unset.",
"Tenant resolution for Bitbucket reuses the existing TenantResolverService with platform='bitbucket'; extraction of repoFullName from the Bitbucket payload uses repository.full_name (same key as GitHub).",
"PR base branch is rc/atlassian-integration (per issue) — branch already exists on the worktree."
],
"open_questions": [
"Should CIDR parsing be supported in BITBUCKET_WEBHOOK_IP_ALLOWLIST, or is a plain IP list sufficient for v1?"
]
}