@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
|
@@ -1,220 +1 @@
|
|
|
1
|
-
|
|
2
|
-
function createRepositoryTarget({ owner = DEFAULT_REPOSITORY_OWNER, repo, product, surfaces, }) {
|
|
3
|
-
return {
|
|
4
|
-
owner,
|
|
5
|
-
repo,
|
|
6
|
-
fullName: `${owner}/${repo}`,
|
|
7
|
-
product,
|
|
8
|
-
surfaces,
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
const TODOIST_REPOSITORIES = [
|
|
12
|
-
createRepositoryTarget({
|
|
13
|
-
repo: 'Todoist',
|
|
14
|
-
product: 'Todoist',
|
|
15
|
-
surfaces: ['backend'],
|
|
16
|
-
}),
|
|
17
|
-
createRepositoryTarget({
|
|
18
|
-
repo: 'todoist-web',
|
|
19
|
-
product: 'Todoist',
|
|
20
|
-
surfaces: ['web'],
|
|
21
|
-
}),
|
|
22
|
-
createRepositoryTarget({
|
|
23
|
-
repo: 'reactist',
|
|
24
|
-
product: 'Todoist',
|
|
25
|
-
surfaces: ['web'],
|
|
26
|
-
}),
|
|
27
|
-
createRepositoryTarget({
|
|
28
|
-
repo: 'todoist-ios',
|
|
29
|
-
product: 'Todoist',
|
|
30
|
-
surfaces: ['ios'],
|
|
31
|
-
}),
|
|
32
|
-
createRepositoryTarget({
|
|
33
|
-
repo: 'dateist-ios',
|
|
34
|
-
product: 'Todoist',
|
|
35
|
-
surfaces: ['ios'],
|
|
36
|
-
}),
|
|
37
|
-
createRepositoryTarget({
|
|
38
|
-
repo: 'textist-swift',
|
|
39
|
-
product: 'Todoist',
|
|
40
|
-
surfaces: ['ios'],
|
|
41
|
-
}),
|
|
42
|
-
createRepositoryTarget({
|
|
43
|
-
repo: 'Todoist-Android',
|
|
44
|
-
product: 'Todoist',
|
|
45
|
-
surfaces: ['android'],
|
|
46
|
-
}),
|
|
47
|
-
createRepositoryTarget({
|
|
48
|
-
repo: 'Androist',
|
|
49
|
-
product: 'Todoist',
|
|
50
|
-
surfaces: ['android'],
|
|
51
|
-
}),
|
|
52
|
-
createRepositoryTarget({
|
|
53
|
-
repo: 'todoist-electron',
|
|
54
|
-
product: 'Todoist',
|
|
55
|
-
surfaces: ['desktop'],
|
|
56
|
-
}),
|
|
57
|
-
createRepositoryTarget({
|
|
58
|
-
repo: 'todoist-macos',
|
|
59
|
-
product: 'Todoist',
|
|
60
|
-
surfaces: ['desktop'],
|
|
61
|
-
}),
|
|
62
|
-
createRepositoryTarget({
|
|
63
|
-
repo: 'FilteristKt',
|
|
64
|
-
product: 'Todoist',
|
|
65
|
-
surfaces: ['android', 'ios'],
|
|
66
|
-
}),
|
|
67
|
-
createRepositoryTarget({
|
|
68
|
-
repo: 'todoist-mcp',
|
|
69
|
-
product: 'Todoist',
|
|
70
|
-
surfaces: ['mcp'],
|
|
71
|
-
}),
|
|
72
|
-
createRepositoryTarget({
|
|
73
|
-
repo: 'todoist-cli',
|
|
74
|
-
product: 'Todoist',
|
|
75
|
-
surfaces: ['cli'],
|
|
76
|
-
}),
|
|
77
|
-
];
|
|
78
|
-
const TWIST_REPOSITORIES = [
|
|
79
|
-
createRepositoryTarget({
|
|
80
|
-
repo: 'Twist',
|
|
81
|
-
product: 'Twist',
|
|
82
|
-
surfaces: ['backend'],
|
|
83
|
-
}),
|
|
84
|
-
createRepositoryTarget({
|
|
85
|
-
repo: 'twist-web',
|
|
86
|
-
product: 'Twist',
|
|
87
|
-
surfaces: ['web'],
|
|
88
|
-
}),
|
|
89
|
-
createRepositoryTarget({
|
|
90
|
-
repo: 'reactist',
|
|
91
|
-
product: 'Twist',
|
|
92
|
-
surfaces: ['web'],
|
|
93
|
-
}),
|
|
94
|
-
createRepositoryTarget({
|
|
95
|
-
repo: 'twist-ios',
|
|
96
|
-
product: 'Twist',
|
|
97
|
-
surfaces: ['ios'],
|
|
98
|
-
}),
|
|
99
|
-
createRepositoryTarget({
|
|
100
|
-
repo: 'doistui-swift',
|
|
101
|
-
product: 'Twist',
|
|
102
|
-
surfaces: ['ios'],
|
|
103
|
-
}),
|
|
104
|
-
createRepositoryTarget({
|
|
105
|
-
repo: 'emojist-swift',
|
|
106
|
-
product: 'Twist',
|
|
107
|
-
surfaces: ['ios'],
|
|
108
|
-
}),
|
|
109
|
-
createRepositoryTarget({
|
|
110
|
-
repo: 'keyboardist-swift',
|
|
111
|
-
product: 'Twist',
|
|
112
|
-
surfaces: ['ios'],
|
|
113
|
-
}),
|
|
114
|
-
createRepositoryTarget({
|
|
115
|
-
repo: 'participantist-swift',
|
|
116
|
-
product: 'Twist',
|
|
117
|
-
surfaces: ['ios'],
|
|
118
|
-
}),
|
|
119
|
-
createRepositoryTarget({
|
|
120
|
-
repo: 'reactionist-swift',
|
|
121
|
-
product: 'Twist',
|
|
122
|
-
surfaces: ['ios'],
|
|
123
|
-
}),
|
|
124
|
-
createRepositoryTarget({
|
|
125
|
-
repo: 'Twist-Android',
|
|
126
|
-
product: 'Twist',
|
|
127
|
-
surfaces: ['android'],
|
|
128
|
-
}),
|
|
129
|
-
createRepositoryTarget({
|
|
130
|
-
repo: 'Androist',
|
|
131
|
-
product: 'Twist',
|
|
132
|
-
surfaces: ['android'],
|
|
133
|
-
}),
|
|
134
|
-
createRepositoryTarget({
|
|
135
|
-
repo: 'twist-electron',
|
|
136
|
-
product: 'Twist',
|
|
137
|
-
surfaces: ['desktop'],
|
|
138
|
-
}),
|
|
139
|
-
createRepositoryTarget({
|
|
140
|
-
repo: 'twist-ai',
|
|
141
|
-
product: 'Twist',
|
|
142
|
-
surfaces: ['mcp'],
|
|
143
|
-
}),
|
|
144
|
-
createRepositoryTarget({
|
|
145
|
-
repo: 'twist-cli',
|
|
146
|
-
product: 'Twist',
|
|
147
|
-
surfaces: ['cli'],
|
|
148
|
-
}),
|
|
149
|
-
];
|
|
150
|
-
function dedupeRepositories(repositories) {
|
|
151
|
-
const unique = new Map();
|
|
152
|
-
for (const repository of repositories) {
|
|
153
|
-
unique.set(repository.fullName, repository);
|
|
154
|
-
}
|
|
155
|
-
return [...unique.values()];
|
|
156
|
-
}
|
|
157
|
-
function selectBySurface(repositories, surfaces) {
|
|
158
|
-
return dedupeRepositories(repositories.filter((repository) => repository.surfaces.some((surface) => surfaces.includes(surface))));
|
|
159
|
-
}
|
|
160
|
-
export const PRODUCT_REPOSITORIES = {
|
|
161
|
-
Todoist: TODOIST_REPOSITORIES,
|
|
162
|
-
Twist: TWIST_REPOSITORIES,
|
|
163
|
-
};
|
|
164
|
-
export const APP_TO_REPO_SELECTORS = {
|
|
165
|
-
all: (repositories) => dedupeRepositories(repositories),
|
|
166
|
-
android: (repositories) => selectBySurface(repositories, ['android']),
|
|
167
|
-
backend: (repositories) => selectBySurface(repositories, ['backend']),
|
|
168
|
-
cli: (repositories) => selectBySurface(repositories, ['cli']),
|
|
169
|
-
desktop: (repositories) => selectBySurface(repositories, ['desktop']),
|
|
170
|
-
ios: (repositories) => selectBySurface(repositories, ['ios']),
|
|
171
|
-
mcp: (repositories) => selectBySurface(repositories, ['mcp']),
|
|
172
|
-
web: (repositories) => selectBySurface(repositories, ['web']),
|
|
173
|
-
};
|
|
174
|
-
export const TEAM_TO_REPO_SELECTORS = {
|
|
175
|
-
all: (repositories) => dedupeRepositories(repositories),
|
|
176
|
-
android: (repositories) => selectBySurface(repositories, ['android']),
|
|
177
|
-
apple: (repositories) => selectBySurface(repositories, ['ios']),
|
|
178
|
-
backend: (repositories) => selectBySurface(repositories, ['backend']),
|
|
179
|
-
desktop: (repositories) => selectBySurface(repositories, ['desktop']),
|
|
180
|
-
frontend: (repositories) => selectBySurface(repositories, ['web']),
|
|
181
|
-
ios: (repositories) => selectBySurface(repositories, ['ios']),
|
|
182
|
-
mobile: (repositories) => dedupeRepositories([
|
|
183
|
-
...selectBySurface(repositories, ['ios']),
|
|
184
|
-
...selectBySurface(repositories, ['android']),
|
|
185
|
-
]),
|
|
186
|
-
web: (repositories) => selectBySurface(repositories, ['web']),
|
|
187
|
-
};
|
|
188
|
-
export const RESOLUTION_PRECEDENCE = [
|
|
189
|
-
'Product label is required to resolve product-specific repositories.',
|
|
190
|
-
'App selectors take precedence over team selectors when App:* labels are present.',
|
|
191
|
-
'App:All expands to every repository in the selected product inventory.',
|
|
192
|
-
'Specific app labels remain authoritative even when team labels are broad.',
|
|
193
|
-
'Team selectors expand scope only when App:* labels are absent.',
|
|
194
|
-
'Missing, contradictory, or unsupported label sets return an unresolved result.',
|
|
195
|
-
];
|
|
196
|
-
export const PRODUCT_LABEL_ALIASES = {
|
|
197
|
-
todoist: 'Todoist',
|
|
198
|
-
twist: 'Twist',
|
|
199
|
-
};
|
|
200
|
-
export const APP_LABEL_ALIASES = {
|
|
201
|
-
all: 'all',
|
|
202
|
-
android: 'android',
|
|
203
|
-
backend: 'backend',
|
|
204
|
-
cli: 'cli',
|
|
205
|
-
desktop: 'desktop',
|
|
206
|
-
ios: 'ios',
|
|
207
|
-
mcp: 'mcp',
|
|
208
|
-
web: 'web',
|
|
209
|
-
};
|
|
210
|
-
export const TEAM_LABEL_ALIASES = {
|
|
211
|
-
all: 'all',
|
|
212
|
-
android: 'android',
|
|
213
|
-
apple: 'apple',
|
|
214
|
-
backend: 'backend',
|
|
215
|
-
desktop: 'desktop',
|
|
216
|
-
frontend: 'frontend',
|
|
217
|
-
ios: 'ios',
|
|
218
|
-
mobile: 'mobile',
|
|
219
|
-
web: 'web',
|
|
220
|
-
};
|
|
1
|
+
export { APP_LABEL_ALIASES, APP_TO_ROUTE_SELECTORS, findIssueRoute, findRepositoryRoutes, findRepositoryTarget, PRODUCT_LABEL_ALIASES, PRODUCT_ROUTES, RESOLUTION_PRECEDENCE, TEAM_LABEL_ALIASES, TEAM_TO_ROUTE_SELECTORS, } from 'doistbot-issue-routing';
|
|
@@ -1,166 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { APP_LABEL_ALIASES, APP_TO_REPO_SELECTORS, PRODUCT_LABEL_ALIASES, PRODUCT_REPOSITORIES, RESOLUTION_PRECEDENCE, TEAM_LABEL_ALIASES, TEAM_TO_REPO_SELECTORS, } from './repository-map.js';
|
|
3
|
-
function uniqueValues(values) {
|
|
4
|
-
return [...new Set(values)];
|
|
5
|
-
}
|
|
6
|
-
function resolveProductLabels(productLabels) {
|
|
7
|
-
const products = [];
|
|
8
|
-
const unknown = [];
|
|
9
|
-
for (const rawLabel of productLabels) {
|
|
10
|
-
const canonical = PRODUCT_LABEL_ALIASES[normalizeScopedLabelValue(rawLabel)];
|
|
11
|
-
if (canonical) {
|
|
12
|
-
products.push(canonical);
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
unknown.push(rawLabel);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return {
|
|
19
|
-
products: uniqueValues(products),
|
|
20
|
-
unknown: uniqueValues(unknown),
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
function resolveScopedLabels(labels, aliases) {
|
|
24
|
-
const resolved = [];
|
|
25
|
-
const unknown = [];
|
|
26
|
-
for (const rawLabel of labels) {
|
|
27
|
-
const canonical = aliases[normalizeScopedLabelValue(rawLabel)];
|
|
28
|
-
if (canonical) {
|
|
29
|
-
resolved.push(canonical);
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
unknown.push(rawLabel);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
resolved: uniqueValues(resolved),
|
|
37
|
-
unknown: uniqueValues(unknown),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function dedupeRepositories(repositories) {
|
|
41
|
-
const unique = new Map();
|
|
42
|
-
for (const repository of repositories) {
|
|
43
|
-
unique.set(repository.fullName, repository);
|
|
44
|
-
}
|
|
45
|
-
return [...unique.values()];
|
|
46
|
-
}
|
|
47
|
-
function parseLabelScopes(labels) {
|
|
48
|
-
return {
|
|
49
|
-
productLabels: extractScopedLabelValues(labels, 'Product'),
|
|
50
|
-
appLabels: extractScopedLabelValues(labels, 'App'),
|
|
51
|
-
teamLabels: extractScopedLabelValues(labels, 'Team'),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
export function resolveIssueRepositories(labels) {
|
|
55
|
-
const decisionPath = [...RESOLUTION_PRECEDENCE];
|
|
56
|
-
const { productLabels, appLabels, teamLabels } = parseLabelScopes(labels);
|
|
57
|
-
const productResolution = resolveProductLabels(productLabels);
|
|
58
|
-
const appResolution = resolveScopedLabels(appLabels, APP_LABEL_ALIASES);
|
|
59
|
-
const teamResolution = resolveScopedLabels(teamLabels, TEAM_LABEL_ALIASES);
|
|
60
|
-
const parsedLabels = {
|
|
61
|
-
allLabels: labels,
|
|
62
|
-
productLabels: productResolution.products,
|
|
63
|
-
appLabels: appResolution.resolved,
|
|
64
|
-
teamLabels: teamResolution.resolved,
|
|
65
|
-
unknownProductLabels: productResolution.unknown,
|
|
66
|
-
unknownAppLabels: appResolution.unknown,
|
|
67
|
-
unknownTeamLabels: teamResolution.unknown,
|
|
68
|
-
};
|
|
69
|
-
if (productResolution.products.length === 0) {
|
|
70
|
-
return {
|
|
71
|
-
status: 'unresolved',
|
|
72
|
-
reason: 'Missing Product:* label. Please add Product:Todoist or Product:Twist.',
|
|
73
|
-
confidence: 'low',
|
|
74
|
-
decisionPath,
|
|
75
|
-
repositories: [],
|
|
76
|
-
parsedLabels,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
if (productResolution.products.length > 1) {
|
|
80
|
-
return {
|
|
81
|
-
status: 'unresolved',
|
|
82
|
-
reason: `Contradictory product labels: ${productResolution.products.join(', ')}`,
|
|
83
|
-
confidence: 'low',
|
|
84
|
-
decisionPath,
|
|
85
|
-
repositories: [],
|
|
86
|
-
parsedLabels,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
const product = productResolution.products[0];
|
|
90
|
-
if (!product) {
|
|
91
|
-
return {
|
|
92
|
-
status: 'unresolved',
|
|
93
|
-
reason: 'Unable to resolve product labels.',
|
|
94
|
-
confidence: 'low',
|
|
95
|
-
decisionPath,
|
|
96
|
-
repositories: [],
|
|
97
|
-
parsedLabels,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
const repositoriesForProduct = PRODUCT_REPOSITORIES[product];
|
|
101
|
-
if (appResolution.resolved.length > 0) {
|
|
102
|
-
decisionPath.push('Applied app selectors and skipped team selectors due App:* precedence.');
|
|
103
|
-
const selectedRepositories = dedupeRepositories(appResolution.resolved.flatMap((appLabel) => {
|
|
104
|
-
const selector = APP_TO_REPO_SELECTORS[appLabel];
|
|
105
|
-
return selector ? selector(repositoriesForProduct) : [];
|
|
106
|
-
}));
|
|
107
|
-
if (selectedRepositories.length === 0) {
|
|
108
|
-
return {
|
|
109
|
-
status: 'unresolved',
|
|
110
|
-
reason: `App labels did not map to repositories for product ${product}.`,
|
|
111
|
-
confidence: 'low',
|
|
112
|
-
decisionPath,
|
|
113
|
-
product,
|
|
114
|
-
repositories: [],
|
|
115
|
-
parsedLabels,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
status: 'resolved',
|
|
120
|
-
reason: appResolution.resolved.includes('all')
|
|
121
|
-
? `Resolved using Product:${product} + App:All.`
|
|
122
|
-
: `Resolved using Product:${product} + App selectors (${appResolution.resolved.join(', ')}).`,
|
|
123
|
-
confidence: 'high',
|
|
124
|
-
decisionPath,
|
|
125
|
-
product,
|
|
126
|
-
repositories: selectedRepositories,
|
|
127
|
-
parsedLabels,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
if (teamResolution.resolved.length > 0) {
|
|
131
|
-
decisionPath.push('No App:* labels found; applied team selectors.');
|
|
132
|
-
const selectedRepositories = dedupeRepositories(teamResolution.resolved.flatMap((teamLabel) => {
|
|
133
|
-
const selector = TEAM_TO_REPO_SELECTORS[teamLabel];
|
|
134
|
-
return selector ? selector(repositoriesForProduct) : [];
|
|
135
|
-
}));
|
|
136
|
-
if (selectedRepositories.length === 0) {
|
|
137
|
-
return {
|
|
138
|
-
status: 'unresolved',
|
|
139
|
-
reason: `Team labels did not map to repositories for product ${product}.`,
|
|
140
|
-
confidence: 'low',
|
|
141
|
-
decisionPath,
|
|
142
|
-
product,
|
|
143
|
-
repositories: [],
|
|
144
|
-
parsedLabels,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
return {
|
|
148
|
-
status: 'resolved',
|
|
149
|
-
reason: `Resolved using Product:${product} + Team selectors (${teamResolution.resolved.join(', ')}).`,
|
|
150
|
-
confidence: 'medium',
|
|
151
|
-
decisionPath,
|
|
152
|
-
product,
|
|
153
|
-
repositories: selectedRepositories,
|
|
154
|
-
parsedLabels,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
status: 'unresolved',
|
|
159
|
-
reason: 'Missing App:* and Team:* labels. Add scope labels to continue triage.',
|
|
160
|
-
confidence: 'low',
|
|
161
|
-
decisionPath,
|
|
162
|
-
product,
|
|
163
|
-
repositories: [],
|
|
164
|
-
parsedLabels,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
1
|
+
export { isKnownIssueRoutingLabel, resolveIssueRepositories } from 'doistbot-issue-routing';
|
|
@@ -141,6 +141,9 @@ export function parseRawFindings(comments, modelName) {
|
|
|
141
141
|
side,
|
|
142
142
|
body: comment.body.trim(),
|
|
143
143
|
sourceModel: modelName,
|
|
144
|
+
...(comment.translationResolution
|
|
145
|
+
? { translationResolution: comment.translationResolution }
|
|
146
|
+
: {}),
|
|
144
147
|
};
|
|
145
148
|
});
|
|
146
149
|
}
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { logger } from '../core/logging.js';
|
|
2
|
-
import { createPiInvoker, parsePiAuthProviderId, PI_PROVIDER_BY_DOISTBOT_PROVIDER, } from '../core/pi.js';
|
|
2
|
+
import { createPiInvoker, parsePiAuthProviderId, PI_PROVIDER_BY_DOISTBOT_PROVIDER, toPiInvocationTelemetryFields, } from '../core/pi.js';
|
|
3
3
|
import { logPiProgressEvent } from '../core/pi-progress-logging.js';
|
|
4
4
|
import { WORKSPACE_DIR } from '../core/shared.js';
|
|
5
|
+
import { DEFAULT_PROVIDER_TIMEOUT_MS } from './config.js';
|
|
5
6
|
import { logRawModelOutput, normalizeParsedModelOutput } from './helpers.js';
|
|
6
7
|
const VERBOSE_PI_PROGRESS_LOGS = process.env.DOISTBOT_VERBOSE_PI_PROGRESS === 'true';
|
|
7
8
|
const VERBOSE_PI_RAW_OUTPUT_LOGS = process.env.DOISTBOT_VERBOSE_PI_RAW_OUTPUT === 'true';
|
|
8
9
|
const REVIEW_LOG_ENGINE = 'multi_focus';
|
|
10
|
+
export const REVIEW_PI_EXECUTION_LIMITS = {
|
|
11
|
+
softTimeMs: 5 * 60 * 1000,
|
|
12
|
+
softToolCalls: 40,
|
|
13
|
+
hardTimeMs: 8 * 60 * 1000,
|
|
14
|
+
hardToolCalls: 65,
|
|
15
|
+
};
|
|
16
|
+
export const REVIEW_PI_PROCESS_KILL_MS = DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
9
17
|
const REVIEW_PI_SYSTEM_PROMPT = 'You are running inside doistbot in a non-interactive environment. Stay tightly focused on the review task in the user prompt. Read only the files that are directly relevant to the changed code, the explicitly named guidance files, and any immediately connected definitions needed to verify a concrete finding. Do not broadly explore the repository, inspect unrelated modules, or search history unless it is necessary to validate a specific review concern. Stop gathering context once you have enough evidence to answer the review task accurately.';
|
|
10
|
-
function buildModelFailureResult({ model, error, durationMs, timedOut = false, }) {
|
|
18
|
+
function buildModelFailureResult({ model, error, durationMs, timedOut = false, telemetry, }) {
|
|
11
19
|
return {
|
|
12
20
|
model,
|
|
13
21
|
summary: '',
|
|
@@ -15,9 +23,36 @@ function buildModelFailureResult({ model, error, durationMs, timedOut = false, }
|
|
|
15
23
|
error,
|
|
16
24
|
timedOut,
|
|
17
25
|
durationMs,
|
|
26
|
+
...(telemetry ? { telemetry } : {}),
|
|
18
27
|
};
|
|
19
28
|
}
|
|
29
|
+
function logReviewLatency({ provider, modelName, attempt, attemptsTotal, result, }) {
|
|
30
|
+
const telemetry = result.telemetry;
|
|
31
|
+
logger.info('Review model latency', {
|
|
32
|
+
model: provider,
|
|
33
|
+
engine: REVIEW_LOG_ENGINE,
|
|
34
|
+
model_name: modelName,
|
|
35
|
+
attempt,
|
|
36
|
+
attempts_total: attemptsTotal,
|
|
37
|
+
success: result.ok,
|
|
38
|
+
timed_out: !result.ok && result.timedOut,
|
|
39
|
+
stop_reason: result.stopReason,
|
|
40
|
+
duration_ms: result.durationMs,
|
|
41
|
+
...(telemetry ? toPiInvocationTelemetryFields(telemetry) : {}),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
20
44
|
function logReviewModelProgress(model, event) {
|
|
45
|
+
if (event.type === 'limit_reached') {
|
|
46
|
+
logger.warn('Review model execution limit reached', {
|
|
47
|
+
model,
|
|
48
|
+
engine: REVIEW_LOG_ENGINE,
|
|
49
|
+
level: event.level,
|
|
50
|
+
trigger: event.trigger,
|
|
51
|
+
elapsed_ms: event.elapsedMs,
|
|
52
|
+
tool_call_count: event.toolCallCount,
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
21
56
|
logPiProgressEvent({
|
|
22
57
|
event,
|
|
23
58
|
messages: {
|
|
@@ -61,10 +96,11 @@ export async function runPiReviewProvider(params) {
|
|
|
61
96
|
}
|
|
62
97
|
async function runPiReviewProviderCore({ provider, modelName, prompt, config, prompts = [prompt], parseOutput, }) {
|
|
63
98
|
const startTime = Date.now();
|
|
64
|
-
const timeoutMs = config.timeout ??
|
|
99
|
+
const timeoutMs = Math.min(config.timeout ?? REVIEW_PI_PROCESS_KILL_MS, REVIEW_PI_PROCESS_KILL_MS);
|
|
65
100
|
let stdout = '';
|
|
66
101
|
let stderr = '';
|
|
67
102
|
let status = null;
|
|
103
|
+
let latestTelemetry;
|
|
68
104
|
logger.info('Review model request', {
|
|
69
105
|
model: provider,
|
|
70
106
|
engine: REVIEW_LOG_ENGINE,
|
|
@@ -89,6 +125,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
|
|
|
89
125
|
systemPrompt: REVIEW_PI_SYSTEM_PROMPT,
|
|
90
126
|
noContextFiles: true,
|
|
91
127
|
openRouterSessionId: config.openRouterSessionId,
|
|
128
|
+
executionLimits: REVIEW_PI_EXECUTION_LIMITS,
|
|
92
129
|
});
|
|
93
130
|
for (let attempt = 0; attempt < prompts.length; attempt += 1) {
|
|
94
131
|
const result = await invoker.invoke({
|
|
@@ -97,6 +134,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
|
|
|
97
134
|
timeoutMs,
|
|
98
135
|
});
|
|
99
136
|
const durationMs = Date.now() - startTime;
|
|
137
|
+
latestTelemetry = result.telemetry;
|
|
100
138
|
stdout = result.ok ? result.output : '';
|
|
101
139
|
stderr = result.ok ? '' : result.error;
|
|
102
140
|
status = result.ok ? 0 : null;
|
|
@@ -108,6 +146,13 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
|
|
|
108
146
|
attemptDurationMs: result.durationMs,
|
|
109
147
|
toolUsage: result.toolUsage,
|
|
110
148
|
});
|
|
149
|
+
logReviewLatency({
|
|
150
|
+
provider,
|
|
151
|
+
modelName,
|
|
152
|
+
attempt: attempt + 1,
|
|
153
|
+
attemptsTotal: prompts.length,
|
|
154
|
+
result,
|
|
155
|
+
});
|
|
111
156
|
if (!result.ok && result.timedOut) {
|
|
112
157
|
logReviewRawModelOutput({
|
|
113
158
|
model: provider,
|
|
@@ -130,6 +175,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
|
|
|
130
175
|
error,
|
|
131
176
|
durationMs,
|
|
132
177
|
timedOut: true,
|
|
178
|
+
telemetry: result.telemetry,
|
|
133
179
|
});
|
|
134
180
|
}
|
|
135
181
|
if (!result.ok) {
|
|
@@ -153,6 +199,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
|
|
|
153
199
|
model: provider,
|
|
154
200
|
error,
|
|
155
201
|
durationMs,
|
|
202
|
+
telemetry: result.telemetry,
|
|
156
203
|
});
|
|
157
204
|
}
|
|
158
205
|
const output = result.output;
|
|
@@ -170,6 +217,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
|
|
|
170
217
|
model: provider,
|
|
171
218
|
error: `Empty response from Pi ${provider} request`,
|
|
172
219
|
durationMs,
|
|
220
|
+
telemetry: result.telemetry,
|
|
173
221
|
});
|
|
174
222
|
}
|
|
175
223
|
try {
|
|
@@ -202,6 +250,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
|
|
|
202
250
|
summary: normalized.summary,
|
|
203
251
|
findings: normalized.findings,
|
|
204
252
|
durationMs,
|
|
253
|
+
telemetry: result.telemetry,
|
|
205
254
|
};
|
|
206
255
|
}
|
|
207
256
|
catch (error) {
|
|
@@ -246,6 +295,7 @@ async function runPiReviewProviderCore({ provider, modelName, prompt, config, pr
|
|
|
246
295
|
error: errorMessage,
|
|
247
296
|
durationMs,
|
|
248
297
|
timedOut: errorMessage.toLowerCase().includes('timed out'),
|
|
298
|
+
telemetry: latestTelemetry,
|
|
249
299
|
});
|
|
250
300
|
}
|
|
251
301
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Ajv } from 'ajv';
|
|
2
|
+
import { TRANSLATION_RESOLUTIONS } from './types.js';
|
|
2
3
|
export const REVIEW_SCHEMA = {
|
|
3
4
|
type: 'object',
|
|
4
5
|
additionalProperties: false,
|
|
@@ -14,7 +15,13 @@ export const REVIEW_SCHEMA = {
|
|
|
14
15
|
line: { type: 'integer', minimum: 1 },
|
|
15
16
|
side: { type: 'string', enum: ['LEFT', 'RIGHT'] },
|
|
16
17
|
body: { type: 'string', minLength: 1 },
|
|
18
|
+
translationResolution: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
enum: TRANSLATION_RESOLUTIONS,
|
|
21
|
+
},
|
|
17
22
|
},
|
|
23
|
+
// Classification stays optional so omission keeps the finding instead of failing
|
|
24
|
+
// the entire review pass.
|
|
18
25
|
required: ['path', 'line', 'side', 'body'],
|
|
19
26
|
},
|
|
20
27
|
},
|