Skip to content

fix(api,mcp): route programmatic and MCP PR creation through content resolution - #28

Open
chrissena wants to merge 11 commits into
mainfrom
feat/route-api-mcp-through-pr-content
Open

chrissena wants to merge 11 commits into
mainfrom
feat/route-api-mcp-through-pr-content

Conversation

@chrissena

Copy link
Copy Markdown
Member

Summary

Part 1 taught wt new to accept caller-supplied PR content, but it fixed only
one of three entry points. The programmatic API (src/api/create.ts) and the
MCP worktree_create_pr tool still hardcode a stub body:

## Summary\n\n...\n\n## Changes\n\n-\n\n## Test Plan\n\n- [ ]

So an agent driving the library or the MCP server — rather than the CLI —
still produces exactly the stub PRs this work set out to eliminate.

Changes

  • Route both createPr call sites in src/api/create.ts through the same
    content resolution the CLI uses, instead of embedding a literal template.
  • Accept caller-supplied title/body on the API and on the MCP
    worktree_create_pr tool, with the same per-field precedence.
  • Tests covering supplied / generated / template for each entry point.

Test Plan

  • Unit tests for both api/create.ts sites
  • MCP tool passes content through
  • Full suite green, tsc + prettier clean

🤖 Opened with the --title/--body-file flags this PR extends.

Branch created for: route api and mcp entry points through PR content resolution

🤖 Created with newpr
@claude

claude Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Claude finished @chrissena's task in 2m 19s —— View job


PR Review — fix(api,mcp): route programmatic and MCP PR creation through content resolution

  • Gather context
  • Inspect git diff and existing code
  • Post review findings

Verdict: request changes

The PR branch contains only the initialization commit (chore: initialize feat/route-api-mcp-through-pr-content) — no files were changed. The review below describes the full implementation needed to match the PR description, with precise locations so the work can land in a single follow-up push.


What needs to change

1. CreatePrOptions — missing content fields (src/api/create.ts:35–48)

The interface exposes description but none of the per-field overrides that resolvePRContent expects. Add them so callers can supply content with the same per-field precedence as the CLI:

export interface CreatePrOptions {
  description: string;
  action?: StateActionKey;
  draft?: boolean;
  baseBranch?: string;
  branchName?: string;
  cwd?: string;
  // --- add these ---
  /** Exact PR title (overrides AI and template) */
  title?: string;
  /** Exact PR body */
  body?: string;
  /** Path to a file holding the PR body */
  bodyFile?: string;
  /** Force AI generation even when flags supply content */
  forceAi?: boolean;
  /** Skip AI generation entirely */
  skipAi?: boolean;
}

2. Hardcoded stub — existing-branch path (src/api/create.ts:419–430)

const
  .replace(...)
  ...

const pr = github.createPr({
  title,
  body: `## Summary\n\nPR created from existing branch: \`${currentBranch}\`\n\n## Changes\n\n-\n\n## Test Plan\n\n- [ ]\n\n---\n🤖 PR created with \`newpr\``,
  base: baseBranch,
  head: currentBranch,
  draft,
});

Replace with resolvePRContent. The CLI's modeExistingBranch (in src/cli/newpr.ts:829–854) is the exact pattern to follow — copy its defaultBody, context, and overrides shape:

import {
  resolvePRContent,
  PRContentError,
} from '../lib/newpr/pr-content.js';

const descriptionFromBranch = currentBranch
  .replace(/^(feat|fix|chore)\//, '')
  .replace(/-/g, ' ')
  .replace(/\b\w/g, (c) => c.toUpperCase());

const defaultBody = `## Summary\n\nPR created from existing branch: \`${currentBranch}\`\n\n## Changes\n\n-\n\n## Test Plan\n\n- [ ]\n\n---\n🤖 PR created with \`newpr --branch\``;

let prContent;
try {
  prContent = await resolvePRContent({
    config,
    context: {
      description: descriptionFromBranch,
      branchName: currentBranch,
      baseBranch,
      changedFiles: git.getChangedFiles(`origin/${baseBranch}`, currentBranch, repoRoot),
      commitMessages: git.getCommitMessages(`origin/${baseBranch}`, currentBranch, repoRoot),
    },
    overrides: {
      title: options.title,
      body: options.body,
      bodyFile: options.bodyFile,
      forceAi: options.forceAi,
      skipAi: options.skipAi,
    },
    defaultBody,
  });
} catch (error) {
  if (error instanceof PRContentError) {
    return createErrorResult('newpr', ErrorCode.INVALID_ARGUMENT, error.message);
  }
  throw error;
}

const pr = github.createPr({
  title: prContent.title,
  body: prContent.body,
  base: baseBranch,
  head: currentBranch,
  draft,
});

Note: CreatePrOptions fields (title, body, etc.) need to be destructured at the top of createPr() and passed as options here.

3. Hardcoded stub — new-branch path (src/api/create.ts:550–556)

Same pattern. The CLI's modeNewFeature (in src/cli/newpr.ts:1220–1261) is the reference. The description variable is already available in this scope, and config is already loaded. Wire up resolvePRContent identically to point 2, using description (not descriptionFromBranch) as the context.description and the description-seeded defaultBody.

resolvePRContent is async; the outer createPr function is already async, so a simple await is enough.

4. MCP tool schema — title/body not exposed (src/mcp/server.ts:143–169)

The worktree_create_pr tool's inputSchema only has description, action, draft, baseBranch, branchName. Add the new fields so MCP callers can supply content:

title: {
  type: 'string',
  description: 'Exact PR title (overrides AI and template)',
},
body: {
  type: 'string',
  description: 'Exact PR body (mutually exclusive with bodyFile)',
},
bodyFile: {
  type: 'string',
  description: 'Path to a file containing the PR body (mutually exclusive with body)',
},
forceAi: {
  type: 'boolean',
  description: 'Force AI generation even when title/body are supplied',
},
skipAi: {
  type: 'boolean',
  description: 'Skip AI generation entirely',
},

Then thread them through to the createPr call in the worktree_create_pr handler (server.ts:478–484).

5. Tests (src/api/create.test.ts)

The existing tests don't verify what is passed to github.createPr. Three cases need coverage per call site (×2 = 6 tests minimum):

  • supplied: title/body in CreatePrOptions → createPr called with those exact strings
  • template: no flags, no AI (mock resolvePRContent to return template source) → createPr called with the template body
  • ai: mock resolvePRContent to return titleSource: 'ai' → createPr called with the generated strings

Follow the existing mock pattern in create.test.ts: mock resolvePRContent from '../lib/newpr/pr-content.js' at the top of the file, and assert on vi.mocked(github.createPr).mock.calls[0][0].

The MCP server tests (src/mcp/server.test.ts) should similarly verify that title/body fields are forwarded to createPr.


Release note

The commit message (fix(api,mcp):) is correct for semantic-release — no BREAKING CHANGE needed because CreatePrOptions is additive (new optional fields). The MCP schema change is backward-compatible. No README update is required unless the public API docs explicitly document CreatePrOptions fields.


| Branch

@codecov

codecov Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.76%. Comparing base (4087f41) to head (22f5269).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main      #28   +/-   ##
=======================================
  Coverage   81.76%   81.76%           
=======================================
  Files          97       97           
  Lines       20839    20839           
  Branches     4546     4546           
=======================================
  Hits        17040    17040           
  Misses       3799     3799           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Both createPr() call sites in src/api/create.ts embedded a hardcoded
stub PR body and never accepted caller-supplied content, so the
programmatic API and the MCP worktree_create_pr tool (which wraps it)
still produced useless template PRs even after `wt new` gained
--title/--body/--body-file support.

- CreatePrOptions now accepts title/body/bodyFile; both createPr()
  branches call the existing resolvePRContent() resolver instead of
  building a literal, matching the CLI's flag -> AI -> template
  precedence and reporting the same titleSource/bodySource/aiProvider/
  aiError provenance. Template text is byte-identical to before.
- worktree_create_pr's input schema gains title/body/bodyFile, its
  description documents them, and its output schema documents the
  provenance fields; the handler threads the new args through.

Claude-Session: https://claude.ai/code/session_01RRi2HJGVDvyPx4Bm7CvqPk
The worktree_create_pr tests called the mocked createPr() directly with
hand-written args, so they only proved the test file could call a mock —
deleting title/body/bodyFile from the real createPr({...}) call inside
server.ts's request handler left all tests green. Since this PR exists
because one of three PR-creation entry points was fixed while two were
missed, tests that can't detect this class of regression recreate the
same failure mode one layer down.

- Extract the CallToolRequestSchema switch body into an exported
  handleToolCall(name, args) function; the setRequestHandler callback
  now just delegates to it. Purely mechanical — same body, same
  behaviour, same response shape.
- Rewrite the worktree_create_pr handler tests to call handleToolCall(...)
  directly (mocking only createPr), so they exercise the real dispatch
  and arg-forwarding logic instead of restating it.

Verified: deleting title/body/bodyFile from the createPr({...}) call in
server.ts makes 2 of the new tests fail red; restoring it turns them
green again (both checked manually before this commit).

Claude-Session: https://claude.ai/code/session_01RRi2HJGVDvyPx4Bm7CvqPk
@claude

claude Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Claude finished @chrissena's task in 5m 25s —— View job


PR Review — fix(api,mcp): route programmatic and MCP PR creation through content resolution

  • Gather context
  • Read changed files
  • Read pr-content.ts for precedence logic
  • Post inline findings
  • Post overall summary

Verdict: request changes

The core routing is correct — both createPr call sites now go through resolvePRContent instead of embedding a literal stub, and the MCP handler correctly forwards title/body/bodyFile. The MCP tests drive handleToolCall directly (not the mock), which is the right mutation-detection shape.

Three things to address:

1. forceAi/skipAi missing throughout (correctness gap vs. stated scope)

The PR description says "with the same per-field precedence." ContentOverrides has five fields: title, body, bodyFile, forceAi, skipAi. The PR adds three of them. The missing two matter: without skipAi, an API caller supplying only title (not body) cannot suppress AI from filling the body field. Without forceAi, callers can't run AI even when partial overrides are present.

Affected locations — all have inline suggestions:

  • CreatePrOptions interface (src/api/create.ts:57–59)
  • Existing-branch overrides object (src/api/create.ts:451–455)
  • New-branch overrides object (src/api/create.ts:608–612)
  • MCP inputSchema (src/mcp/server.ts:180–185) + handler extraction at line 480

2. fs mock missing readFileSync (latent footgun)

pr-content.ts is not mocked — the real readBodyOverride runs in the new content-resolution suites. readBodyOverride calls fs.readFileSync when bodyFile is supplied. The fs mock at the top of create.test.ts only mocks existsSync. Any bodyFile test added to those suites will silently hit the real filesystem and fail confusingly. Add readFileSync: vi.fn() to the mock now.

(Note: the bodyFile path through resolvePRContent is also untested in create.test.ts — only the MCP passthrough test covers it, and that one mocks createPr itself so readBodyOverride is never exercised.)

3. Missing symmetric test in new-branch suite (minor)

The existing-branch suite has four cases; the new-branch suite has three — "title-flag + AI-body" is absent. Per CLAUDE.md's testing policy this is the suite's weakest point: a regression that accidentally swaps which override field goes where in the new-branch resolvePRContent call would not be caught.


| Branch

Comment thread src/api/create.ts
Comment thread src/api/create.ts Outdated
Comment thread src/api/create.ts Outdated
Comment thread src/mcp/server.ts
Comment thread src/api/create.test.ts
Comment thread src/api/create.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f215796c1a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/api/create.ts Outdated
Comment thread src/mcp/server.ts Outdated
ContentOverrides has five fields (title, body, bodyFile, forceAi,
skipAi) and the CLI forwards all five, but the API's two createPr()
overrides objects and the MCP worktree_create_pr tool forwarded only
three — forceAi/skipAi were silently dropped, breaking the PR
description's claim of "the same per-field precedence" as the CLI.

- CreatePrOptions gains forceAi?/skipAi?, forwarded in both
  resolvePRContent() call sites in src/api/create.ts.
- worktree_create_pr's input schema gains forceAi/skipAi, documented
  in the tool description, and threaded through the handler alongside
  title/body/bodyFile.
- Tests prove the wiring: forceAi makes AI win over a supplied title,
  skipAi suppresses generatePRContentAsync entirely even when content
  is missing (both API-level and via the MCP handleToolCall path).

Addresses reviewer thread A (4 threads) on PR #28.

Claude-Session: https://claude.ai/code/session_01RRi2HJGVDvyPx4Bm7CvqPk
createPr() pushed the new/current branch to origin, then resolved
caller-supplied --body/--body-file only afterwards. A bad combination
(both body and bodyFile) or an unreadable bodyFile surfaced as
INVALID_ARGUMENT only after the branch was already committed and
pushed — leaving an orphaned remote branch with no PR, and a retry
that then fails with BRANCH_EXISTS.

Hoist the validation (reusing readBodyOverride from pr-content.ts,
not a second reader) to the top of createPr(), before any git
mutation, and pass the already-resolved body string down to
resolvePRContent() so it is never re-read from disk.

Also fixes the `fs` mock in create.test.ts: pr-content.ts isn't
mocked in this file, so the real readBodyOverride() runs and calls
fs.readFileSync — missing from the vi.mock('fs') factory until now,
which the new test below depends on to simulate an unreadable file.

New test proves the fix: an unreadable bodyFile on the new-branch
path returns INVALID_ARGUMENT and git.push is never called.

Addresses reviewer thread B on PR #28 (codex P2). Also partially
addresses thread D (the fs.readFileSync mock half).

Claude-Session: https://claude.ai/code/session_01RRi2HJGVDvyPx4Bm7CvqPk
The worktree_create_pr tool description claimed aiError is null
"if not attempted or it succeeded", and the example response showed
a template-sourced body with aiError: null. But resolvePRContent
deliberately sets aiError to a descriptive string (e.g. "AI disabled
(ai.provider = 'none')") whenever a field falls back to the template
with the default, unconfigured AI provider — which is the common
case for a caller who hasn't set up AI generation. An agent following
the documented contract would misread that perfectly normal response
as a failure.

Rewrite the description bullet and the example to match pr-content.ts's
actual behaviour: aiError is null only when AI wasn't needed at all or
it succeeded, not just because the overall response looks normal.

Addresses reviewer thread C on PR #28 (codex P2).

Claude-Session: https://claude.ai/code/session_01RRi2HJGVDvyPx4Bm7CvqPk
The existing-branch PR content resolution suite covers all four
combinations of per-field precedence (nothing supplied, both
supplied, title-only, body-only), but the new-branch suite was
missing the "title from flag, body from AI" case — the mirror of
the "body from flag, title from AI" test already there.

Addresses the remaining half of reviewer thread D on PR #28 (the
fs.readFileSync mock half shipped with the earlier validation-order
fix, since that test depended on it).

Claude-Session: https://claude.ai/code/session_01RRi2HJGVDvyPx4Bm7CvqPk
@claude

claude Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Claude finished @chrissena's task in 6m 32s —— View job


PR Review — fix(api,mcp): route programmatic and MCP PR creation through content resolution

  • Gather context
  • Read changed files
  • Review findings from previous round
  • Post inline findings and summary

Verdict: request changes

All three findings from the previous round are resolved:

  • forceAi/skipAi threaded through CreatePrOptions, both resolvePRContent call sites, and the MCP schema ✓
  • fs mock extended with readFileSync ✓
  • Symmetric title-flag+AI-body test added to the new-branch suite ✓
  • Codex's early-validation finding addressed in d017cff ✓
  • aiError documentation corrected in df66142 ✓

Two issues remain:

1. Dead PRContentError catches created dead by d017cff (correctness gap)

d017cff correctly moved readBodyOverride before any git mutations, converting any bad bodyFile to an early return. But it didn't clean up the now-unreachable PRContentError catches inside both resolvePRContent blocks. resolvePRContent can never throw PRContentError when called with body: bodyOverride (not bodyFile), so both catches are dead code.

The new-branch one at line 654 is the more dangerous: if it were ever live (someone later passed bodyFile in the overrides), it restores actionResult.stashRef but not unstagedStashRef, stranding the user's unstaged changes. The existing-branch one at line 489 is simpler dead code.

Suggestion at each: remove the inner catch and let the outer catch handle any unexpected errors.

2. Three MCP passthrough tests missing forceAi/skipAi in assertion shape (minor)

The forceAi: true, skipAi: false test at line 561 pins the new fields correctly. The three earlier passthrough tests (basic call, action+draft, custom branch) don't include forceAi: undefined, skipAi: undefined. Since vitest treats {..., forceAi: undefined} as equal to {...}, a regression that drops those fields from the handler call would go undetected by those three tests. Inline suggestions add the missing fields to each.


| Branch

Comment thread src/api/create.ts Outdated
Comment thread src/api/create.ts Outdated
Comment thread src/mcp/server.test.ts
Comment thread src/mcp/server.test.ts
Comment thread src/mcp/server.test.ts
…ests

Addresses PR review comments by claude.

Both PRContentError throw sites live in readBodyOverride and both
require `bodyFile`. Since d017cff hoisted that read to the top of
createPr() and passes the contents as `body`, resolvePRContent can no
longer raise it — so the two inner catches were unreachable. Removing
them also removes a latent hazard the reviewer spotted: the new-branch
handler restored actionResult.stashRef but silently abandoned
unstagedStashRef, so had it ever become reachable it would have
stranded the user's unstaged changes. The outer catch handles both.

The three older MCP passthrough assertions omitted forceAi/skipAi.
Vitest treats a missing key and an explicit `undefined` as equal, so
dropping those fields from the handler would not have failed them —
the same mutation-blindness this PR already fixed once. Now pinned.

Claude-Session: https://claude.ai/code/session_01RRi2HJGVDvyPx4Bm7CvqPk
@claude

claude Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Claude finished @chrissena's task in 3m 39s —— View job


PR Review — fix(api,mcp): route programmatic and MCP PR creation through content resolution

  • Gather context
  • Read changed files
  • Verify previous findings are resolved
  • Post inline findings and summary

Verdict: request changes (one remaining gap)

All findings from the previous round are resolved:

  • Dead PRContentError catches removed and annotated with clear explanatory comments explaining why they can't fire ✓
  • Three older MCP passthrough tests now pin forceAi: undefined, skipAi: undefined ✓

One new gap introduced in this batch:

Two new MCP passthrough tests missing forceAi/skipAi pins

The "title/body supplied" and "bodyFile" tests added in this batch have the same assertion-shape gap that was just fixed in the three older passthrough tests. Vitest's toHaveBeenCalledWith treats {..., forceAi: undefined} equal to {...}, so a handler regression that drops those two fields from the createPr call would not be caught by either test. The forceAi: true, skipAi: false test at line 567 correctly pins non-undefined values but only covers that code path. Inline suggestions on both assertions.


| Branch

Comment thread src/mcp/server.test.ts
Comment thread src/mcp/server.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfdccc479e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/api/create.ts
Comment thread src/api/create.ts
…tion

Addresses PR review comments by chatgpt-codex-connector and claude.

P1 — MCP stdout corruption. stdout on the MCP server process IS the
JSON-RPC channel, and print()/printStatus() fall back to console.log
unless JSON mode is on. Before this PR the MCP path never invoked AI,
so printStatus was unreachable; routing PR creation through
resolvePRContent makes it reachable — an AI-backed generation emits a
status line on both success and failure, which would corrupt the
protocol stream. main() now enables JSON mode before connecting.

P2 — blank title. A whitespace-only title is a *defined* value, so it
suppresses generation and reaches `gh pr create`, which rejects it —
on the new-branch path only after the push, orphaning the branch just
as an unreadable bodyFile did before d017cff hoisted that check. Now
validated alongside the other overrides, before any git mutation.

Also pins forceAi/skipAi in the last two MCP assertions; vitest treats
a missing key and an explicit undefined as equal, so they could not
have caught the handler dropping those fields.

All four new tests mutation-verified: disabling either fix turns them
red (2 each), restoring turns them green.

Claude-Session: https://claude.ai/code/session_01RRi2HJGVDvyPx4Bm7CvqPk
@claude

claude Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Claude finished @chrissena's task in 4m 16s —— View job


PR Review — fix(api,mcp): route programmatic and MCP PR creation through content resolution

  • Gather context
  • Read changed files
  • Verify Codex P1/P2 findings and prior round
  • Post inline findings and summary

Verdict: request changes (one small testing gap)

All findings from the previous round are resolved:

  • Two new MCP passthrough tests ("title/body supplied", "bodyFile") now pin forceAi: undefined, skipAi: undefined ✓
  • Dead PRContentError catches removed ✓

The Codex P1 (AI status output corrupting the JSON-RPC stream) is addressed: setJsonMode(true) is called in main() before server.connect(transport), so printStatus is suppressed for the lifetime of the server process. The Codex P2 (blank title before git mutation) is addressed by the early validation block at create.ts:292–306.

One remaining gap — skipAi/forceAi not cross-covered between call sites

The existing-branch suite tests forceAi (line 346) but not skipAi. The new-branch suite tests skipAi (line 508) but not forceAi. The overrides objects at create.ts:500–506 (existing-branch) and create.ts:655–660 (new-branch) each forward both flags, but only one flag per site is mechanically verified. A regression dropping the other from either overrides object would be invisible to the current test suite. Inline suggestion at create.test.ts:373 shows the missing existing-branch case; the new-branch forceAi gap mirrors it.


| Branch

Comment thread src/api/create.test.ts
Addresses PR review comment by claude.

The existing-branch suite covered forceAi but not skipAi; the
new-branch suite covered skipAi but not forceAi. Dropping the
unmirrored key from either overrides object would have passed every
test. Both suites now cover both flags.

Claude-Session: https://claude.ai/code/session_01RRi2HJGVDvyPx4Bm7CvqPk
@claude

claude Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Claude finished @chrissena's task in 4m 13s —— View job


PR Review — fix(api,mcp): route programmatic and MCP PR creation through content resolution

  • Gather context
  • Read changed files (src/api/create.ts, src/api/create.test.ts, src/mcp/server.ts, src/mcp/server.test.ts)
  • Check git diff vs origin/main
  • Post inline findings
  • Post overall summary

Verdict: request changes (two gaps)

All findings from the previous round are resolved: forceAi/skipAi cross-coverage added to both suites, the dead PRContentError catches removed, the three older and two newer MCP passthrough assertions pinned, setJsonMode(true) called before server.connect(), and blank title/body rejection hoisted before any git mutation.

Two remaining issues:

1. Misleading safety comment (src/api/create.ts:645) — correctness

The comment says "The outer catch handles both" (implying it restores both actionResult.stashRef and unstagedStashRef). The outer catch at lines 719–726 only pops actionResult.stashRef. Any failure between git.stash() (line 591) and git.stashApply() (line 694) — including the newly-added async resolvePRContent call — strands the user's unstaged changes. The pre-existing stash-leak isn't fixed by this PR; the comment creates a false sense that it is. Inline suggestion corrects the comment.

2. Two tests missing github.createPr assertion — minor

skipAi in the existing-branch suite (create.test.ts:377) and forceAi in the new-branch suite (create.test.ts:539) verify source metadata but don't pin the arguments to github.createPr, unlike every other test in those suites. A regression swapping title/body in either override block would pass both tests. Inline suggestions add the missing assertions.


| Branch

Comment thread src/api/create.ts Outdated
Comment thread src/api/create.test.ts
Comment thread src/api/create.test.ts
Addresses PR review comments by claude.

The comment added when the dead PRContentError handler was removed
claimed "the outer catch handles both" stash refs. It does not: the
outer catch pops actionResult.stashRef only, so unstagedStashRef
(stashed at 591, applied at 694) is never restored on an error path
and any throw between those points strands the user's unstaged
changes. The comment now names that gap as pre-existing, unfixed, and
out of scope rather than implying it is handled — a comment asserting
a safety property that does not hold is worse than no comment.

The two mirror tests added last round asserted only source metadata,
unlike every sibling in their suites, so a regression swapping
title/body on those paths would have gone undetected. Both now pin the
full github.createPr call shape.

Claude-Session: https://claude.ai/code/session_01RRi2HJGVDvyPx4Bm7CvqPk
@claude

claude Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Claude finished @chrissena's task in 2m 42s —— View job


PR Review — fix(api,mcp): route programmatic and MCP PR creation through content resolution

  • Gather context
  • Read changed files
  • Verify all previous findings resolved
  • Post overall summary

Verdict: approve

All findings from the six prior rounds are resolved. Full read of all four changed files in their current state — no new issues found.

Resolved in this batch:

  • Stash-gap comment (create.ts:641–652) corrected — no longer claims "the outer catch handles both." It now explicitly names unstagedStashRef as the pre-existing gap and explains why removing the dead PRContentError catch neither caused nor worsened it. ✓
  • github.createPr assertion added to the skipAi test in the existing-branch suite (create.test.ts:383–389) — full call shape now pinned, matching every other test in that suite. ✓
  • github.createPr assertion added to the forceAi test in the new-branch suite (create.test.ts:567–573) — mirrors the existing-branch suite. ✓

All prior round findings confirmed resolved:

  • forceAi/skipAi wired through CreatePrOptions, both resolvePRContent sites, and the MCP schema ✓
  • fs.readFileSync in the vi.mock('fs', …) factory ✓
  • Dead PRContentError catches removed at both call sites with accurate explanatory comments ✓
  • All six MCP passthrough assertions pin forceAi/skipAi (three older tests fixed in dfdccc4; two newer tests pinned in 819125b) ✓
  • setJsonMode(true) called before server.connect() to prevent AI status output corrupting the JSON-RPC stream ✓
  • readBodyOverride and blank title/body checks hoisted before any git mutation ✓
  • Symmetric forceAi/skipAi test coverage across both suites (each suite now has one test per flag) ✓

| Branch

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22f52695dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/api/create.ts
// resolvePRContent() never re-reads the file.
let bodyOverride: string | undefined;
try {
bodyOverride = readBodyOverride({ body: bodyOverrideRaw, bodyFile: bodyFileOverride });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve body files against the requested working directory

When a programmatic caller sets cwd to a repository other than process.cwd() and supplies a relative bodyFile, this preflight reads the path relative to the process directory rather than the requested working directory. The call therefore returns INVALID_ARGUMENT (or reads an unrelated same-named file) even though the body file exists relative to cwd; resolve the file path against cwd before passing it to readBodyOverride.

Useful? React with 👍 / 👎.

Comment thread src/api/create.ts
// top of this function and pass the contents as `body`, never `bodyFile`,
// so resolvePRContent cannot raise it. A catch that can never fire is
// dead code that only makes a future real error look handled.
const prContent: ResolvedPRContent = await resolvePRContent({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Suppress AI status output for direct API callers

When a configured AI provider succeeds or fails, this resolver reaches generatePRContentAsync, which calls printStatus and therefore writes to stdout while JSON mode is false. Direct consumers of the exported createPr API do not run the MCP server's setJsonMode(true), so both creation paths can unexpectedly pollute a host application's structured stdout. Fresh evidence beyond the earlier MCP issue is that the suppression fix exists only in src/mcp/server.ts startup; make content generation quiet for programmatic calls rather than relying on that server-global setting.

Useful? React with 👍 / 👎.

Comment thread src/mcp/server.ts
Comment on lines +491 to +495
const title = args?.title as string | undefined;
const body = args?.body as string | undefined;
const bodyFile = args?.bodyFile as string | undefined;
const forceAi = args?.forceAi as boolean | undefined;
const skipAi = args?.skipAi as boolean | undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate MCP content argument types before forwarding

When a raw or buggy MCP client sends a non-string bodyFile, this assertion performs no runtime validation before the value reaches fs.readFileSync. In particular, JSON number 0 is treated by Node as the stdin file descriptor, so the server can block on or consume its own JSON-RPC input stream; non-string title or body values likewise produce UNKNOWN_ERROR failures at .trim(). Reject values whose runtime types do not match the advertised schema with INVALID_ARGUMENT before calling createPr.

Useful? React with 👍 / 👎.

Comment thread src/api/create.ts
Comment on lines +493 to +496
context: {
description: title,
branchName: currentBranch,
baseBranch,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include the repository root in AI generation context

When API or MCP PR creation uses a configured AI provider, this context omits the already-resolved repoRoot. generatePRContentAsync only gathers README and package metadata when context.repoRoot is present, so these newly enabled entry points always generate without the repository documentation that supplies project-specific terminology and conventions. Pass repoRoot at both resolver call sites so programmatic generation receives the same repository context the generation API supports.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant