Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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"]) {
Expand Down
9 changes: 7 additions & 2 deletions apps/server/src/mcp/toolkits/pullRequests/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
PullRequestProjectNotFoundError,
PullRequestsToolkit,
type ThreadPullRequestEntry,
CHILD_PULL_REQUEST_REPORT_NEXT,
} from "./tools.ts";

interface ResolvedTarget {
Expand Down Expand Up @@ -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",
Expand All @@ -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) =>
Expand Down
5 changes: 5 additions & 0 deletions apps/server/src/mcp/toolkits/pullRequests/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <full URL>, head <SHA>, checks and reviews <state>.";

export const LinkPullRequestResult = Schema.Struct({
...PullRequestIdentity,
alreadyLinked: Schema.Boolean,
next: Schema.optional(Schema.String),
});
export type LinkPullRequestResult = typeof LinkPullRequestResult.Type;

Expand Down
Loading