@doist/doistbot-cli 1.0.10 → 1.0.12
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/dist/actions/review-usage.js +4 -4
- package/dist/actions/review.js +31 -21
- package/dist/config.js +61 -16
- package/dist/index.js +8 -2
- package/dist/terminal.js +4 -0
- package/package.json +10 -9
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +1 -1
- package/sandbox/dist/core/datadog-metrics.js +7 -2
- package/sandbox/dist/core/label-parsing.js +1 -22
- package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
- package/sandbox/dist/core/pi-progress-logging.js +4 -0
- package/sandbox/dist/core/pi.js +286 -67
- package/sandbox/dist/core/repository-map.js +1 -220
- package/sandbox/dist/core/repository-resolver.js +1 -166
- package/sandbox/dist/providers/config.js +1 -1
- package/sandbox/dist/providers/helpers.js +3 -0
- package/sandbox/dist/providers/pi-review.js +53 -3
- package/sandbox/dist/providers/schemas.js +7 -0
- package/sandbox/dist/providers/types.js +4 -1
- package/sandbox/dist/tasks/chat/chat.js +95 -45
- package/sandbox/dist/tasks/chat/prompt.js +5 -0
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +1 -0
- package/sandbox/dist/tasks/issue-summarize/model.js +4 -4
- package/sandbox/dist/tasks/issue-triage/context.js +2 -0
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +2 -1
- package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +4 -7
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +65 -41
- package/sandbox/dist/tasks/issue-triage/output.js +1 -1
- package/sandbox/dist/tasks/issue-triage/resolution.js +1 -0
- package/sandbox/dist/tasks/issue-triage/shared.js +2 -12
- package/sandbox/dist/tasks/issue-triage/triage.js +2 -0
- package/sandbox/dist/tasks/review/diff.js +1 -1
- package/sandbox/dist/tasks/review/engines/multi-focus.js +47 -12
- package/sandbox/dist/tasks/review/local-git.js +62 -8
- package/sandbox/dist/tasks/review/prompt.js +2 -0
- package/sandbox/dist/tasks/review/review-summary.js +1 -1
- package/sandbox/dist/tasks/review/summary-model.js +2 -2
- package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +1 -1
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +18 -1
|
@@ -8,12 +8,15 @@ import { loadPromptTemplateFile, resolvePromptFile } from '../../core/prompt-tem
|
|
|
8
8
|
import { removeIssueCommentReactionIfPresent } from '../../core/reactions.js';
|
|
9
9
|
import { cloneRepository, createGitHubClients, parseBaseContext, requiredEnv, WORKSPACE_DIR, } from '../../core/shared.js';
|
|
10
10
|
import { withPhase } from '../../core/tracing.js';
|
|
11
|
-
import { buildPrompt } from './prompt.js';
|
|
11
|
+
import { buildBroPrompt, buildPrompt } from './prompt.js';
|
|
12
12
|
const CHAT_PROMPT_FILE = 'chat-prompt.md';
|
|
13
|
+
const BRO_PROMPT_FILE = 'bro-prompt.md';
|
|
13
14
|
const CHAT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
14
15
|
const CHAT_PROVIDER = 'openai';
|
|
15
16
|
const CHAT_MODEL = 'gpt-5.6-luna';
|
|
16
17
|
const CHAT_THINKING_LEVEL = 'high';
|
|
18
|
+
const BRO_SYSTEM_PROMPT = 'You are Doistbot re-explaining one of your own review comments. Use only the source comment in the task prompt. Do not inspect files, call tools, or add facts. Output only the rewritten explanation.';
|
|
19
|
+
const EMPTY_BRO_RESPONSE = "There's no earlier Doistbot comment in this review thread for me to simplify, bro.";
|
|
17
20
|
function parseContext() {
|
|
18
21
|
const base = parseBaseContext();
|
|
19
22
|
const reactionIdRaw = process.env.REACTION_ID;
|
|
@@ -24,23 +27,29 @@ function parseContext() {
|
|
|
24
27
|
reactionId: Number.isFinite(reactionId) ? reactionId : undefined,
|
|
25
28
|
commentType: requiredEnv('COMMENT_TYPE'),
|
|
26
29
|
eventName: requiredEnv('EVENT_NAME'),
|
|
30
|
+
mode: process.env.CHAT_MODE === 'bro' ? 'bro' : 'chat',
|
|
27
31
|
dryRun: process.env.DRY_RUN?.toLowerCase() === 'true',
|
|
28
32
|
};
|
|
29
33
|
}
|
|
34
|
+
function findLatestDoistbotComment(entries) {
|
|
35
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
36
|
+
const entry = entries[index];
|
|
37
|
+
if (entry && isKnownDoistbotLogin(entry.author)) {
|
|
38
|
+
return entry.body;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
30
43
|
/**
|
|
31
44
|
* Gathers context for a review comment (inline code comment thread).
|
|
32
45
|
*/
|
|
33
|
-
async function gatherReviewCommentContext(octokit, owner, repo, commentId) {
|
|
46
|
+
export async function gatherReviewCommentContext(octokit, owner, repo, commentId, mode = 'chat') {
|
|
34
47
|
const comment = await octokit.rest.pulls.getReviewComment({
|
|
35
48
|
owner,
|
|
36
49
|
repo,
|
|
37
50
|
comment_id: commentId,
|
|
38
51
|
});
|
|
39
|
-
const
|
|
40
|
-
const commentBody = comment.data.body ?? '';
|
|
41
|
-
// Build thread history by walking up the chain
|
|
42
|
-
const threadHistory = [];
|
|
43
|
-
const currentCommentAuthor = comment.data.user?.login ?? 'unknown';
|
|
52
|
+
const threadEntries = [];
|
|
44
53
|
let inReplyToId = comment.data.in_reply_to_id;
|
|
45
54
|
while (inReplyToId) {
|
|
46
55
|
try {
|
|
@@ -51,7 +60,16 @@ async function gatherReviewCommentContext(octokit, owner, repo, commentId) {
|
|
|
51
60
|
});
|
|
52
61
|
const parentAuthor = parentComment.data.user?.login ?? 'unknown';
|
|
53
62
|
const parentBody = parentComment.data.body ?? '';
|
|
54
|
-
|
|
63
|
+
if (mode === 'bro' && isKnownDoistbotLogin(parentAuthor)) {
|
|
64
|
+
return {
|
|
65
|
+
previousDoistbotComment: parentBody,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (mode === 'bro') {
|
|
69
|
+
inReplyToId = parentComment.data.in_reply_to_id;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
threadEntries.unshift({ author: parentAuthor, body: parentBody });
|
|
55
73
|
inReplyToId = parentComment.data.in_reply_to_id;
|
|
56
74
|
}
|
|
57
75
|
catch (error) {
|
|
@@ -59,16 +77,28 @@ async function gatherReviewCommentContext(octokit, owner, repo, commentId) {
|
|
|
59
77
|
commentId: inReplyToId,
|
|
60
78
|
error: error instanceof Error ? error.message : String(error),
|
|
61
79
|
});
|
|
80
|
+
if (mode === 'bro') {
|
|
81
|
+
throw new Error('Failed to fetch the review thread for bro mode', { cause: error });
|
|
82
|
+
}
|
|
62
83
|
break;
|
|
63
84
|
}
|
|
64
85
|
}
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
if (mode === 'bro') {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
const codeSnippet = comment.data.diff_hunk ?? '';
|
|
90
|
+
const commentBody = comment.data.body ?? '';
|
|
91
|
+
const currentCommentAuthor = comment.data.user?.login ?? 'unknown';
|
|
92
|
+
const previousDoistbotComment = findLatestDoistbotComment(threadEntries);
|
|
93
|
+
threadEntries.push({ author: currentCommentAuthor, body: commentBody });
|
|
67
94
|
const userQuestion = commentBody.replace(/@doistbot\s*/i, '').trim();
|
|
68
95
|
return {
|
|
69
|
-
threadHistory:
|
|
96
|
+
threadHistory: threadEntries
|
|
97
|
+
.map(({ author, body }) => `**${author}**: ${body}`)
|
|
98
|
+
.join('\n\n'),
|
|
70
99
|
userQuestion,
|
|
71
100
|
codeSnippet,
|
|
101
|
+
previousDoistbotComment,
|
|
72
102
|
};
|
|
73
103
|
}
|
|
74
104
|
/**
|
|
@@ -125,7 +155,7 @@ async function gatherIssueCommentContext(octokit, owner, repo, prNumber, comment
|
|
|
125
155
|
prAuthor,
|
|
126
156
|
};
|
|
127
157
|
}
|
|
128
|
-
export async function invokeChatModel(prompt) {
|
|
158
|
+
export async function invokeChatModel(prompt, mode = 'chat') {
|
|
129
159
|
const apiKey = process.env.OPENAI_KEY?.trim();
|
|
130
160
|
if (!apiKey) {
|
|
131
161
|
// Config error: no model call attempted, so no model span. The reason is
|
|
@@ -144,10 +174,16 @@ export async function invokeChatModel(prompt) {
|
|
|
144
174
|
prompt,
|
|
145
175
|
cwd: WORKSPACE_DIR,
|
|
146
176
|
timeoutMs: CHAT_TIMEOUT_MS,
|
|
147
|
-
capabilityPreset: 'workspace-inspection',
|
|
177
|
+
capabilityPreset: mode === 'bro' ? 'none' : 'workspace-inspection',
|
|
148
178
|
thinkingLevel: CHAT_THINKING_LEVEL,
|
|
149
179
|
// Keep the executed model in sync with the recorded span attr.
|
|
150
180
|
modelName: CHAT_MODEL,
|
|
181
|
+
...(mode === 'bro'
|
|
182
|
+
? {
|
|
183
|
+
systemPrompt: BRO_SYSTEM_PROMPT,
|
|
184
|
+
noContextFiles: true,
|
|
185
|
+
}
|
|
186
|
+
: {}),
|
|
151
187
|
});
|
|
152
188
|
// Record why the model stopped, before the ok/failure branch so
|
|
153
189
|
// terminal "error"/"aborted"/"length" endings are reported too.
|
|
@@ -215,6 +251,7 @@ function formatChatDryRunOutput(ctx, response) {
|
|
|
215
251
|
commentId: ctx.commentId,
|
|
216
252
|
commentType: ctx.commentType,
|
|
217
253
|
eventName: ctx.eventName,
|
|
254
|
+
mode: ctx.mode,
|
|
218
255
|
response,
|
|
219
256
|
}, null, 2);
|
|
220
257
|
}
|
|
@@ -265,6 +302,7 @@ async function main() {
|
|
|
265
302
|
task: 'chat',
|
|
266
303
|
'gen_ai.operation.name': 'invoke_agent',
|
|
267
304
|
'gen_ai.agent.name': 'doistbot.chat',
|
|
305
|
+
'chat.mode': ctx.mode,
|
|
268
306
|
'pr.number': ctx.prNumber,
|
|
269
307
|
'comment.id': ctx.commentId,
|
|
270
308
|
'comment.type': ctx.commentType,
|
|
@@ -274,47 +312,59 @@ async function main() {
|
|
|
274
312
|
// Inside the span so auth/setup failures are captured by the root span.
|
|
275
313
|
const { app, readOctokit, writeOctokit } = await createGitHubClients(ctx);
|
|
276
314
|
try {
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
315
|
+
// /bro only rewrites an existing comment, so it does not need repository contents.
|
|
316
|
+
if (ctx.mode !== 'bro') {
|
|
317
|
+
await cloneRepository({
|
|
318
|
+
app,
|
|
319
|
+
owner: ctx.owner,
|
|
320
|
+
repo: ctx.repo,
|
|
321
|
+
prNumber: ctx.prNumber,
|
|
322
|
+
installationId: ctx.installationId,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
285
325
|
// Gather thread context from GitHub API
|
|
286
326
|
let threadContext;
|
|
287
327
|
if (ctx.commentType === 'review') {
|
|
288
|
-
threadContext = await gatherReviewCommentContext(readOctokit, ctx.owner, ctx.repo, ctx.commentId);
|
|
328
|
+
threadContext = await gatherReviewCommentContext(readOctokit, ctx.owner, ctx.repo, ctx.commentId, ctx.mode);
|
|
289
329
|
}
|
|
290
330
|
else {
|
|
291
331
|
threadContext = await gatherIssueCommentContext(readOctokit, ctx.owner, ctx.repo, ctx.prNumber, ctx.commentId);
|
|
292
332
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
repo: ctx.repo,
|
|
302
|
-
prNumber: ctx.prNumber,
|
|
303
|
-
threadHistory: threadContext.threadHistory,
|
|
304
|
-
userQuestion: threadContext.userQuestion,
|
|
305
|
-
codeSnippet: threadContext.codeSnippet,
|
|
306
|
-
prTitle: threadContext.prTitle,
|
|
307
|
-
prAuthor: threadContext.prAuthor,
|
|
308
|
-
});
|
|
309
|
-
const modelResult = await invokeChatModel(prompt);
|
|
310
|
-
if (!modelResult.ok) {
|
|
311
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: modelResult.reason });
|
|
312
|
-
logger.warn('Chat model produced no usable response', {
|
|
313
|
-
reason: modelResult.reason,
|
|
333
|
+
let response;
|
|
334
|
+
if (ctx.mode === 'bro' && !threadContext.previousDoistbotComment) {
|
|
335
|
+
response = EMPTY_BRO_RESPONSE;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const promptTemplate = loadPromptTemplateFile({
|
|
339
|
+
fileName: resolvePromptFile(import.meta.url, ctx.mode === 'bro' ? BRO_PROMPT_FILE : CHAT_PROMPT_FILE),
|
|
340
|
+
promptLabel: ctx.mode,
|
|
314
341
|
});
|
|
315
|
-
|
|
342
|
+
const prompt = ctx.mode === 'bro'
|
|
343
|
+
? buildBroPrompt({
|
|
344
|
+
promptTemplate,
|
|
345
|
+
previousDoistbotComment: threadContext.previousDoistbotComment ?? '',
|
|
346
|
+
})
|
|
347
|
+
: buildPrompt({
|
|
348
|
+
promptTemplate,
|
|
349
|
+
owner: ctx.owner,
|
|
350
|
+
repo: ctx.repo,
|
|
351
|
+
prNumber: ctx.prNumber,
|
|
352
|
+
threadHistory: threadContext.threadHistory ?? '',
|
|
353
|
+
userQuestion: threadContext.userQuestion ?? '',
|
|
354
|
+
codeSnippet: threadContext.codeSnippet,
|
|
355
|
+
prTitle: threadContext.prTitle,
|
|
356
|
+
prAuthor: threadContext.prAuthor,
|
|
357
|
+
});
|
|
358
|
+
const modelResult = await invokeChatModel(prompt, ctx.mode);
|
|
359
|
+
if (!modelResult.ok) {
|
|
360
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: modelResult.reason });
|
|
361
|
+
logger.warn('Chat model produced no usable response', {
|
|
362
|
+
reason: modelResult.reason,
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
response = modelResult.output;
|
|
316
367
|
}
|
|
317
|
-
const response = modelResult.output;
|
|
318
368
|
if (ctx.dryRun) {
|
|
319
369
|
logger.info('Dry run enabled, printing chat response instead of posting to GitHub');
|
|
320
370
|
process.stdout.write(`${formatChatDryRunOutput(ctx, response)}\n`);
|
|
@@ -13,3 +13,8 @@ export function buildPrompt({ promptTemplate, owner, repo, prNumber, threadHisto
|
|
|
13
13
|
CODE_SNIPPET: codeSnippet ? `\`\`\`\n${codeSnippet}\n\`\`\`` : 'No code context available.',
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
+
export function buildBroPrompt({ promptTemplate, previousDoistbotComment, }) {
|
|
17
|
+
return fillPromptTemplate(promptTemplate, {
|
|
18
|
+
PREVIOUS_DOISTBOT_COMMENT: previousDoistbotComment,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -4,11 +4,11 @@ import { buildPiImageContent, createPiInvoker, DEFAULT_OPENROUTER_MODELS } from
|
|
|
4
4
|
import { withPhase } from '../../core/tracing.js';
|
|
5
5
|
const ISSUE_SUMMARIZE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
6
6
|
const ISSUE_SUMMARIZE_MULTIMODAL_MODEL = 'google/gemini-2.5-pro';
|
|
7
|
-
const SUMMARIZE_TEXT_PROVIDER = '
|
|
8
|
-
// Single source of truth: inherit the repo-wide
|
|
7
|
+
const SUMMARIZE_TEXT_PROVIDER = 'zai';
|
|
8
|
+
// Single source of truth: inherit the repo-wide Z.ai default so a default-model
|
|
9
9
|
// bump in core/pi.ts moves summarize too, and the executed model matches the
|
|
10
10
|
// recorded gen_ai.request.model.
|
|
11
|
-
const SUMMARIZE_TEXT_MODEL = DEFAULT_OPENROUTER_MODELS.
|
|
11
|
+
const SUMMARIZE_TEXT_MODEL = DEFAULT_OPENROUTER_MODELS.zai;
|
|
12
12
|
const SUMMARIZE_MULTIMODAL_PROVIDER = 'gcp.gemini';
|
|
13
13
|
// One model span per actual model.invoke. The final summary uses
|
|
14
14
|
// `issue-summarize.model`; each chunk summary uses `issue-summarize.chunk`.
|
|
@@ -16,7 +16,7 @@ const SUMMARIZE_MODEL_PHASE = 'issue-summarize.model';
|
|
|
16
16
|
const SUMMARIZE_CHUNK_PHASE = 'issue-summarize.chunk';
|
|
17
17
|
export function createIssueSummarizeModel(apiKey) {
|
|
18
18
|
return createPiInvoker({
|
|
19
|
-
provider: '
|
|
19
|
+
provider: 'zai',
|
|
20
20
|
apiKey,
|
|
21
21
|
piProviderId: 'openrouter',
|
|
22
22
|
cwd: process.cwd(),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseSandboxVariant } from 'doistbot-issue-routing';
|
|
1
2
|
import { optionalPositiveInt, requiredEnv, requiredPositiveInt } from '../../core/shared.js';
|
|
2
3
|
import { withPhase } from '../../core/tracing.js';
|
|
3
4
|
import { extractLabelNames, fetchIssue, fetchIssueComments, } from './shared.js';
|
|
@@ -37,6 +38,7 @@ export function parseIssueTriageContext(env = process.env) {
|
|
|
37
38
|
deliveryId,
|
|
38
39
|
dryRun: env.DRY_RUN?.toLowerCase() === 'true',
|
|
39
40
|
triggerType,
|
|
41
|
+
sandboxVariant: parseSandboxVariant(env.SANDBOX_VARIANT),
|
|
40
42
|
enableAutoFixOverride: parseOptionalBoolean(env.ENABLE_AUTO_FIX),
|
|
41
43
|
};
|
|
42
44
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'child_process';
|
|
2
2
|
import { logger } from '../../core/logging.js';
|
|
3
3
|
import { fillPromptTemplate, loadPromptTemplateFile, resolvePromptFile, } from '../../core/prompt-template.js';
|
|
4
|
+
import { findRepositoryRoutes } from '../../core/repository-map.js';
|
|
4
5
|
import { resolveAutofixCapabilityEnv } from './autofix-capabilities.js';
|
|
5
6
|
import { validateDiff } from './diff-validator.js';
|
|
6
7
|
import { CODEX_CANNOT_FIX_REASON, FIX_LOOP_FAILED_REASON_PREFIX } from './fix-failure-reasons.js';
|
|
@@ -288,7 +289,7 @@ async function postFollowUpComment({ octokit, ctx, prUrl, prNumber, targetRepo,
|
|
|
288
289
|
task: 'issue-triage',
|
|
289
290
|
issueNumber: ctx.issueNumber,
|
|
290
291
|
targetRepo: targetRepo.fullName,
|
|
291
|
-
|
|
292
|
+
routes: findRepositoryRoutes(targetRepo).map((route) => route.id),
|
|
292
293
|
});
|
|
293
294
|
}
|
|
294
295
|
const body = buildFollowUpCommentBody({
|
|
@@ -25,18 +25,15 @@ export function parsePromotionTaskInput() {
|
|
|
25
25
|
*/
|
|
26
26
|
export function buildPromotionSession({ input, octokit, }) {
|
|
27
27
|
const { owner, repo } = input.base;
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// route by surface without re-scanning PRODUCT_REPOSITORIES.
|
|
28
|
+
// Use the issue-routing catalog when the target repository is supported.
|
|
29
|
+
// The review effect can then derive its configured route and hero team.
|
|
31
30
|
const repository = findRepositoryTarget(owner, repo) ?? {
|
|
32
31
|
owner,
|
|
33
32
|
repo,
|
|
34
33
|
fullName: `${owner}/${repo}`,
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// for unknown targets.
|
|
34
|
+
// Keep the existing Product type for an unknown target. The repository
|
|
35
|
+
// does not occur in that product's routes, so hero routing uses cx-hero.
|
|
38
36
|
product: 'Todoist',
|
|
39
|
-
surfaces: [],
|
|
40
37
|
};
|
|
41
38
|
return {
|
|
42
39
|
kind: 'fix-loop-promotion',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { normalizeScopedLabelValue, scopedLabelValue } from '../../core/label-parsing.js';
|
|
2
|
-
import {
|
|
2
|
+
import { APP_LABEL_ALIASES, findIssueRoute, findRepositoryRoutes, findRepositoryTarget as findRoutingRepositoryTarget, PRODUCT_LABEL_ALIASES, PRODUCT_ROUTES, } from '../../core/repository-map.js';
|
|
3
3
|
const HERO_GROUPS = {
|
|
4
4
|
'android-hero': { key: 'android-hero', mention: '@Doist/android-hero' },
|
|
5
5
|
'app-copy-hero': { key: 'app-copy-hero', mention: '@Doist/app-copy-hero' },
|
|
@@ -39,37 +39,17 @@ const TEAM_LABEL_TO_HERO = {
|
|
|
39
39
|
product: 'product-hero',
|
|
40
40
|
support: 'cx-hero',
|
|
41
41
|
};
|
|
42
|
-
const SURFACE_TO_HERO = {
|
|
43
|
-
android: 'android-hero',
|
|
44
|
-
backend: 'backend-hero',
|
|
45
|
-
cli: 'frontend-hero',
|
|
46
|
-
desktop: 'desktop-hero',
|
|
47
|
-
ios: 'apple-hero',
|
|
48
|
-
mcp: 'frontend-hero',
|
|
49
|
-
web: 'frontend-hero',
|
|
50
|
-
};
|
|
51
42
|
export const CX_HERO_MANUAL_HANDLE = HERO_GROUPS['cx-hero'].mention;
|
|
52
43
|
/**
|
|
53
|
-
* Looks up a
|
|
54
|
-
* Returns the canonical RepositoryTarget (with surfaces) or undefined when
|
|
55
|
-
* the repo isn't in the registry. Used at session construction to populate
|
|
56
|
-
* targetRepo.repository.surfaces accurately, so downstream effects can
|
|
57
|
-
* reuse the surface map without re-scanning the registry.
|
|
44
|
+
* Looks up a repository supported by issue routing.
|
|
58
45
|
*/
|
|
59
46
|
export function findRepositoryTarget(owner, repo) {
|
|
60
|
-
|
|
61
|
-
const match = list.find((target) => target.owner === owner && target.repo === repo);
|
|
62
|
-
if (match) {
|
|
63
|
-
return match;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return undefined;
|
|
47
|
+
return findRoutingRepositoryTarget(owner, repo);
|
|
67
48
|
}
|
|
68
49
|
/**
|
|
69
|
-
* Derives the GitHub team slug
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* and for user-mention groups like dateist (`@amix`) that aren't real teams.
|
|
50
|
+
* Derives the GitHub team slug for a PR from its configured issue route.
|
|
51
|
+
* Repositories that are unknown or belong to routes with different hero teams
|
|
52
|
+
* use cx-hero because the repository alone does not give enough context.
|
|
73
53
|
*/
|
|
74
54
|
export function resolveHeroTeamSlug(target) {
|
|
75
55
|
const group = resolveHeroGroup([], target);
|
|
@@ -80,6 +60,9 @@ export function resolveHeroTeamSlug(target) {
|
|
|
80
60
|
// HeroGroupKey to HeroTeamSlug here.
|
|
81
61
|
return group.key;
|
|
82
62
|
}
|
|
63
|
+
function resolveAppLabelAlias(value) {
|
|
64
|
+
return APP_LABEL_ALIASES[normalizeScopedLabelValue(value)];
|
|
65
|
+
}
|
|
83
66
|
export function isKnownHeroRoutingLabel(label) {
|
|
84
67
|
const teamValue = scopedLabelValue(label, 'Team');
|
|
85
68
|
if (teamValue && TEAM_LABEL_TO_HERO[normalizeScopedLabelValue(teamValue)]) {
|
|
@@ -89,7 +72,50 @@ export function isKnownHeroRoutingLabel(label) {
|
|
|
89
72
|
if (!appValue) {
|
|
90
73
|
return false;
|
|
91
74
|
}
|
|
92
|
-
|
|
75
|
+
const appLabel = resolveAppLabelAlias(appValue);
|
|
76
|
+
return (appLabel !== undefined &&
|
|
77
|
+
appLabel !== 'all' &&
|
|
78
|
+
Object.values(PRODUCT_ROUTES).some((routes) => routes.some((route) => route.id === appLabel)));
|
|
79
|
+
}
|
|
80
|
+
function resolveProduct(labels) {
|
|
81
|
+
const products = new Set();
|
|
82
|
+
for (const label of labels) {
|
|
83
|
+
const productValue = scopedLabelValue(label, 'Product');
|
|
84
|
+
if (!productValue) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const product = PRODUCT_LABEL_ALIASES[normalizeScopedLabelValue(productValue)];
|
|
88
|
+
if (product) {
|
|
89
|
+
products.add(product);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return products.size === 1 ? products.values().next().value : undefined;
|
|
93
|
+
}
|
|
94
|
+
function resolveAppRouteHeroKeys(labels) {
|
|
95
|
+
const routeIds = new Set();
|
|
96
|
+
for (const label of labels) {
|
|
97
|
+
const appValue = scopedLabelValue(label, 'App');
|
|
98
|
+
if (!appValue) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const appLabel = resolveAppLabelAlias(appValue);
|
|
102
|
+
if (appLabel && appLabel !== 'all') {
|
|
103
|
+
routeIds.add(appLabel);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const product = resolveProduct(labels);
|
|
107
|
+
const heroKeys = new Set();
|
|
108
|
+
for (const routeId of routeIds) {
|
|
109
|
+
const routes = product
|
|
110
|
+
? [findIssueRoute(product, routeId)].filter((route) => route !== undefined)
|
|
111
|
+
: Object.values(PRODUCT_ROUTES)
|
|
112
|
+
.flat()
|
|
113
|
+
.filter((route) => route.id === routeId);
|
|
114
|
+
for (const route of routes) {
|
|
115
|
+
heroKeys.add(route.heroTeam);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return heroKeys;
|
|
93
119
|
}
|
|
94
120
|
export function resolveHeroGroup(labels, primaryRepo) {
|
|
95
121
|
const teamHeroKeys = new Set();
|
|
@@ -109,22 +135,20 @@ export function resolveHeroGroup(labels, primaryRepo) {
|
|
|
109
135
|
if (teamHeroKeys.size > 1) {
|
|
110
136
|
return null;
|
|
111
137
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (heroKey) {
|
|
116
|
-
return HERO_GROUPS[heroKey];
|
|
117
|
-
}
|
|
118
|
-
}
|
|
138
|
+
const appHeroKeys = resolveAppRouteHeroKeys(labels);
|
|
139
|
+
if (appHeroKeys.size === 1) {
|
|
140
|
+
return HERO_GROUPS[appHeroKeys.values().next().value];
|
|
119
141
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
if (appHeroKeys.size > 1) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (primaryRepo) {
|
|
146
|
+
const repositoryHeroKeys = new Set(findRepositoryRoutes(primaryRepo).map((route) => route.heroTeam));
|
|
147
|
+
if (repositoryHeroKeys.size === 1) {
|
|
148
|
+
return HERO_GROUPS[repositoryHeroKeys.values().next().value];
|
|
124
149
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return HERO_GROUPS[heroKey];
|
|
150
|
+
if (repositoryHeroKeys.size > 1) {
|
|
151
|
+
return null;
|
|
128
152
|
}
|
|
129
153
|
}
|
|
130
154
|
return null;
|
|
@@ -20,7 +20,7 @@ Detected labels: Product=[$PRODUCTS] App=[$APPS] Team=[$TEAMS]
|
|
|
20
20
|
Please update labels and run \`@doistbot /triage\` again.
|
|
21
21
|
Expected examples:
|
|
22
22
|
- \`Product:Todoist\` + \`App:Android\`
|
|
23
|
-
- \`Product:
|
|
23
|
+
- \`Product:Comms\` + \`App:Android\`
|
|
24
24
|
- \`Product:Todoist\` + \`App:All\``;
|
|
25
25
|
function hasHeading(body, heading) {
|
|
26
26
|
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -12,6 +12,7 @@ export async function resolveTriageScope(params) {
|
|
|
12
12
|
parsed_products: result.parsedLabels.productLabels,
|
|
13
13
|
parsed_apps: result.parsedLabels.appLabels,
|
|
14
14
|
parsed_teams: result.parsedLabels.teamLabels,
|
|
15
|
+
matched_routes: result.matchedRoutes,
|
|
15
16
|
unknown_product_labels: result.parsedLabels.unknownProductLabels,
|
|
16
17
|
unknown_app_labels: result.parsedLabels.unknownAppLabels,
|
|
17
18
|
unknown_team_labels: result.parsedLabels.unknownTeamLabels,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'child_process';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { extractIssueLabelNames } from 'doistbot-issue-routing';
|
|
4
5
|
import { App } from 'octokit';
|
|
5
6
|
import { logger } from '../../core/logging.js';
|
|
6
7
|
import { REPOS_SUBDIR, resolveWriteOctokit, WORKSPACE_DIR } from '../../core/shared.js';
|
|
@@ -10,18 +11,7 @@ export function getIssueUrl(owner, repo, issueNumber) {
|
|
|
10
11
|
return `https://github.com/${owner}/${repo}/issues/${issueNumber}`;
|
|
11
12
|
}
|
|
12
13
|
export function extractLabelNames(labels) {
|
|
13
|
-
|
|
14
|
-
return [];
|
|
15
|
-
}
|
|
16
|
-
const names = labels
|
|
17
|
-
.map((label) => {
|
|
18
|
-
if (typeof label === 'string') {
|
|
19
|
-
return label.trim();
|
|
20
|
-
}
|
|
21
|
-
return label.name?.trim() ?? '';
|
|
22
|
-
})
|
|
23
|
-
.filter((label) => label.length > 0);
|
|
24
|
-
return [...new Set(names)];
|
|
14
|
+
return extractIssueLabelNames(labels);
|
|
25
15
|
}
|
|
26
16
|
export function prepareWorkspaceReposDirectory() {
|
|
27
17
|
const reposDir = path.join(WORKSPACE_DIR, REPOS_SUBDIR);
|
|
@@ -430,6 +430,7 @@ async function runIssueTriagePipeline(ctx, mode) {
|
|
|
430
430
|
model: TRIAGE_PROVIDER,
|
|
431
431
|
outcome: pendingFixMetric.outcome,
|
|
432
432
|
durationMs: performance.now() - pendingFixMetric.startedAt,
|
|
433
|
+
sandboxVariant: ctx.sandboxVariant,
|
|
433
434
|
});
|
|
434
435
|
}
|
|
435
436
|
if (shouldRecordTriageMetric) {
|
|
@@ -439,6 +440,7 @@ async function runIssueTriagePipeline(ctx, mode) {
|
|
|
439
440
|
model: TRIAGE_PROVIDER,
|
|
440
441
|
outcome: triageMetricOutcome,
|
|
441
442
|
durationMs: triageDurationMs ?? performance.now() - startedAt,
|
|
443
|
+
sandboxVariant: ctx.sandboxVariant,
|
|
442
444
|
});
|
|
443
445
|
}
|
|
444
446
|
}
|