Jira Integration for ABCA — Scaffold Plan
Context
ABCA (Autonomous Background Coding Agents on AWS) currently ingests coding tasks from CLI, GitHub webhooks, Slack, and Linear, then opens PRs autonomously. Linear is the only "issue tracker" channel today. Many teams use Jira instead, so we want a parity-level Jira Cloud integration: an issue gets a bgagent label → ABCA picks it up → an agent run produces a PR → status flows back into the Jira issue.
The existing Linear integration (cdk/src/constructs/linear-integration.ts + sibling handlers + agent/src/channel_mcp.py) is the canonical template. Confirmed scope:
- Jira Cloud only (REST v3, cloud webhooks)
- Per-workspace OAuth 3LO stored via AgentCore credential provider, mirroring
bgagent-linear-oauth-*
- Trigger: label added (default
bgagent), parity with Linear
- Outbound: Atlassian's Remote MCP Server, registered into
.mcp.json when channel_source == "jira" — same shape as the Linear MCP path
Reference files (read these first)
Linear is the closest analog and should be copied/adapted, not reinvented.
cdk/src/constructs/linear-integration.ts — webhook + processor + link Lambdas, dedup table, secrets, API routes
cdk/src/constructs/linear-project-mapping-table.ts, linear-user-mapping-table.ts, linear-workspace-registry-table.ts — DDB schemas
cdk/src/handlers/linear-webhook.ts — HMAC verify, dedup, async-invoke processor
cdk/src/handlers/linear-webhook-processor.ts — label-trigger logic, calls createTaskCore, writes channel metadata
cdk/src/handlers/linear-link.ts — Cognito-authenticated user→workspace linking
cdk/src/stacks/agent.ts — how LinearIntegration is wired (api, userPool, taskTable, taskEventsTable, repoTable, orchestratorFunctionArn, guardrails) and how the orchestrator is granted read on its workspace registry
agent/src/channel_mcp.py — gates Linear MCP server entry into .mcp.json based on channel_source
agent/src/linear_reactions.py — outbound progress hooks (consult for parity, may not need a Jira twin if MCP covers it)
cli/src/** — bgagent linear setup, bgagent linear link <code> commands (find the exact files during impl)
docs/guides/LINEAR_SETUP_GUIDE.md — template for JIRA_SETUP_GUIDE.md
contracts/constants.json / constants.md — channel source enum lives somewhere in contracts/ or cdk/src/types/; locate during impl and add "jira"
Architecture summary
Inbound (Jira → ABCA):
Jira Cloud webhook
→ POST /jira/webhook (API GW, no Cognito, HMAC-verified)
→ JiraWebhookFn (verify signature, dedup, async invoke)
→ JiraWebhookProcessorFn (resolve workspace OAuth, look up project→repo, build task, call createTaskCore)
→ existing orchestrator pipeline (unchanged)
Outbound (Agent → Jira) — MCP only:
runner.py picks task with channel_source="jira"
→ channel_mcp.py writes `jira-server` entry into .mcp.json (Atlassian Remote MCP, OAuth token from secret)
→ Claude Agent SDK exposes Jira tools to the agent
→ agent posts comments / transitions / links PR via MCP tools
No DynamoDB Streams consumer, no outbound-notify Lambda — matches Linear's "inbound-only adapter" stance.
Implementation plan
1. Contracts & constants
- Add
"jira" to whatever channel-source enum/union exists (search cdk/src/types/, contracts/, agent/src/models.py).
- Add Jira-specific constants alongside Linear ones (env var names, default trigger label
bgagent, secret prefix bgagent-jira-oauth-).
2. CDK constructs (mirror Linear, file-for-file)
New files under cdk/src/constructs/:
jira-integration.ts — top-level construct; props mirror LinearIntegrationProps (api, userPool, taskTable, taskEventsTable, repoTable, orchestratorFunctionArn, guardrailId, guardrailVersion)
jira-project-mapping-table.ts — Jira cloudId + projectKey → repo
jira-user-mapping-table.ts — Jira accountId → platform user
jira-workspace-registry-table.ts — cloudId → AgentCore credential-provider name; this is the table the orchestrator reads to resolve OAuth at agent-launch time
The construct must create:
JiraWebhookFn Lambda + POST /jira/webhook route (no auth, signature-verified)
JiraWebhookProcessorFn Lambda (async-invoked, 512 MB to match Linear's attachment-screening footprint)
JiraLinkFn Lambda + POST /jira/link route (Cognito-authenticated)
JiraWebhookSecret placeholder in Secrets Manager (populated by bgagent jira setup)
JiraWebhookDedupTable — composite key {issueKey}#{webhookEventTimestamp}, 8-hr TTL (Jira retries far less than Linear; 8 hr is safe parity)
- IAM: webhook Fn reads webhook secret + dedup table +
bgagent-jira-oauth-* prefix; processor reads mapping/registry tables, writes task tables, invokes orchestrator alias, applies Bedrock guardrail; link Fn read/writes user mapping
3. Lambda handlers (new, under cdk/src/handlers/)
jira-webhook.ts — verify Atlassian webhook signature (X-Hub-Signature HMAC-SHA256 over body using shared secret), parse JSON, accept only jira:issue_updated and jira:issue_created events, dedup, async-invoke processor. Silent 200 for unsupported events.
jira-webhook-processor.ts — port Linear's logic:
- Trigger:
issue_created with bgagent label already on, OR issue_updated whose changelog.items shows label bgagent newly added (Jira sends label diffs in changelog, not in the issue body — handle this carefully; this is the one place we can't blindly copy Linear)
- Resolve
cloudId → workspace registry → AgentCore credential-provider name (don't fetch the token here; just hand off)
- Build task description from
issue.fields.summary + ADF→markdown of issue.fields.description
- Call
createTaskCore with channelSource: 'jira' and metadata { jira_cloud_id, jira_project_key, jira_issue_key, jira_issue_id }
- On failure, post a comment back via REST (one-shot, mirrors
reportIssueFailure)
jira-link.ts — Cognito JWT in, linking-code lookup, dry-run preview of Atlassian identity, then persist user mapping. Direct port of linear-link.ts.
4. Stack wiring (cdk/src/stacks/agent.ts)
Append after Linear:
const jiraIntegration = new JiraIntegration(this, 'JiraIntegration', {
api: taskApi.api,
userPool: taskApi.userPool,
taskTable: taskTable.table,
taskEventsTable: taskEventsTable.table,
repoTable: repoTable.table,
orchestratorFunctionArn: orchestrator.alias.functionArn,
guardrailId: inputGuardrail.guardrailId,
guardrailVersion: inputGuardrail.guardrailVersion,
});
jiraIntegration.workspaceRegistryTable.grantReadData(orchestrator.fn);
orchestrator.fn.addEnvironment(
'JIRA_WORKSPACE_REGISTRY_TABLE_NAME',
jiraIntegration.workspaceRegistryTable.tableName,
);
orchestrator.fn.addToRolePolicy(new iam.PolicyStatement({
actions: ['secretsmanager:GetSecretValue', 'secretsmanager:PutSecretValue'],
resources: [`arn:aws:secretsmanager:${region}:${account}:secret:bgagent-jira-oauth-*`],
}));
5. Agent-side outbound (MCP only)
Modify agent/src/channel_mcp.py:
- Replace the
if channel_source != "linear": return False gate with a small registry/dispatch:
CHANNEL_MCP_BUILDERS = {
"linear": _linear_server_entry,
"jira": _jira_server_entry,
}
- Add
_jira_server_entry() — writes Atlassian Remote MCP config (URL https://mcp.atlassian.com/v1/sse or current per Atlassian docs; OAuth token resolved from bgagent-jira-oauth-<slug> secret, env var name JIRA_API_TOKEN or per-Atlassian-MCP convention).
- This is a small refactor, not a rewrite — keep the diff minimal.
No jira_reactions.py for now. If MCP coverage is insufficient (e.g. structured failure comments at orchestrator boundary), we can add one later, exactly mirroring linear_reactions.py.
6. CLI (cli/src/**)
Add subcommands by copying Linear's:
bgagent jira setup — populate JiraWebhookSecret, register the AgentCore credential provider, print webhook URL for the customer to paste into Atlassian's webhook config
bgagent jira link <code> — invoke POST /jira/link
bgagent jira map <project-key> <repo> — write to project mapping table
7. Docs
- New
docs/guides/JIRA_SETUP_GUIDE.md — port LINEAR_SETUP_GUIDE.md step-by-step, swapping Linear for Jira Cloud, including: creating the OAuth 3LO app in Atlassian developer console, scopes (read:jira-work, write:jira-work, read:jira-user), webhook URL registration, label setup, project mapping.
- One-line addition to
README.md under integrations.
- Add
decisions/ ADR if the repo uses them — capture "Jira Cloud only, OAuth 3LO, label trigger, MCP outbound."
Things to watch (non-obvious)
- Webhook signature: Atlassian uses
X-Hub-Signature: sha256=<hex>; ensure constant-time compare and that the body is the raw unparsed bytes, not the parsed-and-restringified JSON.
- Label-add detection on update events: Jira's
webhookEvent: jira:issue_updated payload has changelog.items[] with field: "labels", fromString, toString. Diff these — not current issue.fields.labels. Linear's processor does the equivalent diff using its data.updatedFrom; same idea, different shape.
- ADF to markdown: Jira description is Atlassian Document Format, not markdown. Use a small ADF→md helper or @atlaskit utility; don't roll a full converter — extract text + headings + lists is enough.
cloudId is the tenant key, not domain or workspace name. Webhook payloads include it; OAuth flows return it. Index everything on cloudId.
- Webhook dedup key: Jira does retry on 5xx but less aggressively than Linear. Use
{issueKey}#{timestamp} rather than {issueKey}#{eventType} so two distinct label-adds in quick succession aren't collapsed.
- Atlassian Remote MCP: confirm current public URL + auth contract before wiring. If it's still gated/preview, fall back to direct REST in
jira_reactions.py (Plan B; do not start with this).
Verification plan
End-to-end smoke (after deploy):
cd cdk && npm run build && cdk synth — no type errors, both LinearIntegration and JiraIntegration synthesize.
cdk deploy to a dev account.
- Run
bgagent jira setup, complete OAuth dance, paste webhook URL into a test Jira Cloud instance.
bgagent jira link <code>, then bgagent jira map TEST <github-org/test-repo>.
- Create a Jira issue in
TEST, add label bgagent → confirm task appears in bgagent list within ~30s.
- Watch the agent run; confirm it comments back on the Jira issue (proves outbound MCP works) and opens a PR.
- Negative paths: signed webhook with wrong secret → 401; duplicate webhook within 8h →
{ ok: true, deduped: true }; non-issue event → silent 200.
Unit-test surface (mirror Linear's tests, found under cdk/test/ and agent/tests/):
jira-webhook.test.ts — signature pass/fail, dedup, event filtering
jira-webhook-processor.test.ts — label-add-on-create vs label-add-on-update vs label-already-present-no-change
channel_mcp test — channel_source="jira" writes a jira-server entry; "slack" does not
Out of scope (explicit)
- Jira Server / Data Center
- Forge/Connect app distribution
- Status-transition or comment-command triggers (label only for v1)
- Outbound REST module (
jira_reactions.py) — only add if MCP gaps emerge
- Bidirectional sync of issue state (status, assignee) beyond what the agent posts via MCP
Jira Integration for ABCA — Scaffold Plan
Context
ABCA (Autonomous Background Coding Agents on AWS) currently ingests coding tasks from CLI, GitHub webhooks, Slack, and Linear, then opens PRs autonomously. Linear is the only "issue tracker" channel today. Many teams use Jira instead, so we want a parity-level Jira Cloud integration: an issue gets a
bgagentlabel → ABCA picks it up → an agent run produces a PR → status flows back into the Jira issue.The existing Linear integration (
cdk/src/constructs/linear-integration.ts+ sibling handlers +agent/src/channel_mcp.py) is the canonical template. Confirmed scope:bgagent-linear-oauth-*bgagent), parity with Linear.mcp.jsonwhenchannel_source == "jira"— same shape as the Linear MCP pathReference files (read these first)
Linear is the closest analog and should be copied/adapted, not reinvented.
cdk/src/constructs/linear-integration.ts— webhook + processor + link Lambdas, dedup table, secrets, API routescdk/src/constructs/linear-project-mapping-table.ts,linear-user-mapping-table.ts,linear-workspace-registry-table.ts— DDB schemascdk/src/handlers/linear-webhook.ts— HMAC verify, dedup, async-invoke processorcdk/src/handlers/linear-webhook-processor.ts— label-trigger logic, callscreateTaskCore, writes channel metadatacdk/src/handlers/linear-link.ts— Cognito-authenticated user→workspace linkingcdk/src/stacks/agent.ts— howLinearIntegrationis wired (api, userPool, taskTable, taskEventsTable, repoTable, orchestratorFunctionArn, guardrails) and how the orchestrator is granted read on its workspace registryagent/src/channel_mcp.py— gates Linear MCP server entry into.mcp.jsonbased onchannel_sourceagent/src/linear_reactions.py— outbound progress hooks (consult for parity, may not need a Jira twin if MCP covers it)cli/src/**—bgagent linear setup,bgagent linear link <code>commands (find the exact files during impl)docs/guides/LINEAR_SETUP_GUIDE.md— template forJIRA_SETUP_GUIDE.mdcontracts/constants.json/constants.md— channel source enum lives somewhere incontracts/orcdk/src/types/; locate during impl and add"jira"Architecture summary
Inbound (Jira → ABCA):
Outbound (Agent → Jira) — MCP only:
No DynamoDB Streams consumer, no outbound-notify Lambda — matches Linear's "inbound-only adapter" stance.
Implementation plan
1. Contracts & constants
"jira"to whatever channel-source enum/union exists (searchcdk/src/types/,contracts/,agent/src/models.py).bgagent, secret prefixbgagent-jira-oauth-).2. CDK constructs (mirror Linear, file-for-file)
New files under
cdk/src/constructs/:jira-integration.ts— top-level construct; props mirrorLinearIntegrationProps(api,userPool,taskTable,taskEventsTable,repoTable,orchestratorFunctionArn,guardrailId,guardrailVersion)jira-project-mapping-table.ts— JiracloudId+projectKey→ repojira-user-mapping-table.ts— JiraaccountId→ platform userjira-workspace-registry-table.ts—cloudId→ AgentCore credential-provider name; this is the table the orchestrator reads to resolve OAuth at agent-launch timeThe construct must create:
JiraWebhookFnLambda +POST /jira/webhookroute (no auth, signature-verified)JiraWebhookProcessorFnLambda (async-invoked, 512 MB to match Linear's attachment-screening footprint)JiraLinkFnLambda +POST /jira/linkroute (Cognito-authenticated)JiraWebhookSecretplaceholder in Secrets Manager (populated bybgagent jira setup)JiraWebhookDedupTable— composite key{issueKey}#{webhookEventTimestamp}, 8-hr TTL (Jira retries far less than Linear; 8 hr is safe parity)bgagent-jira-oauth-*prefix; processor reads mapping/registry tables, writes task tables, invokes orchestrator alias, applies Bedrock guardrail; link Fn read/writes user mapping3. Lambda handlers (new, under
cdk/src/handlers/)jira-webhook.ts— verify Atlassian webhook signature (X-Hub-SignatureHMAC-SHA256 over body using shared secret), parse JSON, accept onlyjira:issue_updatedandjira:issue_createdevents, dedup, async-invoke processor. Silent 200 for unsupported events.jira-webhook-processor.ts— port Linear's logic:issue_createdwithbgagentlabel already on, ORissue_updatedwhosechangelog.itemsshows labelbgagentnewly added (Jira sends label diffs inchangelog, not in the issue body — handle this carefully; this is the one place we can't blindly copy Linear)cloudId→ workspace registry → AgentCore credential-provider name (don't fetch the token here; just hand off)issue.fields.summary+ ADF→markdown ofissue.fields.descriptioncreateTaskCorewithchannelSource: 'jira'and metadata{ jira_cloud_id, jira_project_key, jira_issue_key, jira_issue_id }reportIssueFailure)jira-link.ts— Cognito JWT in, linking-code lookup, dry-run preview of Atlassian identity, then persist user mapping. Direct port oflinear-link.ts.4. Stack wiring (
cdk/src/stacks/agent.ts)Append after Linear:
5. Agent-side outbound (MCP only)
Modify
agent/src/channel_mcp.py:if channel_source != "linear": return Falsegate with a small registry/dispatch:_jira_server_entry()— writes Atlassian Remote MCP config (URLhttps://mcp.atlassian.com/v1/sseor current per Atlassian docs; OAuth token resolved frombgagent-jira-oauth-<slug>secret, env var nameJIRA_API_TOKENor per-Atlassian-MCP convention).No
jira_reactions.pyfor now. If MCP coverage is insufficient (e.g. structured failure comments at orchestrator boundary), we can add one later, exactly mirroringlinear_reactions.py.6. CLI (
cli/src/**)Add subcommands by copying Linear's:
bgagent jira setup— populateJiraWebhookSecret, register the AgentCore credential provider, print webhook URL for the customer to paste into Atlassian's webhook configbgagent jira link <code>— invokePOST /jira/linkbgagent jira map <project-key> <repo>— write to project mapping table7. Docs
docs/guides/JIRA_SETUP_GUIDE.md— portLINEAR_SETUP_GUIDE.mdstep-by-step, swapping Linear for Jira Cloud, including: creating the OAuth 3LO app in Atlassian developer console, scopes (read:jira-work,write:jira-work,read:jira-user), webhook URL registration, label setup, project mapping.README.mdunder integrations.decisions/ADR if the repo uses them — capture "Jira Cloud only, OAuth 3LO, label trigger, MCP outbound."Things to watch (non-obvious)
X-Hub-Signature: sha256=<hex>; ensure constant-time compare and that the body is the raw unparsed bytes, not the parsed-and-restringified JSON.webhookEvent: jira:issue_updatedpayload haschangelog.items[]withfield: "labels",fromString,toString. Diff these — not currentissue.fields.labels. Linear's processor does the equivalent diff using itsdata.updatedFrom; same idea, different shape.cloudIdis the tenant key, not domain or workspace name. Webhook payloads include it; OAuth flows return it. Index everything oncloudId.{issueKey}#{timestamp}rather than{issueKey}#{eventType}so two distinct label-adds in quick succession aren't collapsed.jira_reactions.py(Plan B; do not start with this).Verification plan
End-to-end smoke (after deploy):
cd cdk && npm run build && cdk synth— no type errors, bothLinearIntegrationandJiraIntegrationsynthesize.cdk deployto a dev account.bgagent jira setup, complete OAuth dance, paste webhook URL into a test Jira Cloud instance.bgagent jira link <code>, thenbgagent jira map TEST <github-org/test-repo>.TEST, add labelbgagent→ confirm task appears inbgagent listwithin ~30s.{ ok: true, deduped: true }; non-issue event → silent 200.Unit-test surface (mirror Linear's tests, found under
cdk/test/andagent/tests/):jira-webhook.test.ts— signature pass/fail, dedup, event filteringjira-webhook-processor.test.ts— label-add-on-create vs label-add-on-update vs label-already-present-no-changechannel_mcptest —channel_source="jira"writes ajira-serverentry;"slack"does notOut of scope (explicit)
jira_reactions.py) — only add if MCP gaps emerge