@doist/doistbot-cli 1.0.4 → 1.0.6
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/auth.js +62 -28
- package/dist/actions/doctor.js +16 -11
- package/dist/actions/review.js +77 -11
- package/dist/auth.js +53 -8
- package/dist/config.js +40 -6
- package/dist/env.js +1 -1
- package/dist/file-store.js +1 -1
- package/dist/runtime.js +1 -1
- package/dist/terminal.js +2 -7
- package/package.json +7 -7
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +9 -85
- package/sandbox/dist/core/bot-logins.js +0 -13
- package/sandbox/dist/core/check-run.js +16 -0
- package/sandbox/dist/core/datadog-metrics.js +6 -54
- package/sandbox/dist/core/logger-span-processor.js +78 -0
- package/sandbox/dist/core/pi.js +270 -186
- package/sandbox/dist/core/tracing-setup.js +15 -0
- package/sandbox/dist/core/tracing.js +71 -0
- package/sandbox/dist/main.js +4 -1
- package/sandbox/dist/providers/config.js +1 -1
- package/sandbox/dist/providers/credentials.js +40 -0
- package/sandbox/dist/providers/gemini.js +8 -10
- package/sandbox/dist/providers/helpers.js +18 -38
- package/sandbox/dist/providers/index.js +4 -3
- package/sandbox/dist/providers/openai.js +1 -8
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +28 -55
- package/sandbox/dist/providers/schemas.js +0 -39
- package/sandbox/dist/tasks/chat/chat.js +162 -101
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
- package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
- package/sandbox/dist/tasks/issue-summarize/model.js +4 -2
- package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +25 -2
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +31 -8
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
- package/sandbox/dist/tasks/issue-triage/model.js +120 -36
- package/sandbox/dist/tasks/issue-triage/output.js +27 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +3 -2
- package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
- package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +288 -418
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
- package/sandbox/dist/tasks/review/check-run.js +2 -1
- package/sandbox/dist/tasks/review/conversation-context.js +15 -8
- package/sandbox/dist/tasks/review/engines/dedupe.js +19 -9
- package/sandbox/dist/tasks/review/engines/multi-focus.js +62 -36
- package/sandbox/dist/tasks/review/engines/shared.js +15 -61
- package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
- package/sandbox/dist/tasks/review/local.js +2 -29
- package/sandbox/dist/tasks/review/prompt.js +2 -49
- package/sandbox/dist/tasks/review/review-summary.js +120 -0
- package/sandbox/dist/tasks/review/review.js +49 -69
- package/sandbox/dist/tasks/review/runner.js +57 -52
- package/sandbox/dist/tasks/review/summary-model.js +172 -16
- package/sandbox/dist/tasks/review/thinking-level.js +4 -2
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +9 -85
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/dist/providers/anthropic.js +0 -30
- package/sandbox/dist/tasks/review/author-format.js +0 -14
- package/sandbox/dist/tasks/review/author-profile.js +0 -67
- package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
- package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
- package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
- package/sandbox/dist/tasks/review/council/summary.js +0 -192
- package/sandbox/dist/tasks/review/council/types.js +0 -4
- package/sandbox/dist/tasks/review/council/voter.js +0 -384
- package/sandbox/dist/tasks/review/engines/council.js +0 -126
- package/sandbox/dist/tasks/review/engines/index.js +0 -12
- package/sandbox/dist/tasks/review/review-engine.js +0 -5
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/review-prompt.md +0 -209
- package/sandbox/vote-prompt.md +0 -69
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { context, SpanStatusCode, trace } from '@opentelemetry/api';
|
|
2
|
+
// recordDecision/recordEvent emit directly (bypassing the processor), so they
|
|
3
|
+
// apply the same per-attribute cap themselves to keep large arrays/objects
|
|
4
|
+
// from blowing the log-size budget.
|
|
5
|
+
import { truncateAttrs } from './logger-span-processor.js';
|
|
6
|
+
import { logger } from './logging.js';
|
|
7
|
+
const tracer = trace.getTracer('doistbot-sandbox');
|
|
8
|
+
/**
|
|
9
|
+
* THE phase-boundary API. Works for sequential AND parallel phases — OTel's
|
|
10
|
+
* async-hooks context manager (registered by setupTracing) parents nested
|
|
11
|
+
* spans correctly even across Promise.all.
|
|
12
|
+
*
|
|
13
|
+
* On throw: records the exception, sets span status ERROR, re-throws.
|
|
14
|
+
* On normal return: status is left UNSET (OTel's recommended success default;
|
|
15
|
+
* the LoggerSpanProcessor maps UNSET → 'ok'). We deliberately do NOT force
|
|
16
|
+
* OK, so a callback that returns a failure result (instead of throwing) and
|
|
17
|
+
* sets ERROR itself keeps its ERROR status.
|
|
18
|
+
* Always ends the span.
|
|
19
|
+
*/
|
|
20
|
+
export async function withPhase(name, attributes, fn) {
|
|
21
|
+
return tracer.startActiveSpan(name, { attributes: { ...attributes, 'doistbot.kind': 'phase' } }, async (span) => {
|
|
22
|
+
try {
|
|
23
|
+
return await fn(span);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
span.recordException(err);
|
|
27
|
+
span.setStatus({
|
|
28
|
+
code: SpanStatusCode.ERROR,
|
|
29
|
+
message: err instanceof Error ? err.message : String(err),
|
|
30
|
+
});
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
span.end();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Start a tool span manually. The ONE exception to "withPhase everywhere":
|
|
40
|
+
* pi emits tool start/end across separate async callbacks (onProgress), so
|
|
41
|
+
* there is no single scope to wrap. Parent = the currently-active span (the
|
|
42
|
+
* model phase). Caller MUST call `span.end()` when the matching tool_end
|
|
43
|
+
* arrives (and may `setStatus({ code: ERROR })` first on failure).
|
|
44
|
+
*/
|
|
45
|
+
export function startToolSpan(name, attributes) {
|
|
46
|
+
return tracer.startSpan(name, { attributes: { ...attributes, 'doistbot.kind': 'tool' } }, context.active());
|
|
47
|
+
}
|
|
48
|
+
// Point-in-time lines bypass OTel spans entirely; they read the active span
|
|
49
|
+
// id only for correlation. Meta is spread FIRST so callers can never clobber
|
|
50
|
+
// the envelope fields (event/name/span_id). Timestamps come from the logging
|
|
51
|
+
// system, never from meta.
|
|
52
|
+
function emitPointInTime(kind, name, meta) {
|
|
53
|
+
const span = trace.getActiveSpan();
|
|
54
|
+
logger.info(`${kind}: ${name}`, {
|
|
55
|
+
...truncateAttrs(meta),
|
|
56
|
+
event: kind,
|
|
57
|
+
name,
|
|
58
|
+
span_id: span?.spanContext().spanId ?? null,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Record a point-in-time decision (branch taken, scope resolved, gate passed)
|
|
63
|
+
* correlated to the active span. Not a span — no duration.
|
|
64
|
+
*/
|
|
65
|
+
export function recordDecision(name, meta) {
|
|
66
|
+
emitPointInTime('decision', name, meta);
|
|
67
|
+
}
|
|
68
|
+
/** Generic point-in-time annotation. Same mechanics as recordDecision. */
|
|
69
|
+
export function recordEvent(name, meta) {
|
|
70
|
+
emitPointInTime('event', name, meta);
|
|
71
|
+
}
|
package/sandbox/dist/main.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { setupTracing } from './core/tracing-setup.js';
|
|
2
|
+
// Register the OTel SDK before any task module loads — withPhase/startToolSpan
|
|
3
|
+
// are no-ops until a tracer provider is registered.
|
|
4
|
+
setupTracing();
|
|
1
5
|
const taskType = process.env.TASK_TYPE ?? 'review';
|
|
2
6
|
switch (taskType) {
|
|
3
7
|
case 'chat':
|
|
@@ -32,4 +36,3 @@ switch (taskType) {
|
|
|
32
36
|
process.stderr.write(`Unknown TASK_TYPE: ${taskType}\n`);
|
|
33
37
|
process.exit(1);
|
|
34
38
|
}
|
|
35
|
-
export {};
|
|
@@ -15,7 +15,7 @@ export function resolveProviderTimeoutMs(explicitTimeoutMs) {
|
|
|
15
15
|
explicitTimeoutMs > 0) {
|
|
16
16
|
return explicitTimeoutMs;
|
|
17
17
|
}
|
|
18
|
-
const sharedTimeout = parseTimeoutMs(process.env.
|
|
18
|
+
const sharedTimeout = parseTimeoutMs(process.env.REVIEW_MODEL_TIMEOUT_MS);
|
|
19
19
|
if (sharedTimeout != null) {
|
|
20
20
|
return sharedTimeout;
|
|
21
21
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { parsePiAuthProviderId } from '../core/pi.js';
|
|
2
|
+
export function resolveReviewProviderCredential(credential) {
|
|
3
|
+
if (typeof credential === 'string') {
|
|
4
|
+
const apiKey = credential.trim();
|
|
5
|
+
if (!apiKey) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const piAuthProviderId = parsePiAuthProviderId(apiKey);
|
|
9
|
+
return {
|
|
10
|
+
apiKey,
|
|
11
|
+
...(piAuthProviderId ? { piProviderId: piAuthProviderId } : {}),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (!credential) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const apiKey = credential.apiKey.trim();
|
|
18
|
+
if (!apiKey) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const piAuthProviderId = parsePiAuthProviderId(apiKey);
|
|
22
|
+
const piProviderId = piAuthProviderId ?? credential.piProviderId;
|
|
23
|
+
return {
|
|
24
|
+
apiKey,
|
|
25
|
+
...(piProviderId ? { piProviderId } : {}),
|
|
26
|
+
...(credential.modelName ? { modelName: credential.modelName } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function hasReviewProviderCredential(credential) {
|
|
30
|
+
return resolveReviewProviderCredential(credential) !== undefined;
|
|
31
|
+
}
|
|
32
|
+
export function getReviewProviderCredential(apiKeys, provider) {
|
|
33
|
+
return resolveReviewProviderCredential(apiKeys[provider]);
|
|
34
|
+
}
|
|
35
|
+
export function createOpenRouterCredential(apiKey) {
|
|
36
|
+
return {
|
|
37
|
+
apiKey,
|
|
38
|
+
piProviderId: 'openrouter',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -2,29 +2,27 @@
|
|
|
2
2
|
* Gemini provider for the multi-model code review system.
|
|
3
3
|
* Uses the Pi harness instead of the Gemini CLI.
|
|
4
4
|
*/
|
|
5
|
-
import { DEFAULT_PI_MODELS } from '../core/pi.js';
|
|
5
|
+
import { DEFAULT_OPENROUTER_MODELS, DEFAULT_PI_MODELS } from '../core/pi.js';
|
|
6
6
|
import { resolveProviderTimeoutMs } from './config.js';
|
|
7
7
|
import { extractJsonFromResponse } from './helpers.js';
|
|
8
8
|
import { runPiReviewProvider } from './pi-review.js';
|
|
9
9
|
export class GeminiProvider {
|
|
10
10
|
name = 'gemini';
|
|
11
|
-
async review(prompt, config
|
|
11
|
+
async review(prompt, config) {
|
|
12
|
+
const modelName = config.modelName ??
|
|
13
|
+
(config.piProviderId === 'openrouter'
|
|
14
|
+
? DEFAULT_OPENROUTER_MODELS.gemini
|
|
15
|
+
: DEFAULT_PI_MODELS.gemini) ??
|
|
16
|
+
DEFAULT_PI_MODELS.gemini;
|
|
12
17
|
return await runPiReviewProvider({
|
|
13
18
|
provider: this.name,
|
|
14
|
-
modelName
|
|
19
|
+
modelName,
|
|
15
20
|
prompt,
|
|
16
21
|
config: {
|
|
17
22
|
...config,
|
|
18
23
|
timeout: resolveProviderTimeoutMs(config.timeout),
|
|
19
24
|
},
|
|
20
|
-
prompts: config.jsonSchema
|
|
21
|
-
? [
|
|
22
|
-
prompt,
|
|
23
|
-
`${prompt}\n\nReturn only JSON that matches the provided schema. Do not include analysis text.`,
|
|
24
|
-
]
|
|
25
|
-
: [prompt],
|
|
26
25
|
parseOutput: (output) => JSON.parse(extractJsonFromResponse(output)),
|
|
27
|
-
logOptions,
|
|
28
26
|
});
|
|
29
27
|
}
|
|
30
28
|
}
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
5
5
|
import { logger } from '../core/logging.js';
|
|
6
|
-
import {
|
|
6
|
+
import { isRawReviewResult } from './schemas.js';
|
|
7
7
|
const RAW_MODEL_LOG_CHUNK_CHARS = 8000;
|
|
8
|
+
const REVIEW_LOG_ENGINE = 'multi_focus';
|
|
8
9
|
function redactSensitiveStrings(content) {
|
|
9
10
|
return content
|
|
10
11
|
.replace(/(sk-[A-Za-z0-9_-]{10,})/g, '[REDACTED_OPENAI_KEY]')
|
|
@@ -34,7 +35,7 @@ export function buildRawModelOutputEntries(content) {
|
|
|
34
35
|
originalLength: content.length,
|
|
35
36
|
}));
|
|
36
37
|
}
|
|
37
|
-
export function logRawModelOutput({ model,
|
|
38
|
+
export function logRawModelOutput({ model, stage, success, status, durationMs, stdout, stderr, }) {
|
|
38
39
|
const streams = [
|
|
39
40
|
{ name: 'stdout', value: stdout },
|
|
40
41
|
{ name: 'stderr', value: stderr },
|
|
@@ -48,7 +49,7 @@ export function logRawModelOutput({ model, engine = 'council', stage, success, s
|
|
|
48
49
|
for (const entry of entries) {
|
|
49
50
|
logger.info('Review model raw output', {
|
|
50
51
|
model,
|
|
51
|
-
engine,
|
|
52
|
+
engine: REVIEW_LOG_ENGINE,
|
|
52
53
|
stage,
|
|
53
54
|
success,
|
|
54
55
|
status: status ?? null,
|
|
@@ -95,30 +96,14 @@ export function parseRawFindings(comments, modelName) {
|
|
|
95
96
|
/**
|
|
96
97
|
* Normalizes structured model output into a single provider response shape.
|
|
97
98
|
*/
|
|
98
|
-
export function normalizeParsedModelOutput(parsed, modelName
|
|
99
|
+
export function normalizeParsedModelOutput(parsed, modelName) {
|
|
99
100
|
if (isRawReviewResult(parsed)) {
|
|
100
101
|
return {
|
|
101
102
|
summary: parsed.summary,
|
|
102
103
|
findings: parseRawFindings(parsed.comments, modelName),
|
|
103
104
|
};
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
-
isVoteResponse(parsed)) {
|
|
107
|
-
return {
|
|
108
|
-
summary: '',
|
|
109
|
-
findings: [],
|
|
110
|
-
structuredOutput: parsed,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
if ((structuredOutputKind === 'auto' || structuredOutputKind === 'batch-vote') &&
|
|
114
|
-
isBatchVoteResponse(parsed)) {
|
|
115
|
-
return {
|
|
116
|
-
summary: '',
|
|
117
|
-
findings: [],
|
|
118
|
-
structuredOutput: parsed,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
throw new Error('Model output did not match review or vote schema');
|
|
106
|
+
throw new Error('Model output did not match review schema');
|
|
122
107
|
}
|
|
123
108
|
function extractFencedJsonCandidates(content) {
|
|
124
109
|
const matches = content.match(/```json\s*([\s\S]*?)\s*```/gi);
|
|
@@ -181,29 +166,24 @@ function extractBalancedJsonCandidates(content) {
|
|
|
181
166
|
}
|
|
182
167
|
return candidates;
|
|
183
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Returns JSON-looking candidates from a response string in parser preference order.
|
|
171
|
+
* Fenced JSON blocks are preferred over balanced objects because models often include
|
|
172
|
+
* code snippets or object-shaped prose before the final fenced response.
|
|
173
|
+
*/
|
|
174
|
+
export function extractJsonCandidatesFromResponse(content) {
|
|
175
|
+
return [
|
|
176
|
+
...extractFencedJsonCandidates(content).reverse(),
|
|
177
|
+
...extractBalancedJsonCandidates(content).reverse(),
|
|
178
|
+
];
|
|
179
|
+
}
|
|
184
180
|
/**
|
|
185
181
|
* Extracts JSON from a response string that may contain additional text.
|
|
186
182
|
* Prefers valid fenced JSON blocks, then valid balanced JSON objects.
|
|
187
183
|
* Throws when no valid JSON candidate can be parsed.
|
|
188
184
|
*/
|
|
189
185
|
export function extractJsonFromResponse(content) {
|
|
190
|
-
const
|
|
191
|
-
for (let index = fencedCandidates.length - 1; index >= 0; index--) {
|
|
192
|
-
const candidate = fencedCandidates[index];
|
|
193
|
-
if (!candidate) {
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
try {
|
|
197
|
-
JSON.parse(candidate);
|
|
198
|
-
return candidate;
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const balancedCandidates = extractBalancedJsonCandidates(content);
|
|
205
|
-
for (let index = balancedCandidates.length - 1; index >= 0; index--) {
|
|
206
|
-
const candidate = balancedCandidates[index];
|
|
186
|
+
for (const candidate of extractJsonCandidatesFromResponse(content)) {
|
|
207
187
|
if (!candidate) {
|
|
208
188
|
continue;
|
|
209
189
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Provider factory and exports for the multi-model code review system.
|
|
3
3
|
*/
|
|
4
|
-
import { AnthropicProvider } from './anthropic.js';
|
|
5
4
|
import { GeminiProvider } from './gemini.js';
|
|
6
5
|
import { OpenAIProvider } from './openai.js';
|
|
7
|
-
|
|
6
|
+
import { OpenRouterPiProvider } from './openrouter-pi.js';
|
|
8
7
|
export { GeminiProvider } from './gemini.js';
|
|
9
8
|
export { OpenAIProvider } from './openai.js';
|
|
9
|
+
export { OpenRouterPiProvider } from './openrouter-pi.js';
|
|
10
10
|
export * from './types.js';
|
|
11
11
|
const providers = {
|
|
12
|
+
deepseek: new OpenRouterPiProvider('deepseek'),
|
|
12
13
|
gemini: new GeminiProvider(),
|
|
14
|
+
zai: new OpenRouterPiProvider('zai'),
|
|
13
15
|
openai: new OpenAIProvider(),
|
|
14
|
-
anthropic: new AnthropicProvider(),
|
|
15
16
|
};
|
|
16
17
|
/**
|
|
17
18
|
* Gets a provider instance by name.
|
|
@@ -8,7 +8,7 @@ import { extractJsonFromResponse } from './helpers.js';
|
|
|
8
8
|
import { runPiReviewProvider } from './pi-review.js';
|
|
9
9
|
export class OpenAIProvider {
|
|
10
10
|
name = 'openai';
|
|
11
|
-
async review(prompt, config
|
|
11
|
+
async review(prompt, config) {
|
|
12
12
|
return await runPiReviewProvider({
|
|
13
13
|
provider: this.name,
|
|
14
14
|
modelName: config.modelName ?? DEFAULT_PI_MODELS.openai,
|
|
@@ -17,14 +17,7 @@ export class OpenAIProvider {
|
|
|
17
17
|
...config,
|
|
18
18
|
timeout: resolveProviderTimeoutMs(config.timeout),
|
|
19
19
|
},
|
|
20
|
-
prompts: config.jsonSchema
|
|
21
|
-
? [
|
|
22
|
-
prompt,
|
|
23
|
-
`${prompt}\n\nReturn only JSON that matches the provided schema. Do not include analysis text.`,
|
|
24
|
-
]
|
|
25
|
-
: [prompt],
|
|
26
20
|
parseOutput: (output) => JSON.parse(extractJsonFromResponse(output)),
|
|
27
|
-
logOptions,
|
|
28
21
|
});
|
|
29
22
|
}
|
|
30
23
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DEFAULT_OPENROUTER_MODELS, DEFAULT_PI_MODELS } from '../core/pi.js';
|
|
2
|
+
import { resolveProviderTimeoutMs } from './config.js';
|
|
3
|
+
import { extractJsonFromResponse } from './helpers.js';
|
|
4
|
+
import { runPiReviewProvider } from './pi-review.js';
|
|
5
|
+
export class OpenRouterPiProvider {
|
|
6
|
+
name;
|
|
7
|
+
fallbackModelName;
|
|
8
|
+
constructor(name, fallbackModelName = DEFAULT_OPENROUTER_MODELS[name] ??
|
|
9
|
+
DEFAULT_PI_MODELS[name]) {
|
|
10
|
+
this.name = name;
|
|
11
|
+
this.fallbackModelName = fallbackModelName;
|
|
12
|
+
}
|
|
13
|
+
async review(prompt, config) {
|
|
14
|
+
return await runPiReviewProvider({
|
|
15
|
+
provider: this.name,
|
|
16
|
+
modelName: config.modelName ?? this.fallbackModelName,
|
|
17
|
+
prompt,
|
|
18
|
+
config: {
|
|
19
|
+
...config,
|
|
20
|
+
timeout: resolveProviderTimeoutMs(config.timeout),
|
|
21
|
+
},
|
|
22
|
+
parseOutput: (output) => JSON.parse(extractJsonFromResponse(output)),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { logger } from '../core/logging.js';
|
|
2
|
-
import { createPiInvoker } from '../core/pi.js';
|
|
2
|
+
import { createPiInvoker, parsePiAuthProviderId, PI_PROVIDER_BY_DOISTBOT_PROVIDER, } from '../core/pi.js';
|
|
3
3
|
import { logPiProgressEvent } from '../core/pi-progress-logging.js';
|
|
4
4
|
import { WORKSPACE_DIR } from '../core/shared.js';
|
|
5
5
|
import { logRawModelOutput, normalizeParsedModelOutput } from './helpers.js';
|
|
6
|
-
import { BATCH_VOTE_SCHEMA, VOTE_SCHEMA } from './schemas.js';
|
|
7
6
|
const VERBOSE_PI_PROGRESS_LOGS = process.env.DOISTBOT_VERBOSE_PI_PROGRESS === 'true';
|
|
8
7
|
const VERBOSE_PI_RAW_OUTPUT_LOGS = process.env.DOISTBOT_VERBOSE_PI_RAW_OUTPUT === 'true';
|
|
8
|
+
const REVIEW_LOG_ENGINE = 'multi_focus';
|
|
9
9
|
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
|
-
const DEFAULT_REVIEW_LOG_ENGINE = 'council';
|
|
11
|
-
function getStructuredOutputKind(jsonSchema) {
|
|
12
|
-
if (jsonSchema === BATCH_VOTE_SCHEMA) {
|
|
13
|
-
return 'batch-vote';
|
|
14
|
-
}
|
|
15
|
-
if (jsonSchema === VOTE_SCHEMA) {
|
|
16
|
-
return 'vote';
|
|
17
|
-
}
|
|
18
|
-
return 'auto';
|
|
19
|
-
}
|
|
20
10
|
function buildModelFailureResult({ model, error, durationMs, timedOut = false, }) {
|
|
21
11
|
return {
|
|
22
12
|
model,
|
|
@@ -27,7 +17,7 @@ function buildModelFailureResult({ model, error, durationMs, timedOut = false, }
|
|
|
27
17
|
durationMs,
|
|
28
18
|
};
|
|
29
19
|
}
|
|
30
|
-
function logReviewModelProgress(model, event
|
|
20
|
+
function logReviewModelProgress(model, event) {
|
|
31
21
|
logPiProgressEvent({
|
|
32
22
|
event,
|
|
33
23
|
messages: {
|
|
@@ -39,17 +29,17 @@ function logReviewModelProgress(model, event, engine) {
|
|
|
39
29
|
toolUpdate: 'Review model tool still running',
|
|
40
30
|
toolEnd: 'Review model tool finished',
|
|
41
31
|
},
|
|
42
|
-
meta: { model, engine },
|
|
32
|
+
meta: { model, engine: REVIEW_LOG_ENGINE },
|
|
43
33
|
logResponding: VERBOSE_PI_PROGRESS_LOGS,
|
|
44
34
|
logTools: VERBOSE_PI_PROGRESS_LOGS,
|
|
45
35
|
});
|
|
46
36
|
}
|
|
47
|
-
function emitReviewModelProgress(provider, event, config
|
|
37
|
+
function emitReviewModelProgress(provider, event, config) {
|
|
48
38
|
config.onProgress?.({
|
|
49
39
|
...event,
|
|
50
40
|
provider,
|
|
51
41
|
});
|
|
52
|
-
logReviewModelProgress(provider, event
|
|
42
|
+
logReviewModelProgress(provider, event);
|
|
53
43
|
}
|
|
54
44
|
function logReviewRawModelOutput(args) {
|
|
55
45
|
if (!VERBOSE_PI_RAW_OUTPUT_LOGS) {
|
|
@@ -57,31 +47,42 @@ function logReviewRawModelOutput(args) {
|
|
|
57
47
|
}
|
|
58
48
|
logRawModelOutput(args);
|
|
59
49
|
}
|
|
60
|
-
|
|
50
|
+
function isOpenRouterBackedReviewRequest(provider, config) {
|
|
51
|
+
const resolvedPiProviderId = parsePiAuthProviderId(config.apiKey) ??
|
|
52
|
+
config.piProviderId ??
|
|
53
|
+
PI_PROVIDER_BY_DOISTBOT_PROVIDER[provider];
|
|
54
|
+
return resolvedPiProviderId === 'openrouter';
|
|
55
|
+
}
|
|
56
|
+
export async function runPiReviewProvider({ provider, modelName, prompt, config, prompts = [prompt], parseOutput, }) {
|
|
61
57
|
const startTime = Date.now();
|
|
62
58
|
const timeoutMs = config.timeout ?? 0;
|
|
63
|
-
const engine = logOptions?.engine ?? DEFAULT_REVIEW_LOG_ENGINE;
|
|
64
59
|
let stdout = '';
|
|
65
60
|
let stderr = '';
|
|
66
61
|
let status = null;
|
|
67
62
|
logger.info('Review model request', {
|
|
68
63
|
model: provider,
|
|
69
|
-
engine,
|
|
64
|
+
engine: REVIEW_LOG_ENGINE,
|
|
70
65
|
promptLength: prompt.length,
|
|
71
66
|
modelName,
|
|
72
67
|
timeoutMs,
|
|
68
|
+
...(isOpenRouterBackedReviewRequest(provider, config) && config.openRouterSessionId
|
|
69
|
+
? { openRouterSessionId: config.openRouterSessionId }
|
|
70
|
+
: {}),
|
|
73
71
|
});
|
|
74
72
|
try {
|
|
75
73
|
const invoker = createPiInvoker({
|
|
76
74
|
provider,
|
|
77
75
|
apiKey: config.apiKey,
|
|
76
|
+
piProviderId: config.piProviderId,
|
|
78
77
|
cwd: config.workspaceDir ?? WORKSPACE_DIR,
|
|
79
78
|
onProgress: (event) => {
|
|
80
|
-
emitReviewModelProgress(provider, event, config
|
|
79
|
+
emitReviewModelProgress(provider, event, config);
|
|
81
80
|
},
|
|
82
81
|
capabilityPreset: 'read-only',
|
|
83
82
|
thinkingLevel: config.thinkingLevel,
|
|
84
83
|
systemPrompt: REVIEW_PI_SYSTEM_PROMPT,
|
|
84
|
+
noContextFiles: true,
|
|
85
|
+
openRouterSessionId: config.openRouterSessionId,
|
|
85
86
|
});
|
|
86
87
|
for (let attempt = 0; attempt < prompts.length; attempt += 1) {
|
|
87
88
|
const result = await invoker.invoke({
|
|
@@ -95,7 +96,7 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
|
|
|
95
96
|
status = result.ok ? 0 : null;
|
|
96
97
|
logger.info('Review model tool usage', {
|
|
97
98
|
model: provider,
|
|
98
|
-
engine,
|
|
99
|
+
engine: REVIEW_LOG_ENGINE,
|
|
99
100
|
attempt: attempt + 1,
|
|
100
101
|
attemptsTotal: prompts.length,
|
|
101
102
|
attemptDurationMs: result.durationMs,
|
|
@@ -104,7 +105,6 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
|
|
|
104
105
|
if (!result.ok && result.timedOut) {
|
|
105
106
|
logReviewRawModelOutput({
|
|
106
107
|
model: provider,
|
|
107
|
-
engine,
|
|
108
108
|
stage: 'timeout',
|
|
109
109
|
success: false,
|
|
110
110
|
status,
|
|
@@ -115,7 +115,7 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
|
|
|
115
115
|
const error = result.error || `Pi ${provider} request timed out after ${timeoutMs}ms`;
|
|
116
116
|
logger.error('Review model failed', {
|
|
117
117
|
model: provider,
|
|
118
|
-
engine,
|
|
118
|
+
engine: REVIEW_LOG_ENGINE,
|
|
119
119
|
error,
|
|
120
120
|
durationMs,
|
|
121
121
|
});
|
|
@@ -129,7 +129,6 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
|
|
|
129
129
|
if (!result.ok) {
|
|
130
130
|
logReviewRawModelOutput({
|
|
131
131
|
model: provider,
|
|
132
|
-
engine,
|
|
133
132
|
stage: 'non-zero-exit',
|
|
134
133
|
success: false,
|
|
135
134
|
status,
|
|
@@ -140,7 +139,7 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
|
|
|
140
139
|
const error = `Pi ${provider} request failed: ${result.error.slice(0, 500)}`;
|
|
141
140
|
logger.error('Review model failed', {
|
|
142
141
|
model: provider,
|
|
143
|
-
engine,
|
|
142
|
+
engine: REVIEW_LOG_ENGINE,
|
|
144
143
|
error,
|
|
145
144
|
durationMs,
|
|
146
145
|
});
|
|
@@ -154,7 +153,6 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
|
|
|
154
153
|
if (!output.trim()) {
|
|
155
154
|
logReviewRawModelOutput({
|
|
156
155
|
model: provider,
|
|
157
|
-
engine,
|
|
158
156
|
stage: 'empty-output',
|
|
159
157
|
success: false,
|
|
160
158
|
status,
|
|
@@ -169,10 +167,10 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
|
|
|
169
167
|
});
|
|
170
168
|
}
|
|
171
169
|
try {
|
|
172
|
-
const normalized = normalizeParsedModelOutput(parseOutput(output), provider
|
|
170
|
+
const normalized = normalizeParsedModelOutput(parseOutput(output), provider);
|
|
173
171
|
logger.info('Review model response', {
|
|
174
172
|
model: provider,
|
|
175
|
-
engine,
|
|
173
|
+
engine: REVIEW_LOG_ENGINE,
|
|
176
174
|
findingsCount: normalized.findings.length,
|
|
177
175
|
durationMs,
|
|
178
176
|
parseMethod: 'extract_json',
|
|
@@ -180,13 +178,12 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
|
|
|
180
178
|
if (VERBOSE_PI_RAW_OUTPUT_LOGS) {
|
|
181
179
|
logger.info('Review model findings', {
|
|
182
180
|
model: provider,
|
|
183
|
-
engine,
|
|
181
|
+
engine: REVIEW_LOG_ENGINE,
|
|
184
182
|
findings: normalized.findings,
|
|
185
183
|
});
|
|
186
184
|
}
|
|
187
185
|
logReviewRawModelOutput({
|
|
188
186
|
model: provider,
|
|
189
|
-
engine,
|
|
190
187
|
stage: 'success',
|
|
191
188
|
success: true,
|
|
192
189
|
status,
|
|
@@ -198,35 +195,12 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
|
|
|
198
195
|
model: provider,
|
|
199
196
|
summary: normalized.summary,
|
|
200
197
|
findings: normalized.findings,
|
|
201
|
-
structuredOutput: normalized.structuredOutput,
|
|
202
198
|
durationMs,
|
|
203
199
|
};
|
|
204
200
|
}
|
|
205
201
|
catch (error) {
|
|
206
|
-
if (config.jsonSchema && attempt < prompts.length - 1) {
|
|
207
|
-
const retryError = error instanceof Error ? error.message : String(error);
|
|
208
|
-
logReviewRawModelOutput({
|
|
209
|
-
model: provider,
|
|
210
|
-
engine,
|
|
211
|
-
stage: 'schema-retry',
|
|
212
|
-
success: false,
|
|
213
|
-
status,
|
|
214
|
-
durationMs,
|
|
215
|
-
stdout,
|
|
216
|
-
stderr,
|
|
217
|
-
});
|
|
218
|
-
logger.warn('Review model schema retry', {
|
|
219
|
-
model: provider,
|
|
220
|
-
engine,
|
|
221
|
-
durationMs,
|
|
222
|
-
attempt: attempt + 1,
|
|
223
|
-
error: retryError,
|
|
224
|
-
});
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
202
|
logReviewRawModelOutput({
|
|
228
203
|
model: provider,
|
|
229
|
-
engine,
|
|
230
204
|
stage: 'parse-error',
|
|
231
205
|
success: false,
|
|
232
206
|
status,
|
|
@@ -248,7 +222,6 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
|
|
|
248
222
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
249
223
|
logReviewRawModelOutput({
|
|
250
224
|
model: provider,
|
|
251
|
-
engine,
|
|
252
225
|
stage: 'exception',
|
|
253
226
|
success: false,
|
|
254
227
|
status,
|
|
@@ -258,7 +231,7 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
|
|
|
258
231
|
});
|
|
259
232
|
logger.error('Review model failed', {
|
|
260
233
|
model: provider,
|
|
261
|
-
engine,
|
|
234
|
+
engine: REVIEW_LOG_ENGINE,
|
|
262
235
|
error: errorMessage,
|
|
263
236
|
durationMs,
|
|
264
237
|
});
|
|
@@ -21,37 +21,6 @@ export const REVIEW_SCHEMA = {
|
|
|
21
21
|
},
|
|
22
22
|
required: ['summary', 'comments'],
|
|
23
23
|
};
|
|
24
|
-
export const VOTE_SCHEMA = {
|
|
25
|
-
type: 'object',
|
|
26
|
-
additionalProperties: false,
|
|
27
|
-
properties: {
|
|
28
|
-
approve: { type: 'boolean' },
|
|
29
|
-
confidence: { type: 'number', minimum: 0, maximum: 1 },
|
|
30
|
-
reasoning: { type: 'string', minLength: 1 },
|
|
31
|
-
},
|
|
32
|
-
required: ['approve', 'confidence', 'reasoning'],
|
|
33
|
-
};
|
|
34
|
-
export const BATCH_VOTE_SCHEMA = {
|
|
35
|
-
type: 'object',
|
|
36
|
-
additionalProperties: false,
|
|
37
|
-
properties: {
|
|
38
|
-
votes: {
|
|
39
|
-
type: 'array',
|
|
40
|
-
items: {
|
|
41
|
-
type: 'object',
|
|
42
|
-
additionalProperties: false,
|
|
43
|
-
properties: {
|
|
44
|
-
findingId: { type: 'string', minLength: 1 },
|
|
45
|
-
approve: { type: 'boolean' },
|
|
46
|
-
confidence: { type: 'number', minimum: 0, maximum: 1 },
|
|
47
|
-
reasoning: { type: 'string', minLength: 1 },
|
|
48
|
-
},
|
|
49
|
-
required: ['findingId', 'approve', 'confidence', 'reasoning'],
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
required: ['votes'],
|
|
54
|
-
};
|
|
55
24
|
export const DEDUPE_SCHEMA = {
|
|
56
25
|
type: 'object',
|
|
57
26
|
additionalProperties: false,
|
|
@@ -76,18 +45,10 @@ export const DEDUPE_SCHEMA = {
|
|
|
76
45
|
};
|
|
77
46
|
const ajv = new Ajv();
|
|
78
47
|
const validateRawReviewResult = ajv.compile(REVIEW_SCHEMA);
|
|
79
|
-
const validateVoteResponse = ajv.compile(VOTE_SCHEMA);
|
|
80
|
-
const validateBatchVoteResponse = ajv.compile(BATCH_VOTE_SCHEMA);
|
|
81
48
|
const validateDedupeResponse = ajv.compile(DEDUPE_SCHEMA);
|
|
82
49
|
export function isRawReviewResult(value) {
|
|
83
50
|
return validateRawReviewResult(value);
|
|
84
51
|
}
|
|
85
|
-
export function isVoteResponse(value) {
|
|
86
|
-
return validateVoteResponse(value);
|
|
87
|
-
}
|
|
88
|
-
export function isBatchVoteResponse(value) {
|
|
89
|
-
return validateBatchVoteResponse(value);
|
|
90
|
-
}
|
|
91
52
|
export function isDedupeResponse(value) {
|
|
92
53
|
return validateDedupeResponse(value);
|
|
93
54
|
}
|