From 129cdb09b5b8ea72d9494c9895baae75aa832bbb Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sun, 25 Oct 2026 16:59:38 -0700 Subject: [PATCH] feat(server): link_pull_request asks a subagent to end with its PR report Co-Authored-By: Claude Opus 5.5 --- .../toolkits/pullRequests/handlers.test.ts | 21 ++++++++++++++++++- .../src/mcp/toolkits/pullRequests/handlers.ts | 9 ++++++-- .../src/mcp/toolkits/pullRequests/tools.ts | 5 +++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts b/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts index 507381531ac4..88691423c0ef 100644 --- a/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts +++ b/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts @@ -32,7 +32,7 @@ import * as McpToolAccess from "../../McpToolAccess.ts"; import * as McpToolAccessTestkit from "../../McpToolAccess.testkit.ts"; import { listThreadPullRequests } from "./handlers.ts"; import * as PullRequestsHandlers from "./handlers.ts"; -import { PullRequestsToolkit } from "./tools.ts"; +import { CHILD_PULL_REQUEST_REPORT_NEXT, PullRequestsToolkit } from "./tools.ts"; const PROJECT_ID = ProjectId.make("project-1"); const THREAD_ID = ThreadId.make("thread-1"); @@ -353,6 +353,25 @@ describe("pull request toolkit handlers", () => { }), ); + it.effect("asks a subagent thread to end with its PR report after it links", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ + thread: { + ...makeThread([]), + lineage: { + rootThreadId: ThreadId.make("parent"), + parentThreadId: ThreadId.make("parent"), + relationshipToParent: "subagent", + }, + }, + }); + const result = yield* harness.call("link_pull_request", { + url: "https://github.com/t3tools/t3code/pull/124", + }); + expect(result).toMatchObject({ number: 124, next: CHILD_PULL_REQUEST_REPORT_NEXT }); + }), + ); + it.effect("canonicalizes GitCafe links independently of the thread's project host", () => Effect.gen(function* () { for (const host of ["git.cafe", "staging.git.cafe"]) { diff --git a/apps/server/src/mcp/toolkits/pullRequests/handlers.ts b/apps/server/src/mcp/toolkits/pullRequests/handlers.ts index ca37361d244c..a4cd07b89fb4 100644 --- a/apps/server/src/mcp/toolkits/pullRequests/handlers.ts +++ b/apps/server/src/mcp/toolkits/pullRequests/handlers.ts @@ -46,6 +46,7 @@ import { PullRequestProjectNotFoundError, PullRequestsToolkit, type ThreadPullRequestEntry, + CHILD_PULL_REQUEST_REPORT_NEXT, } from "./tools.ts"; interface ResolvedTarget { @@ -361,13 +362,17 @@ const make = Effect.gen(function* () { link_pull_request: writesThread((input) => Effect.gen(function* () { const thread = yield* requireThread(PullRequestLinkFailedError, input.threadId); + const next = + thread.lineage.relationshipToParent === "subagent" + ? { next: CHILD_PULL_REQUEST_REPORT_NEXT } + : {}; const project = yield* projectOf(thread, PullRequestLinkFailedError); const target = yield* resolveTarget(input, project); const existing = threadPullRequestsOf(thread).find((link) => threadPullRequestKeysEqual(link, target), ); if (existing && existing.source !== "stack-dismissed") - return { ...target, alreadyLinked: true }; + return { ...target, alreadyLinked: true, ...next }; const alreadyLinked = yield* engine .dispatch({ type: "thread.pull-request.link", @@ -384,7 +389,7 @@ const make = Effect.gen(function* () { Effect.catchCause(dispatchFailure(PullRequestLinkFailedError)), ); - return { ...target, alreadyLinked }; + return { ...target, alreadyLinked, ...next }; }), ), unlink_pull_request: writesThread((input) => diff --git a/apps/server/src/mcp/toolkits/pullRequests/tools.ts b/apps/server/src/mcp/toolkits/pullRequests/tools.ts index 878fd6c3702c..ef314b863ede 100644 --- a/apps/server/src/mcp/toolkits/pullRequests/tools.ts +++ b/apps/server/src/mcp/toolkits/pullRequests/tools.ts @@ -210,9 +210,14 @@ const PullRequestIdentity = { url: Schema.String, }; +/** A delegated child's report is how its parent learns the PR, so the link result asks for it. */ +export const CHILD_PULL_REQUEST_REPORT_NEXT = + "End your reply with: PR , head , checks and reviews ."; + export const LinkPullRequestResult = Schema.Struct({ ...PullRequestIdentity, alreadyLinked: Schema.Boolean, + next: Schema.optional(Schema.String), }); export type LinkPullRequestResult = typeof LinkPullRequestResult.Type;