Skip to content

fix: draft PR body missing issue context at creation time#954

Merged
acreeger merged 1 commit intomainfrom
feat/issue-953__pr-body-issue-context
Mar 21, 2026
Merged

fix: draft PR body missing issue context at creation time#954
acreeger merged 1 commit intomainfrom
feat/issue-953__pr-body-issue-context

Conversation

@acreeger
Copy link
Collaborator

Fixes #953

Draft PR body is generic/useless — should include issue context

Problem

When creating a draft PR in draft-pr mode during il start, the PR body is generic and unhelpful:

Fixes #948


This PR was created automatically by iloom.

The code at src/lib/LoomManager.ts:292-294 does attempt to include the issue body:

const issueBody = issueData?.body ? `\n\n## ${issueData.title}\n\n${issueData.body}` : ''
prBody = `Fixes ${prManager.issuePrefix}${input.identifier}${issueBody}\n\n---\n*This PR was created automatically by iloom.*`

But there are two issues:

  1. Possible regression: The issue body may not be getting populated at fetch time, resulting in the empty-body fallback (just Fixes #N).
  2. Even when working, the content is low-value: Dumping raw issue text into a PR body isn't very useful. By the time a draft PR is created, we have the issue context — we could generate a more meaningful initial description.

Investigation needed

  • Confirm whether fetchIssueData() is returning the body correctly at draft PR creation time
  • If it is, determine why the body isn't appearing in the PR
  • If it isn't, fix the data fetching

Location

  • src/lib/LoomManager.ts lines 286-297 — draft PR body generation
  • src/lib/LoomManager.ts line 92 — fetchIssueData() call
  • src/lib/LoomManager.ts lines 618-637 — fetchIssueData() implementation

This PR was created automatically by iloom.

@acreeger acreeger changed the title Draft PR body is generic/useless — should include issue context fix: draft PR body missing issue context at creation time Mar 21, 2026
@acreeger
Copy link
Collaborator Author

Issue Analyzer: Technical Analysis for #953

Root Cause Analysis

The data-fetching pipeline is working correctly. The fetchIssueData() method (LoomManager.ts:618-637) properly delegates to the issue tracker, and all three provider implementations (GitHub, Linear, Jira) correctly populate the body field on the returned Issue object. The issue reported in the bug (PR body showing only Fixes #948) is not a regression — it is expected behavior given the input data.

Root cause: Issue #948 has an empty body (""). I confirmed this by querying the GitHub API:

gh issue view 948 --json body  →  {"body":""}

The empty string "" is falsy in JavaScript, so the conditional at LoomManager.ts:293:

const issueBody = issueData?.body ? `\n\n## ${issueData.title}\n\n${issueData.body}` : ''

...correctly evaluates to '', producing the minimal PR body: Fixes #948\n\n---\n*This PR was created automatically by iloom.*

This is working as designed, but the design itself is the problem — the fallback for empty-body issues produces a useless PR description.

Data Flow Trace

il start 948
  → LoomManager.createIloom()                     (line 89)
    → this.fetchIssueData(input)                   (line 92)
      → this.issueTracker.fetchIssue(948)           (line 622)
        → GitHubService.fetchIssue(948)             (GitHubService.ts:83)
          → fetchGhIssue(948)                       (github.ts:112)
            → gh issue view 948 --json number,title,body,state,labels,assignees,url,...
              → returns { body: "" }                ← Empty body from GitHub API
          → mapGitHubIssueToIssue()                 (GitHubService.ts:407-417)
            → { body: "" }                          ← Faithfully mapped
    → issueData.body === ""                         ← Falsy
    → issueBody = ''                                ← Empty fallback
    → prBody = "Fixes #948\n\n---\n*This PR was created automatically by iloom.*"
    → prManager.createDraftPR(branch, title, prBody, ...)
      → gh pr create --body "Fixes #948..."         (PRManager.ts:404)

Questions & Assumed Answers

# Question Assumed Answer
1 Is fetchIssueData() returning the body correctly? Yes. The pipeline works. GitHub returns "" for issues with no body, which is faithfully propagated. The body field is typed as string (non-optional) in the Issue interface, but the GitHub API can return an empty string.
2 Why isn't the body appearing in the PR? Because issue #948 has no body. The empty string "" is falsy, so the ternary falls through to the empty-string case. This is correct behavior for the current code.
3 Is this a regression? No. The code has always worked this way. The behavior is correct for the design; the design just produces unhelpful output for empty-body issues.
4 Does this affect Linear and Jira too? Yes. Linear maps undefined descriptions to '' via linear.description ?? '' (LinearService.ts:245). Jira maps null descriptions through adfToMarkdown() which also produces empty strings. All three providers can produce empty body values.
5 Would issues WITH a body produce a good PR description? Partially. The raw issue body gets dumped verbatim, which is better than nothing but is "low-value" as the issue states. A lengthy issue body with investigation notes, screenshots, etc. makes for a cluttered PR description.

Affected Files

File Lines Role
src/lib/LoomManager.ts 292-294 Primary fix location. PR body generation for issue/epic types in draft-pr mode.
src/lib/LoomManager.ts 295-296 Branch-mode PR body generation (separate but related).
src/lib/LoomManager.ts 618-637 fetchIssueData() — working correctly, no fix needed.
src/lib/GitHubService.ts 407-417 mapGitHubIssueToIssue() — working correctly, no fix needed.
src/lib/LinearService.ts 241-251 mapLinearIssueToIssue() — working correctly, body can be ''.
src/lib/providers/jira/JiraIssueTracker.ts 439-464 mapJiraIssueToIssue() — working correctly, body can be ''.
src/types/index.ts 54-62 Issue interface — body: string (non-optional, but can be empty).
src/lib/PRManager.ts 377-417 createDraftPR() — passes body directly to gh pr create --body. No transformation.
src/lib/LoomManager.test.ts 694-700 Existing draft-pr tests use expect.stringContaining('Fixes 123') — test only checks the Fixes prefix, not the issue body content.

Technical Findings

  1. No regression exists. The data pipeline works correctly end-to-end. The body field is populated when the source issue has content, and empty when it does not.

  2. The falsy check is semantically wrong for the use case. issueData?.body ? ... : '' conflates "issue has no body" with "issue body is just whitespace." While the practical difference is minimal, the real issue is that the fallback (empty string) produces a useless PR body regardless.

  3. The "even when working" problem is real. When body IS populated, the current code dumps the entire raw issue body into the PR description under a heading with the issue title. This can be very long and includes investigation notes, screenshots, acceptance criteria, etc. that are not useful as a PR description. The PR body should summarize what the PR does, not repeat the issue verbatim.

  4. The fix scope is narrow. Only LoomManager.ts:292-294 needs to change. The data-fetching layer, type definitions, and provider mappings are all correct and should not be modified. The improvement should focus on generating a more useful initial PR body that includes issue context in a structured way (e.g., issue title, link, and optionally a summary) rather than dumping raw content.

  5. Test coverage gap. The existing tests at LoomManager.test.ts:694-700 only assert that the PR body contains "Fixes 123" — they don't verify that issue body content is included. Test mocks use body: 'Test description' (line 286) in one test and body: '' (line 525, 561, 580) in others, but the PR body content is never asserted for issue body inclusion.

@acreeger
Copy link
Collaborator Author

Implementation Plan for #953: Draft PR body should include issue context

Summary

The draft PR body generated during il start in draft-pr mode is generic and unhelpful. The root cause is confirmed: no regression exists, but issues with empty bodies produce a bare Fixes #N PR description, and even when the body IS populated, raw-dumping it is low-value. The fix should produce a structured, useful draft PR body that includes issue context in a readable format.

Key Design Decision: Template-based, no AI generation at draft time

The finish flow already has Claude-powered PR body generation via PRManager.generatePRBody(). However, at il start time (when the draft PR is created), there are no code changes yet — Claude has nothing to summarize. Using AI here would add latency for minimal value. Instead, we should create a well-structured template that includes issue context in a useful format, understanding that:

  1. The agent will update the PR body during implementation (the implementer/reviewer agents can use edit_issue to update the PR description with actual implementation details)
  2. The finish flow does NOT update the draft PR body — it only marks the PR as ready for review. So the initial body (or whatever the agent updates it to) is what reviewers see.
  3. The initial body should provide useful context for anyone who views the PR before the agent finishes — e.g., the developer checking on progress, or another agent that needs to understand the PR's purpose.

Questions & Assumed Answers

# Question Assumed Answer
1 Should we use Claude AI to generate the draft PR body? No. At draft creation time there are no code changes. Claude would just be reformatting the issue body. A structured template is faster and more predictable. The agent can update the body later with implementation details.
2 Should we include the full issue body in the PR description? Yes, but structured. Include issue title as a heading, the body in a collapsible <details> block so it doesn't clutter the PR, and a clear "Fixes #N" link.
3 Should we handle empty issue bodies differently? Yes. When the issue body is empty, include the title and Fixes link but omit the empty details section. The current falsy check is correct but the fallback should still be useful.
4 Should we change the branch-mode PR body too? No. Branch mode has no issue context to include. The current Branch: <name> body is fine. Keep changes scoped to issue/epic types.
5 What about the *This PR was created automatically by iloom.* footer? Keep it — it signals that the PR is auto-generated and the body will be updated by the agent.

Affected Files

File Change Lines
src/lib/LoomManager.ts Rewrite draft PR body generation for issue/epic types 292-294
src/lib/LoomManager.test.ts Add test assertions for issue body inclusion in PR body ~694 (Linear test), ~912 (child loom test)

Detailed Changes

1. src/lib/LoomManager.ts — Lines 292-294

Current code:

if (input.type === 'issue' || input.type === 'epic') {
  const issueBody = issueData?.body ? `\n\n## ${issueData.title}\n\n${issueData.body}` : ''
  prBody = `Fixes ${prManager.issuePrefix}${input.identifier}${issueBody}\n\n---\n*This PR was created automatically by iloom.*`
}

New code:

if (input.type === 'issue' || input.type === 'epic') {
  const issueRef = `Fixes ${prManager.issuePrefix}${input.identifier}`
  const issueBodyContent = issueData?.body?.trim()
  
  if (issueBodyContent) {
    prBody = [
      issueRef,
      '',
      '<details>',
      `<summary>Issue: ${issueData!.title}</summary>`,
      '',
      issueBodyContent,
      '',
      '</details>',
      '',
      '---',
      '*This PR was created automatically by iloom.*',
    ].join('\n')
  } else {
    prBody = [
      issueRef,
      '',
      `> ${issueData?.title ?? `Issue ${input.identifier}`}`,
      '',
      '---',
      '*This PR was created automatically by iloom.*',
    ].join('\n')
  }
}

Rationale:

  • When issue body exists: Uses a collapsible <details> block with the issue title as summary. This keeps the PR description clean while making the full issue context available with one click. The Fixes #N link is prominent at the top.
  • When issue body is empty: Shows the issue title as a blockquote for context instead of leaving the PR body bare. This handles the original bug report case (issue Recap entries added by the Swarm code reviewer in wave verficaiton tasks don't seem to be getting added to the recap file #948 had an empty body).
  • Uses .trim() instead of bare falsy check to also handle whitespace-only bodies.
  • Array .join('\n') is more readable than template literals with \n escapes.

2. src/lib/LoomManager.test.ts — Enhance existing test assertions

Test: "should succeed with Linear provider and draft-pr merge mode" (~line 694)

The mock already has body: 'Test description'. Update the assertion from:

expect.stringContaining('Fixes 123'), // PR body with Fixes keyword (no prefix for Linear)

to:

expect.stringContaining('Fixes 123'), // PR body with Fixes keyword (no prefix for Linear)
// Also verify issue body is included

And add a separate assertion after the toHaveBeenCalledWith:

const prBodyArg = mockCreateDraftPR.mock.calls[0][2] as string
expect(prBodyArg).toContain('Test description')
expect(prBodyArg).toContain('Test Linear Issue')

Test: "should create draft PR targeting parent branch for child looms" (~line 909)

Add assertion that the issue body is included:

const prBodyArg = mockCreateDraftPR.mock.calls[0][2] as string
expect(prBodyArg).toContain('Child description')
expect(prBodyArg).toContain('Child Issue')

New test: "should handle empty issue body gracefully in draft-pr mode"

Add a new test case that creates an issue with body: '' and verifies:

  • The PR body still contains the Fixes reference
  • The PR body contains the issue title
  • The PR body does NOT contain <details> (no collapsible since there's nothing to collapse)
it('should handle empty issue body gracefully in draft-pr mode', async () => {
  mockCreateDraftPR.mockResolvedValue({ number: 99, url: 'https://github.com/owner/repo/pull/99' })
  mockCheckForExistingPR.mockResolvedValue(null)

  vi.mocked(mockSettings.loadSettings).mockResolvedValue({
    mainBranch: 'main',
    worktreeDir: '/test/worktrees',
    mergeBehavior: { mode: 'draft-pr' },
  })

  vi.mocked(mockGitHub.fetchIssue).mockResolvedValue({
    number: 123,
    title: 'Issue With No Body',
    body: '',
    state: 'open',
    labels: [],
    assignees: [],
    url: 'https://github.com/owner/repo/issues/123',
  })

  const expectedPath = '/test/worktree-issue-123'
  vi.mocked(mockGitWorktree.generateWorktreePath).mockReturnValue(expectedPath)
  vi.mocked(mockGitWorktree.createWorktree).mockResolvedValue(expectedPath)
  vi.mocked(mockEnvironment.calculatePort).mockReturnValue(3123)

  await manager.createIloom(baseInput)

  const prBodyArg = mockCreateDraftPR.mock.calls[0][2] as string
  expect(prBodyArg).toContain('Fixes #123')
  expect(prBodyArg).toContain('Issue With No Body')
  expect(prBodyArg).not.toContain('<details>')
})

New test: "should include issue body in collapsible details in draft-pr mode"

Add a test that verifies the collapsible format when the body IS populated:

it('should include issue body in collapsible details in draft-pr mode', async () => {
  mockCreateDraftPR.mockResolvedValue({ number: 99, url: 'https://github.com/owner/repo/pull/99' })
  mockCheckForExistingPR.mockResolvedValue(null)

  vi.mocked(mockSettings.loadSettings).mockResolvedValue({
    mainBranch: 'main',
    worktreeDir: '/test/worktrees',
    mergeBehavior: { mode: 'draft-pr' },
  })

  vi.mocked(mockGitHub.fetchIssue).mockResolvedValue({
    number: 123,
    title: 'Issue With Body',
    body: '## Problem\n\nSomething is broken.',
    state: 'open',
    labels: [],
    assignees: [],
    url: 'https://github.com/owner/repo/issues/123',
  })

  const expectedPath = '/test/worktree-issue-123'
  vi.mocked(mockGitWorktree.generateWorktreePath).mockReturnValue(expectedPath)
  vi.mocked(mockGitWorktree.createWorktree).mockResolvedValue(expectedPath)
  vi.mocked(mockEnvironment.calculatePort).mockReturnValue(3123)

  await manager.createIloom(baseInput)

  const prBodyArg = mockCreateDraftPR.mock.calls[0][2] as string
  expect(prBodyArg).toContain('Fixes #123')
  expect(prBodyArg).toContain('<details>')
  expect(prBodyArg).toContain('Issue With Body')
  expect(prBodyArg).toContain('Something is broken.')
  expect(prBodyArg).toContain('iloom')
})

What NOT to change

  • fetchIssueData() (lines 618-637): Working correctly. No changes needed.
  • PRManager.generatePRBody(): This is used at finish time and is unrelated to the draft PR body at start time.
  • Issue type definitions: body: string is correct as-is.
  • Branch-mode PR body (line 296): No issue context available; current format is fine.
  • Provider implementations: All correctly populate the body field.

Execution Plan

  1. Modify src/lib/LoomManager.ts lines 292-294: Replace the current draft PR body generation for issue/epic types with the new structured format using <details> for non-empty issue bodies and blockquote fallback for empty bodies. Use .trim() instead of bare falsy check.

  2. Add test: "should handle empty issue body gracefully in draft-pr mode" in src/lib/LoomManager.test.ts: Mock an issue with body: '', verify the PR body contains the Fixes reference and issue title but no <details> block.

  3. Add test: "should include issue body in collapsible details in draft-pr mode" in src/lib/LoomManager.test.ts: Mock an issue with a non-empty body, verify the PR body contains <details>, the issue title, the body content, and the Fixes reference.

  4. Enhance existing test assertions in the Linear and child-loom draft PR tests: Add assertions that verify issue body and title are included in the PR body argument passed to createDraftPR.

  5. Run pnpm build to verify TypeScript compiles successfully.

  6. Run pnpm test src/lib/LoomManager.test.ts to verify all tests pass, including existing ones.

Plan for Caller

This plan modifies 2 files with a narrow scope:

  • src/lib/LoomManager.ts (lines 292-294) — rewrite draft PR body generation
  • src/lib/LoomManager.test.ts — add 2 new tests + enhance 2 existing test assertions

No new dependencies, no type changes, no provider changes. The fix is purely in the PR body string construction at draft creation time.

@acreeger
Copy link
Collaborator Author

acreeger commented Mar 21, 2026

Implementation Complete

Summary

Replaced the generic draft PR body generation with a structured format that includes issue context. When the issue body is non-empty, it's wrapped in a collapsible <details> block. When empty, a blockquote with the issue title is used instead.

Changes Made

  • src/lib/LoomManager.ts: Replaced lines 292-294 with structured PR body generation using .trim() check and <details> collapsible section
  • src/lib/LoomManager.test.ts: Added 2 new tests and enhanced 2 existing tests for draft PR body assertions

Validation Results

  • ✅ Tests: 112 passed / 112 total
  • ✅ Build: Passed

Detailed Changes by File (click to expand)

src/lib/LoomManager.ts

Changes: Replaced draft PR body generation (lines 292-310)

  • Use .trim() instead of bare falsy check for issue body
  • Non-empty body: <details> collapsible with issue title as summary
  • Empty body: blockquote with issue title fallback
  • Both formats retain Fixes #N reference and iloom footer

src/lib/LoomManager.test.ts

Changes: Added and enhanced draft PR body tests

  • Added: "should handle empty issue body gracefully in draft-pr mode"
  • Added: "should include issue body in collapsible details in draft-pr mode"
  • Enhanced: Linear provider draft-pr test with body/title assertions
  • Enhanced: Child loom draft-pr test with body/title assertions

Replace generic draft PR body with structured format that includes
issue title as heading and issue body in collapsible details section.
Handle empty issue bodies gracefully with title-only fallback.
@acreeger acreeger force-pushed the feat/issue-953__pr-body-issue-context branch from a0e3407 to b735966 Compare March 21, 2026 04:31
@acreeger acreeger marked this pull request as ready for review March 21, 2026 04:54
@acreeger acreeger merged commit bf7fedc into main Mar 21, 2026
4 checks passed
@acreeger
Copy link
Collaborator Author

iloom Session Summary

Key Themes:

  • The empty PR body was not a regression — it was correct behavior masking a real design gap in the template.
  • Issue body content is best shown with the title visible and the full body in a collapsible <details> block, balancing context vs. clutter.
  • AI summarization of issue bodies at PR creation time was explicitly ruled out as over-engineering.

Session Details (click to expand)

Key Insights

  • fetchIssueData() works correctly end-to-end across all three providers (GitHub, Linear, Jira). The empty PR body seen in Recap entries added by the Swarm code reviewer in wave verficaiton tasks don't seem to be getting added to the recap file #948 was because that specific issue had an empty body (""), which is falsy in JavaScript, causing the ternary to fall through to the empty-body path. No data fetching bug exists.
  • The original code at LoomManager.ts:292-294 used issueData?.body ? ... : '' — a bare falsy check that also silently fails for whitespace-only bodies. Using .trim() is the correct guard.
  • The original design dumped raw issue text inline, which is cluttered for long issue bodies. The real fix is structural, not data-fetching.

Decisions Made

  • PR body format: Issue title as a visible ## heading + full issue body in a collapsible <details> block. This keeps the PR summary readable at a glance while preserving the full context one click away. Raw inline dumping (original) was rejected as cluttered; title-only was rejected as losing too much context.
  • No AI summarization: Generating a meaningful prose summary of the issue at il start time was considered but rejected — it adds latency, complexity, and a potential failure point for a marginal UX gain when the Fixes #N link already gives full access to the issue.
  • Non-null assertion removed: issueData!.title was replaced with issueData?.title ?? \Issue ${input.identifier}`even thoughissueDatais always populated at this code path (insidetype === 'issue' || type === 'epic'`), for defensive correctness.

Challenges Resolved

  • The <details> block initially wrapped everything including the title, making the PR body completely opaque until expanded. The title belongs outside as a heading so the PR communicates its purpose without any interaction required.

Lessons Learned

  • When issue body is the same in empty ("") and missing (null/undefined) cases, both must be handled via .trim() — bare falsy checks treat whitespace-only bodies the same as empty strings, which is actually desirable here.
  • The draft PR body code path is narrow — LoomManager.ts:292-294 — but sits inside a complex createIloom method that covers both issue and epic types. Existing tests only asserted expect.stringContaining('Fixes 123') and never verified body inclusion, leaving the regression undetected.

Generated with 🤖❤️ by iloom.ai

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

Labels

None yet

Projects

Status: Done

1 participant