@ddtcorex/dsh-maestro-review 0.4.0 → 0.5.0
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/README.md +12 -0
- package/cordis.patch.yml +18 -0
- package/lib/ci-clone.d.ts +21 -0
- package/lib/ci-clone.d.ts.map +1 -0
- package/lib/ci-clone.js +35 -0
- package/lib/ci-clone.js.map +1 -0
- package/lib/ci-coexist.d.ts +9 -0
- package/lib/ci-coexist.d.ts.map +1 -0
- package/lib/ci-coexist.js +36 -0
- package/lib/ci-coexist.js.map +1 -0
- package/lib/config-store.d.ts +13 -6
- package/lib/config-store.d.ts.map +1 -1
- package/lib/config-store.js.map +1 -1
- package/lib/events.d.ts +23 -0
- package/lib/events.d.ts.map +1 -1
- package/lib/gitlab-auth.d.ts +11 -0
- package/lib/gitlab-auth.d.ts.map +1 -0
- package/lib/gitlab-auth.js +15 -0
- package/lib/gitlab-auth.js.map +1 -0
- package/lib/gitlab-client.d.ts.map +1 -1
- package/lib/gitlab-client.js +2 -1
- package/lib/gitlab-client.js.map +1 -1
- package/lib/incremental.d.ts.map +1 -1
- package/lib/incremental.js +3 -2
- package/lib/incremental.js.map +1 -1
- package/lib/orchestrator.d.ts +51 -4
- package/lib/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator.js +201 -59
- package/lib/orchestrator.js.map +1 -1
- package/lib/providers/ci-trigger.d.ts +43 -0
- package/lib/providers/ci-trigger.d.ts.map +1 -0
- package/lib/providers/ci-trigger.js +146 -0
- package/lib/providers/ci-trigger.js.map +1 -0
- package/lib/providers/gitlab.d.ts.map +1 -1
- package/lib/providers/gitlab.js +2 -1
- package/lib/providers/gitlab.js.map +1 -1
- package/lib/review-marker.d.ts +17 -0
- package/lib/review-marker.d.ts.map +1 -0
- package/lib/review-marker.js +23 -0
- package/lib/review-marker.js.map +1 -0
- package/lib/review-signals.d.ts.map +1 -1
- package/lib/review-signals.js +4 -8
- package/lib/review-signals.js.map +1 -1
- package/lib/settings-rpc.js +1 -1
- package/lib/settings-rpc.js.map +1 -1
- package/package.json +14 -9
- package/profiles/reviewer-ci/cordis.patch.yml +61 -0
- package/profiles/reviewer-ci/package.json +19 -0
- package/profiles/reviewer-ci/pnpm-lock.yaml +62 -0
- package/profiles/reviewer-ci/pnpm-workspace.yaml +12 -0
- package/src/host/ci-clone.ts +54 -0
- package/src/host/ci-coexist.ts +43 -0
- package/src/host/config-store.ts +14 -1
- package/src/host/events.ts +25 -0
- package/src/host/gitlab-auth.ts +13 -0
- package/src/host/gitlab-client.ts +2 -1
- package/src/host/incremental.ts +3 -2
- package/src/host/orchestrator.ts +202 -37
- package/src/host/providers/ci-trigger.ts +164 -0
- package/src/host/providers/gitlab.ts +2 -1
- package/src/host/review-marker.ts +25 -0
- package/src/host/review-signals.ts +4 -3
- package/src/host/settings-rpc.ts +1 -1
- package/templates/reviewer-project.gitlab-ci.yml +90 -0
- package/templates/source-project.gitlab-ci.yml +42 -0
package/lib/orchestrator.js
CHANGED
|
@@ -31,6 +31,9 @@ import { reviewDigestText } from './notify.js';
|
|
|
31
31
|
import { loadedReviewProfile } from './skills-tool.js';
|
|
32
32
|
import { gitlabProvider } from './providers/gitlab.js';
|
|
33
33
|
import './events.js';
|
|
34
|
+
import { gitlabAuthHeaders } from './gitlab-auth.js';
|
|
35
|
+
import { reviewMarker, resolveFlow } from './review-marker.js';
|
|
36
|
+
import { cloneSourceRepo, defaultRun } from './ci-clone.js';
|
|
34
37
|
// Provider-aware wrapper — orchestrator can run reviews via any ReviewProvider.
|
|
35
38
|
// This keeps the GitLab-specific flow intact while allowing Phase C to add GitHub/Jira without modifying core logic.
|
|
36
39
|
export async function runReviewWithProvider(provider, request) {
|
|
@@ -132,7 +135,8 @@ export function buildReviewComment(opts) {
|
|
|
132
135
|
? `\n\n<details>\n<summary>⚠️ Failed to post (${opts.failures.length})</summary>\n\n${opts.failures.map((f) => `- \`${f}\``).join('\n')}\n\n</details>`
|
|
133
136
|
: '';
|
|
134
137
|
const footer = `\n\n---\n\n<sub>Generated by Maestro · [View MR →](${mrUrl})</sub>`;
|
|
135
|
-
|
|
138
|
+
const markerLine = opts.marker === undefined ? '' : `\n\n${reviewMarker(opts.marker.sha, opts.marker.flow)}`;
|
|
139
|
+
return `${title}\n\n${metaLine}\n\n${scopeNote}${opts.summary}${findingsLine}${failuresBlock}${footer}${markerLine}`;
|
|
136
140
|
}
|
|
137
141
|
export function buildNotStartedComment(opts) {
|
|
138
142
|
const mrUrl = `${opts.gitlabBaseUrl.replace(/\/$/, '')}/${opts.projectPath}/-/merge_requests/${opts.mrIid}`;
|
|
@@ -147,6 +151,7 @@ export function buildNotStartedComment(opts) {
|
|
|
147
151
|
`---`,
|
|
148
152
|
'',
|
|
149
153
|
`<sub>Generated by Maestro · [View MR →](${mrUrl})</sub>`,
|
|
154
|
+
...(opts.marker === undefined ? [] : [``, `${reviewMarker(opts.marker.sha, opts.marker.flow)}`]),
|
|
150
155
|
].join('\n');
|
|
151
156
|
}
|
|
152
157
|
export const name = 'maestro-orchestrator';
|
|
@@ -199,10 +204,12 @@ export function getTurnErrorMessage(handle) {
|
|
|
199
204
|
}
|
|
200
205
|
/**
|
|
201
206
|
* Resolve the model to use for an automated review. Priority: per-project
|
|
202
|
-
* override > global reviewModel >
|
|
207
|
+
* override > global reviewModel (Maestro Settings) > row-config reviewModel
|
|
208
|
+
* (lets a headless profile such as reviewer-ci pin a model from env vars,
|
|
209
|
+
* where there is no Settings UI) > DSH default (`fallback`).
|
|
203
210
|
*/
|
|
204
|
-
export function resolveReviewModel(userConfig, mapping, fallback) {
|
|
205
|
-
const raw = mapping?.reviewModel ?? userConfig.reviewModel;
|
|
211
|
+
export function resolveReviewModel(userConfig, mapping, fallback, rowSelection) {
|
|
212
|
+
const raw = mapping?.reviewModel ?? userConfig.reviewModel ?? rowSelection;
|
|
206
213
|
if (raw === undefined || raw === null)
|
|
207
214
|
return fallback;
|
|
208
215
|
return {
|
|
@@ -211,6 +218,13 @@ export function resolveReviewModel(userConfig, mapping, fallback) {
|
|
|
211
218
|
...(raw.reasoningEffort === undefined ? {} : { reasoningEffort: ReasoningEffortId(raw.reasoningEffort) }),
|
|
212
219
|
};
|
|
213
220
|
}
|
|
221
|
+
/** Effective auto-trigger flags: per-project row overrides global, unset inherits (design §4). */
|
|
222
|
+
export function resolveReviewTriggers(userConfig, mapping) {
|
|
223
|
+
return {
|
|
224
|
+
onPush: mapping?.rereviewOnPush ?? userConfig.autoRereviewOnPush ?? false,
|
|
225
|
+
onAssign: mapping?.reviewOnAssign ?? userConfig.autoReviewOnAssign ?? true,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
214
228
|
/** Compose a newly-created agent from the preset service owned by the root context. */
|
|
215
229
|
export async function mountAgentPreset(agentPresets, agentCtx, id) {
|
|
216
230
|
await agentPresets.mount(agentCtx, id);
|
|
@@ -321,7 +335,7 @@ function normalizedDiffPath(path) {
|
|
|
321
335
|
export async function postReviewFindings(findings, config) {
|
|
322
336
|
const fetcher = config.fetcher ?? fetch;
|
|
323
337
|
const apiBase = `${config.baseUrl}/api/v4/projects/${config.projectId}/merge_requests/${config.mrIid}`;
|
|
324
|
-
const headers = {
|
|
338
|
+
const headers = { ...gitlabAuthHeaders(config.token), 'Content-Type': 'application/json' };
|
|
325
339
|
for (const finding of findings) {
|
|
326
340
|
let response;
|
|
327
341
|
const label = severityPrefix(finding.severity);
|
|
@@ -432,6 +446,58 @@ export function auditorOutputFromSession(session) {
|
|
|
432
446
|
return finalAssistantOutput(events) ?? [];
|
|
433
447
|
}
|
|
434
448
|
/** Full review + performance audit; resolves to the comment body that was posted. */
|
|
449
|
+
/** Marker for CI-posted comments; webhook payloads carry no headSha and stay marker-free. */
|
|
450
|
+
function commentMarker(payload) {
|
|
451
|
+
if (payload.headSha === undefined)
|
|
452
|
+
return undefined;
|
|
453
|
+
return { sha: payload.headSha, flow: resolveFlow(payload.mode) };
|
|
454
|
+
}
|
|
455
|
+
/** CI-deep decision: deep mode + CI env + head SHA (spec §3). Mapping is checked by the caller. */
|
|
456
|
+
export function shouldCiDeepReview(payload) {
|
|
457
|
+
return payload.mode === 'deep'
|
|
458
|
+
&& payload.headSha !== undefined
|
|
459
|
+
&& process.env.SOURCE_PROJECT_ID !== undefined
|
|
460
|
+
&& process.env.MR_IID !== undefined;
|
|
461
|
+
}
|
|
462
|
+
/** CI quick-with-profile decision: quick mode + non-generic profile + CI env
|
|
463
|
+
* + head SHA. Quick + generic/unset stays diff-only (no clone cost); a real
|
|
464
|
+
* profile opts into the clone branch reviewer-only (no auditor — quick never
|
|
465
|
+
* audits, see shouldAudit in runReviewAndAudit). Mapping is checked by the caller. */
|
|
466
|
+
export function shouldCiQuickProfileReview(payload) {
|
|
467
|
+
return payload.mode === 'quick'
|
|
468
|
+
&& payload.reviewProfile !== undefined
|
|
469
|
+
&& payload.reviewProfile !== 'generic'
|
|
470
|
+
&& payload.headSha !== undefined
|
|
471
|
+
&& process.env.SOURCE_PROJECT_ID !== undefined
|
|
472
|
+
&& process.env.MR_IID !== undefined;
|
|
473
|
+
}
|
|
474
|
+
/** Auditor degrade for CI (no runtime env): a govard throw becomes reviewer-only, never a failed review. */
|
|
475
|
+
export function withAuditorDegrade(runAuditor) {
|
|
476
|
+
return async (worktreePath, payload) => {
|
|
477
|
+
try {
|
|
478
|
+
return await runAuditor(worktreePath, payload);
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return 'Auditor skipped — no runtime environment in CI (reviewer-only review).';
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Auditor instruction. The mapped flow keeps the full environment + test-suite
|
|
487
|
+
* workflow; the CI flow has no runtime, so the prompt countermands the
|
|
488
|
+
* auditor preset's environment steps and drops the Environment & Test Suite
|
|
489
|
+
* section entirely instead of reporting it "blocked".
|
|
490
|
+
*/
|
|
491
|
+
export function buildAuditorPrompt(opts) {
|
|
492
|
+
if (!opts.staticOnly) {
|
|
493
|
+
return 'Audit this merge request\'s performance: bring up the environment, run the test suite, look for regressions, then write a Markdown report and tear the environment down.';
|
|
494
|
+
}
|
|
495
|
+
return 'Audit this merge request\'s performance from the static diff and checked-out code only. '
|
|
496
|
+
+ 'No runtime environment exists in this container: ignore the auditor preset\'s environment steps '
|
|
497
|
+
+ '(do not bring anything up, do not run the test suite, do not tear anything down). '
|
|
498
|
+
+ 'Look for regressions by static analysis (diff, dependencies, query/shape risks), then write a Markdown report '
|
|
499
|
+
+ 'and OMIT the Environment & Test Suite section entirely — never report it as blocked.';
|
|
500
|
+
}
|
|
435
501
|
export async function runReviewAndAudit(payload, deps) {
|
|
436
502
|
assertSafeId(payload.projectId, 'projectId');
|
|
437
503
|
assertSafeId(payload.mrIid, 'mrIid');
|
|
@@ -468,6 +534,7 @@ export async function runReviewAndAudit(payload, deps) {
|
|
|
468
534
|
summary: summaryText,
|
|
469
535
|
failures,
|
|
470
536
|
findings: { newCount, replyCount, severityCounts },
|
|
537
|
+
marker: commentMarker(payload),
|
|
471
538
|
})
|
|
472
539
|
: `## 🤖 Maestro Review\n\n**\`${payload.projectPath}\` !${payload.mrIid}** · ✅ Completed · \`${payload.mode}\`${deps.reviewProfile !== undefined ? ` · \`${deps.reviewProfile}\`` : ''}\n\n${summaryText}${failures.length > 0 ? `\n\n<details>\n<summary>⚠️ Failed to post (${failures.length})</summary>\n\n${failures.map((f) => `- \`${f}\``).join('\n')}\n\n</details>` : ''}`;
|
|
473
540
|
sections.push(richOpts);
|
|
@@ -489,7 +556,8 @@ export async function runReviewAndAudit(payload, deps) {
|
|
|
489
556
|
else
|
|
490
557
|
await deps.postComment(body);
|
|
491
558
|
}
|
|
492
|
-
catch {
|
|
559
|
+
catch (err) {
|
|
560
|
+
console.error(`maestro-orchestrator: posting review comment failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
493
561
|
await deps.writeFailedReport(payload.mrIid, body);
|
|
494
562
|
}
|
|
495
563
|
return body;
|
|
@@ -526,6 +594,7 @@ export async function runDiffOnlyReview(payload, deps) {
|
|
|
526
594
|
summary: summary ?? '',
|
|
527
595
|
failures,
|
|
528
596
|
isDiffOnly: true,
|
|
597
|
+
marker: commentMarker(payload),
|
|
529
598
|
})
|
|
530
599
|
: `## 🤖 Maestro Review — Diff-only\n\n> **Scope:** Diff-only — reviewed the GitLab diff without a local checkout, Magento environment, static analysis, or tests. For a full review, add this project in Settings → Maestro and mention again.\n\n${summary}${failures.length > 0 ? `\n\n<details>\n<summary>⚠️ Failed to post (${failures.length})</summary>\n\n${failures.map((f) => `- \`${f}\``).join('\n')}\n\n</details>` : ''}`;
|
|
531
600
|
const body = diffBody;
|
|
@@ -535,7 +604,8 @@ export async function runDiffOnlyReview(payload, deps) {
|
|
|
535
604
|
else
|
|
536
605
|
await deps.postComment(body);
|
|
537
606
|
}
|
|
538
|
-
catch {
|
|
607
|
+
catch (err) {
|
|
608
|
+
console.error(`maestro-orchestrator: posting review comment failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
539
609
|
await deps.writeFailedReport(payload.mrIid, body);
|
|
540
610
|
}
|
|
541
611
|
return body;
|
|
@@ -555,7 +625,7 @@ export async function declineUnmappedDeepReview(payload, deps) {
|
|
|
555
625
|
const depsWithUrl = deps;
|
|
556
626
|
const baseUrl = depsWithUrl.gitlabBaseUrl;
|
|
557
627
|
const body = baseUrl !== undefined
|
|
558
|
-
? buildNotStartedComment({ gitlabBaseUrl: baseUrl, projectPath: payload.projectPath, mrIid: payload.mrIid })
|
|
628
|
+
? buildNotStartedComment({ gitlabBaseUrl: baseUrl, projectPath: payload.projectPath, mrIid: payload.mrIid, marker: commentMarker(payload) })
|
|
559
629
|
: '## 🤖 Maestro Review — Not started\n\n**`' + payload.projectPath + '` !' + payload.mrIid + '** · ⏸️ Not started\n\n> Deep review requires a project mapping with a local checkout and Magento environment.\n> Add this project in **Settings → Maestro**, then mention the reviewer again.';
|
|
560
630
|
try {
|
|
561
631
|
try {
|
|
@@ -564,7 +634,8 @@ export async function declineUnmappedDeepReview(payload, deps) {
|
|
|
564
634
|
else
|
|
565
635
|
await deps.postComment(body);
|
|
566
636
|
}
|
|
567
|
-
catch {
|
|
637
|
+
catch (err) {
|
|
638
|
+
console.error(`maestro-orchestrator: posting review comment failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
568
639
|
await deps.writeFailedReport(payload.mrIid, body);
|
|
569
640
|
}
|
|
570
641
|
}
|
|
@@ -626,7 +697,7 @@ export function govardWorktreeOverride(projectId, mrIid, keySuffix) {
|
|
|
626
697
|
*/
|
|
627
698
|
export async function fetchMrBaseSha(baseUrl, token, projectId, mrIid, fetcher = fetch) {
|
|
628
699
|
try {
|
|
629
|
-
const response = await fetcher(`${baseUrl}/api/v4/projects/${projectId}/merge_requests/${mrIid}`, { headers:
|
|
700
|
+
const response = await fetcher(`${baseUrl}/api/v4/projects/${projectId}/merge_requests/${mrIid}`, { headers: gitlabAuthHeaders(token) });
|
|
630
701
|
if (!response.ok)
|
|
631
702
|
return undefined;
|
|
632
703
|
const mr = await response.json();
|
|
@@ -942,7 +1013,7 @@ export function apply(ctx, config) {
|
|
|
942
1013
|
throw err;
|
|
943
1014
|
}
|
|
944
1015
|
}
|
|
945
|
-
async function runAuditor(worktreePath, payload, effective, modelSelection) {
|
|
1016
|
+
async function runAuditor(worktreePath, payload, effective, modelSelection, opts) {
|
|
946
1017
|
let handle;
|
|
947
1018
|
const agentOptions = agentOptionsForModel(modelSelection ?? ctx.agentDefaultModel.currentSelection());
|
|
948
1019
|
try {
|
|
@@ -970,7 +1041,7 @@ export function apply(ctx, config) {
|
|
|
970
1041
|
}
|
|
971
1042
|
ctx.sessionTitle.rename(handle.agent.session, `Maestro Auditor — MR !${payload.mrIid} (${payload.projectPath})`);
|
|
972
1043
|
try {
|
|
973
|
-
const prompt =
|
|
1044
|
+
const prompt = buildAuditorPrompt({ staticOnly: opts?.staticOnly === true });
|
|
974
1045
|
handle.agent.followup(createUserMessage({ content: [{ type: 'text', text: prompt }], source: { kind: 'user' } }));
|
|
975
1046
|
await whenIdleWithTimeout(handle, effectiveAgentTimeoutMs);
|
|
976
1047
|
const output = auditorOutputFromSession(handle.agent.session);
|
|
@@ -981,8 +1052,9 @@ export function apply(ctx, config) {
|
|
|
981
1052
|
await handle.dispose();
|
|
982
1053
|
}
|
|
983
1054
|
}
|
|
984
|
-
|
|
985
|
-
|
|
1055
|
+
async function runReview(payload) {
|
|
1056
|
+
const t0 = Date.now();
|
|
1057
|
+
try {
|
|
986
1058
|
const userConfig = await loadUserConfig();
|
|
987
1059
|
const effective = {
|
|
988
1060
|
gitlabBaseUrl: userConfig.gitlabBaseUrl ?? config.gitlabBaseUrl,
|
|
@@ -1000,18 +1072,37 @@ export function apply(ctx, config) {
|
|
|
1000
1072
|
});
|
|
1001
1073
|
}
|
|
1002
1074
|
const mapping = effective.projectMappings.find(m => m.projectPath === payload.projectPath);
|
|
1075
|
+
// CI-deep (spec §3): unmapped deep review with CI env + head SHA skips
|
|
1076
|
+
// both the decline and the diff-only fallback and runs the clone branch
|
|
1077
|
+
// below, which shares the mapped path's completion tail.
|
|
1078
|
+
const ciDeep = mapping === undefined && payload.mode === 'deep' && shouldCiDeepReview(payload);
|
|
1079
|
+
// CI quick-with-profile: unmapped quick + non-generic profile + CI env
|
|
1080
|
+
// skips the diff-only fallback and joins the clone branch below, which
|
|
1081
|
+
// runs reviewer-only (quick never audits).
|
|
1082
|
+
const ciQuickProfile = mapping === undefined && shouldCiQuickProfileReview(payload);
|
|
1003
1083
|
// Unmapped reviewer assignments remain no-ops. Only an explicit mention
|
|
1004
1084
|
// may opt into the intentionally limited, diff-only fallback below.
|
|
1005
|
-
if (mapping === undefined && payload.trigger !== 'mention')
|
|
1006
|
-
return;
|
|
1085
|
+
if (mapping === undefined && payload.trigger !== 'mention') {
|
|
1086
|
+
return { ok: true, failures: [], durationMs: Date.now() - t0 };
|
|
1087
|
+
}
|
|
1088
|
+
const triggers = resolveReviewTriggers(userConfig, mapping);
|
|
1089
|
+
// Gated-off auto-triggers stay fully silent: no history, no signals, no comment.
|
|
1090
|
+
if (payload.trigger === 'reviewer-assignment' && !triggers.onAssign) {
|
|
1091
|
+
return { ok: true, failures: [], durationMs: Date.now() - t0 };
|
|
1092
|
+
}
|
|
1093
|
+
if (payload.trigger === 'push' && !triggers.onPush) {
|
|
1094
|
+
return { ok: true, failures: [], durationMs: Date.now() - t0 };
|
|
1095
|
+
}
|
|
1007
1096
|
// A push only re-reviews an MR that already has a completed review;
|
|
1008
1097
|
// otherwise every newly opened MR would be reviewed twice.
|
|
1009
|
-
if (payload.trigger === 'push' && !(await hasCompletedReview(payload.projectId, payload.mrIid)))
|
|
1010
|
-
return;
|
|
1098
|
+
if (payload.trigger === 'push' && !(await hasCompletedReview(payload.projectId, payload.mrIid))) {
|
|
1099
|
+
return { ok: true, failures: [], durationMs: Date.now() - t0 };
|
|
1100
|
+
}
|
|
1011
1101
|
const { gitlabToken } = effective;
|
|
1012
1102
|
if (gitlabToken === undefined) {
|
|
1013
|
-
|
|
1014
|
-
|
|
1103
|
+
const message = `MR !${String(payload.mrIid)} for project ${payload.projectPath} has no GitLab token — set one in Maestro Settings or MAESTRO_GITLAB_TOKEN`;
|
|
1104
|
+
console.error(`maestro-orchestrator: ${message}`);
|
|
1105
|
+
return { ok: false, failures: [message], durationMs: Date.now() - t0 };
|
|
1015
1106
|
}
|
|
1016
1107
|
const resolved = { ...effective, gitlabToken };
|
|
1017
1108
|
const historyId = `${payload.projectId}-${payload.mrIid}-${Date.now()}`;
|
|
@@ -1043,7 +1134,7 @@ export function apply(ctx, config) {
|
|
|
1043
1134
|
const fallbackSelection = ctx.get?.('agentDefaultModel')?.currentSelection()
|
|
1044
1135
|
?? ctx.agentDefaultModel?.currentSelection()
|
|
1045
1136
|
?? { provider: 'fallback', model: 'fallback' };
|
|
1046
|
-
const reviewModelSelection = resolveReviewModel(userConfig, mapping, fallbackSelection);
|
|
1137
|
+
const reviewModelSelection = resolveReviewModel(userConfig, mapping, fallbackSelection, config.reviewModel);
|
|
1047
1138
|
// Opt-in Telegram digest; a delivery failure is logged and dropped.
|
|
1048
1139
|
const reviewStartMsOuter = Date.now();
|
|
1049
1140
|
const notifyTelegram = (status, summary, extra) => {
|
|
@@ -1095,7 +1186,7 @@ export function apply(ctx, config) {
|
|
|
1095
1186
|
const postComment = async (body) => {
|
|
1096
1187
|
const response = await fetch(`${resolved.gitlabBaseUrl}/api/v4/projects/${payload.projectId}/merge_requests/${payload.mrIid}/notes`, {
|
|
1097
1188
|
method: 'POST',
|
|
1098
|
-
headers: {
|
|
1189
|
+
headers: { ...gitlabAuthHeaders(resolved.gitlabToken), 'Content-Type': 'application/json' },
|
|
1099
1190
|
body: JSON.stringify({ body }),
|
|
1100
1191
|
});
|
|
1101
1192
|
if (!response.ok)
|
|
@@ -1104,23 +1195,25 @@ export function apply(ctx, config) {
|
|
|
1104
1195
|
const replyToDiscussion = async (discussionId, body) => {
|
|
1105
1196
|
const response = await fetch(`${resolved.gitlabBaseUrl}/api/v4/projects/${payload.projectId}/merge_requests/${payload.mrIid}/discussions/${encodeURIComponent(discussionId)}/notes`, {
|
|
1106
1197
|
method: 'POST',
|
|
1107
|
-
headers: {
|
|
1198
|
+
headers: { ...gitlabAuthHeaders(resolved.gitlabToken), 'Content-Type': 'application/json' },
|
|
1108
1199
|
body: JSON.stringify({ body }),
|
|
1109
1200
|
});
|
|
1110
1201
|
if (!response.ok)
|
|
1111
1202
|
throw new Error(`GitLab API error ${response.status}: ${await response.text()}`);
|
|
1112
1203
|
};
|
|
1113
1204
|
try {
|
|
1114
|
-
if (mapping === undefined) {
|
|
1115
|
-
|
|
1205
|
+
if (mapping === undefined && !ciDeep && !ciQuickProfile) {
|
|
1206
|
+
// CI-deep (spec §3) skips the decline and falls through to the clone
|
|
1207
|
+
// branch below; every other unmapped deep review still declines here.
|
|
1208
|
+
if (payload.mode === 'deep' && !shouldCiDeepReview(payload)) {
|
|
1116
1209
|
await declineUnmappedDeepReview(payload, { postComment, replyToDiscussion, writeFailedReport, gitlabBaseUrl: resolved.gitlabBaseUrl });
|
|
1117
1210
|
await recordReviewFinish(historyId, { status: 'completed', summary: 'Deep review declined (unmapped project)' });
|
|
1118
1211
|
notifyTelegram('completed', 'Deep review declined (unmapped project)');
|
|
1119
1212
|
await signals?.finish('completed');
|
|
1120
|
-
return;
|
|
1213
|
+
return { ok: true, summary: 'Deep review declined (unmapped project)', failures: [], durationMs: Date.now() - t0 };
|
|
1121
1214
|
}
|
|
1122
1215
|
const diffBody = await runDiffOnlyReview(payload, {
|
|
1123
|
-
runReviewer: (p) => runReviewer(undefined, p, resolved,
|
|
1216
|
+
runReviewer: (p) => runReviewer(undefined, p, resolved, payload.reviewProfile, reviewModelSelection, incrementalBlock),
|
|
1124
1217
|
postComment,
|
|
1125
1218
|
replyToDiscussion,
|
|
1126
1219
|
writeFailedReport,
|
|
@@ -1129,59 +1222,108 @@ export function apply(ctx, config) {
|
|
|
1129
1222
|
await recordReviewFinish(historyId, { status: 'completed', summary: summarize(diffBody) });
|
|
1130
1223
|
notifyTelegram('completed', summarize(diffBody));
|
|
1131
1224
|
await signals?.finish('completed');
|
|
1132
|
-
return;
|
|
1225
|
+
return { ok: true, summary: summarize(diffBody), failures: [], durationMs: Date.now() - t0 };
|
|
1133
1226
|
}
|
|
1134
1227
|
let fullBody;
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1228
|
+
if (ciDeep || ciQuickProfile) {
|
|
1229
|
+
// CI-deep (mapping is always undefined here — the unmapped block above
|
|
1230
|
+
// returned for every other case): clone the source at the head SHA,
|
|
1231
|
+
// map it in memory, and reuse the mapped machinery reviewer-only.
|
|
1232
|
+
// CI quick-with-profile joins the same branch; runReviewAndAudit skips
|
|
1233
|
+
// the auditor for quick (shouldAudit), so it stays reviewer-only.
|
|
1234
|
+
// No govard/vendor linking (ciEnsureWorktree is plain fetch + worktree).
|
|
1235
|
+
const ciHost = process.env.GITLAB_HOST?.trim() || new URL(resolved.gitlabBaseUrl).hostname;
|
|
1236
|
+
const cloneDir = join('/tmp', `maestro-ci-src-${payload.projectId}-${payload.mrIid}`);
|
|
1237
|
+
await cloneSourceRepo({ fetcher: fetch, run: defaultRun, host: ciHost, projectId: payload.projectId, sourceBranch: payload.sourceBranch, headSha: payload.headSha, token: resolved.gitlabToken, dir: cloneDir });
|
|
1238
|
+
try {
|
|
1239
|
+
const ciEnsureWorktree = async (repoPath, branch, pid, iid, suffix) => {
|
|
1240
|
+
assertSafeBranchName(branch);
|
|
1241
|
+
const wt = join('/tmp', `maestro-mr-${pid}-${iid}${suffix === undefined ? '' : `-${suffix}`}`);
|
|
1242
|
+
await defaultRun('git', ['fetch', '--depth', '50', 'origin', branch], repoPath);
|
|
1243
|
+
await defaultRun('git', ['worktree', 'remove', '--force', wt], repoPath).catch(() => { });
|
|
1244
|
+
await defaultRun('git', ['worktree', 'add', '--detach', '--', wt, payload.headSha], repoPath);
|
|
1245
|
+
return wt;
|
|
1246
|
+
};
|
|
1247
|
+
fullBody = await runReviewAndAudit(payload, {
|
|
1248
|
+
localRepoPath: cloneDir,
|
|
1249
|
+
ensureWorktree: ciEnsureWorktree,
|
|
1250
|
+
removeWorktree,
|
|
1251
|
+
runReviewer: (worktreePath, p) => runReviewer(worktreePath, p, resolved, payload.reviewProfile ?? 'generic', reviewModelSelection, incrementalBlock),
|
|
1252
|
+
runAuditor: withAuditorDegrade((worktreePath, p) => runAuditor(worktreePath, p, resolved, reviewModelSelection, { staticOnly: true })),
|
|
1253
|
+
postComment,
|
|
1254
|
+
replyToDiscussion,
|
|
1255
|
+
writeFailedReport,
|
|
1256
|
+
gitlabBaseUrl: resolved.gitlabBaseUrl,
|
|
1257
|
+
reviewProfile: payload.reviewProfile ?? 'generic',
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
finally {
|
|
1261
|
+
await defaultRun('rm', ['-rf', cloneDir]).catch(() => { });
|
|
1262
|
+
}
|
|
1148
1263
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1264
|
+
else {
|
|
1265
|
+
// Unreachable when mapping is undefined: the unmapped block returned
|
|
1266
|
+
// for every non-CI-deep case, and ciDeep took the branch above.
|
|
1267
|
+
if (mapping === undefined)
|
|
1268
|
+
throw new Error('unreachable: unmapped review without CI-deep decision');
|
|
1269
|
+
try {
|
|
1270
|
+
fullBody = await runReviewAndAudit(payload, {
|
|
1271
|
+
localRepoPath: mapping.localRepoPath,
|
|
1272
|
+
ensureWorktree,
|
|
1273
|
+
removeWorktree,
|
|
1274
|
+
runReviewer: (worktreePath, p) => runReviewer(worktreePath, p, resolved, mapping.reviewProfile ?? 'magento2', reviewModelSelection, incrementalBlock),
|
|
1275
|
+
runAuditor: (worktreePath, p) => runAuditor(worktreePath, p, resolved, reviewModelSelection),
|
|
1154
1276
|
postComment,
|
|
1155
1277
|
replyToDiscussion,
|
|
1156
1278
|
writeFailedReport,
|
|
1157
1279
|
gitlabBaseUrl: resolved.gitlabBaseUrl,
|
|
1280
|
+
reviewProfile: mapping.reviewProfile ?? 'magento2',
|
|
1158
1281
|
});
|
|
1159
|
-
const summary = summarize(diffBody);
|
|
1160
|
-
const branchNote = `fallback diff-only: branch ${payload.sourceBranch} not found on origin — ${summary ?? ''}`.trim();
|
|
1161
|
-
await recordReviewFinish(historyId, { status: 'completed', summary: branchNote });
|
|
1162
|
-
notifyTelegram('completed', branchNote);
|
|
1163
|
-
await signals?.finish('completed');
|
|
1164
|
-
return;
|
|
1165
1282
|
}
|
|
1166
|
-
|
|
1283
|
+
catch (err) {
|
|
1284
|
+
const isBranchNotFound = err?.code === 'BRANCH_NOT_FOUND' || isBranchNotFoundError(err);
|
|
1285
|
+
if (isBranchNotFound) {
|
|
1286
|
+
const diffBody = await runDiffOnlyReview(payload, {
|
|
1287
|
+
runReviewer: (p) => runReviewer(undefined, p, resolved, payload.reviewProfile, reviewModelSelection, incrementalBlock),
|
|
1288
|
+
postComment,
|
|
1289
|
+
replyToDiscussion,
|
|
1290
|
+
writeFailedReport,
|
|
1291
|
+
gitlabBaseUrl: resolved.gitlabBaseUrl,
|
|
1292
|
+
});
|
|
1293
|
+
const summary = summarize(diffBody);
|
|
1294
|
+
const branchNote = `fallback diff-only: branch ${payload.sourceBranch} not found on origin — ${summary ?? ''}`.trim();
|
|
1295
|
+
await recordReviewFinish(historyId, { status: 'completed', summary: branchNote });
|
|
1296
|
+
notifyTelegram('completed', branchNote);
|
|
1297
|
+
await signals?.finish('completed');
|
|
1298
|
+
return { ok: true, summary: branchNote, failures: [], durationMs: Date.now() - t0 };
|
|
1299
|
+
}
|
|
1300
|
+
throw err;
|
|
1301
|
+
}
|
|
1167
1302
|
}
|
|
1168
1303
|
await recordReviewFinish(historyId, { status: 'completed', summary: summarize(fullBody) });
|
|
1169
1304
|
notifyTelegram('completed', summarize(fullBody));
|
|
1170
1305
|
await signals?.finish('completed');
|
|
1306
|
+
return { ok: true, summary: summarize(fullBody), failures: [], durationMs: Date.now() - t0 };
|
|
1171
1307
|
}
|
|
1172
1308
|
catch (err) {
|
|
1173
1309
|
const message = err instanceof Error ? err.message : String(err);
|
|
1174
1310
|
await recordReviewFinish(historyId, { status: 'failed', error: message }).catch(() => { });
|
|
1175
1311
|
notifyTelegram('failed', message);
|
|
1176
1312
|
await signals?.finish('failed');
|
|
1177
|
-
|
|
1313
|
+
return { ok: false, summary: undefined, failures: [message], durationMs: Date.now() - t0 };
|
|
1178
1314
|
}
|
|
1179
|
-
}
|
|
1315
|
+
}
|
|
1316
|
+
catch (err) {
|
|
1180
1317
|
// Worktree creation, agent creation, and fallback delivery all run from
|
|
1181
|
-
//
|
|
1182
|
-
// fire-and-forget
|
|
1318
|
+
// this function — surface failures as a result instead of an unhandled
|
|
1319
|
+
// rejection, so both the fire-and-forget webhook path and an awaiting
|
|
1320
|
+
// CI caller observe the same outcome.
|
|
1321
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1183
1322
|
console.error(`maestro-orchestrator: review run failed for MR !${String(payload.mrIid)}:`, err);
|
|
1184
|
-
|
|
1185
|
-
|
|
1323
|
+
return { ok: false, summary: undefined, failures: [message], durationMs: Date.now() - t0 };
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
ctx.on('maestro/review-request', (payload) => { void runReview(payload); });
|
|
1327
|
+
ctx.provide('reviewRunner', runReview);
|
|
1186
1328
|
}
|
|
1187
1329
|
//# sourceMappingURL=orchestrator.js.map
|