@geminixiang/mikan 1.0.0-beta.13 → 1.0.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +10 -10
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +26 -67
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +17 -43
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/github/bot.d.ts +69 -1
- package/dist/adapters/github/bot.d.ts.map +1 -1
- package/dist/adapters/github/bot.js +428 -57
- package/dist/adapters/github/bot.js.map +1 -1
- package/dist/adapters/github/client.d.ts +32 -1
- package/dist/adapters/github/client.d.ts.map +1 -1
- package/dist/adapters/github/client.js +79 -0
- package/dist/adapters/github/client.js.map +1 -1
- package/dist/adapters/github/cloudbuild.d.ts +24 -0
- package/dist/adapters/github/cloudbuild.d.ts.map +1 -0
- package/dist/adapters/github/cloudbuild.js +71 -0
- package/dist/adapters/github/cloudbuild.js.map +1 -0
- package/dist/adapters/github/context.d.ts.map +1 -1
- package/dist/adapters/github/context.js +34 -31
- package/dist/adapters/github/context.js.map +1 -1
- package/dist/adapters/github/gcp-auth.d.ts +31 -0
- package/dist/adapters/github/gcp-auth.d.ts.map +1 -0
- package/dist/adapters/github/gcp-auth.js +196 -0
- package/dist/adapters/github/gcp-auth.js.map +1 -0
- package/dist/adapters/github/ids.d.ts +9 -0
- package/dist/adapters/github/ids.d.ts.map +1 -1
- package/dist/adapters/github/ids.js +14 -0
- package/dist/adapters/github/ids.js.map +1 -1
- package/dist/adapters/github/repo.d.ts +13 -2
- package/dist/adapters/github/repo.d.ts.map +1 -1
- package/dist/adapters/github/repo.js +80 -2
- package/dist/adapters/github/repo.js.map +1 -1
- package/dist/adapters/github/tool-pack.d.ts +3 -1
- package/dist/adapters/github/tool-pack.d.ts.map +1 -1
- package/dist/adapters/github/tool-pack.js +30 -4
- package/dist/adapters/github/tool-pack.js.map +1 -1
- package/dist/{tools/github-checks.d.ts → adapters/github/tools/checks.d.ts} +4 -2
- package/dist/adapters/github/tools/checks.d.ts.map +1 -0
- package/dist/{tools/github-checks.js → adapters/github/tools/checks.js} +28 -11
- package/dist/adapters/github/tools/checks.js.map +1 -0
- package/dist/adapters/github/tools/issue.d.ts +22 -0
- package/dist/adapters/github/tools/issue.d.ts.map +1 -0
- package/dist/adapters/github/tools/issue.js +59 -0
- package/dist/adapters/github/tools/issue.js.map +1 -0
- package/dist/{tools/github-pr.d.ts → adapters/github/tools/pr.d.ts} +2 -2
- package/dist/adapters/github/tools/pr.d.ts.map +1 -0
- package/dist/{tools/github-pr.js → adapters/github/tools/pr.js} +8 -4
- package/dist/adapters/github/tools/pr.js.map +1 -0
- package/dist/adapters/github/tools/read.d.ts +22 -0
- package/dist/adapters/github/tools/read.d.ts.map +1 -0
- package/dist/adapters/github/tools/read.js +152 -0
- package/dist/adapters/github/tools/read.js.map +1 -0
- package/dist/adapters/github/tools/review-reply.d.ts +19 -0
- package/dist/adapters/github/tools/review-reply.d.ts.map +1 -0
- package/dist/adapters/github/tools/review-reply.js +49 -0
- package/dist/adapters/github/tools/review-reply.js.map +1 -0
- package/dist/adapters/github/tools/sync.d.ts +17 -0
- package/dist/adapters/github/tools/sync.d.ts.map +1 -0
- package/dist/adapters/github/tools/sync.js +45 -0
- package/dist/adapters/github/tools/sync.js.map +1 -0
- package/dist/adapters/github/types.d.ts +174 -1
- package/dist/adapters/github/types.d.ts.map +1 -1
- package/dist/adapters/github/types.js.map +1 -1
- package/dist/adapters/intake.d.ts +16 -5
- package/dist/adapters/intake.d.ts.map +1 -1
- package/dist/adapters/intake.js +79 -22
- package/dist/adapters/intake.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +7 -6
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +65 -135
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +3 -15
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/response-lifecycle.d.ts.map +1 -1
- package/dist/adapters/slack/response-lifecycle.js +7 -16
- package/dist/adapters/slack/response-lifecycle.js.map +1 -1
- package/dist/adapters/slack/session.d.ts.map +1 -1
- package/dist/adapters/slack/session.js +5 -8
- package/dist/adapters/slack/session.js.map +1 -1
- package/dist/adapters/streaming.d.ts +4 -0
- package/dist/adapters/streaming.d.ts.map +1 -1
- package/dist/adapters/streaming.js +11 -0
- package/dist/adapters/streaming.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +0 -2
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +16 -62
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +8 -28
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/types.d.ts +28 -2
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/types.js.map +1 -1
- package/dist/agent.d.ts +21 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +22 -9
- package/dist/agent.js.map +1 -1
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +2 -1
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/auto-reply.d.ts.map +1 -1
- package/dist/commands/auto-reply.js +2 -6
- package/dist/commands/auto-reply.js.map +1 -1
- package/dist/commands/extensions.d.ts.map +1 -1
- package/dist/commands/extensions.js +2 -1
- package/dist/commands/extensions.js.map +1 -1
- package/dist/commands/login.d.ts +2 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +30 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/manifest.d.ts +72 -0
- package/dist/commands/manifest.d.ts.map +1 -0
- package/dist/commands/manifest.js +121 -0
- package/dist/commands/manifest.js.map +1 -0
- package/dist/commands/model.d.ts.map +1 -1
- package/dist/commands/model.js +2 -1
- package/dist/commands/model.js.map +1 -1
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +2 -1
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/sandbox.d.ts.map +1 -1
- package/dist/commands/sandbox.js +3 -2
- package/dist/commands/sandbox.js.map +1 -1
- package/dist/commands/text.d.ts +2 -0
- package/dist/commands/text.d.ts.map +1 -0
- package/dist/commands/text.js +16 -0
- package/dist/commands/text.js.map +1 -0
- package/dist/commands/types.d.ts +12 -1
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/types.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +49 -54
- package/dist/config.js.map +1 -1
- package/dist/events.d.ts +10 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +14 -5
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +3 -3
- package/dist/execution-resolver.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.js +25 -5
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +2 -5
- package/dist/provisioner.js.map +1 -1
- package/dist/runtime/conversation-runtime.d.ts +1 -1
- package/dist/runtime/conversation-runtime.d.ts.map +1 -1
- package/dist/runtime/conversation-runtime.js +230 -37
- package/dist/runtime/conversation-runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +19 -4
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js.map +1 -1
- package/dist/sandbox/cloudflare.d.ts +2 -0
- package/dist/sandbox/cloudflare.d.ts.map +1 -1
- package/dist/sandbox/cloudflare.js +7 -0
- package/dist/sandbox/cloudflare.js.map +1 -1
- package/dist/sandbox/container.d.ts +2 -0
- package/dist/sandbox/container.d.ts.map +1 -1
- package/dist/sandbox/container.js +7 -1
- package/dist/sandbox/container.js.map +1 -1
- package/dist/sandbox/firecracker.d.ts +2 -0
- package/dist/sandbox/firecracker.d.ts.map +1 -1
- package/dist/sandbox/firecracker.js +7 -1
- package/dist/sandbox/firecracker.js.map +1 -1
- package/dist/sandbox/host.d.ts +2 -0
- package/dist/sandbox/host.d.ts.map +1 -1
- package/dist/sandbox/host.js +11 -0
- package/dist/sandbox/host.js.map +1 -1
- package/dist/sandbox/identity.d.ts +21 -0
- package/dist/sandbox/identity.d.ts.map +1 -0
- package/dist/sandbox/identity.js +35 -0
- package/dist/sandbox/identity.js.map +1 -0
- package/dist/sandbox/types.d.ts +12 -0
- package/dist/sandbox/types.d.ts.map +1 -1
- package/dist/sandbox/types.js.map +1 -1
- package/dist/sandbox/utils.d.ts +11 -0
- package/dist/sandbox/utils.d.ts.map +1 -1
- package/dist/sandbox/utils.js +35 -0
- package/dist/sandbox/utils.js.map +1 -1
- package/dist/sessions/agent-memory-file-manager.d.ts.map +1 -1
- package/dist/sessions/agent-memory-file-manager.js +16 -19
- package/dist/sessions/agent-memory-file-manager.js.map +1 -1
- package/dist/sessions/history-line.d.ts +20 -0
- package/dist/sessions/history-line.d.ts.map +1 -0
- package/dist/sessions/history-line.js +24 -0
- package/dist/sessions/history-line.js.map +1 -0
- package/dist/sessions/policy.d.ts.map +1 -1
- package/dist/sessions/policy.js +4 -3
- package/dist/sessions/policy.js.map +1 -1
- package/dist/sessions/session-key.d.ts +31 -0
- package/dist/sessions/session-key.d.ts.map +1 -0
- package/dist/sessions/session-key.js +44 -0
- package/dist/sessions/session-key.js.map +1 -0
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +2 -2
- package/dist/sessions/store.js.map +1 -1
- package/dist/tools/bash.d.ts +3 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +24 -17
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +9 -12
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/event.d.ts +1 -1
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js +33 -4
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/types.d.ts +6 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js.map +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +1 -8
- package/dist/tools/write.js.map +1 -1
- package/dist/types.d.ts +13 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vault/disabled.d.ts +9 -0
- package/dist/vault/disabled.d.ts.map +1 -0
- package/dist/vault/disabled.js +23 -0
- package/dist/vault/disabled.js.map +1 -0
- package/dist/vault/index.d.ts.map +1 -1
- package/dist/vault/index.js +2 -7
- package/dist/vault/index.js.map +1 -1
- package/dist/vault/routing.d.ts +1 -0
- package/dist/vault/routing.d.ts.map +1 -1
- package/dist/vault/routing.js +3 -10
- package/dist/vault/routing.js.map +1 -1
- package/dist/web/admin/portal.d.ts +27 -0
- package/dist/web/admin/portal.d.ts.map +1 -1
- package/dist/web/admin/portal.js +87 -126
- package/dist/web/admin/portal.js.map +1 -1
- package/dist/web/admin/types.d.ts +3 -0
- package/dist/web/admin/types.d.ts.map +1 -1
- package/dist/web/admin/types.js.map +1 -1
- package/dist/web/login/oauth.d.ts +2 -3
- package/dist/web/login/oauth.d.ts.map +1 -1
- package/dist/web/login/oauth.js +0 -31
- package/dist/web/login/oauth.js.map +1 -1
- package/dist/web/login/types.d.ts +0 -11
- package/dist/web/login/types.d.ts.map +1 -1
- package/dist/web/login/types.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +7 -0
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-view/command.d.ts.map +1 -1
- package/dist/web/session-view/command.js +4 -1
- package/dist/web/session-view/command.js.map +1 -1
- package/dist/web/session-view/portal.d.ts.map +1 -1
- package/dist/web/session-view/portal.js +3 -2
- package/dist/web/session-view/portal.js.map +1 -1
- package/dist/web/session-view/service.d.ts.map +1 -1
- package/dist/web/session-view/service.js +2 -1
- package/dist/web/session-view/service.js.map +1 -1
- package/dist/web/session-view/types.d.ts +2 -1
- package/dist/web/session-view/types.d.ts.map +1 -1
- package/dist/web/session-view/types.js.map +1 -1
- package/package.json +1 -1
- package/dist/runtime/agent-run-controller.d.ts +0 -31
- package/dist/runtime/agent-run-controller.d.ts.map +0 -1
- package/dist/runtime/agent-run-controller.js +0 -195
- package/dist/runtime/agent-run-controller.js.map +0 -1
- package/dist/tools/github-checks.d.ts.map +0 -1
- package/dist/tools/github-checks.js.map +0 -1
- package/dist/tools/github-pr.d.ts.map +0 -1
- package/dist/tools/github-pr.js.map +0 -1
|
@@ -5,19 +5,22 @@ import * as log from "../../log.js";
|
|
|
5
5
|
import { ensureDirExists, readJsonSchemaFileIfExists } from "../../utils/file-guards.js";
|
|
6
6
|
import { atomicWritePrivateFile } from "../../utils/fs-atomic.js";
|
|
7
7
|
import { resolveChatSessionKey } from "../../sessions/policy.js";
|
|
8
|
-
import {
|
|
9
|
-
import { appendBotResponseLog, appendChannelLog, MessagingEventQueue, resolveStopTarget, splitText, withRetry, } from "../shared.js";
|
|
8
|
+
import { appendBotResponseLog, appendChannelLog, MessagingEventQueue, splitText, withRetry, } from "../shared.js";
|
|
10
9
|
import { processMessageIntake } from "../intake.js";
|
|
11
10
|
import { GithubApiError, GithubClient, githubIsRateLimited } from "./client.js";
|
|
11
|
+
import { CLOUD_BUILD_APP_SLUG, CloudBuildLogUnavailableError, fetchCloudBuildLog, isCloudBuildId, projectFromDetailsUrl, } from "./cloudbuild.js";
|
|
12
12
|
import { createGithubAdapters } from "./context.js";
|
|
13
|
-
import { cloneRepo, GITHUB_PUSH_BRANCH_PATTERN, pushBranch } from "./repo.js";
|
|
14
|
-
import { buildGithubConversationId, GITHUB_ISSUE_BODY_TS, parseGithubConversationId, } from "./ids.js";
|
|
13
|
+
import { cloneRepo, GITHUB_PUSH_BRANCH_PATTERN, pushBranch, syncRepo } from "./repo.js";
|
|
14
|
+
import { buildGithubConversationId, GITHUB_ISSUE_BODY_TS, githubReviewCommentTs, parseGithubConversationId, parseReviewCommentTs, } from "./ids.js";
|
|
15
15
|
const SyncStateSchema = Type.Object({
|
|
16
16
|
repos: Type.Record(Type.String(), Type.Object({
|
|
17
17
|
baseline: Type.String(),
|
|
18
18
|
cursor: Type.String(),
|
|
19
19
|
seenComments: Type.Array(Type.Number()),
|
|
20
20
|
seenIssues: Type.Array(Type.Number()),
|
|
21
|
+
// Optional: state files written before review-comment polling must keep
|
|
22
|
+
// loading — a schema failure re-baselines and silently drops dedup state.
|
|
23
|
+
seenReviewComments: Type.Optional(Type.Array(Type.Number())),
|
|
21
24
|
})),
|
|
22
25
|
});
|
|
23
26
|
/**
|
|
@@ -94,6 +97,18 @@ function issueNumberFromUrl(issueUrl) {
|
|
|
94
97
|
}
|
|
95
98
|
return Number(match[1]);
|
|
96
99
|
}
|
|
100
|
+
/** PR number from a review comment's `pull_request_url` (…/repos/o/r/pulls/42). */
|
|
101
|
+
function prNumberFromUrl(pullRequestUrl) {
|
|
102
|
+
const match = /\/pulls\/(\d+)$/.exec(pullRequestUrl);
|
|
103
|
+
if (!match) {
|
|
104
|
+
throw new Error(`Cannot parse PR number from ${pullRequestUrl}`);
|
|
105
|
+
}
|
|
106
|
+
return Number(match[1]);
|
|
107
|
+
}
|
|
108
|
+
/** Keep the tail: a review comment anchors to the hunk's final lines. */
|
|
109
|
+
const MAX_DIFF_HUNK_CHARS = 1500;
|
|
110
|
+
const MAX_THREAD_TURNS = 10;
|
|
111
|
+
const MAX_THREAD_TURN_CHARS = 500;
|
|
97
112
|
export class GithubMessagingBot {
|
|
98
113
|
constructor(handler, config, client) {
|
|
99
114
|
this.appSlug = null;
|
|
@@ -102,6 +117,8 @@ export class GithubMessagingBot {
|
|
|
102
117
|
this.queues = new Map();
|
|
103
118
|
this.repoState = new Map();
|
|
104
119
|
this.permissionCache = new Map();
|
|
120
|
+
/** Cloud Build ids seen in getChecks summaries → their GCP project. */
|
|
121
|
+
this.buildRef = new Map();
|
|
105
122
|
this.pollInFlight = false;
|
|
106
123
|
this.handler = handler;
|
|
107
124
|
this.config = config;
|
|
@@ -151,8 +168,15 @@ export class GithubMessagingBot {
|
|
|
151
168
|
cursor: saved.cursor,
|
|
152
169
|
seenComments: new Set(saved.seenComments),
|
|
153
170
|
seenIssues: new Set(saved.seenIssues),
|
|
171
|
+
seenReviewComments: new Set(saved.seenReviewComments ?? []),
|
|
154
172
|
}
|
|
155
|
-
: {
|
|
173
|
+
: {
|
|
174
|
+
baseline: now,
|
|
175
|
+
cursor: now,
|
|
176
|
+
seenComments: new Set(),
|
|
177
|
+
seenIssues: new Set(),
|
|
178
|
+
seenReviewComments: new Set(),
|
|
179
|
+
});
|
|
156
180
|
}
|
|
157
181
|
this.persistSyncState();
|
|
158
182
|
setInterval(() => {
|
|
@@ -180,9 +204,16 @@ export class GithubMessagingBot {
|
|
|
180
204
|
if (!content) {
|
|
181
205
|
throw new Error(`GitHub does not support reaction '${emoji}' (supported: ${Object.keys(GITHUB_REACTIONS).join(", ")})`);
|
|
182
206
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
207
|
+
const reviewCommentId = parseReviewCommentTs(messageTs);
|
|
208
|
+
await githubRetry(() => {
|
|
209
|
+
if (messageTs === GITHUB_ISSUE_BODY_TS) {
|
|
210
|
+
return this.client.createIssueReaction(ref.owner, ref.repo, ref.number, content);
|
|
211
|
+
}
|
|
212
|
+
if (reviewCommentId !== null) {
|
|
213
|
+
return this.client.createReviewCommentReaction(ref.owner, ref.repo, reviewCommentId, content);
|
|
214
|
+
}
|
|
215
|
+
return this.client.createCommentReaction(ref.owner, ref.repo, Number(messageTs), content);
|
|
216
|
+
});
|
|
186
217
|
}
|
|
187
218
|
enqueueEvent(event) {
|
|
188
219
|
const conversationId = event.conversationId;
|
|
@@ -203,8 +234,8 @@ export class GithubMessagingBot {
|
|
|
203
234
|
name: "github",
|
|
204
235
|
trustModel: "open-trigger",
|
|
205
236
|
formattingGuide: "## GitHub Formatting (GitHub Flavored Markdown)\n" +
|
|
206
|
-
"Standard Markdown plus tables, task lists, and ```suggestion blocks.\n" +
|
|
207
|
-
"Reference issues/PRs as #123 and users as @login.",
|
|
237
|
+
"Standard Markdown plus tables, task lists, fenced code blocks, and ```suggestion blocks.\n" +
|
|
238
|
+
"Reference issues/PRs as #123 and users as @login (mentions notify people — use sparingly).",
|
|
208
239
|
channels: [],
|
|
209
240
|
users: [],
|
|
210
241
|
};
|
|
@@ -312,8 +343,41 @@ export class GithubMessagingBot {
|
|
|
312
343
|
createdAt: comment.created_at,
|
|
313
344
|
});
|
|
314
345
|
}
|
|
346
|
+
// Inline PR review comments live in their own endpoint and id space; the
|
|
347
|
+
// same watermark discipline applies. The bot's own thread replies (the
|
|
348
|
+
// github_review_reply tool) re-surface here and are dropped as Bot-authored.
|
|
349
|
+
const reviewComments = (await this.client.listPullReviewCommentsSince(repo.owner, repo.repo, since)) ?? [];
|
|
350
|
+
for (const comment of reviewComments) {
|
|
351
|
+
if (comment.updated_at > state.cursor) {
|
|
352
|
+
state.cursor = comment.updated_at;
|
|
353
|
+
changed = true;
|
|
354
|
+
}
|
|
355
|
+
if (comment.created_at < state.baseline || state.seenReviewComments.has(comment.id)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
state.seenReviewComments.add(comment.id);
|
|
359
|
+
changed = true;
|
|
360
|
+
if (comment.user.type === "Bot")
|
|
361
|
+
continue;
|
|
362
|
+
await this.handleIncoming({
|
|
363
|
+
ref: { ...repo, number: prNumberFromUrl(comment.pull_request_url) },
|
|
364
|
+
ts: githubReviewCommentTs(comment.id),
|
|
365
|
+
user: comment.user.login,
|
|
366
|
+
text: comment.body,
|
|
367
|
+
createdAt: comment.created_at,
|
|
368
|
+
isPr: true,
|
|
369
|
+
review: {
|
|
370
|
+
commentId: comment.id,
|
|
371
|
+
path: comment.path,
|
|
372
|
+
line: comment.line,
|
|
373
|
+
diffHunk: comment.diff_hunk,
|
|
374
|
+
inReplyToId: comment.in_reply_to_id,
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
}
|
|
315
378
|
pruneSeen(state.seenComments);
|
|
316
379
|
pruneSeen(state.seenIssues);
|
|
380
|
+
pruneSeen(state.seenReviewComments);
|
|
317
381
|
return changed;
|
|
318
382
|
}
|
|
319
383
|
// ==========================================================================
|
|
@@ -338,6 +402,7 @@ export class GithubMessagingBot {
|
|
|
338
402
|
cursor: watermark.cursor,
|
|
339
403
|
seenComments: [...watermark.seenComments],
|
|
340
404
|
seenIssues: [...watermark.seenIssues],
|
|
405
|
+
seenReviewComments: [...watermark.seenReviewComments],
|
|
341
406
|
};
|
|
342
407
|
}
|
|
343
408
|
ensureDirExists(dirname(this.config.syncStatePath));
|
|
@@ -411,40 +476,9 @@ export class GithubMessagingBot {
|
|
|
411
476
|
messageId: item.ts,
|
|
412
477
|
persistentTopLevel: true,
|
|
413
478
|
});
|
|
414
|
-
|
|
415
|
-
this.
|
|
416
|
-
|
|
417
|
-
ts: item.ts,
|
|
418
|
-
user: item.user,
|
|
419
|
-
userName: item.user,
|
|
420
|
-
text: cleanedText,
|
|
421
|
-
attachments: [],
|
|
422
|
-
isMessagingBot: false,
|
|
423
|
-
});
|
|
424
|
-
const stopTarget = resolveStopTarget({ handler: this.handler, conversationId, sessionKey });
|
|
425
|
-
if (stopTarget) {
|
|
426
|
-
await this.handler.handleStop(stopTarget, conversationId, this);
|
|
427
|
-
}
|
|
428
|
-
else if (mentioned) {
|
|
429
|
-
await this.postMessage(conversationId, formatNothingRunning("github"));
|
|
430
|
-
}
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
// First contact: log the issue title/body ahead of the comment so the
|
|
434
|
-
// session history starts with what the thread is about. The clone is
|
|
435
|
-
// (re)attempted on every trigger while ./repo is missing — a no-op once
|
|
436
|
-
// it exists — so a failed first clone (e.g. App permissions granted
|
|
437
|
-
// later) heals on the next mention instead of wedging the conversation.
|
|
438
|
-
let isPrHint = item.isPr;
|
|
439
|
-
if (!participating && item.ts !== GITHUB_ISSUE_BODY_TS) {
|
|
440
|
-
const issue = await this.logIssueContext(item.ref, conversationId, item.createdAt);
|
|
441
|
-
if (issue)
|
|
442
|
-
isPrHint = Boolean(issue.pull_request);
|
|
443
|
-
}
|
|
444
|
-
if (!existsSync(this.repoDir(conversationId))) {
|
|
445
|
-
const isPr = isPrHint ?? (await this.fetchIsPr(item.ref));
|
|
446
|
-
await this.ensureRepoClone(item.ref, conversationId, isPr);
|
|
447
|
-
}
|
|
479
|
+
const messageText = item.review
|
|
480
|
+
? await this.formatReviewMessage(item, cleanedText)
|
|
481
|
+
: cleanedText;
|
|
448
482
|
const eventBase = {
|
|
449
483
|
type: item.ts === GITHUB_ISSUE_BODY_TS ? "issue" : "message",
|
|
450
484
|
conversationId,
|
|
@@ -453,22 +487,31 @@ export class GithubMessagingBot {
|
|
|
453
487
|
sessionKey,
|
|
454
488
|
user: item.user,
|
|
455
489
|
userName: item.user,
|
|
456
|
-
text:
|
|
490
|
+
text: messageText,
|
|
457
491
|
};
|
|
458
492
|
await processMessageIntake({
|
|
459
493
|
eventBase,
|
|
460
494
|
workingDir: this.config.workingDir,
|
|
461
495
|
isAutoReplyCandidate: false,
|
|
496
|
+
// Match on the user-typed text, not the review-decorated messageText.
|
|
497
|
+
magicWord: { text: cleanedText, addressed: mentioned, scopeFallback: "never" },
|
|
498
|
+
busyPolicy: "queue",
|
|
462
499
|
logEntryBase: {
|
|
463
500
|
date: item.createdAt,
|
|
464
501
|
ts: item.ts,
|
|
465
502
|
user: item.user,
|
|
466
503
|
userName: item.user,
|
|
467
|
-
text:
|
|
504
|
+
text: messageText,
|
|
468
505
|
isMessagingBot: false,
|
|
469
506
|
},
|
|
470
507
|
log: (entry) => this.logToFile(conversationId, entry),
|
|
471
|
-
|
|
508
|
+
// GitHub has no attachments; the prep hook is used to lay down the
|
|
509
|
+
// issue context and repo clone only for messages that will dispatch,
|
|
510
|
+
// and before the comment itself is logged.
|
|
511
|
+
processAttachments: async () => {
|
|
512
|
+
await this.prepareConversation(item, conversationId, participating);
|
|
513
|
+
return [];
|
|
514
|
+
},
|
|
472
515
|
queueKey: conversationId,
|
|
473
516
|
enqueue: (queueKey, work) => this.getQueue(queueKey).enqueue(work),
|
|
474
517
|
handler: this.handler,
|
|
@@ -476,6 +519,74 @@ export class GithubMessagingBot {
|
|
|
476
519
|
createContext: (event) => createGithubAdapters(event, this),
|
|
477
520
|
});
|
|
478
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* First contact: log the issue title/body ahead of the comment so the
|
|
524
|
+
* session history starts with what the thread is about. The clone is
|
|
525
|
+
* (re)attempted on every trigger while ./repo is missing — a no-op once
|
|
526
|
+
* it exists — so a failed first clone (e.g. App permissions granted
|
|
527
|
+
* later) heals on the next mention instead of wedging the conversation.
|
|
528
|
+
*/
|
|
529
|
+
async prepareConversation(item, conversationId, participating) {
|
|
530
|
+
let isPrHint = item.isPr;
|
|
531
|
+
if (!participating && item.ts !== GITHUB_ISSUE_BODY_TS) {
|
|
532
|
+
const issue = await this.logIssueContext(item.ref, conversationId, item.createdAt);
|
|
533
|
+
// Only comments arrive without the PR-ness hint; review comments carry
|
|
534
|
+
// isPr: true (they only exist on PRs) and that knowledge is authoritative.
|
|
535
|
+
if (issue && isPrHint === undefined)
|
|
536
|
+
isPrHint = Boolean(issue.pull_request);
|
|
537
|
+
}
|
|
538
|
+
if (!existsSync(this.repoDir(conversationId))) {
|
|
539
|
+
const isPr = isPrHint ?? (await this.fetchIsPr(item.ref));
|
|
540
|
+
await this.ensureRepoClone(item.ref, conversationId, isPr);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Decorates an inline review comment with its diff anchor and (for replies)
|
|
545
|
+
* the thread's earlier turns, so the agent knows which code the feedback is
|
|
546
|
+
* about without hunting through the PR. The rc-<id> in the header is what
|
|
547
|
+
* github_review_reply takes to answer in-thread.
|
|
548
|
+
*/
|
|
549
|
+
async formatReviewMessage(item, cleanedText) {
|
|
550
|
+
const review = item.review;
|
|
551
|
+
const location = review.line !== null ? `${review.path}:${review.line}` : review.path;
|
|
552
|
+
const parts = [`[PR review comment ${githubReviewCommentTs(review.commentId)} on ${location}]`];
|
|
553
|
+
if (review.diffHunk) {
|
|
554
|
+
const hunk = review.diffHunk.length > MAX_DIFF_HUNK_CHARS
|
|
555
|
+
? `…${review.diffHunk.slice(-MAX_DIFF_HUNK_CHARS)}`
|
|
556
|
+
: review.diffHunk;
|
|
557
|
+
parts.push(`\`\`\`diff\n${hunk}\n\`\`\``);
|
|
558
|
+
}
|
|
559
|
+
if (review.inReplyToId !== undefined) {
|
|
560
|
+
const turns = await this.reviewThreadContext(item.ref, review.inReplyToId, review.commentId);
|
|
561
|
+
if (turns.length > 0) {
|
|
562
|
+
parts.push(`Thread so far:\n${turns.join("\n")}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
parts.push(cleanedText);
|
|
566
|
+
return parts.join("\n");
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Earlier turns of one review thread (root + replies before the trigger),
|
|
570
|
+
* newest-truncated. Fail-soft: context is a nicety, the trigger is not.
|
|
571
|
+
*/
|
|
572
|
+
async reviewThreadContext(ref, rootId, beforeId) {
|
|
573
|
+
try {
|
|
574
|
+
const all = await githubRetry(() => this.client.listPullReviewComments(ref.owner, ref.repo, ref.number));
|
|
575
|
+
return all
|
|
576
|
+
.filter((comment) => (comment.id === rootId || comment.in_reply_to_id === rootId) && comment.id < beforeId)
|
|
577
|
+
.slice(-MAX_THREAD_TURNS)
|
|
578
|
+
.map((comment) => {
|
|
579
|
+
const body = comment.body.length > MAX_THREAD_TURN_CHARS
|
|
580
|
+
? `${comment.body.slice(0, MAX_THREAD_TURN_CHARS)}…`
|
|
581
|
+
: comment.body;
|
|
582
|
+
return `@${comment.user.login}: ${body}`;
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
log.logWarning(`GitHub: could not fetch review thread context for ${ref.owner}/${ref.repo}#${ref.number}`, err instanceof Error ? err.message : String(err));
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
}
|
|
479
590
|
async logIssueContext(ref, conversationId, triggerCreatedAt) {
|
|
480
591
|
let issue;
|
|
481
592
|
try {
|
|
@@ -519,6 +630,23 @@ export class GithubMessagingBot {
|
|
|
519
630
|
return false;
|
|
520
631
|
}
|
|
521
632
|
}
|
|
633
|
+
/**
|
|
634
|
+
* The PR's head branch name when it lives in this repo — that is the name
|
|
635
|
+
* the local checkout should carry, so committing on it and calling
|
|
636
|
+
* github_pr pushes back to the PR itself instead of opening a new one.
|
|
637
|
+
* Fork heads (unpushable with the repo-scoped token) and failed lookups
|
|
638
|
+
* return undefined; callers then fall back to the synthetic pr-<n>.
|
|
639
|
+
*/
|
|
640
|
+
async fetchPrHeadBranch(ref) {
|
|
641
|
+
try {
|
|
642
|
+
const pr = await githubRetry(() => this.client.getPullRequest(ref.owner, ref.repo, ref.number));
|
|
643
|
+
const sameRepo = pr.head?.repo?.full_name?.toLowerCase() === `${ref.owner}/${ref.repo}`.toLowerCase();
|
|
644
|
+
return sameRepo ? pr.head?.ref : undefined;
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
522
650
|
/**
|
|
523
651
|
* Clone the conversation's repo into its dir (bind-mounted into the
|
|
524
652
|
* sandbox) using an ephemeral contents:read token that never leaves the
|
|
@@ -532,6 +660,7 @@ export class GithubMessagingBot {
|
|
|
532
660
|
const token = await this.client.createScopedInstallationToken(ref.repo, {
|
|
533
661
|
contents: "read",
|
|
534
662
|
});
|
|
663
|
+
const prHeadBranch = isPr ? await this.fetchPrHeadBranch(ref) : undefined;
|
|
535
664
|
await cloneRepo({
|
|
536
665
|
url: `https://github.com/${ref.owner}/${ref.repo}.git`,
|
|
537
666
|
dir,
|
|
@@ -539,8 +668,9 @@ export class GithubMessagingBot {
|
|
|
539
668
|
botLogin: `${this.appSlug}[bot]`,
|
|
540
669
|
botEmail: this.botEmail ?? `${this.appSlug}[bot]@users.noreply.github.com`,
|
|
541
670
|
prNumber: isPr ? ref.number : undefined,
|
|
671
|
+
prHeadBranch,
|
|
542
672
|
});
|
|
543
|
-
log.logInfo(`[${conversationId}] Cloned ${ref.owner}/${ref.repo}${isPr ? ` and checked out PR #${ref.number} head` : ""}`);
|
|
673
|
+
log.logInfo(`[${conversationId}] Cloned ${ref.owner}/${ref.repo}${isPr ? ` and checked out PR #${ref.number} head as ${prHeadBranch ?? `pr-${ref.number}`}` : ""}`);
|
|
544
674
|
}
|
|
545
675
|
catch (err) {
|
|
546
676
|
log.logWarning(`GitHub: repo clone failed for ${conversationId}`, err instanceof Error ? err.message : String(err));
|
|
@@ -616,15 +746,256 @@ export class GithubMessagingBot {
|
|
|
616
746
|
target = pr.head.sha;
|
|
617
747
|
}
|
|
618
748
|
const runs = await githubRetry(() => this.client.listCheckRuns(ref.owner, ref.repo, target));
|
|
619
|
-
return runs.map((run) =>
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
749
|
+
return runs.map((run) => {
|
|
750
|
+
const externalId = run.external_id?.trim() || null;
|
|
751
|
+
let buildLogAvailable = false;
|
|
752
|
+
// Cloud Build runs carry their build UUID in external_id and the GCP
|
|
753
|
+
// project in details_url. Remember project-by-build so getBuildLog only
|
|
754
|
+
// serves builds the agent actually saw on this repo's checks.
|
|
755
|
+
if (this.config.cloudBuild &&
|
|
756
|
+
run.app?.slug === CLOUD_BUILD_APP_SLUG &&
|
|
757
|
+
externalId &&
|
|
758
|
+
isCloudBuildId(externalId)) {
|
|
759
|
+
const project = projectFromDetailsUrl(run.details_url) ?? this.config.cloudBuild.projectFallback;
|
|
760
|
+
if (project) {
|
|
761
|
+
this.rememberBuild(externalId, project);
|
|
762
|
+
buildLogAvailable = true;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return {
|
|
766
|
+
id: run.id,
|
|
767
|
+
name: run.name,
|
|
768
|
+
status: run.status,
|
|
769
|
+
conclusion: run.conclusion,
|
|
770
|
+
url: run.html_url,
|
|
771
|
+
appSlug: run.app?.slug ?? null,
|
|
772
|
+
outputSummary: run.output?.summary?.trim() ? run.output.summary.slice(0, 500) : null,
|
|
773
|
+
externalId,
|
|
774
|
+
buildLogAvailable,
|
|
775
|
+
};
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
rememberBuild(buildId, project) {
|
|
779
|
+
// Insertion-ordered Map as a small LRU: re-insert on touch, prune oldest.
|
|
780
|
+
this.buildRef.delete(buildId);
|
|
781
|
+
this.buildRef.set(buildId, { project });
|
|
782
|
+
if (this.buildRef.size > 200) {
|
|
783
|
+
const oldest = this.buildRef.keys().next().value;
|
|
784
|
+
if (oldest !== undefined)
|
|
785
|
+
this.buildRef.delete(oldest);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Backs the Cloud Build log mode of `github_checks`. GCP credentials stay
|
|
790
|
+
* host-side (ADC via GOOGLE_APPLICATION_CREDENTIALS — e.g. a Workload
|
|
791
|
+
* Identity Federation external_account file); the build must have appeared
|
|
792
|
+
* in a github_checks summary first, which is also where its project is
|
|
793
|
+
* learned. Unreadable logs degrade to the console url, never a dead end.
|
|
794
|
+
*/
|
|
795
|
+
async getBuildLog(conversationId, buildId) {
|
|
796
|
+
if (!this.config.cloudBuild) {
|
|
797
|
+
throw new Error("Cloud Build log access is not configured on this host (no GCP credentials).");
|
|
798
|
+
}
|
|
799
|
+
const known = this.buildRef.get(buildId);
|
|
800
|
+
if (!known) {
|
|
801
|
+
throw new Error(`Unknown build id ${buildId}: run github_checks first and take a [build …] id from ` +
|
|
802
|
+
`the summary.`);
|
|
803
|
+
}
|
|
804
|
+
let logText;
|
|
805
|
+
try {
|
|
806
|
+
logText = await fetchCloudBuildLog({
|
|
807
|
+
tokenProvider: this.config.cloudBuild.tokenProvider,
|
|
808
|
+
project: known.project,
|
|
809
|
+
buildId,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
if (err instanceof CloudBuildLogUnavailableError) {
|
|
814
|
+
throw new Error(`${err.message}${err.consoleUrl ? ` Console: ${err.consoleUrl}` : ""} Use the check's ` +
|
|
815
|
+
`summary/url from github_checks, or reproduce the failure locally in ./repo.`, { cause: err });
|
|
816
|
+
}
|
|
817
|
+
throw err;
|
|
818
|
+
}
|
|
819
|
+
const MAX_LOG_CHARS = 20000;
|
|
820
|
+
return logText.length > MAX_LOG_CHARS
|
|
821
|
+
? `…(truncated to the last ${MAX_LOG_CHARS} chars)\n${logText.slice(-MAX_LOG_CHARS)}`
|
|
822
|
+
: logText;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Backs the `github_sync` tool: refresh the conversation's ./repo clone
|
|
826
|
+
* from origin with an ephemeral read token. Fetch-only when moving the
|
|
827
|
+
* checkout could lose agent work; the formatted report tells the agent
|
|
828
|
+
* what happened and what to do next.
|
|
829
|
+
*/
|
|
830
|
+
async syncRepo(conversationId, branch) {
|
|
831
|
+
const ref = parseGithubConversationId(conversationId);
|
|
832
|
+
const dir = this.repoDir(conversationId);
|
|
833
|
+
if (!existsSync(dir)) {
|
|
834
|
+
throw new Error("This conversation has no ./repo clone to sync.");
|
|
835
|
+
}
|
|
836
|
+
const token = await this.client.createScopedInstallationToken(ref.repo, {
|
|
837
|
+
contents: "read",
|
|
838
|
+
});
|
|
839
|
+
let prNumber;
|
|
840
|
+
let prHeadBranch;
|
|
841
|
+
let defaultBranch;
|
|
842
|
+
if (!branch) {
|
|
843
|
+
if (await this.fetchIsPr(ref)) {
|
|
844
|
+
prNumber = ref.number;
|
|
845
|
+
prHeadBranch = await this.fetchPrHeadBranch(ref);
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
const repository = await githubRetry(() => this.client.getRepository(ref.owner, ref.repo));
|
|
849
|
+
defaultBranch = repository.default_branch;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const result = await syncRepo({ dir, token, branch, prNumber, prHeadBranch, defaultBranch });
|
|
853
|
+
if (result.updatedCheckout) {
|
|
854
|
+
return `Updated ./repo: branch ${result.target} is now at ${result.fetchedSha.slice(0, 12)}.`;
|
|
855
|
+
}
|
|
856
|
+
const reasons = [];
|
|
857
|
+
if (result.currentBranch !== result.target) {
|
|
858
|
+
reasons.push(`the checkout is on '${result.currentBranch}', not '${result.target}'`);
|
|
859
|
+
}
|
|
860
|
+
if (result.dirty) {
|
|
861
|
+
reasons.push("the working tree has uncommitted changes");
|
|
862
|
+
}
|
|
863
|
+
if (result.localCommits > 0) {
|
|
864
|
+
reasons.push(`'${result.target}' has ${result.localCommits} local commit(s) not on origin`);
|
|
865
|
+
}
|
|
866
|
+
return (`Fetched ${result.target} to FETCH_HEAD (${result.fetchedSha.slice(0, 12)}) but left the ` +
|
|
867
|
+
`checkout alone: ${reasons.join("; ")}. Merge or rebase FETCH_HEAD yourself ` +
|
|
868
|
+
`(e.g. git merge FETCH_HEAD), or commit your work to a pi/* branch first.`);
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Backs the `github_read` tool: PR/issue metadata the ./repo clone cannot
|
|
872
|
+
* provide (diff stats, review state, other issues in the repo). Owner/repo
|
|
873
|
+
* always come from the conversation id, so reads never leave this repo.
|
|
874
|
+
*/
|
|
875
|
+
async readGithub(conversationId, request) {
|
|
876
|
+
const ref = parseGithubConversationId(conversationId);
|
|
877
|
+
const number = request.number ?? ref.number;
|
|
878
|
+
switch (request.action) {
|
|
879
|
+
case "pr": {
|
|
880
|
+
const pr = await githubRetry(() => this.client.getPullRequest(ref.owner, ref.repo, number));
|
|
881
|
+
return { kind: "pr", pr };
|
|
882
|
+
}
|
|
883
|
+
case "pr_files": {
|
|
884
|
+
const files = await githubRetry(() => this.client.listPullRequestFiles(ref.owner, ref.repo, number));
|
|
885
|
+
return { kind: "pr_files", files };
|
|
886
|
+
}
|
|
887
|
+
case "pr_reviews": {
|
|
888
|
+
const [reviews, threads] = await Promise.all([
|
|
889
|
+
githubRetry(() => this.client.listPullRequestReviews(ref.owner, ref.repo, number)),
|
|
890
|
+
githubRetry(() => this.client.listPullReviewComments(ref.owner, ref.repo, number)),
|
|
891
|
+
]);
|
|
892
|
+
return { kind: "pr_reviews", reviews, threads };
|
|
893
|
+
}
|
|
894
|
+
case "issue": {
|
|
895
|
+
const issue = await githubRetry(() => this.client.getIssue(ref.owner, ref.repo, number));
|
|
896
|
+
return { kind: "issue", issue };
|
|
897
|
+
}
|
|
898
|
+
case "comments": {
|
|
899
|
+
const comments = await githubRetry(() => this.client.listIssueComments(ref.owner, ref.repo, number));
|
|
900
|
+
return { kind: "comments", comments };
|
|
901
|
+
}
|
|
902
|
+
case "list": {
|
|
903
|
+
const issues = await githubRetry(() => this.client.listIssues(ref.owner, ref.repo, {
|
|
904
|
+
state: request.state,
|
|
905
|
+
labels: request.labels,
|
|
906
|
+
creator: request.creator,
|
|
907
|
+
}));
|
|
908
|
+
return { kind: "list", issues };
|
|
909
|
+
}
|
|
910
|
+
default:
|
|
911
|
+
throw new Error(`Unknown github_read action: ${String(request.action)}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Backs the `github_issue` tool: labels, assignees, and open/closed state
|
|
916
|
+
* on any issue of this conversation's repo (triage). Same-repo by
|
|
917
|
+
* construction — owner/repo come from the conversation id; the action enum
|
|
918
|
+
* is closed, so lock/delete/transfer cannot be expressed at all.
|
|
919
|
+
*/
|
|
920
|
+
async manageIssue(conversationId, request) {
|
|
921
|
+
const ref = parseGithubConversationId(conversationId);
|
|
922
|
+
const number = request.number ?? ref.number;
|
|
923
|
+
try {
|
|
924
|
+
switch (request.action) {
|
|
925
|
+
case "add_labels": {
|
|
926
|
+
if (!request.labels?.length) {
|
|
927
|
+
throw new Error("add_labels requires a non-empty labels array.");
|
|
928
|
+
}
|
|
929
|
+
await githubRetry(() => this.client.addIssueLabels(ref.owner, ref.repo, number, request.labels));
|
|
930
|
+
return `Added label(s) ${request.labels.join(", ")} to #${number}.`;
|
|
931
|
+
}
|
|
932
|
+
case "remove_label": {
|
|
933
|
+
if (!request.label) {
|
|
934
|
+
throw new Error("remove_label requires a label name.");
|
|
935
|
+
}
|
|
936
|
+
await githubRetry(() => this.client.removeIssueLabel(ref.owner, ref.repo, number, request.label));
|
|
937
|
+
return `Removed label ${request.label} from #${number}.`;
|
|
938
|
+
}
|
|
939
|
+
case "add_assignees": {
|
|
940
|
+
if (!request.assignees?.length) {
|
|
941
|
+
throw new Error("add_assignees requires a non-empty assignees array.");
|
|
942
|
+
}
|
|
943
|
+
await githubRetry(() => this.client.addIssueAssignees(ref.owner, ref.repo, number, request.assignees));
|
|
944
|
+
return `Assigned ${request.assignees.map((login) => `@${login}`).join(", ")} to #${number}.`;
|
|
945
|
+
}
|
|
946
|
+
case "remove_assignees": {
|
|
947
|
+
if (!request.assignees?.length) {
|
|
948
|
+
throw new Error("remove_assignees requires a non-empty assignees array.");
|
|
949
|
+
}
|
|
950
|
+
await githubRetry(() => this.client.removeIssueAssignees(ref.owner, ref.repo, number, request.assignees));
|
|
951
|
+
return `Unassigned ${request.assignees.map((login) => `@${login}`).join(", ")} from #${number}.`;
|
|
952
|
+
}
|
|
953
|
+
case "close": {
|
|
954
|
+
await githubRetry(() => this.client.updateIssueState(ref.owner, ref.repo, number, "closed", request.state_reason));
|
|
955
|
+
return `Closed #${number}${request.state_reason ? ` as ${request.state_reason}` : ""}.`;
|
|
956
|
+
}
|
|
957
|
+
case "reopen": {
|
|
958
|
+
await githubRetry(() => this.client.updateIssueState(ref.owner, ref.repo, number, "open"));
|
|
959
|
+
return `Reopened #${number}.`;
|
|
960
|
+
}
|
|
961
|
+
default:
|
|
962
|
+
throw new Error(`Unknown github_issue action: ${String(request.action)}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
catch (err) {
|
|
966
|
+
if (err instanceof GithubApiError && err.status === 404) {
|
|
967
|
+
throw new Error(`Issue #${number} not found in ${ref.owner}/${ref.repo}.`, { cause: err });
|
|
968
|
+
}
|
|
969
|
+
throw err;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Backs the `github_review_reply` tool: answer inside one inline review
|
|
974
|
+
* thread of this conversation's PR. The comment id comes from an
|
|
975
|
+
* `[PR review comment rc-<id> …]` message; GitHub validates it belongs to
|
|
976
|
+
* this PR (404 otherwise, translated into agent-actionable guidance).
|
|
977
|
+
*/
|
|
978
|
+
async replyToReviewThread(conversationId, commentId, body) {
|
|
979
|
+
if (!Number.isInteger(commentId) || commentId <= 0) {
|
|
980
|
+
throw new Error("comment_id must be the numeric id from an [PR review comment rc-<id> …] message.");
|
|
981
|
+
}
|
|
982
|
+
const ref = parseGithubConversationId(conversationId);
|
|
983
|
+
const text = body.length > GITHUB_MAX_COMMENT_LENGTH
|
|
984
|
+
? `${body.slice(0, GITHUB_MAX_COMMENT_LENGTH)}\n…(truncated)`
|
|
985
|
+
: body;
|
|
986
|
+
try {
|
|
987
|
+
const reply = await githubRetry(() => this.client.replyToReviewComment(ref.owner, ref.repo, ref.number, commentId, text));
|
|
988
|
+
return {
|
|
989
|
+
url: `https://github.com/${ref.owner}/${ref.repo}/pull/${ref.number}#discussion_r${reply.id}`,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
catch (err) {
|
|
993
|
+
if (err instanceof GithubApiError && err.status === 404) {
|
|
994
|
+
throw new Error(`Comment ${commentId} is not a review comment on this PR. Take the id from an ` +
|
|
995
|
+
`[PR review comment rc-<id> …] message in this conversation; do not guess ids.`, { cause: err });
|
|
996
|
+
}
|
|
997
|
+
throw err;
|
|
998
|
+
}
|
|
628
999
|
}
|
|
629
1000
|
/**
|
|
630
1001
|
* Backs the log mode of `github_checks`: the tail of one CI job's log so
|