@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.
@@ -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
- assertTurnSucceeded(handle)
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) return
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 { /* signalling must never break the review */ }
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 { /* signalling must never break the review */ }
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.0"
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: