@doist/doistbot-cli 1.0.5 → 1.0.7

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 (61) hide show
  1. package/dist/actions/auth.js +78 -24
  2. package/dist/actions/doctor.js +14 -7
  3. package/dist/actions/review.js +67 -10
  4. package/dist/auth.js +51 -6
  5. package/dist/config.js +77 -5
  6. package/dist/env.js +1 -0
  7. package/dist/runtime.js +1 -1
  8. package/dist/terminal.js +2 -0
  9. package/package.json +4 -4
  10. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  11. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -20
  12. package/sandbox/dist/core/check-run.js +16 -0
  13. package/sandbox/dist/core/datadog-metrics.js +6 -52
  14. package/sandbox/dist/core/logger-span-processor.js +78 -0
  15. package/sandbox/dist/core/pi.js +285 -174
  16. package/sandbox/dist/core/prompt-template.js +45 -3
  17. package/sandbox/dist/core/repository-map.js +24 -0
  18. package/sandbox/dist/core/tracing-setup.js +15 -0
  19. package/sandbox/dist/core/tracing.js +80 -0
  20. package/sandbox/dist/main.js +4 -1
  21. package/sandbox/dist/providers/credentials.js +12 -2
  22. package/sandbox/dist/providers/gemini.js +5 -1
  23. package/sandbox/dist/providers/helpers.js +63 -17
  24. package/sandbox/dist/providers/index.js +4 -0
  25. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  26. package/sandbox/dist/providers/pi-review.js +19 -2
  27. package/sandbox/dist/tasks/chat/chat.js +173 -102
  28. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
  29. package/sandbox/dist/tasks/issue-summarize/model.js +120 -49
  30. package/sandbox/dist/tasks/issue-summarize/prompt.js +2 -2
  31. package/sandbox/dist/tasks/issue-summarize/summarize.js +104 -88
  32. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  33. package/sandbox/dist/tasks/issue-triage/context.js +25 -1
  34. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +33 -10
  35. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
  36. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
  37. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +2 -0
  38. package/sandbox/dist/tasks/issue-triage/model.js +120 -36
  39. package/sandbox/dist/tasks/issue-triage/output.js +27 -0
  40. package/sandbox/dist/tasks/issue-triage/pr-creator.js +5 -4
  41. package/sandbox/dist/tasks/issue-triage/prompt.js +2 -2
  42. package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
  43. package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
  44. package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
  45. package/sandbox/dist/tasks/issue-triage/triage.js +290 -420
  46. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +3 -3
  47. package/sandbox/dist/tasks/review/check-run.js +2 -1
  48. package/sandbox/dist/tasks/review/conversation-context.js +2 -1
  49. package/sandbox/dist/tasks/review/engines/dedupe.js +110 -44
  50. package/sandbox/dist/tasks/review/engines/multi-focus.js +254 -113
  51. package/sandbox/dist/tasks/review/engines/shared.js +43 -6
  52. package/sandbox/dist/tasks/review/github-comment-format.js +72 -10
  53. package/sandbox/dist/tasks/review/local.js +7 -4
  54. package/sandbox/dist/tasks/review/review-summary.js +48 -7
  55. package/sandbox/dist/tasks/review/review.js +267 -186
  56. package/sandbox/dist/tasks/review/runner.js +33 -7
  57. package/sandbox/dist/tasks/review/summary-model.js +189 -20
  58. package/sandbox/dist/tasks/review/thinking-level.js +4 -0
  59. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  60. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
  61. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +1 -0
@@ -1,13 +1,15 @@
1
+ import { createHash } from 'crypto';
1
2
  import fs from 'fs';
2
3
  import { logger } from '../../core/logging.js';
3
- import { hasReviewProviderCredential } from '../../providers/credentials.js';
4
4
  import { runMultiFocusReviewEngine } from './engines/multi-focus.js';
5
+ import { getEnabledReviewModels } from './engines/shared.js';
5
6
  import {} from './engines/types.js';
6
7
  import { buildAnnotatedDiff, buildDiffSection } from './diff.js';
7
8
  import { buildMultiFocusSharedPrompt } from './multi-focus-prompt.js';
8
9
  import { loadStandardsSection } from './prompt.js';
9
10
  const REVIEW_GUIDANCE_FILES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
10
11
  const MASKED_PR_AUTHOR_LABEL = 'PR author';
12
+ const OPENROUTER_SESSION_REPO_SEGMENT_MAX_LENGTH = 96;
11
13
  function getGuidanceFilePresence(workspaceDir) {
12
14
  return Object.fromEntries(REVIEW_GUIDANCE_FILES.map((fileName) => [
13
15
  fileName,
@@ -47,12 +49,23 @@ function buildEnginePrompt({ input, diffSection, truncated, standardsSection, pr
47
49
  prompt: buildMultiFocusSharedPrompt(sharedPromptArgs),
48
50
  };
49
51
  }
50
- function getEnabledReviewModels({ apiKeys, reviewModelsEnabled, }) {
51
- return reviewModelsEnabled.filter((model) => hasReviewProviderCredential(apiKeys[model]));
52
- }
53
52
  function getMissingRequiredReviewModels(availableModels, requiredReviewModels = []) {
54
53
  return requiredReviewModels.filter((model) => !availableModels.includes(model));
55
54
  }
55
+ function toOpenRouterSessionSegment(value, fallback) {
56
+ const normalized = value
57
+ .trim()
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9._-]+/g, '-')
60
+ .replace(/^-+|-+$/g, '')
61
+ .slice(0, OPENROUTER_SESSION_REPO_SEGMENT_MAX_LENGTH);
62
+ return normalized || fallback;
63
+ }
64
+ export function buildOpenRouterReviewSessionId({ owner, repo, prNumber, prompt, }) {
65
+ const repoSegment = toOpenRouterSessionSegment(`${owner}-${repo}`, 'repo');
66
+ const promptHash = createHash('sha256').update(prompt).digest('hex').slice(0, 32);
67
+ return `doistbot-review-${repoSegment}-pr-${prNumber}-${promptHash}`;
68
+ }
56
69
  export async function runSharedReview(input) {
57
70
  const { annotatedFiles, lineLookup } = buildAnnotatedDiff(input.files);
58
71
  if (annotatedFiles.length === 0) {
@@ -76,10 +89,11 @@ export async function runSharedReview(input) {
76
89
  providerFailures: [],
77
90
  };
78
91
  }
79
- const configuredAvailableModels = getEnabledReviewModels(input);
92
+ const configuredAvailableModels = getEnabledReviewModels(input.apiKeys, input.reviewModelsEnabled);
80
93
  if (configuredAvailableModels.length === 0) {
81
94
  logger.error('No enabled review models with API keys are available, skipping review', {
82
95
  review_models_enabled: input.reviewModelsEnabled,
96
+ review_focuses_enabled: input.reviewFocusesEnabled,
83
97
  });
84
98
  return {
85
99
  status: 'skipped',
@@ -94,6 +108,7 @@ export async function runSharedReview(input) {
94
108
  if (missingRequiredModels.length > 0) {
95
109
  logger.error('Required review models with API keys are unavailable, skipping review', {
96
110
  review_models_enabled: input.reviewModelsEnabled,
111
+ review_focuses_enabled: input.reviewFocusesEnabled,
97
112
  available_models: configuredAvailableModels,
98
113
  missing_required_models: missingRequiredModels,
99
114
  });
@@ -134,9 +149,19 @@ export async function runSharedReview(input) {
134
149
  repo: input.repo,
135
150
  prNumber: input.prNumber,
136
151
  workspaceDir: input.workspaceDir,
137
- geminiCredential: input.apiKeys.gemini,
138
- openaiApiKey: input.apiKeys.openai,
152
+ apiKeys: input.apiKeys,
139
153
  reviewModelsEnabled: input.reviewModelsEnabled,
154
+ ...(input.reviewModelsByFocus ? { reviewModelsByFocus: input.reviewModelsByFocus } : {}),
155
+ ...(input.geminiFallbackFocuses
156
+ ? { geminiFallbackFocuses: input.geminiFallbackFocuses }
157
+ : {}),
158
+ ...(input.reviewSummaryModel ? { reviewSummaryModel: input.reviewSummaryModel } : {}),
159
+ openRouterSessionId: buildOpenRouterReviewSessionId({
160
+ owner: input.owner,
161
+ repo: input.repo,
162
+ prNumber: input.prNumber,
163
+ prompt,
164
+ }),
140
165
  };
141
166
  const { review, availableModels, metricModels, mode, providerFailures } = await runMultiFocusReviewEngine({
142
167
  ctx: engineContext,
@@ -146,6 +171,7 @@ export async function runSharedReview(input) {
146
171
  prTitle: input.prTitle,
147
172
  prBody: input.prBody,
148
173
  lowPriorityFindingPlacement: input.lowPriorityFindingPlacement,
174
+ enabledFocuses: input.reviewFocusesEnabled,
149
175
  onProgress: input.onProgress,
150
176
  });
151
177
  if (availableModels.length === 0) {
@@ -1,8 +1,14 @@
1
+ import { SpanStatusCode } from '@opentelemetry/api';
1
2
  import { logger } from '../../core/logging.js';
2
- import { invokePiPrompt } from '../../core/pi.js';
3
+ import { DEFAULT_GEMINI_FLASH_MODEL, DEFAULT_OPENROUTER_MODELS, DEFAULT_PI_MODELS, invokePiPrompt, PI_PROVIDER_BY_DOISTBOT_PROVIDER, } from '../../core/pi.js';
3
4
  import { WORKSPACE_DIR } from '../../core/shared.js';
5
+ import { genAiProviderName, withPhase } from '../../core/tracing.js';
4
6
  import { resolveReviewProviderCredential } from '../../providers/credentials.js';
7
+ import { extractJsonCandidatesFromResponse } from '../../providers/helpers.js';
5
8
  const REVIEW_SUMMARY_TIMEOUT_MS = 5 * 60 * 1000;
9
+ const REVIEW_SUMMARY_THINKING_LEVEL = 'medium';
10
+ // One model span per summary attempt; a fallback chain emits one span per model.
11
+ const SUMMARY_PHASE = 'review.summary';
6
12
  const DIFF_SIDE = {
7
13
  LEFT: 'LEFT',
8
14
  RIGHT: 'RIGHT',
@@ -10,53 +16,216 @@ const DIFF_SIDE = {
10
16
  function getWorkspaceDir() {
11
17
  return WORKSPACE_DIR;
12
18
  }
13
- export async function requestReviewSummary({ credential, prompt, workspaceDir, }) {
14
- try {
15
- const resolvedCredential = resolveReviewProviderCredential(credential);
16
- if (!resolvedCredential) {
17
- return null;
19
+ function getReviewSummaryModelNameForLog({ provider, piProviderId, modelName, }) {
20
+ if (modelName) {
21
+ return modelName;
22
+ }
23
+ const resolvedPiProviderId = piProviderId ?? PI_PROVIDER_BY_DOISTBOT_PROVIDER[provider];
24
+ return ((resolvedPiProviderId === 'openrouter' ? DEFAULT_OPENROUTER_MODELS[provider] : undefined) ??
25
+ DEFAULT_PI_MODELS[provider]);
26
+ }
27
+ export function buildGeminiFlashReviewSummaryModel(credential) {
28
+ if (credential === undefined) {
29
+ return undefined;
30
+ }
31
+ const resolvedCredential = resolveReviewProviderCredential(credential);
32
+ if (!resolvedCredential) {
33
+ return undefined;
34
+ }
35
+ return {
36
+ provider: 'gemini',
37
+ credential,
38
+ modelName: DEFAULT_GEMINI_FLASH_MODEL,
39
+ };
40
+ }
41
+ function failedReviewSummaryRequest({ reason, error, timedOut, durationMs, outputChars, }) {
42
+ return {
43
+ status: 'failed',
44
+ reason,
45
+ ...(error ? { error } : {}),
46
+ ...(timedOut !== undefined ? { timedOut } : {}),
47
+ ...(durationMs !== undefined ? { durationMs } : {}),
48
+ ...(outputChars !== undefined ? { outputChars } : {}),
49
+ };
50
+ }
51
+ function getRequestFailureReason(result) {
52
+ if (result.timedOut) {
53
+ return 'timeout';
54
+ }
55
+ return result.error.toLowerCase().includes('did not produce an assistant response')
56
+ ? 'no_assistant_response'
57
+ : 'request_failed';
58
+ }
59
+ export async function requestReviewSummary({ credential, provider = 'zai', modelName, prompt, workspaceDir, }) {
60
+ const resolvedCredential = resolveReviewProviderCredential(credential);
61
+ if (!resolvedCredential) {
62
+ return failedReviewSummaryRequest({ reason: 'missing_credential' });
63
+ }
64
+ const resolvedModelName = modelName ?? resolvedCredential.modelName;
65
+ const logModelName = getReviewSummaryModelNameForLog({
66
+ provider,
67
+ piProviderId: resolvedCredential.piProviderId,
68
+ modelName: resolvedModelName,
69
+ });
70
+ // Span wraps the actual model invoke (after the credential check), so a
71
+ // missing-credential config error returns before any span and never shows a fake model attempt.
72
+ return withPhase(SUMMARY_PHASE, {
73
+ 'gen_ai.operation.name': 'chat',
74
+ 'gen_ai.provider.name': genAiProviderName(provider),
75
+ 'gen_ai.request.model': logModelName,
76
+ }, async (span) => {
77
+ const result = await runReviewSummaryRequest({
78
+ provider,
79
+ resolvedCredential,
80
+ resolvedModelName,
81
+ logModelName,
82
+ prompt,
83
+ workspaceDir,
84
+ });
85
+ if (result.status === 'failed') {
86
+ span.setStatus({
87
+ code: SpanStatusCode.ERROR,
88
+ message: result.error ?? result.reason,
89
+ });
18
90
  }
91
+ return result;
92
+ });
93
+ }
94
+ async function runReviewSummaryRequest({ provider, resolvedCredential, resolvedModelName, logModelName, prompt, workspaceDir, }) {
95
+ try {
96
+ logger.info('Review summary model request', {
97
+ provider,
98
+ modelName: logModelName,
99
+ promptLength: prompt.length,
100
+ timeoutMs: REVIEW_SUMMARY_TIMEOUT_MS,
101
+ thinkingLevel: REVIEW_SUMMARY_THINKING_LEVEL,
102
+ });
19
103
  const result = await invokePiPrompt({
20
- provider: 'gemini',
104
+ provider,
21
105
  apiKey: resolvedCredential.apiKey,
22
106
  ...(resolvedCredential.piProviderId
23
107
  ? { piProviderId: resolvedCredential.piProviderId }
24
108
  : {}),
25
- ...(resolvedCredential.modelName ? { modelName: resolvedCredential.modelName } : {}),
109
+ ...(resolvedModelName ? { modelName: resolvedModelName } : {}),
26
110
  prompt,
27
111
  cwd: workspaceDir ?? getWorkspaceDir(),
28
112
  timeoutMs: REVIEW_SUMMARY_TIMEOUT_MS,
29
113
  capabilityPreset: 'read-only',
114
+ thinkingLevel: REVIEW_SUMMARY_THINKING_LEVEL,
30
115
  });
31
116
  if (!result.ok) {
117
+ const reason = getRequestFailureReason(result);
32
118
  logger.warn('Review summary model request failed', {
119
+ reason,
120
+ provider,
121
+ modelName: logModelName,
122
+ error: result.error,
123
+ timedOut: result.timedOut,
124
+ durationMs: result.durationMs,
125
+ promptLength: prompt.length,
126
+ });
127
+ return failedReviewSummaryRequest({
128
+ reason,
33
129
  error: result.error,
34
130
  timedOut: result.timedOut,
131
+ durationMs: result.durationMs,
35
132
  });
36
- return null;
37
133
  }
38
134
  const output = result.output;
39
135
  if (!output?.trim()) {
40
- return null;
136
+ return failedReviewSummaryRequest({
137
+ reason: 'empty_output',
138
+ durationMs: result.durationMs,
139
+ outputChars: output?.length ?? 0,
140
+ });
141
+ }
142
+ const jsonCandidates = extractJsonCandidatesFromResponse(output);
143
+ if (jsonCandidates.length === 0) {
144
+ logger.warn('Review summary model output missing JSON', {
145
+ provider,
146
+ modelName: logModelName,
147
+ durationMs: result.durationMs,
148
+ outputChars: output.length,
149
+ });
150
+ return failedReviewSummaryRequest({
151
+ reason: 'missing_json',
152
+ durationMs: result.durationMs,
153
+ outputChars: output.length,
154
+ });
155
+ }
156
+ let parsed;
157
+ let parsedSuccessfully = false;
158
+ let parseError;
159
+ for (const jsonCandidate of jsonCandidates) {
160
+ try {
161
+ parsed = JSON.parse(jsonCandidate);
162
+ parsedSuccessfully = true;
163
+ break;
164
+ }
165
+ catch (error) {
166
+ parseError = error;
167
+ }
168
+ }
169
+ if (!parsedSuccessfully) {
170
+ logger.warn('Review summary model output contained invalid JSON', {
171
+ provider,
172
+ modelName: logModelName,
173
+ error: parseError instanceof Error ? parseError.message : String(parseError),
174
+ durationMs: result.durationMs,
175
+ outputChars: output.length,
176
+ });
177
+ return failedReviewSummaryRequest({
178
+ reason: 'invalid_json',
179
+ error: parseError instanceof Error ? parseError.message : String(parseError),
180
+ durationMs: result.durationMs,
181
+ outputChars: output.length,
182
+ });
41
183
  }
42
- const start = output.indexOf('{');
43
- const end = output.lastIndexOf('}');
44
- if (start === -1 || end === -1 || end <= start) {
45
- logger.warn('Review summary model output missing JSON delimiters');
46
- return null;
184
+ if (!parsed || typeof parsed !== 'object') {
185
+ return failedReviewSummaryRequest({
186
+ reason: 'invalid_schema',
187
+ durationMs: result.durationMs,
188
+ outputChars: output.length,
189
+ });
47
190
  }
48
- const jsonText = output.slice(start, end + 1);
49
- const parsed = JSON.parse(jsonText);
191
+ const summaryResult = parsed;
192
+ if (typeof summaryResult.summary !== 'string') {
193
+ return failedReviewSummaryRequest({
194
+ reason: 'invalid_schema',
195
+ durationMs: result.durationMs,
196
+ outputChars: output.length,
197
+ });
198
+ }
199
+ if (!summaryResult.summary.trim()) {
200
+ return failedReviewSummaryRequest({
201
+ reason: 'empty_summary',
202
+ durationMs: result.durationMs,
203
+ outputChars: output.length,
204
+ });
205
+ }
206
+ logger.info('Review summary model response', {
207
+ provider,
208
+ modelName: logModelName,
209
+ durationMs: result.durationMs,
210
+ outputChars: output.length,
211
+ summaryChars: summaryResult.summary.trim().length,
212
+ });
50
213
  return {
51
- summary: typeof parsed.summary === 'string' ? parsed.summary : '',
52
- comments: Array.isArray(parsed.comments) ? parsed.comments : [],
214
+ status: 'succeeded',
215
+ summary: summaryResult.summary,
216
+ comments: Array.isArray(summaryResult.comments) ? summaryResult.comments : [],
217
+ durationMs: result.durationMs,
218
+ outputChars: output.length,
53
219
  };
54
220
  }
55
221
  catch (error) {
56
222
  logger.error('Review summary model request or parsing failed', {
57
223
  error: error instanceof Error ? error.message : String(error),
58
224
  });
59
- return null;
225
+ return failedReviewSummaryRequest({
226
+ reason: 'request_failed',
227
+ error: error instanceof Error ? error.message : String(error),
228
+ });
60
229
  }
61
230
  }
62
231
  export function sanitizeInlineComments(comments, lineLookup) {
@@ -1,7 +1,11 @@
1
1
  export function getReviewThinkingLevel(model) {
2
2
  switch (model) {
3
+ case 'deepseek':
4
+ return 'high';
3
5
  case 'openai':
4
6
  return 'high';
7
+ case 'zai':
8
+ return 'high';
5
9
  case 'gemini':
6
10
  return 'high';
7
11
  }
@@ -1,12 +1,8 @@
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 ["gemini", "openai"];
5
- export declare const REQUIRED_REVIEW_MODELS: readonly ["gemini", "openai"];
6
- type ParseReviewModelsEnabledOptions = {
7
- defaultModels?: readonly string[];
8
- requiredModels?: readonly string[];
9
- };
4
+ export declare const DEFAULT_REVIEW_MODELS: readonly ["openai", "deepseek", "zai"];
5
+ export type DefaultReviewModel = (typeof DEFAULT_REVIEW_MODELS)[number];
10
6
  export type RepoConfig = {
11
7
  review?: {
12
8
  skip_forks?: boolean;
@@ -50,6 +46,6 @@ export declare function getLocalRepoConfig({ workspaceDir, logger, }: {
50
46
  workspaceDir: string;
51
47
  logger: Logger;
52
48
  }): RepoConfig | null;
53
- export declare function parseReviewModelsEnabled(raw: string | undefined, { defaultModels, requiredModels, }?: ParseReviewModelsEnabledOptions): string[];
54
- export declare function getDefaultReviewModels(): string[];
49
+ export declare function getDefaultReviewModels(): DefaultReviewModel[];
50
+ export declare function parseReviewModelsEnabled(value: string | undefined): string[];
55
51
  export {};
@@ -2,8 +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 = ['gemini', 'openai'];
6
- export const REQUIRED_REVIEW_MODELS = DEFAULT_REVIEW_MODELS;
5
+ export const DEFAULT_REVIEW_MODELS = ['openai', 'deepseek', 'zai'];
7
6
  function parseRepoConfigContent(content) {
8
7
  return yamlLoad(content);
9
8
  }
@@ -91,24 +90,13 @@ export function getLocalRepoConfig({ workspaceDir, logger, }) {
91
90
  return null;
92
91
  }
93
92
  }
94
- export function parseReviewModelsEnabled(raw, { defaultModels = DEFAULT_REVIEW_MODELS, requiredModels = REQUIRED_REVIEW_MODELS, } = {}) {
95
- if (!raw?.trim()) {
96
- return [...defaultModels];
97
- }
98
- const allowedModels = new Set(DEFAULT_REVIEW_MODELS);
99
- const models = [];
100
- for (const token of raw.split(',')) {
101
- const model = token.trim().toLowerCase();
102
- if (!model || !allowedModels.has(model)) {
103
- continue;
104
- }
105
- if (!models.includes(model)) {
106
- models.push(model);
107
- }
108
- }
109
- const hasRequiredModels = requiredModels.every((model) => models.includes(model));
110
- return models.length > 0 && hasRequiredModels ? models : [...defaultModels];
111
- }
112
93
  export function getDefaultReviewModels() {
113
94
  return [...DEFAULT_REVIEW_MODELS];
114
95
  }
96
+ export function parseReviewModelsEnabled(value) {
97
+ const models = value
98
+ ?.split(',')
99
+ .map((model) => model.trim())
100
+ .filter(Boolean) ?? [];
101
+ return models.length > 0 ? [...new Set(models)] : getDefaultReviewModels();
102
+ }
@@ -71,6 +71,7 @@ $DIFF_SECTION
71
71
  - Use one comment per distinct issue.
72
72
  - Ignore trivial style unless it obscures meaning or violates documented standards.
73
73
  - Keep code ranges tight and comments easy to understand without close reading.
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).
74
75
 
75
76
  ## Writing Style
76
77