@ddtcorex/dsh-maestro-review 0.6.0 → 0.6.1
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/lib/orchestrator.d.ts +11 -0
- package/lib/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator.js +21 -2
- package/lib/orchestrator.js.map +1 -1
- package/lib/review-signals.d.ts.map +1 -1
- package/lib/review-signals.js +25 -6
- package/lib/review-signals.js.map +1 -1
- package/package.json +2 -2
- package/profiles/reviewer-ci/cordis.patch.yml +12 -1
- package/profiles/reviewer-ci/package.json +2 -1
- package/profiles/reviewer-ci/pnpm-lock.yaml +3219 -874
- package/src/host/orchestrator.ts +21 -2
- package/src/host/review-signals.ts +24 -6
- package/templates/reviewer-project.gitlab-ci.yml +1 -1
package/src/host/orchestrator.ts
CHANGED
|
@@ -250,6 +250,25 @@ export function assertTurnSucceeded(handle: unknown): void {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Like assertTurnSucceeded, but only throws when there is nothing left to
|
|
255
|
+
* salvage. A turn can error AFTER already producing real output — findings
|
|
256
|
+
* reported via the tool, or audit text written — when only the turn's own
|
|
257
|
+
* closing step fails afterward (a trailing rate-limit/auth hiccup). Throwing
|
|
258
|
+
* unconditionally there discarded already-produced, real work for no
|
|
259
|
+
* benefit; this only fails closed when the turn errored AND there is
|
|
260
|
+
* nothing usable to fall back on. Either way the turn error is never
|
|
261
|
+
* silent: logged as a warning when salvaging, thrown when not.
|
|
262
|
+
*/
|
|
263
|
+
export function assertTurnSucceededOrSalvage(handle: unknown, hasSalvageableOutput: boolean, label: string): void {
|
|
264
|
+
const turnMsg = getTurnErrorMessage(handle)
|
|
265
|
+
if (turnMsg === undefined) return
|
|
266
|
+
if (!hasSalvageableOutput) {
|
|
267
|
+
throw new Error(`Review turn failed before completing: ${turnMsg}`)
|
|
268
|
+
}
|
|
269
|
+
console.warn(`maestro-orchestrator: ${label} turn ended with an error after already producing usable output — using it anyway: ${turnMsg}`)
|
|
270
|
+
}
|
|
271
|
+
|
|
253
272
|
/**
|
|
254
273
|
* Resolve the model to use for an automated review. Priority: per-project
|
|
255
274
|
* override > global reviewModel (Maestro Settings) > row-config reviewModel
|
|
@@ -1097,7 +1116,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1097
1116
|
source: { kind: 'user' },
|
|
1098
1117
|
}))
|
|
1099
1118
|
await whenIdleWithTimeout(handle, effectiveAgentTimeoutMs)
|
|
1100
|
-
|
|
1119
|
+
assertTurnSucceededOrSalvage(handle, capturedFindings.length > 0, 'reviewer')
|
|
1101
1120
|
if (reviewProfile !== undefined && (reviewerContext === undefined || loadedReviewProfile(reviewerContext) !== reviewProfile)) {
|
|
1102
1121
|
throw new Error(`reviewer did not successfully load the required ${reviewProfile} review skill profile; no findings were posted`)
|
|
1103
1122
|
}
|
|
@@ -1195,9 +1214,9 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1195
1214
|
const prompt = buildAuditorPrompt({ staticOnly: opts?.staticOnly === true })
|
|
1196
1215
|
handle.agent.followup(createUserMessage({ content: [{ type: 'text', text: prompt }], source: { kind: 'user' } }))
|
|
1197
1216
|
await whenIdleWithTimeout(handle, effectiveAgentTimeoutMs)
|
|
1198
|
-
assertTurnSucceeded(handle)
|
|
1199
1217
|
const output = auditorOutputFromSession(handle.agent.session)
|
|
1200
1218
|
const text = output.map(block => ('text' in block ? block.text : '')).join('')
|
|
1219
|
+
assertTurnSucceededOrSalvage(handle, text.trim() !== '', 'auditor')
|
|
1201
1220
|
return `## Maestro Performance Audit\n\n${text}`
|
|
1202
1221
|
} finally {
|
|
1203
1222
|
await handle.dispose()
|
|
@@ -13,11 +13,15 @@ export interface ReviewSignals {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
async function award(baseUrl: string, token: string, projectId: number, mrIid: number, name: string): Promise<void> {
|
|
16
|
-
await fetchWithTimeout(`${baseUrl}/api/v4/projects/${projectId}/merge_requests/${mrIid}/award_emoji`, {
|
|
16
|
+
const response = await fetchWithTimeout(`${baseUrl}/api/v4/projects/${projectId}/merge_requests/${mrIid}/award_emoji`, {
|
|
17
17
|
method: 'POST',
|
|
18
18
|
headers: { ...gitlabAuthHeaders(token), 'Content-Type': 'application/json' },
|
|
19
19
|
body: JSON.stringify({ name }),
|
|
20
20
|
})
|
|
21
|
+
// fetchWithTimeout only throws on a network/abort failure; a non-2xx status
|
|
22
|
+
// (expired token, missing scope, rate-limited) resolves normally and must
|
|
23
|
+
// be checked explicitly, or the caller's failure logging never fires.
|
|
24
|
+
if (!response.ok) throw new Error(`GitLab API error ${response.status} awarding "${name}": ${await response.text()}`)
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/** Remove only this bot's stale running markers; other users' awards stay untouched. */
|
|
@@ -25,14 +29,20 @@ async function unawardOwn(baseUrl: string, token: string, projectId: number, mrI
|
|
|
25
29
|
const response = await fetchWithTimeout(`${baseUrl}/api/v4/projects/${projectId}/merge_requests/${mrIid}/award_emoji`, {
|
|
26
30
|
headers: gitlabAuthHeaders(token),
|
|
27
31
|
})
|
|
28
|
-
if (!response.ok)
|
|
32
|
+
if (!response.ok) throw new Error(`GitLab API error ${response.status} listing award emoji: ${await response.text()}`)
|
|
29
33
|
const awards = (await response.json()) as Array<{ id?: number; name?: string; user?: { username?: string } }>
|
|
30
34
|
for (const awardItem of Array.isArray(awards) ? awards : []) {
|
|
31
35
|
if (awardItem.name !== 'eyes' || awardItem.user?.username !== botUsername || typeof awardItem.id !== 'number') continue
|
|
32
|
-
await fetchWithTimeout(`${baseUrl}/api/v4/projects/${projectId}/merge_requests/${mrIid}/award_emoji/${awardItem.id}`, {
|
|
36
|
+
const deleteResponse = await fetchWithTimeout(`${baseUrl}/api/v4/projects/${projectId}/merge_requests/${mrIid}/award_emoji/${awardItem.id}`, {
|
|
33
37
|
method: 'DELETE',
|
|
34
38
|
headers: gitlabAuthHeaders(token),
|
|
35
|
-
}).catch(() => {
|
|
39
|
+
}).catch((err: unknown) => {
|
|
40
|
+
console.error(`review-signals: failed to delete stale eyes marker ${awardItem.id} on MR !${mrIid}`, err)
|
|
41
|
+
return undefined
|
|
42
|
+
})
|
|
43
|
+
if (deleteResponse !== undefined && !deleteResponse.ok) {
|
|
44
|
+
console.error(`review-signals: GitLab API error ${deleteResponse.status} deleting stale eyes marker ${awardItem.id} on MR !${mrIid}`)
|
|
45
|
+
}
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -43,13 +53,21 @@ export function createReviewSignals(options: { baseUrl: string; token: string; p
|
|
|
43
53
|
try {
|
|
44
54
|
await unawardOwn(baseUrl, token, projectId, mrIid, botUsername)
|
|
45
55
|
await award(baseUrl, token, projectId, mrIid, 'eyes')
|
|
46
|
-
} catch {
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// Signalling must never break the review, but a swallowed failure
|
|
58
|
+
// here is exactly what leaves a stale "eyes" marker stuck on the MR
|
|
59
|
+
// forever (blocking the push-gate's 👀-running check for both the
|
|
60
|
+
// webhook and CI flows) with zero trace of why — log it.
|
|
61
|
+
console.error(`review-signals: failed to set the running marker on MR !${mrIid}`, err)
|
|
62
|
+
}
|
|
47
63
|
},
|
|
48
64
|
async finish(outcome) {
|
|
49
65
|
try {
|
|
50
66
|
await unawardOwn(baseUrl, token, projectId, mrIid, botUsername)
|
|
51
67
|
await award(baseUrl, token, projectId, mrIid, outcome === 'completed' ? 'white_check_mark' : 'warning')
|
|
52
|
-
} catch {
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(`review-signals: failed to clear the running marker / award the final marker on MR !${mrIid}`, err)
|
|
70
|
+
}
|
|
53
71
|
},
|
|
54
72
|
}
|
|
55
73
|
}
|
|
@@ -3,7 +3,7 @@ stages: [review]
|
|
|
3
3
|
|
|
4
4
|
variables:
|
|
5
5
|
# Image registry — pin the SHA for reproducibility
|
|
6
|
-
REVIEWER_IMAGE: "ddtcorex/maestro-reviewer:0.1
|
|
6
|
+
REVIEWER_IMAGE: "ddtcorex/maestro-reviewer:0.6.1"
|
|
7
7
|
# Or use $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA when building the image in the same project
|
|
8
8
|
|
|
9
9
|
review:
|