@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.
Files changed (42) hide show
  1. package/dist/actions/review-usage.js +4 -4
  2. package/dist/actions/review.js +31 -21
  3. package/dist/config.js +61 -16
  4. package/dist/index.js +8 -2
  5. package/dist/terminal.js +4 -0
  6. package/package.json +10 -9
  7. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
  8. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +1 -1
  9. package/sandbox/dist/core/datadog-metrics.js +7 -2
  10. package/sandbox/dist/core/label-parsing.js +1 -22
  11. package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
  12. package/sandbox/dist/core/pi-progress-logging.js +4 -0
  13. package/sandbox/dist/core/pi.js +286 -67
  14. package/sandbox/dist/core/repository-map.js +1 -220
  15. package/sandbox/dist/core/repository-resolver.js +1 -166
  16. package/sandbox/dist/providers/config.js +1 -1
  17. package/sandbox/dist/providers/helpers.js +3 -0
  18. package/sandbox/dist/providers/pi-review.js +53 -3
  19. package/sandbox/dist/providers/schemas.js +7 -0
  20. package/sandbox/dist/providers/types.js +4 -1
  21. package/sandbox/dist/tasks/chat/chat.js +95 -45
  22. package/sandbox/dist/tasks/chat/prompt.js +5 -0
  23. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +1 -0
  24. package/sandbox/dist/tasks/issue-summarize/model.js +4 -4
  25. package/sandbox/dist/tasks/issue-triage/context.js +2 -0
  26. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +2 -1
  27. package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +4 -7
  28. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +65 -41
  29. package/sandbox/dist/tasks/issue-triage/output.js +1 -1
  30. package/sandbox/dist/tasks/issue-triage/resolution.js +1 -0
  31. package/sandbox/dist/tasks/issue-triage/shared.js +2 -12
  32. package/sandbox/dist/tasks/issue-triage/triage.js +2 -0
  33. package/sandbox/dist/tasks/review/diff.js +1 -1
  34. package/sandbox/dist/tasks/review/engines/multi-focus.js +47 -12
  35. package/sandbox/dist/tasks/review/local-git.js +62 -8
  36. package/sandbox/dist/tasks/review/prompt.js +2 -0
  37. package/sandbox/dist/tasks/review/review-summary.js +1 -1
  38. package/sandbox/dist/tasks/review/summary-model.js +2 -2
  39. package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
  40. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
  41. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +1 -1
  42. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +18 -1
@@ -1,5 +1,6 @@
1
1
  import { SpanStatusCode } from '@opentelemetry/api';
2
2
  import { logger } from '../../../core/logging.js';
3
+ import { toPiInvocationTelemetryFields } from '../../../core/pi.js';
3
4
  import { genAiProviderName, withPhase } from '../../../core/tracing.js';
4
5
  import { convertFindingsToComments } from '../comment-mapper.js';
5
6
  import { buildReviewOutputFindings } from '../finding-output.js';
@@ -7,16 +8,17 @@ import { buildMultiFocusPrompt, REVIEW_FOCUS_DEFINITIONS, } from '../multi-focus
7
8
  import { buildFallbackSummary, summarizeReviewFindings } from '../review-summary.js';
8
9
  import { parseSeverity } from '../severity.js';
9
10
  import { buildGeminiFlashReviewSummaryModel } from '../summary-model.js';
11
+ import { filterTranslationSyncOnlyFindings } from '../translation-review-policy.js';
10
12
  import { dedupeMultiFocusFindings } from './dedupe.js';
11
13
  import { buildGeminiFlashCredential, getApiKeys, getAvailableModels, getEnabledReviewModels, getOpenRouterReviewCredential, getProviderFailures, requestModelReview, } from './shared.js';
12
14
  const DEFAULT_REVIEW_MODELS_BY_FOCUS = {
13
- general: ['deepseek', 'grok', 'openai'],
14
- security: ['deepseek', 'grok', 'openai'],
15
- reuse: ['deepseek', 'grok', 'openai'],
16
- quality: ['deepseek', 'grok', 'openai'],
17
- efficiency: ['deepseek', 'grok', 'openai'],
18
- tests: ['deepseek', 'grok'],
19
- standards: ['deepseek', 'grok'],
15
+ general: ['deepseek', 'zai', 'openai'],
16
+ security: ['deepseek', 'zai', 'openai'],
17
+ reuse: ['deepseek', 'zai', 'openai'],
18
+ quality: ['deepseek', 'zai', 'openai'],
19
+ efficiency: ['deepseek', 'zai', 'openai'],
20
+ tests: ['deepseek', 'zai'],
21
+ standards: ['deepseek', 'zai'],
20
22
  };
21
23
  // Default Gemini-flash pass on failure. tests/standards keep no fallback unless
22
24
  // a caller opts into a wider fallback set.
@@ -28,7 +30,17 @@ const DEFAULT_GEMINI_FLASH_FALLBACK_FOCUSES = [
28
30
  'efficiency',
29
31
  ];
30
32
  // Open models whose failed pass triggers that fallback.
31
- const GEMINI_FLASH_FALLBACK_MODELS = ['deepseek', 'grok'];
33
+ const GEMINI_FLASH_FALLBACK_MODELS = ['deepseek', 'zai'];
34
+ function setReviewLatencyAttributes(span, result) {
35
+ const telemetry = result.telemetry;
36
+ if (!telemetry)
37
+ return;
38
+ const attributes = Object.fromEntries(Object.entries(toPiInvocationTelemetryFields(telemetry)).map(([name, value]) => [
39
+ `review.${name}`,
40
+ value,
41
+ ]));
42
+ span.setAttributes(attributes);
43
+ }
32
44
  function lowPrioritySummaryNotice(count) {
33
45
  return count === 1
34
46
  ? 'I also left one optional follow-up note in the details below.'
@@ -60,6 +72,9 @@ async function runFocusPass({ focus, model, basePrompt, standardsSection, apiKey
60
72
  if (result?.requestModel) {
61
73
  span.setAttributes({ 'gen_ai.request.model': result.requestModel });
62
74
  }
75
+ if (result) {
76
+ setReviewLatencyAttributes(span, result);
77
+ }
63
78
  if (!result) {
64
79
  const error = `No review result returned for ${model}`;
65
80
  span.setStatus({ code: SpanStatusCode.ERROR, message: error });
@@ -326,8 +341,26 @@ export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTit
326
341
  providerFailures,
327
342
  };
328
343
  }
329
- const allFindings = successfulRuns.flatMap((run) => run.findings);
330
- const reviewObservations = collectReviewObservations(successfulRuns);
344
+ const modelFindings = successfulRuns.flatMap((run) => run.findings);
345
+ const { findings: allFindings, droppedFindings: translationSyncFindings } = filterTranslationSyncOnlyFindings({
346
+ repoOwner: ctx.owner,
347
+ repoName: ctx.repo,
348
+ findings: modelFindings,
349
+ });
350
+ if (translationSyncFindings.length > 0) {
351
+ logger.info('Filtered translation sync-only findings', {
352
+ repository: `${ctx.owner}/${ctx.repo}`,
353
+ droppedCount: translationSyncFindings.length,
354
+ droppedFindingIds: translationSyncFindings.map((finding) => finding.id),
355
+ });
356
+ }
357
+ const suppressedFindings = new Set(translationSyncFindings);
358
+ const reviewObservationRuns = successfulRuns.map((run) => {
359
+ const hasSuppressedFinding = run.findings.some((finding) => suppressedFindings.has(finding));
360
+ const hasKeptFinding = run.findings.some((finding) => !suppressedFindings.has(finding));
361
+ return hasSuppressedFinding && !hasKeptFinding ? { ...run, summary: '' } : run;
362
+ });
363
+ const reviewObservations = collectReviewObservations(reviewObservationRuns);
331
364
  let dedupedFindings = allFindings;
332
365
  try {
333
366
  dedupedFindings = await dedupeMultiFocusFindings({
@@ -347,7 +380,7 @@ export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTit
347
380
  const routedFindings = routeLowPriorityFindings(dedupedFindings, lowPriorityFindingPlacement);
348
381
  const geminiSummaryFallback = buildGeminiFlashReviewSummaryModel(apiKeys.gemini);
349
382
  const summaryModel = ctx.reviewSummaryModel ??
350
- (apiKeys.grok ? { provider: 'grok', credential: apiKeys.grok } : undefined);
383
+ (apiKeys.zai ? { provider: 'zai', credential: apiKeys.zai } : undefined);
351
384
  const review = await buildMultiFocusReviewOutput({
352
385
  inlineFindings: routedFindings.inlineFindings,
353
386
  summaryFindings: routedFindings.summaryFindings,
@@ -366,7 +399,9 @@ export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTit
366
399
  supportModels: allAvailableModels,
367
400
  focuses: focusDefinitions.map((focus) => focus.name),
368
401
  successfulRuns: successfulRuns.length,
369
- findingsCount: allFindings.length,
402
+ findingsCount: modelFindings.length,
403
+ findingsAfterPolicyCount: allFindings.length,
404
+ translationSyncFindingsCount: translationSyncFindings.length,
370
405
  dedupedFindingsCount: dedupedFindings.length,
371
406
  inlineFindingsCount: routedFindings.inlineFindings.length,
372
407
  summaryOnlyFindingsCount: routedFindings.summaryFindings.length,
@@ -1,14 +1,36 @@
1
1
  import { execFileSync } from 'child_process';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import {} from './diff.js';
4
+ import { MAX_DIFF_CHARS } from './diff.js';
5
5
  const GIT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
6
6
  export function createGitRunner(workspaceDir) {
7
- return (args) => execFileSync('git', ['-C', workspaceDir, '-c', 'core.quotePath=false', ...args], {
8
- encoding: 'utf8',
9
- maxBuffer: GIT_MAX_BUFFER_BYTES,
10
- stdio: ['ignore', 'pipe', 'pipe'],
11
- });
7
+ return (args, options) => {
8
+ try {
9
+ return execFileSync('git', ['-C', workspaceDir, '-c', 'core.quotePath=false', ...args], {
10
+ encoding: 'utf8',
11
+ maxBuffer: options?.maxBufferBytes ?? GIT_MAX_BUFFER_BYTES,
12
+ stdio: ['ignore', 'pipe', 'pipe'],
13
+ });
14
+ }
15
+ catch (error) {
16
+ const failure = error;
17
+ const stdout = typeof failure.stdout === 'string'
18
+ ? failure.stdout
19
+ : Buffer.isBuffer(failure.stdout)
20
+ ? failure.stdout.toString('utf8')
21
+ : '';
22
+ if (failure.code === 'ENOBUFS' &&
23
+ options?.returnPartialOutputOnMaxBuffer === true &&
24
+ options.maxBufferBytes !== undefined) {
25
+ return stdout.slice(0, options.maxBufferBytes);
26
+ }
27
+ if (typeof failure.status === 'number' &&
28
+ options?.allowedExitCodes?.includes(failure.status)) {
29
+ return stdout;
30
+ }
31
+ throw error;
32
+ }
33
+ };
12
34
  }
13
35
  export function tryGit(runner, args) {
14
36
  try {
@@ -75,7 +97,7 @@ function unquoteGitPath(rawPath) {
75
97
  unquoted = trimmed.slice(1, -1);
76
98
  }
77
99
  }
78
- return unquoted.replace(/^[ab]\//, '');
100
+ return unquoted.replace(/^[ab]\//, '').replace(/^\.\//, '');
79
101
  }
80
102
  function readDiffGitPath(input, startIndex) {
81
103
  let index = startIndex;
@@ -162,6 +184,38 @@ export function parseGitDiffToFiles(diff) {
162
184
  flushCurrent();
163
185
  return files;
164
186
  }
187
+ function getUntrackedReviewFiles(runner) {
188
+ const untrackedPaths = runner(['ls-files', '--others', '--exclude-standard', '-z'])
189
+ .split('\0')
190
+ .filter(Boolean);
191
+ if (untrackedPaths.length === 0) {
192
+ return [];
193
+ }
194
+ const files = [];
195
+ let remainingDiffChars = MAX_DIFF_CHARS;
196
+ for (const filename of untrackedPaths) {
197
+ if (remainingDiffChars <= 0) {
198
+ break;
199
+ }
200
+ // The two-file form also works with Git versions before 2.51.
201
+ const diff = runner([
202
+ 'diff',
203
+ '--no-index',
204
+ '--no-ext-diff',
205
+ '--no-color',
206
+ '--',
207
+ '/dev/null',
208
+ `./${filename}`,
209
+ ], {
210
+ allowedExitCodes: [1],
211
+ maxBufferBytes: remainingDiffChars,
212
+ returnPartialOutputOnMaxBuffer: true,
213
+ });
214
+ files.push(...parseGitDiffToFiles(diff));
215
+ remainingDiffChars -= diff.length;
216
+ }
217
+ return files;
218
+ }
165
219
  export function getLocalReviewFiles({ runner, source, }) {
166
220
  if (source === 'staged') {
167
221
  const diff = runner(['diff', '--staged', '--no-ext-diff', '--no-color']);
@@ -183,7 +237,7 @@ export function getLocalReviewFiles({ runner, source, }) {
183
237
  const mergeBase = runner(['merge-base', baseBranch, 'HEAD']).trim();
184
238
  const diff = runner(['diff', '--no-ext-diff', '--no-color', mergeBase]);
185
239
  return {
186
- files: parseGitDiffToFiles(diff),
240
+ files: [...parseGitDiffToFiles(diff), ...getUntrackedReviewFiles(runner)],
187
241
  baseBranch,
188
242
  };
189
243
  }
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { logger } from '../../core/logging.js';
5
+ import { buildTranslationReviewGuidance } from './translation-review-policy.js';
5
6
  const MAX_BODY_CHARS = 4000;
6
7
  let cachedStandardsSection = null;
7
8
  const REVIEW_TASK_DIR = path.dirname(fileURLToPath(import.meta.url));
@@ -75,5 +76,6 @@ export function buildReviewPromptVariables({ repoOwner, repoName, prNumber, prTi
75
76
  PR_CONVERSATION: formatPrConversation(prConversation),
76
77
  DIFF_SECTION: fullDiffSection,
77
78
  STANDARDS_SECTION: standardsSection,
79
+ TRANSLATION_REVIEW_GUIDANCE: buildTranslationReviewGuidance(repoOwner, repoName),
78
80
  };
79
81
  }
@@ -51,7 +51,7 @@ Keep it human and compact: the opener should be 1 sentence, or 2 only if the PR
51
51
  Respond with only valid JSON in this exact shape:
52
52
  {"summary":"<your summary>","comments":[]}`;
53
53
  }
54
- export async function summarizeReviewFindings({ credential, provider = 'grok', modelName, fallbackModels = [], prTitle, prBody, findings, reviewObservations, workspaceDir, requestReview = requestReviewSummary, }) {
54
+ export async function summarizeReviewFindings({ credential, provider = 'zai', modelName, fallbackModels = [], prTitle, prBody, findings, reviewObservations, workspaceDir, requestReview = requestReviewSummary, }) {
55
55
  const prompt = buildReviewSummaryPrompt({
56
56
  prTitle,
57
57
  prBody,
@@ -56,7 +56,7 @@ function getRequestFailureReason(result) {
56
56
  ? 'no_assistant_response'
57
57
  : 'request_failed';
58
58
  }
59
- export async function requestReviewSummary({ credential, provider = 'grok', modelName, prompt, workspaceDir, }) {
59
+ export async function requestReviewSummary({ credential, provider = 'zai', modelName, prompt, workspaceDir, }) {
60
60
  const resolvedCredential = resolveReviewProviderCredential(credential);
61
61
  if (!resolvedCredential) {
62
62
  return failedReviewSummaryRequest({ reason: 'missing_credential' });
@@ -93,7 +93,7 @@ export async function requestReviewSummary({ credential, provider = 'grok', mode
93
93
  }
94
94
  async function runReviewSummaryRequest({ provider, resolvedCredential, resolvedModelName, logModelName, prompt, workspaceDir, }) {
95
95
  try {
96
- const thinkingLevel = provider === 'grok' ? 'high' : DEFAULT_REVIEW_SUMMARY_THINKING_LEVEL;
96
+ const thinkingLevel = provider === 'zai' ? 'high' : DEFAULT_REVIEW_SUMMARY_THINKING_LEVEL;
97
97
  logger.info('Review summary model request', {
98
98
  provider,
99
99
  modelName: logModelName,
@@ -0,0 +1,37 @@
1
+ const ENGLISH_SOURCE_FIRST_REPOSITORIES = new Set([
2
+ 'doist/todoist-web',
3
+ 'doist/todoist-android',
4
+ 'doist/todoist-ios',
5
+ 'doist/todoist-electron',
6
+ 'doist/todoist-macos',
7
+ ]);
8
+ export function usesEnglishSourceFirstTranslations(repoOwner, repoName) {
9
+ return ENGLISH_SOURCE_FIRST_REPOSITORIES.has(`${repoOwner}/${repoName}`.toLowerCase());
10
+ }
11
+ export function buildTranslationReviewGuidance(repoOwner, repoName) {
12
+ if (!usesEnglishSourceFirstTranslations(repoOwner, repoName)) {
13
+ return '';
14
+ }
15
+ return `## Repository-specific Translation Workflow
16
+
17
+ This repository uses an English-source-first workflow: feature PRs change the English source strings, automation sends them to the repository's translation service, and translated locale files arrive through a later automated sync.
18
+
19
+ - Do not create a finding or summary note merely because translated locale files or generated locale bundles are unchanged in the feature PR.
20
+ - Do not ask the author to pull, update, generate, or commit translations manually. If the PR description already mentions the translation-service follow-up, including Transifex, do not repeat it.
21
+ - Continue to report concrete i18n implementation errors introduced by the diff, such as incorrect translation API or gettext usage, broken interpolation placeholders, pluralization or context mistakes, and patterns that prevent source-string extraction.`;
22
+ }
23
+ export function isTranslationSyncOnlyFinding(repoOwner, repoName, finding) {
24
+ return (usesEnglishSourceFirstTranslations(repoOwner, repoName) &&
25
+ finding.translationResolution === 'automated_sync_resolves');
26
+ }
27
+ export function filterTranslationSyncOnlyFindings({ repoOwner, repoName, findings, }) {
28
+ const keptFindings = [];
29
+ const droppedFindings = [];
30
+ for (const finding of findings) {
31
+ const destination = isTranslationSyncOnlyFinding(repoOwner, repoName, finding)
32
+ ? droppedFindings
33
+ : keptFindings;
34
+ destination.push(finding);
35
+ }
36
+ return { findings: keptFindings, droppedFindings };
37
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Logger } from 'doistbot-logging';
2
2
  import type { Octokit } from 'octokit';
3
3
  export declare const CONFIG_FILE_PATH = ".github/doistbot.yml";
4
- export declare const DEFAULT_REVIEW_MODELS: readonly ["openai", "deepseek", "grok"];
4
+ export declare const DEFAULT_REVIEW_MODELS: readonly ["openai", "deepseek", "zai"];
5
5
  export type DefaultReviewModel = (typeof DEFAULT_REVIEW_MODELS)[number];
6
6
  export type RepoConfig = {
7
7
  review?: {
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { load as yamlLoad } from 'js-yaml';
4
4
  export const CONFIG_FILE_PATH = '.github/doistbot.yml';
5
- export const DEFAULT_REVIEW_MODELS = ['openai', 'deepseek', 'grok'];
5
+ export const DEFAULT_REVIEW_MODELS = ['openai', 'deepseek', 'zai'];
6
6
  function parseRepoConfigContent(content) {
7
7
  return yamlLoad(content);
8
8
  }
@@ -73,6 +73,8 @@ $DIFF_SECTION
73
73
  - Keep code ranges tight and comments easy to understand without close reading.
74
74
  - **Do not flag dependency or tooling versions as nonexistent, outdated, or "too new."** Your training data has a cutoff, so you can't know which versions of GitHub Actions, npm/PyPI/etc. packages, or their APIs are currently published. Never tell the author a version doesn't exist, is wrong, or should be downgraded based on your own knowledge — and never claim a library feature or API is unavailable because you assume the project uses an older major version. Raise a version-related finding only when the diff or checked-out repo contains concrete contradicting evidence (e.g., the manifest/lockfile pins a different version, or the referenced symbol is absent from the dependency's actual source on disk).
75
75
 
76
+ $TRANSLATION_REVIEW_GUIDANCE
77
+
76
78
  ## Writing Style
77
79
 
78
80
  - Write clearly, concisely, and directly. Be engaging without sounding generic.
@@ -97,6 +99,20 @@ At the beginning of each finding title, tag it with a priority level:
97
99
 
98
100
  ## Output Format
99
101
 
102
+ For every finding, answer this internal classification question: assuming the repository's normal
103
+ translation automation completes successfully, would the issue be fully resolved without changing
104
+ application code, source-string keys, extraction configuration, lookup behavior, placeholders,
105
+ pluralization, or locale context?
106
+
107
+ - Use `automated_sync_resolves` only when the answer is clearly yes.
108
+ - Use `code_change_required_or_uncertain` for all non-translation findings, concrete i18n
109
+ correctness problems, and any uncertain case.
110
+ - Keep this classification out of the finding body.
111
+ - Missing or stale translated content, requests to pull or commit locale artifacts, and advice to
112
+ wait for the automated sync are `automated_sync_resolves` when no code-level problem remains.
113
+ - Broken extraction, keys, lookups, placeholders, pluralization, or locale handling are
114
+ `code_change_required_or_uncertain`, even when their symptom is a missing translation.
115
+
100
116
  Return strict JSON in this exact shape:
101
117
 
102
118
  ```json
@@ -107,7 +123,8 @@ Return strict JSON in this exact shape:
107
123
  "path": "relative file path exactly as shown in the diff above",
108
124
  "line": 42,
109
125
  "side": "RIGHT",
110
- "body": "[P2] Brief description of the issue and actionable fix."
126
+ "body": "[P2] Brief description of the issue and actionable fix.",
127
+ "translationResolution": "code_change_required_or_uncertain"
111
128
  }
112
129
  ]
113
130
  }