@doist/doistbot-cli 1.0.5 → 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 +55 -21
- package/dist/actions/doctor.js +13 -6
- package/dist/actions/review.js +75 -10
- package/dist/auth.js +51 -6
- package/dist/config.js +34 -5
- package/dist/env.js +1 -0
- package/dist/runtime.js +1 -1
- package/dist/terminal.js +2 -0
- package/package.json +4 -4
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -20
- package/sandbox/dist/core/check-run.js +16 -0
- package/sandbox/dist/core/datadog-metrics.js +6 -52
- package/sandbox/dist/core/logger-span-processor.js +78 -0
- package/sandbox/dist/core/pi.js +257 -176
- 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/credentials.js +12 -2
- package/sandbox/dist/providers/gemini.js +5 -1
- package/sandbox/dist/providers/helpers.js +12 -17
- package/sandbox/dist/providers/index.js +4 -0
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +12 -1
- package/sandbox/dist/tasks/chat/chat.js +161 -101
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
- package/sandbox/dist/tasks/issue-summarize/model.js +1 -1
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +25 -1
- 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/review/check-run.js +2 -1
- package/sandbox/dist/tasks/review/conversation-context.js +2 -1
- package/sandbox/dist/tasks/review/engines/dedupe.js +5 -4
- package/sandbox/dist/tasks/review/engines/multi-focus.js +40 -9
- package/sandbox/dist/tasks/review/engines/shared.js +8 -6
- package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
- package/sandbox/dist/tasks/review/local.js +2 -4
- package/sandbox/dist/tasks/review/review-summary.js +45 -7
- package/sandbox/dist/tasks/review/review.js +38 -24
- package/sandbox/dist/tasks/review/runner.js +23 -2
- package/sandbox/dist/tasks/review/summary-model.js +164 -17
- package/sandbox/dist/tasks/review/thinking-level.js +4 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
1
2
|
import { logger } from '../../core/logging.js';
|
|
2
|
-
import {
|
|
3
|
+
import { withPhase } from '../../core/tracing.js';
|
|
3
4
|
import { DefaultLoopEffects } from './fix-loop/effects.js';
|
|
4
5
|
import { runFixLoop } from './fix-loop/runners.js';
|
|
5
6
|
import { attemptFix, buildFixAssessmentSkipReason, shouldAttemptFix } from './fix-attempt.js';
|
|
@@ -80,111 +81,163 @@ function prUrlFor(targetRepo, prNumber) {
|
|
|
80
81
|
return `https://github.com/${targetRepo.repository.fullName}/pull/${prNumber}`;
|
|
81
82
|
}
|
|
82
83
|
export async function runConfiguredFix(input) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
attempted
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
84
|
+
return withPhase('triage.fix-attempt', { 'gen_ai.operation.name': 'invoke_agent', 'gen_ai.agent.name': 'codex.fix' }, async (span) => {
|
|
85
|
+
function finish(result) {
|
|
86
|
+
const prCreated = 'prCreated' in result ? result.prCreated : false;
|
|
87
|
+
// Only a real PR number counts as PR-evidence — dry-run loop
|
|
88
|
+
// states can carry a placeholder prNumber of 0.
|
|
89
|
+
const prNumber = 'prNumber' in result && result.prNumber !== undefined && result.prNumber > 0
|
|
90
|
+
? result.prNumber
|
|
91
|
+
: undefined;
|
|
92
|
+
// Dry-run skips are expected outcomes, not failures — signaled
|
|
93
|
+
// by the stable dryRun flag on the result, never by parsing
|
|
94
|
+
// the human-readable reason copy.
|
|
95
|
+
const isDryRunSkip = 'dryRun' in result && result.dryRun === true;
|
|
96
|
+
let outcome;
|
|
97
|
+
if (prCreated) {
|
|
98
|
+
outcome = 'pr-created';
|
|
99
|
+
}
|
|
100
|
+
else if (isDryRunSkip) {
|
|
101
|
+
outcome = 'dry-run';
|
|
102
|
+
}
|
|
103
|
+
else if (prNumber !== undefined) {
|
|
104
|
+
// The loop opened a PR earlier (REVIEWING/ITERATING) but
|
|
105
|
+
// ended needing a human: a PR exists, the autonomous
|
|
106
|
+
// attempt did not finish.
|
|
107
|
+
outcome = 'pr-opened-needs-human';
|
|
108
|
+
}
|
|
109
|
+
else if (result.attempted) {
|
|
110
|
+
outcome = 'attempted-no-pr';
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
outcome = 'not-attempted';
|
|
114
|
+
}
|
|
115
|
+
span.setAttributes({
|
|
116
|
+
'fix.attempted': result.attempted,
|
|
117
|
+
'fix.repository': result.repository,
|
|
118
|
+
'fix.pr_created': prCreated,
|
|
119
|
+
...(prNumber !== undefined ? { 'fix.pr_number': prNumber } : {}),
|
|
120
|
+
'fix.outcome': outcome,
|
|
121
|
+
});
|
|
122
|
+
// A real attempt that did not deliver a ready PR is a failure
|
|
123
|
+
// (including pr-opened-needs-human — someone must take over);
|
|
124
|
+
// a legitimate skip or a dry-run is not. withPhase does not
|
|
125
|
+
// force OK on a normal return, so this ERROR survives to
|
|
126
|
+
// phase.end.
|
|
127
|
+
if (result.attempted && !prCreated && !isDryRunSkip && 'reason' in result) {
|
|
128
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: result.reason });
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
const target = resolveFixTarget(input);
|
|
133
|
+
if (!target.ok) {
|
|
134
|
+
return finish({
|
|
135
|
+
attempted: false,
|
|
136
|
+
repository: target.repository,
|
|
137
|
+
reason: target.reason,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const targetRepo = target.targetRepo;
|
|
141
|
+
let repoConfig;
|
|
142
|
+
try {
|
|
143
|
+
repoConfig = await input.loadRepoConfig(targetRepo.repository);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
logger.warn('Fix loop config load failed', {
|
|
147
|
+
task: 'fix-loop',
|
|
148
|
+
repository: targetRepo.repository.fullName,
|
|
149
|
+
error: errorReason(error),
|
|
150
|
+
});
|
|
151
|
+
return finish({
|
|
152
|
+
attempted: true,
|
|
153
|
+
repository: targetRepo.repository.fullName,
|
|
154
|
+
prCreated: false,
|
|
155
|
+
reason: `Fix loop config load failed: ${errorReason(error)}`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const autoFixMode = getAutoFixMode(repoConfig, input.ctx.enableAutoFixOverride);
|
|
159
|
+
if (autoFixMode === 'disabled') {
|
|
160
|
+
return finish({
|
|
161
|
+
attempted: false,
|
|
162
|
+
repository: targetRepo.repository.fullName,
|
|
163
|
+
reason: 'Auto fix disabled by repository config.',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (autoFixMode === 'one-pass') {
|
|
167
|
+
return finish(await attemptFix(toAttemptFixInput(input)));
|
|
168
|
+
}
|
|
169
|
+
if (!shouldAttemptFix(input.fixAssessment)) {
|
|
170
|
+
return finish({
|
|
171
|
+
attempted: false,
|
|
172
|
+
repository: targetRepo.repository.fullName,
|
|
173
|
+
reason: buildFixAssessmentSkipReason(input.fixAssessment),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const session = {
|
|
177
|
+
kind: 'fix-loop',
|
|
178
|
+
ctx: input.ctx,
|
|
179
|
+
octokit: input.octokit,
|
|
180
|
+
triageOutput: input.triageOutput,
|
|
181
|
+
fixAssessment: input.fixAssessment,
|
|
182
|
+
targetRepo,
|
|
183
|
+
issueTitle: input.issueTitle,
|
|
184
|
+
issueUrl: input.issueUrl,
|
|
185
|
+
ghToken: input.ghToken,
|
|
186
|
+
pushToken: input.ctx.githubToken,
|
|
187
|
+
apiKeys: {
|
|
188
|
+
openai: input.ctx.openaiApiKey,
|
|
189
|
+
...(input.ctx.geminiApiKey !== undefined && { gemini: input.ctx.geminiApiKey }),
|
|
190
|
+
},
|
|
154
191
|
};
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
192
|
+
let finalState;
|
|
193
|
+
try {
|
|
194
|
+
finalState = await runFixLoop({
|
|
195
|
+
effects: new DefaultLoopEffects(session, { dryRun: input.ctx.dryRun }),
|
|
196
|
+
...(input.ctx.dryRun ? { onTransition: logDryRunTransition } : {}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
return finish({
|
|
159
201
|
attempted: true,
|
|
160
202
|
repository: targetRepo.repository.fullName,
|
|
161
203
|
prCreated: false,
|
|
162
|
-
reason:
|
|
163
|
-
};
|
|
204
|
+
reason: `${FIX_LOOP_FAILED_REASON_PREFIX} ${errorReason(error)}`,
|
|
205
|
+
});
|
|
164
206
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
207
|
+
if (finalState.kind === 'PUSHED_AWAITING_CI') {
|
|
208
|
+
if (input.ctx.dryRun) {
|
|
209
|
+
return finish({
|
|
210
|
+
attempted: true,
|
|
211
|
+
repository: targetRepo.repository.fullName,
|
|
212
|
+
prCreated: false,
|
|
213
|
+
reason: 'Dry run: PR not created.',
|
|
214
|
+
dryRun: true,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const prNumber = finalState.prNumber;
|
|
218
|
+
const prUrl = prUrlFor(targetRepo, prNumber);
|
|
219
|
+
return finish({
|
|
220
|
+
attempted: true,
|
|
221
|
+
repository: targetRepo.repository.fullName,
|
|
222
|
+
prCreated: true,
|
|
223
|
+
prUrl,
|
|
224
|
+
prNumber,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const reason = finalState.kind === 'DONE_NEEDS_HUMAN'
|
|
228
|
+
? finalState.reason
|
|
229
|
+
: `Fix loop ended in ${finalState.kind}`;
|
|
230
|
+
return finish({
|
|
168
231
|
attempted: true,
|
|
169
232
|
repository: targetRepo.repository.fullName,
|
|
170
|
-
prCreated:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
repository: targetRepo.repository.fullName,
|
|
181
|
-
prCreated: false,
|
|
182
|
-
reason,
|
|
183
|
-
// If a PR was opened earlier in the loop (REVIEWING/ITERATING reached
|
|
184
|
-
// before DONE_NEEDS_HUMAN), surface it so callers can distinguish a
|
|
185
|
-
// silent failure from an in-progress PR.
|
|
186
|
-
...('prNumber' in finalState && finalState.prNumber !== undefined
|
|
187
|
-
? { prNumber: finalState.prNumber }
|
|
188
|
-
: {}),
|
|
189
|
-
};
|
|
233
|
+
prCreated: false,
|
|
234
|
+
reason,
|
|
235
|
+
// If a PR was opened earlier in the loop (REVIEWING/ITERATING reached
|
|
236
|
+
// before DONE_NEEDS_HUMAN), surface it so callers can distinguish a
|
|
237
|
+
// silent failure from an in-progress PR.
|
|
238
|
+
...('prNumber' in finalState && finalState.prNumber !== undefined
|
|
239
|
+
? { prNumber: finalState.prNumber }
|
|
240
|
+
: {}),
|
|
241
|
+
});
|
|
242
|
+
});
|
|
190
243
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { iterationLabel, PROMOTION_COMMENT_MARKER } from 'doistbot-shared-contracts';
|
|
1
2
|
import { logger } from '../../../core/logging.js';
|
|
2
3
|
import { isFixLoopTerminal, isTerminal, next, } from './state-machine.js';
|
|
3
4
|
const CI_FAILED_LABEL = 'doistbot:ci-failed';
|
|
@@ -6,10 +7,13 @@ const AWAITING_CI_LABEL = 'doistbot:awaiting-ci';
|
|
|
6
7
|
// Stamped on terminal comments so postIssueComment can find-or-update an
|
|
7
8
|
// existing comment instead of double-posting when an earlier dispatch
|
|
8
9
|
// succeeded on GitHub but the response timed out.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
//
|
|
11
|
+
// The success and human-escalation paths use DISTINCT markers: merge tracking
|
|
12
|
+
// reads 👍/👎 reactions on the promotion (success) comment to derive the thumbs
|
|
13
|
+
// metric, so it must be uniquely identifiable. A shared marker would let a
|
|
14
|
+
// CI-failed-then-human-merged bot PR expose its failure comment as the reaction
|
|
15
|
+
// target instead.
|
|
16
|
+
const PROMOTION_NEEDS_HUMAN_COMMENT_MARKER = '<!--doistbot-promotion-needs-human-comment-->';
|
|
13
17
|
function formatFailedChecks(checks) {
|
|
14
18
|
return checks.map((c) => (c.url ? `- [${c.name}](${c.url})` : `- ${c.name}`)).join('\n');
|
|
15
19
|
}
|
|
@@ -176,7 +180,12 @@ export async function runPromotionFlow(effects, input) {
|
|
|
176
180
|
await effects.postIssueComment({
|
|
177
181
|
prNumber: input.prNumber,
|
|
178
182
|
marker: PROMOTION_COMMENT_MARKER,
|
|
179
|
-
|
|
183
|
+
// This comment doubles as the thumbs-feedback prompt: merge
|
|
184
|
+
// tracking reads 👍/👎 reactions on this exact comment (located
|
|
185
|
+
// via the marker above) when the PR merges. Keep the reaction
|
|
186
|
+
// ask on the comment people are invited to react to.
|
|
187
|
+
body: 'CI passed — promoting this PR out of draft and requesting hero review.\n\n' +
|
|
188
|
+
'<sub>🤖🫵 Please, react 👍 or 👎 on this comment to rate this bot-authored PR.</sub>',
|
|
180
189
|
});
|
|
181
190
|
await effects.promoteToReady(input.prNumber);
|
|
182
191
|
return state;
|
|
@@ -193,7 +202,7 @@ export async function runPromotionFlow(effects, input) {
|
|
|
193
202
|
: 'This PR needs human review.';
|
|
194
203
|
await effects.postIssueComment({
|
|
195
204
|
prNumber: input.prNumber,
|
|
196
|
-
marker:
|
|
205
|
+
marker: PROMOTION_NEEDS_HUMAN_COMMENT_MARKER,
|
|
197
206
|
body,
|
|
198
207
|
});
|
|
199
208
|
return state;
|
|
@@ -1,39 +1,123 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
2
|
+
import { DEFAULT_PI_MODELS, invokePiPrompt } from '../../core/pi.js';
|
|
3
|
+
import { recordEvent, startToolSpan, withPhase } from '../../core/tracing.js';
|
|
2
4
|
const ISSUE_TRIAGE_TIMEOUT_MS = 12 * 60 * 1000;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
const TRIAGE_PROVIDER = 'openai';
|
|
6
|
+
export async function invokeTriageModel({ prompt, apiKey, cwd, timeoutMs = ISSUE_TRIAGE_TIMEOUT_MS, extraEnv, thinkingLevel, modelName = DEFAULT_PI_MODELS[TRIAGE_PROVIDER],
|
|
7
|
+
// Callers on other paths (e.g. fix generation) pass their own phase name
|
|
8
|
+
// so model telemetry stays separable per flow.
|
|
9
|
+
phaseName = 'triage.model', }) {
|
|
10
|
+
return withPhase(phaseName, {
|
|
11
|
+
'gen_ai.operation.name': 'chat',
|
|
12
|
+
'gen_ai.provider.name': TRIAGE_PROVIDER,
|
|
13
|
+
'gen_ai.request.model': modelName,
|
|
14
|
+
}, async (span) => {
|
|
15
|
+
// pi emits tool start/end across separate onProgress callbacks, so
|
|
16
|
+
// tool spans are managed manually (the documented exception to
|
|
17
|
+
// "withPhase everywhere"). startToolSpan captures context.active()
|
|
18
|
+
// at call time, parenting each tool span under THIS model span.
|
|
19
|
+
const inFlightTools = new Map();
|
|
20
|
+
function toolKey(event) {
|
|
21
|
+
return event.toolCallId ?? event.toolName ?? '';
|
|
22
|
+
}
|
|
23
|
+
function onProgress(event) {
|
|
24
|
+
switch (event.type) {
|
|
25
|
+
case 'tool_start': {
|
|
26
|
+
const toolCallId = event.toolCallId;
|
|
27
|
+
const key = toolKey(event);
|
|
28
|
+
// Overlapping same-name tools without a toolCallId
|
|
29
|
+
// collide on the key; close the displaced span so it
|
|
30
|
+
// is never leaked un-ended.
|
|
31
|
+
const displaced = inFlightTools.get(key);
|
|
32
|
+
if (displaced) {
|
|
33
|
+
displaced.setStatus({
|
|
34
|
+
code: SpanStatusCode.ERROR,
|
|
35
|
+
message: 'replaced by overlapping same-name tool without toolCallId',
|
|
36
|
+
});
|
|
37
|
+
displaced.end();
|
|
38
|
+
}
|
|
39
|
+
const toolSpan = startToolSpan(event.toolName, {
|
|
40
|
+
'gen_ai.operation.name': 'execute_tool',
|
|
41
|
+
'gen_ai.tool.name': event.toolName,
|
|
42
|
+
...(toolCallId ? { 'gen_ai.tool.call.id': toolCallId } : {}),
|
|
43
|
+
});
|
|
44
|
+
inFlightTools.set(key, toolSpan);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
case 'tool_end': {
|
|
48
|
+
const key = toolKey(event);
|
|
49
|
+
const toolSpan = inFlightTools.get(key);
|
|
50
|
+
if (toolSpan) {
|
|
51
|
+
if (event.isError) {
|
|
52
|
+
toolSpan.setStatus({
|
|
53
|
+
code: SpanStatusCode.ERROR,
|
|
54
|
+
message: 'tool reported error',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
toolSpan.end();
|
|
58
|
+
inFlightTools.delete(key);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
case 'thinking':
|
|
63
|
+
case 'responding': {
|
|
64
|
+
// Liveness signal for long model phases: pi already
|
|
65
|
+
// throttles these to one per 30s. Emitted as point-in-time
|
|
66
|
+
// events (correlated to this model span), NOT spans —
|
|
67
|
+
// they are heartbeats, not boundaries.
|
|
68
|
+
recordEvent('model.heartbeat', { state: event.type });
|
|
69
|
+
}
|
|
70
|
+
// agent_start/agent_end duplicate this phase span's own
|
|
71
|
+
// boundaries and tool_update is covered by the open tool
|
|
72
|
+
// span — intentionally no emission for those.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const startedAt = Date.now();
|
|
76
|
+
const result = await invokePiPrompt({
|
|
77
|
+
provider: TRIAGE_PROVIDER,
|
|
78
|
+
apiKey,
|
|
79
|
+
prompt,
|
|
80
|
+
cwd,
|
|
81
|
+
timeoutMs,
|
|
82
|
+
extraEnv,
|
|
83
|
+
capabilityPreset: 'shell-focused',
|
|
84
|
+
...(thinkingLevel ? { thinkingLevel } : {}),
|
|
85
|
+
...(modelName ? { modelName } : {}),
|
|
86
|
+
onProgress,
|
|
87
|
+
});
|
|
88
|
+
// Force-end any tool spans pi never closed (defensive — pi's stream
|
|
89
|
+
// is ordered, but if a tool_end was lost we don't want hanging spans).
|
|
90
|
+
for (const [, toolSpan] of inFlightTools) {
|
|
91
|
+
toolSpan.setStatus({
|
|
92
|
+
code: SpanStatusCode.ERROR,
|
|
93
|
+
message: 'tool span did not receive matching end',
|
|
94
|
+
});
|
|
95
|
+
toolSpan.end();
|
|
96
|
+
}
|
|
97
|
+
inFlightTools.clear();
|
|
98
|
+
const durationMs = Date.now() - startedAt;
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
// Mark the model phase as failed. withPhase does NOT force OK on
|
|
101
|
+
// success, so this ERROR status survives to the phase.end line.
|
|
102
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: result.error });
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: result.error,
|
|
106
|
+
durationMs,
|
|
107
|
+
timedOut: result.timedOut,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const output = result.output.trim();
|
|
111
|
+
if (!output) {
|
|
112
|
+
const message = 'Pi triage model did not produce an assistant response';
|
|
113
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
error: message,
|
|
117
|
+
durationMs,
|
|
118
|
+
timedOut: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { ok: true, output, durationMs };
|
|
15
122
|
});
|
|
16
|
-
const durationMs = Date.now() - startedAt;
|
|
17
|
-
if (!result.ok) {
|
|
18
|
-
return {
|
|
19
|
-
ok: false,
|
|
20
|
-
error: result.error,
|
|
21
|
-
durationMs,
|
|
22
|
-
timedOut: result.timedOut,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
const output = result.output.trim();
|
|
26
|
-
if (!output) {
|
|
27
|
-
return {
|
|
28
|
-
ok: false,
|
|
29
|
-
error: 'Pi triage model did not produce an assistant response',
|
|
30
|
-
durationMs,
|
|
31
|
-
timedOut: false,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
ok: true,
|
|
36
|
-
output,
|
|
37
|
-
durationMs,
|
|
38
|
-
};
|
|
39
123
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
1
2
|
import { fillPromptTemplate } from '../../core/prompt-template.js';
|
|
3
|
+
import { withPhase } from '../../core/tracing.js';
|
|
2
4
|
const REQUIRED_SECTIONS = [
|
|
3
5
|
'Summary',
|
|
4
6
|
'Findings',
|
|
@@ -121,3 +123,28 @@ export function parseFixAssessment(triageOutput) {
|
|
|
121
123
|
};
|
|
122
124
|
}
|
|
123
125
|
export { REQUIRED_HEADINGS };
|
|
126
|
+
export function validateTriageOutput(params) {
|
|
127
|
+
return withPhase('triage.output', { 'output.length': params.rawOutput.length }, async (span) => {
|
|
128
|
+
const normalized = ensureStructuredTriageOutput(params.rawOutput);
|
|
129
|
+
span.setAttributes({
|
|
130
|
+
'output.missing_sections.count': normalized.missingSections.length,
|
|
131
|
+
'output.quality_check.passed': normalized.missingSections.length === 0,
|
|
132
|
+
});
|
|
133
|
+
if (normalized.missingSections.length > 0) {
|
|
134
|
+
// Mirrors the failure path in current triage.ts (missingSections > 0).
|
|
135
|
+
const error = `Triage model returned incomplete output. Missing sections: ${normalized.missingSections.join(', ')}`;
|
|
136
|
+
// ensureStructuredTriageOutput does not throw; mark the span ERROR
|
|
137
|
+
// explicitly. withPhase does not force OK on a normal return, so this
|
|
138
|
+
// survives to phase.end.
|
|
139
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error });
|
|
140
|
+
return { ok: false, error };
|
|
141
|
+
}
|
|
142
|
+
const assessment = parseFixAssessment(normalized.output);
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
commentOutput: normalized.commentOutput,
|
|
146
|
+
output: normalized.output,
|
|
147
|
+
assessment,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'child_process';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { DOISTBOT_AUTO_FIX_LABEL, DOISTBOT_SPECULATIVE_FIX_LABEL } from 'doistbot-shared-contracts';
|
|
4
5
|
import { logger } from '../../core/logging.js';
|
|
5
6
|
import { fillPromptTemplate, loadPromptTemplateFile } from '../../core/prompt-template.js';
|
|
6
7
|
const PR_TEMPLATE_PATHS = [
|
|
@@ -299,8 +300,8 @@ export async function openDraftFixPR(input) {
|
|
|
299
300
|
});
|
|
300
301
|
try {
|
|
301
302
|
const prLabel = input.fixAssessment?.classification === 'speculative'
|
|
302
|
-
?
|
|
303
|
-
:
|
|
303
|
+
? DOISTBOT_SPECULATIVE_FIX_LABEL
|
|
304
|
+
: DOISTBOT_AUTO_FIX_LABEL;
|
|
304
305
|
await input.octokit.rest.issues.addLabels({
|
|
305
306
|
owner: input.repoOwner,
|
|
306
307
|
repo: input.repoName,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { resolveIssueRepositories, } from '../../core/repository-resolver.js';
|
|
2
|
+
import { recordDecision, withPhase } from '../../core/tracing.js';
|
|
3
|
+
export async function resolveTriageScope(params) {
|
|
4
|
+
return withPhase('triage.resolution', { 'labels.count': params.labelNames.length }, async (span) => {
|
|
5
|
+
const result = resolveIssueRepositories(params.labelNames);
|
|
6
|
+
recordDecision('triage.resolution.outcome', {
|
|
7
|
+
trigger: params.triggerType,
|
|
8
|
+
status: result.status,
|
|
9
|
+
confidence: result.confidence,
|
|
10
|
+
reason: result.reason,
|
|
11
|
+
decision_path: result.decisionPath,
|
|
12
|
+
parsed_products: result.parsedLabels.productLabels,
|
|
13
|
+
parsed_apps: result.parsedLabels.appLabels,
|
|
14
|
+
parsed_teams: result.parsedLabels.teamLabels,
|
|
15
|
+
unknown_product_labels: result.parsedLabels.unknownProductLabels,
|
|
16
|
+
unknown_app_labels: result.parsedLabels.unknownAppLabels,
|
|
17
|
+
unknown_team_labels: result.parsedLabels.unknownTeamLabels,
|
|
18
|
+
});
|
|
19
|
+
span.setAttributes({
|
|
20
|
+
'resolution.status': result.status,
|
|
21
|
+
'resolution.confidence': result.confidence,
|
|
22
|
+
'resolution.repos.count': result.status === 'resolved' ? result.repositories.length : 0,
|
|
23
|
+
});
|
|
24
|
+
return result;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { withPhase } from '../../core/tracing.js';
|
|
2
2
|
import { shouldAttemptFix } from './fix-attempt.js';
|
|
3
3
|
import { CX_HERO_MANUAL_HANDLE, resolveHeroGroup } from './hero-group-map.js';
|
|
4
4
|
import { postIssueComment } from './side-effects.js';
|
|
@@ -61,16 +61,16 @@ export function shouldPostHeroRoutingComment(fixAssessment) {
|
|
|
61
61
|
return !shouldAttemptFix(fixAssessment);
|
|
62
62
|
}
|
|
63
63
|
export async function postHeroRoutingComment({ octokit, ctx, body, }) {
|
|
64
|
-
logger.info('Issue triage routing comment post starting', {
|
|
65
|
-
task: 'issue-triage',
|
|
66
|
-
issueNumber: ctx.issueNumber,
|
|
67
|
-
bodyChars: body.length,
|
|
68
|
-
});
|
|
69
64
|
await postIssueComment(ctx, octokit, body, {
|
|
70
65
|
marker: HERO_ROUTING_MARKER,
|
|
71
66
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
}
|
|
68
|
+
export async function postTriageRouting(params) {
|
|
69
|
+
await withPhase('triage.routing', { 'comment.body.length': params.body.length }, async () => {
|
|
70
|
+
await postHeroRoutingComment({
|
|
71
|
+
octokit: params.octokit,
|
|
72
|
+
ctx: params.ctx,
|
|
73
|
+
body: params.body,
|
|
74
|
+
});
|
|
75
75
|
});
|
|
76
76
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logger } from '../../core/logging.js';
|
|
2
|
+
import { withPhase } from '../../core/tracing.js';
|
|
2
3
|
import { TRIAGE_MARKER } from './shared.js';
|
|
3
4
|
export async function postIssueComment(ctx, octokit, body, options = {}) {
|
|
4
5
|
const marker = options.marker;
|
|
@@ -44,6 +45,14 @@ export async function postIssueComment(ctx, octokit, body, options = {}) {
|
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
export async function postTriageOutput(params) {
|
|
49
|
+
await withPhase('triage.side-effects', { 'comment.body.length': params.body.length }, async () => {
|
|
50
|
+
await postIssueComment(params.ctx, params.octokit, params.body, {
|
|
51
|
+
marker: TRIAGE_MARKER,
|
|
52
|
+
placeholderCommentId: params.ctx.placeholderCommentId,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
47
56
|
export async function deleteIssueComment(ctx, octokit, commentId, options = {}) {
|
|
48
57
|
if (ctx.dryRun) {
|
|
49
58
|
logger.info('Issue triage dry run side effect skipped', {
|