Skip to content

feat: add Jira integration as issue tracker provider#588

Merged
acreeger merged 3 commits intoiloom-ai:mainfrom
NoahCardoza:feature/jira-integration
Feb 17, 2026
Merged

feat: add Jira integration as issue tracker provider#588
acreeger merged 3 commits intoiloom-ai:mainfrom
NoahCardoza:feature/jira-integration

Conversation

@NoahCardoza
Copy link
Contributor

Summary

  • Adds full Jira Cloud support as an issue tracker provider alongside GitHub and Linear
  • Implements JiraApiClient, JiraIssueTracker, and ADF/Markdown bidirectional conversion
  • Adds Jira MCP provider for issue management server integration
  • Preserves case-sensitive issue keys (e.g., PROJ-123) in metadata for correct Jira API calls
  • Adds moveIssueToReadyForReview workflow transition support across all providers
  • Prevents wiki markup generation in agent templates and prompts

Note: BitBucket integration (PR #TBD) depends on this branch and will be opened separately once this is merged.

Test plan

  • Configure Jira provider and verify il start PROJ-123 creates a loom
  • Verify issue key case is preserved in metadata and commit trailers
  • Verify ADF markdown conversion with pnpm vitest run src/lib/providers/jira/AdfMarkdownConverter.test.ts
  • Verify metadata preserves issueKey with pnpm vitest run src/lib/MetadataManager.test.ts
  • Verify il finish moves Jira issue to "Ready for Review" state
  • Run pnpm build — compiles successfully

🤖 Generated with Claude Code

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added this change because I use the ASDF tool manager.

Adds full Jira Cloud support including:
- JiraApiClient for REST API communication
- JiraIssueTracker implementing the IssueTracker interface
- ADF/Markdown bidirectional conversion for Jira comments
- Jira MCP provider for issue management server
- Case-sensitive issue key preservation in metadata
- Wiki markup prevention in agent templates
- moveIssueToReadyForReview workflow transition support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@NoahCardoza NoahCardoza force-pushed the feature/jira-integration branch from d47b468 to badf7b0 Compare February 13, 2026 03:37
  Extend JiraApiClient with createIssueWithParent, createIssueLink,
  deleteIssueLink, searchIssues, and DELETE request support. Implement
  createChildIssue, createDependency, getDependencies, removeDependency,
  getChildIssues, and getPR on JiraIssueManagementProvider. Add test-jira
  CLI command for manual testing against a live Jira instance.

  Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@NoahCardoza
Copy link
Contributor Author

I noticed the il issues command. I'm adding Jira provider support to that along with two new (Jira only) flags:

  1. --sprint: filter out only issues that belong to a specific sprint. Use --sprint current to show all issues from open sprints.
  2. --mine: filters out only issues that belong to the logged in user

Add Jira-only flags for filtering issues by sprint and assignee.
--sprint accepts a sprint name or "current" for the active sprint,
--mine filters to issues assigned to the authenticated user.
Also adds doneStatuses config, Jira provider support in issues command,
and jira.ts utility module.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@NoahCardoza
Copy link
Contributor Author

@acreeger This PR is ready to be reviewed/merged. If you see any other places that need integration, let me know. I imagine there are still a few, but we could treat those as bugs later on as they crop up.

@NoahCardoza
Copy link
Contributor Author

This PR is a blocker for #595. It looks like I can't set proper dependencies but I wanted to document this.

@acreeger
Copy link
Collaborator

@NoahCardoza - Thanks for this substantial contribution! The Jira integration is architecturally sound - you clearly studied the existing GitHub and Linear provider patterns and followed them well. The ADF/Markdown converter is solid with thorough tests, the case-sensitive issue key preservation via metadata is clever, and the sprint/mine flags degrade gracefully for non-Jira providers. Really nice work.

I did a deep review across architecture, code quality, test coverage, integration completeness, and UX. Before I start making fixes on this branch, I want to align with you on the findings so you can flag any false positives or things I may have misunderstood.

Critical - Must Fix Before Merge

# Issue Details
1 Factory crash: 8 call sites missing settings IssueManagementProviderFactory.create() requires settings for Jira but 8 call sites don't pass it: issue-management-server.ts (lines 531, 592, 646, 697, 755), SessionSummaryService.ts:411, plan.ts:214, PRManager.ts:30. Runtime crash for Jira users.
2 cleanup.ts hardcodes GitHubService cleanup.ts:103-110 always constructs LoomManager with GitHubService regardless of configured provider.
3 Error swallowing violates project conventions testConnection() (JiraApiClient.ts:373), isValidIssue() (JiraIssueTracker.ts:91), detectInputType() (JiraIssueTracker.ts:68) all catch ALL errors and return defaults. Network outage silently becomes "issue doesn't exist".
4 API token logged in plaintext SettingsManager.ts:851 dumps full settings object via logger.debug(), now including jira.apiToken.

High - Should Fix Before Merge

# Issue Details
5 Issue.state type violation JiraIssueTracker.ts:327 casts arbitrary Jira statuses ("in progress", "to do") to open/closed via as - downstream comparisons won't work.
6 validateIssueState inconsistency JiraIssueTracker.ts:104-113: GitHub/Linear prompt user on closed issues; Jira only logs a warning.
7 No pagination JiraApiClient.ts getComments() and searchIssues() return only first page. Jira defaults to 50 results.
8 No HTTP timeout JiraApiClient.ts:149-198 uses https.request with no timeout. Hanging server hangs CLI indefinitely.
9 delete() is public JiraApiClient.ts:225 - all other HTTP methods are private; this should match.
10 JQL injection risk utils/jira.ts:36-43 and JiraIssueManagementProvider.ts:395 interpolate user-provided values into JQL without escaping.
11 test-jira in production commands/test-jira.ts creates real Jira issues with no dry-run. Should be hidden or removed.
12 list-children.ts missing Jira list-children.ts:99-115 fetchChildIssues() only handles GitHub/Linear. Jira MCP has the support but it's not wired in.
13 No unit tests for 3 major files JiraApiClient.ts (382 LOC), JiraIssueTracker.ts (354 LOC), JiraIssueManagementProvider.ts (404 LOC) all untested. GitHub/Linear MCP providers have 865/695 lines of tests respectively.
14 No il init wizard for Jira init-prompt.txt has no Jira support. Setup requires manual JSON editing while GitHub is zero-config and Linear has a guided wizard.
15 DEBUGGING_MCP.md at repo root Developer debugging doc with CommonJS require() examples in an ESM project. Should move to docs/ or be removed.
16 Cosmetic provider name references start.ts:382, cleanup.ts:465/482, IdentifierParser.ts reference "GitHub" or "Linear" when Jira is active.
17 Comment body logged in debug JiraApiClient.ts:242 logs full body and ADF payload. Inconsistent with other methods that don't log bodies.

Medium - Can Be Follow-Up

# Issue Details
18 adfToMarkdown() no type guard AdfMarkdownConverter.ts:91-99 accepts unknown, casts to ADFDocument without validating shape.
19 processMarkdownImages not called GitHub/Linear MCP providers process authenticated image URLs; Jira doesn't.
20 No rate limiting handling JiraApiClient.ts:170-171 treats 429 responses as generic errors, no retry guidance.

Next Steps

My plan: once you've reviewed and flagged any false positives, I'll make fixes for the Critical and High items on this branch and request your review. Medium items can be tracked as follow-up issues.

@acreeger
Copy link
Collaborator

acreeger commented Feb 16, 2026

Review Fixes Applied

@NoahCardoza — Applied fixes from the review findings. Here's what landed:

Security

  • Credential redaction in SettingsManager debug logging — API tokens now show [REDACTED]
  • JQL injection prevention — added escapeJql() utility, applied to all JQL interpolation points

Bug Fixes

  • Buffer corruption in JiraApiClient — switched from string concatenation to Buffer.concat for HTTP response handling (prevents multi-byte character corruption)
  • Inverted dependency link directioncreateDependency, getDependencies, and removeDependency had inwardIssue/outwardIssue swapped, causing "blocks"/"blocked by" relationships to be reversed in Jira
  • Factory missing settings — added settings parameter to 8 IssueManagementProviderFactory.create() call sites that would crash at runtime for Jira users
  • cleanup.ts hardcoded GitHubService — replaced with IssueTrackerFactory.create(settings) so cleanup works with any provider

Robustness

  • HTTP timeout (30s) on all Jira API requests
  • Pagination safety cap (5000) on searchIssues and getComments
  • Specific error catchingtestConnection() only catches 401/403 (was swallowing all errors), isValidIssue()/detectInputType() only catch 404
  • Jira status mapping — added mapJiraStatusToState() instead of unsafe as 'open' | 'closed' cast

Naming & Docs

  • Renamed 'linear' identifier type to 'project-key' (provider-agnostic)
  • Moved DEBUGGING_MCP.mddocs/debugging-mcp.md, fixed ESM imports
  • Added Jira provider option to il init wizard prompt
  • Updated cosmetic "GitHub issue/PR" references to generic "issue/PR"

Tests

  • Updated assertions in cleanup.test.ts and SessionSummaryService.test.ts to match changes
  • All 2139 tests passing ✅

Still TODO

  • Configurable issue typescreateIssue defaults to 'Task' and createIssueWithParent defaults to 'Sub-task', but Jira instances use different names (e.g., "Subtask" vs "Sub-task"). Planning a fix to make these configurable via settings. Will document in a follow-up comment.

@acreeger
Copy link
Collaborator

acreeger commented Feb 16, 2026

Combined Analysis & Plan: Configurable Jira Issue Types

Executive Summary

The Jira API client hardcodes 'Task' and 'Sub-task' as default issue type names in createIssue() and createIssueWithParent(). Different Jira instances use different names (e.g., "Subtask" vs "Sub-task"), causing 400 errors. The fix adds two optional fields (defaultIssueType, defaultSubtaskType) to the Jira settings schema, flowing them through the existing config pattern used by transitionMappings.

Implementation Overview

High-Level Execution Phases

  1. Schema & Config: Add defaultIssueType and defaultSubtaskType to Zod schemas and JiraTrackerConfig interface
  2. Config Loading: Wire the new fields through getJiraTrackerConfig() (settings + env vars) and mcp.ts env passthrough
  3. Consumption: Use config values instead of hardcoded defaults in JiraApiClient calls from JiraIssueTracker and JiraIssueManagementProvider
  4. Documentation: Update init prompt to mention these optional settings, update docs/iloom-commands.md

Quick Stats

  • 5 files to modify
  • 0 new files to create
  • 0 files to delete
  • Dependencies: None

Complete Analysis & Implementation Details (click to expand)

Research Findings

Problem Space

  • Problem: Jira instances use varied issue type names; hardcoded defaults cause 400 errors on non-standard instances.
  • Architectural context: Follows exact same pattern as transitionMappings -- optional config field with env var override.
  • Edge cases: User provides empty string (Zod .min(1) handles this); user provides invalid type name (Jira API returns descriptive 400 error naturally).

Codebase Research

  • Hardcoded defaults: JiraApiClient.ts:299 (issueType = 'Task') and :323 (issueType = 'Sub-task')
  • Config interface: JiraTrackerConfig at JiraIssueTracker.ts:14-17 extends JiraConfig, already has optional transitionMappings
  • Config loading: getJiraTrackerConfig() at JiraIssueManagementProvider.ts:52-92 reads from settings then env vars
  • Env var passthrough: mcp.ts:98-116 maps settings fields to JIRA_* env vars for MCP server
  • Callers of API client methods: JiraIssueTracker.createIssue() at :141 and JiraIssueManagementProvider.createChildIssue() at :288

Affected Files

  • /src/lib/SettingsManager.ts:380-411 (and :616-648) - Jira Zod schemas (both defaulting and non-defaulting)
  • /src/lib/providers/jira/JiraIssueTracker.ts:14-17,141 - JiraTrackerConfig interface and createIssue() call
  • /src/mcp/JiraIssueManagementProvider.ts:52-92,288 - config loading and createChildIssue() call
  • /src/utils/mcp.ts:98-116 - env var passthrough
  • /templates/prompts/init-prompt.txt:502 - mention of advanced settings

Medium Severity Risks

  • None identified: This change follows an established pattern exactly (transitionMappings), no new architectural patterns introduced.

Implementation Plan

Automated Test Cases to Create

No new test file needed. The existing test patterns don't unit test JiraApiClient or JiraIssueTracker (noted as finding #13 in the review). The Zod schema validation is automatically tested by the existing SettingsManager test suite when new fields are added. A build check (pnpm build) confirms type correctness across the chain.

Files to Modify

1. /src/lib/SettingsManager.ts:380-411 and :616-648

Change: Add defaultIssueType and defaultSubtaskType optional string fields to both IloomSettingsSchema and IloomSettingsSchemaNoDefaults Jira object schemas.

In IloomSettingsSchema (with defaults, around line 402-405, after transitionMappings):

defaultIssueType: z
    .string()
    .min(1, 'Default issue type cannot be empty')
    .optional()
    .default('Task')
    .describe('Default Jira issue type for new issues (e.g., "Task", "Story", "Bug")'),
defaultSubtaskType: z
    .string()
    .min(1, 'Default subtask type cannot be empty')
    .optional()
    .default('Sub-task')
    .describe('Default Jira issue type for child/sub issues (e.g., "Sub-task", "Subtask")'),

In IloomSettingsSchemaNoDefaults (without defaults, around line 638-641, after transitionMappings):

defaultIssueType: z
    .string()
    .min(1, 'Default issue type cannot be empty')
    .optional()
    .describe('Default Jira issue type for new issues (e.g., "Task", "Story", "Bug")'),
defaultSubtaskType: z
    .string()
    .min(1, 'Default subtask type cannot be empty')
    .optional()
    .describe('Default Jira issue type for child/sub issues (e.g., "Sub-task", "Subtask")'),

2. /src/lib/providers/jira/JiraIssueTracker.ts:14-17

Change: Add defaultIssueType? and defaultSubtaskType? optional fields to JiraTrackerConfig interface.

export interface JiraTrackerConfig extends JiraConfig {
    projectKey: string
    transitionMappings?: Record<string, string>
    defaultIssueType?: string    // new
    defaultSubtaskType?: string  // new
}

3. /src/lib/providers/jira/JiraIssueTracker.ts:141-144

Change: Pass this.config.defaultIssueType to this.client.createIssue() instead of relying on the hardcoded default.

const jiraIssue = await this.client.createIssue(
    this.config.projectKey,
    title,
    body,
    this.config.defaultIssueType  // passes undefined if not set, API client keeps its default
)

4. /src/mcp/JiraIssueManagementProvider.ts:52-67

Change: In getJiraTrackerConfig(), read defaultIssueType and defaultSubtaskType from settings and add them to config object (settings path, lines ~60-67).

if (jiraSettings.defaultIssueType) {
    config.defaultIssueType = jiraSettings.defaultIssueType
}
if (jiraSettings.defaultSubtaskType) {
    config.defaultSubtaskType = jiraSettings.defaultSubtaskType
}

5. /src/mcp/JiraIssueManagementProvider.ts:70-86

Change: In getJiraTrackerConfig(), read JIRA_DEFAULT_ISSUE_TYPE and JIRA_DEFAULT_SUBTASK_TYPE env vars (env var path, lines ~77-86).

if (process.env.JIRA_DEFAULT_ISSUE_TYPE) {
    config.defaultIssueType = process.env.JIRA_DEFAULT_ISSUE_TYPE
}
if (process.env.JIRA_DEFAULT_SUBTASK_TYPE) {
    config.defaultSubtaskType = process.env.JIRA_DEFAULT_SUBTASK_TYPE
}

6. /src/mcp/JiraIssueManagementProvider.ts:288-293

Change: In createChildIssue(), pass this.tracker.getConfig().defaultSubtaskType to createIssueWithParent().

const jiraIssue = await this.tracker.getApiClient().createIssueWithParent(
    this.projectKey,
    title,
    body,
    parentKey,
    this.tracker.getConfig().defaultSubtaskType  // passes undefined if not set, API client keeps its default
)

7. /src/utils/mcp.ts:114-116

Change: Add env var passthrough for the two new fields (after transitionMappings passthrough).

if (jiraSettings?.defaultIssueType) {
    envVars.JIRA_DEFAULT_ISSUE_TYPE = jiraSettings.defaultIssueType
}
if (jiraSettings?.defaultSubtaskType) {
    envVars.JIRA_DEFAULT_SUBTASK_TYPE = jiraSettings.defaultSubtaskType
}

8. /templates/prompts/init-prompt.txt:502

Change: Update the note about advanced Jira settings to also mention defaultIssueType and defaultSubtaskType.

Current text:

**Note:** Advanced Jira settings like `transitionMappings` and `doneStatuses` can be configured later by editing the settings files directly. Mention this to the user.

Updated text:

**Note:** Advanced Jira settings like `transitionMappings`, `doneStatuses`, `defaultIssueType`, and `defaultSubtaskType` can be configured later by editing the settings files directly. Mention this to the user.

9. /docs/iloom-commands.md

Change: Add documentation for the two new Jira settings fields in the appropriate configuration section. Include default values and example usage.

Detailed Execution Order

NOTE: These steps are executed in a SINGLE implementation run.

  1. Update Zod schemas - Files: /src/lib/SettingsManager.ts - Add fields to both schema variants -> Verify: pnpm build passes
  2. Update JiraTrackerConfig interface - File: /src/lib/providers/jira/JiraIssueTracker.ts - Add optional fields -> Verify: TypeScript accepts the new fields
  3. Wire config loading - File: /src/mcp/JiraIssueManagementProvider.ts - Read from settings and env vars in getJiraTrackerConfig() -> Verify: both code paths populate the fields
  4. Wire env var passthrough - File: /src/utils/mcp.ts - Pass new fields as env vars -> Verify: MCP server receives the values
  5. Use config values at call sites - Files: /src/lib/providers/jira/JiraIssueTracker.ts, /src/mcp/JiraIssueManagementProvider.ts - Pass config values to API client methods -> Verify: pnpm build passes
  6. Update documentation - Files: /templates/prompts/init-prompt.txt, /docs/iloom-commands.md - Mention new settings -> Verify: docs are accurate
  7. Final build - Run pnpm build to confirm everything compiles

Dependencies and Configuration

None

@acreeger
Copy link
Collaborator

acreeger commented Feb 16, 2026

Fun discovery while fixing the ADF table conversion bug: the extended-markdown-adf-parser library has 3 issues total — and @NoahCardoza, I see you filed the only open one (the inline code mark bug that led to our sanitizeCodeMarks() workaround). We're now adding a second post-processing fix for tables following the same pattern. At this rate we might end up maintaining more ADF fixups than the library has issues. 😄

@acreeger
Copy link
Collaborator

Analysis & Plan: Rewrite checkbox-to-taskList conversion

  • Analyze current implementation and library behavior
  • Research ADF taskList/taskItem spec
  • Design new approach (extract-then-apply)
  • Create implementation plan with test updates

@acreeger
Copy link
Collaborator

acreeger commented Feb 16, 2026

Combined Analysis & Plan - Checkbox-to-taskList Rewrite

Executive Summary

The current checkbox-to-taskList conversion in AdfMarkdownConverter.ts injects zero-width marker characters into markdown before parsing, which is fragile and breaks things. This rewrite replaces that approach with a cleaner two-phase strategy: extract checkbox states from the original markdown before the library parses it, let the library strip [x]/[ ] naturally, then match bulletList nodes in the ADF output against the extracted states to convert them to taskList/taskItem nodes.

Implementation Overview

High-Level Execution Phases

  1. Remove broken marker code: Delete CHECKBOX_DONE_MARKER, CHECKBOX_TODO_MARKER, preprocessCheckboxes(), stripCheckboxMarker(), and the old convertCheckboxesToTaskList()
  2. Add new extraction function: extractCheckboxBlocks() parses original markdown to record contiguous checkbox blocks with states and text
  3. Rewrite conversion function: New convertCheckboxesToTaskList() walks ADF tree, matches bulletList nodes against extracted blocks by text content, and converts matches to taskList/taskItem with proper ADF structure (inline nodes directly under taskItem, not wrapped in paragraph)
  4. Update markdownToAdf(): Remove preprocessCheckboxes() call, pass extracted blocks to the new conversion function
  5. Add tests for formatted checkbox content: Bold, italic, code inside checkboxes

Quick Stats

  • 1 file to modify (AdfMarkdownConverter.ts)
  • 1 file to modify for tests (AdfMarkdownConverter.test.ts)
  • 0 new files
  • Dependencies: None

Complete Analysis and Implementation Details (click to expand)

Research Findings

Problem Space

  • Problem: Zero-width marker characters injected into markdown before parsing are fragile and break rendering in some contexts.
  • Architectural context: markdownToAdf() is a pipeline: preprocess markdown -> library parse -> post-process ADF tree. The checkbox fix only affects the pre/post-processing stages.
  • Edge cases: Checkbox items with formatted text (bold, italic, code), mixed lists (some checkbox, some not), multiple separate checkbox blocks in one document.

Codebase Research

  • Entry point: markdownToAdf() at line 347 orchestrates the pipeline
  • Remove: Lines 122-123 (markers), 130-184 (preprocessCheckboxes), 191-235 (convertCheckboxesToTaskList), 258-273 (stripCheckboxMarker)
  • Keep: Line 115 (taskIdCounter), lines 240-253 (findFirstTextContent), line 360 (call site for convertCheckboxesToTaskList - but rewrite the function)
  • Keep untouched: wrapTableCellContent, sanitizeCodeMarks, convertDetailsToExpandSyntax
  • ADF spec: taskItem contains inline nodes directly (text, hardBreak, mention, emoji, etc.), NOT wrapped in paragraph. Library output for listItem is listItem > paragraph > text, so conversion must unwrap paragraphs.

Affected Files

  • /Users/adam/Documents/Projects/iloom-cli/feature-jira-integration_pr_588/src/lib/providers/jira/AdfMarkdownConverter.ts:115-273,347-362 - All checkbox-related code
  • /Users/adam/Documents/Projects/iloom-cli/feature-jira-integration_pr_588/src/lib/providers/jira/AdfMarkdownConverter.test.ts:704-757 - Existing checkbox tests, plus new tests to add

Implementation Plan

Automated Test Cases to Create

Test File: /Users/adam/Documents/Projects/iloom-cli/feature-jira-integration_pr_588/src/lib/providers/jira/AdfMarkdownConverter.test.ts (MODIFY)

Existing tests at lines 704-757 should continue to pass. Add new tests:

Click to expand test structure (20 lines)
describe('checkbox task list conversion', () => {
  // EXISTING tests stay (lines 705-757)

  test('checkbox list with bold text converts correctly', () => {
    // input: '- [x] **Bold task**\n- [ ] Regular task'
    // verify: taskList with 2 taskItems, first DONE, second TODO
    // verify: first taskItem text includes bold mark
  })

  test('checkbox list with inline code converts correctly', () => {
    // input: '- [x] Run `pnpm build`\n- [ ] Run `pnpm test`'
    // verify: taskList, text content matches, code marks preserved
  })

  test('multiple separate checkbox blocks convert independently', () => {
    // input: '- [x] A\n- [ ] B\n\nSome text\n\n- [ ] C\n- [x] D'
    // verify: two separate taskList nodes with correct states
  })

  test('checkbox block followed by regular list - only checkbox block converts', () => {
    // input: '- [x] Done\n- [ ] Todo\n\n- regular item\n- another item'
    // verify: first list is taskList, second is bulletList
  })
})

Files to Modify

1. /Users/adam/Documents/Projects/iloom-cli/feature-jira-integration_pr_588/src/lib/providers/jira/AdfMarkdownConverter.ts

Lines to DELETE (122-123): Remove CHECKBOX_DONE_MARKER and CHECKBOX_TODO_MARKER constants.

Lines to DELETE (125-184): Remove preprocessCheckboxes() function entirely.

Lines to REWRITE (186-235): Replace old convertCheckboxesToTaskList() with new implementation. The new function signature becomes convertCheckboxesToTaskList(node: AdfNode, blocks: CheckboxBlock[]): AdfNode.

Lines to DELETE (255-273): Remove stripCheckboxMarker() function.

Lines to MODIFY (347-362): Update markdownToAdf():

  • Remove preprocessCheckboxes(preprocessed) call at line 355
  • Before calling parser.markdownToAdf(), call extractCheckboxBlocks(preprocessed) to get blocks
  • Pass blocks to convertCheckboxesToTaskList(result, blocks) at line 360

New code to add (replace lines 122-273):

Click to expand implementation pseudocode (55 lines)
// -- Type for extracted checkbox block --
interface CheckboxBlock {
  items: Array<{ state: 'DONE' | 'TODO'; text: string }>
  consumed: boolean
}

// -- Extract checkbox blocks from original markdown --
// Parse markdown line by line, identify contiguous runs of "- [x] text" / "- [ ] text"
// A block ends when a non-checkbox line (or empty line) is encountered
// For each block, store the state and the text AFTER "[x] " / "[ ] "
// Only record blocks where ALL bullet items have checkbox syntax
function extractCheckboxBlocks(markdown: string): CheckboxBlock[] {
  // Split into lines
  // Walk lines, accumulating checkbox items into current block
  // When a non-checkbox line breaks the block, finalize it
  // Return array of CheckboxBlock (each with consumed: false)
  // Use same regex as old preprocessCheckboxes: /^(\s*)- \[([ xX])\] (.*)$/
  // For partial blocks (mix of checkbox and non-checkbox bullets), discard - don't record
}

// -- Recursively extract ALL text from an ADF node --
// Unlike findFirstTextContent which returns first text, this concatenates ALL text
function extractAllText(node: AdfNode): string {
  if (node.type === 'text' && node.text !== undefined) return node.text
  if (!node.content) return ''
  return node.content.map(extractAllText).join('')
}

// -- Rewritten convertCheckboxesToTaskList --
// Takes root node and the extracted blocks array (mutated via consumed flag)
function convertCheckboxesToTaskList(node: AdfNode, blocks: CheckboxBlock[]): AdfNode {
  // Recurse into children first
  if (node.content) {
    node.content = node.content.map(child => convertCheckboxesToTaskList(child, blocks))
  }

  if (node.type !== 'bulletList' || !node.content?.length) return node

  // Find next unconsumed block
  const block = blocks.find(b => !b.consumed)
  if (!block) return node

  // Check: item count must match
  if (node.content.length !== block.items.length) return node

  // Check: text content of each listItem must match the block's text
  for (let i = 0; i < node.content.length; i++) {
    const itemText = extractAllText(node.content[i])
    if (itemText !== block.items[i].text) return node
  }

  // Match confirmed - consume the block and convert
  block.consumed = true
  node.type = 'taskList'
  node.attrs = { localId: `tasklist-${++taskIdCounter}` }

  for (let i = 0; i < node.content.length; i++) {
    const listItem = node.content[i]
    listItem.type = 'taskItem'
    listItem.attrs = { localId: `task-${++taskIdCounter}`, state: block.items[i].state }

    // Unwrap paragraph: taskItem should contain inline nodes directly
    // If listItem has a single paragraph child, replace listItem.content with paragraph.content
    // If listItem has multiple children, flatten: unwrap any paragraphs, keep other inline nodes
    if (listItem.content?.length === 1 && listItem.content[0].type === 'paragraph') {
      listItem.content = listItem.content[0].content || []
    } else if (listItem.content) {
      // Flatten: for each child, if paragraph -> spread its content, else keep
      listItem.content = listItem.content.flatMap(child =>
        child.type === 'paragraph' ? (child.content || []) : [child]
      )
    }
  }

  return node
}

Key design decisions in the new code:

  • extractCheckboxBlocks() parses markdown to record states and text, but does NOT modify the markdown
  • Text matching uses extractAllText() which recursively concatenates all text from a node tree, handling bold/italic/code formatting
  • Blocks are consumed in document order via a consumed flag, ensuring first bulletList matches first block
  • Paragraph unwrapping happens during conversion to ensure taskItem contains inline nodes directly per ADF spec

2. /Users/adam/Documents/Projects/iloom-cli/feature-jira-integration_pr_588/src/lib/providers/jira/AdfMarkdownConverter.test.ts

Lines 704-757: Keep existing tests (they test the public contract and should still pass).

After line 757: Add new tests for formatted checkbox content, multiple blocks, and mixed scenarios (as specified in test cases above).

Detailed Execution Order

NOTE: These steps are executed in a SINGLE implementation run.

  1. Add new types and functions

    • Files: AdfMarkdownConverter.ts
    • Add CheckboxBlock interface and extractCheckboxBlocks() function at ~line 122 (replacing markers/preprocessCheckboxes) -> Verify: function parses checkbox blocks from markdown without modifying it
  2. Add extractAllText() helper

    • Files: AdfMarkdownConverter.ts
    • Add recursive text extraction at ~line 240 (near existing findFirstTextContent) -> Verify: concatenates all text from nested ADF nodes
  3. Rewrite convertCheckboxesToTaskList()

    • Files: AdfMarkdownConverter.ts
    • Replace old marker-based function with block-matching version at ~line 191 -> Verify: matches bulletList nodes to extracted blocks, converts with paragraph unwrapping
  4. Delete old code

    • Files: AdfMarkdownConverter.ts
    • Remove CHECKBOX_DONE_MARKER, CHECKBOX_TODO_MARKER (lines 122-123), stripCheckboxMarker() (lines 258-273) -> Verify: no references to markers remain
  5. Update markdownToAdf() pipeline

    • Files: AdfMarkdownConverter.ts
    • Remove preprocessCheckboxes(preprocessed) at line 355, add const checkboxBlocks = extractCheckboxBlocks(preprocessed) before parsing, pass blocks to convertCheckboxesToTaskList(result, checkboxBlocks) at line 360 -> Verify: pipeline no longer modifies markdown for checkboxes
  6. Add new tests

    • Files: AdfMarkdownConverter.test.ts
    • Add tests for formatted content, multiple blocks, mixed scenarios after line 757 -> Verify: all new tests pass
  7. Run build and tests

    • pnpm build -> Verify: compilation succeeds
    • pnpm vitest run src/lib/providers/jira/AdfMarkdownConverter.test.ts -> Verify: all tests pass (existing + new)

Dependencies and Configuration

None

acreeger added a commit that referenced this pull request Feb 16, 2026
Fix security, correctness, and robustness issues identified during deep
review of the Jira integration:

- Fix buffer corruption using Buffer.concat instead of string concatenation
- Fix inverted removeDependency link direction (outward→inward)
- Add JQL escaping to prevent injection in search queries
- Add settings parameter to 8 factory call sites that would crash
- Replace hardcoded GitHubService in cleanup.ts with factory
- Redact sensitive fields (apiToken, etc.) in debug logging
- Add HTTP timeout (30s) and pagination caps to API client
- Fix error swallowing: catch only specific error codes (401/403/404)
- Add Jira status-to-state mapping instead of unsafe cast
- Rename 'linear' identifier type to 'project-key' for provider-agnostic naming
- Move DEBUGGING_MCP.md to docs/ with ESM import fixes
- Add Jira provider option to il init wizard prompt
- Update test assertions to match code changes
@acreeger
Copy link
Collaborator

acreeger commented Feb 16, 2026

@NoahCardoza — I've applied fixes from the review findings, added configurable issue types, and fixed the ADF conversion for tables and checkbox task lists (with upstream issues filed on extended-markdown-adf-parser). The plan prompt has also been updated with Jira-specific formatting guidance.

Ready for your review when you get a chance.

acreeger added a commit that referenced this pull request Feb 17, 2026
Fix security, correctness, and robustness issues identified during deep
review of the Jira integration:

- Fix buffer corruption using Buffer.concat instead of string concatenation
- Fix inverted removeDependency link direction (outward→inward)
- Add JQL escaping to prevent injection in search queries
- Add settings parameter to 8 factory call sites that would crash
- Replace hardcoded GitHubService in cleanup.ts with factory
- Redact sensitive fields (apiToken, etc.) in debug logging
- Add HTTP timeout (30s) and pagination caps to API client
- Fix error swallowing: catch only specific error codes (401/403/404)
- Add Jira status-to-state mapping instead of unsafe cast
- Rename 'linear' identifier type to 'project-key' for provider-agnostic naming
- Move DEBUGGING_MCP.md to docs/ with ESM import fixes
- Add Jira provider option to il init wizard prompt
- Update test assertions to match code changes
@acreeger
Copy link
Collaborator

@NoahCardoza , I think we're ready to roll - I'm going to merge this!

@acreeger acreeger merged commit f411d12 into iloom-ai:main Feb 17, 2026
4 checks passed
@NoahCardoza
Copy link
Contributor Author

@acreeger Nice find with the table formatting. I knew there was an issue there because it would intermittently fail. I hadn't got around to debugging but I'm glad that's been resolved!

Lots of good finds in that review. I'm glad you didn't let me merge without pagination, that would have produced some head scratchers later down the road for sure.

I'll work on rebasing the bitbucket branch now! 🚀

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants