@ddtcorex/dsh-maestro-review 0.3.2 → 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.
Files changed (83) hide show
  1. package/README.md +12 -0
  2. package/cordis.patch.yml +18 -0
  3. package/lib/ci-clone.d.ts +21 -0
  4. package/lib/ci-clone.d.ts.map +1 -0
  5. package/lib/ci-clone.js +35 -0
  6. package/lib/ci-clone.js.map +1 -0
  7. package/lib/ci-coexist.d.ts +9 -0
  8. package/lib/ci-coexist.d.ts.map +1 -0
  9. package/lib/ci-coexist.js +36 -0
  10. package/lib/ci-coexist.js.map +1 -0
  11. package/lib/config-store.d.ts +13 -6
  12. package/lib/config-store.d.ts.map +1 -1
  13. package/lib/config-store.js.map +1 -1
  14. package/lib/events.d.ts +23 -0
  15. package/lib/events.d.ts.map +1 -1
  16. package/lib/gitlab-auth.d.ts +11 -0
  17. package/lib/gitlab-auth.d.ts.map +1 -0
  18. package/lib/gitlab-auth.js +15 -0
  19. package/lib/gitlab-auth.js.map +1 -0
  20. package/lib/gitlab-client.d.ts +57 -0
  21. package/lib/gitlab-client.d.ts.map +1 -1
  22. package/lib/gitlab-client.js +120 -17
  23. package/lib/gitlab-client.js.map +1 -1
  24. package/lib/govard-audit-lint-tool.d.ts +81 -0
  25. package/lib/govard-audit-lint-tool.d.ts.map +1 -1
  26. package/lib/govard-audit-lint-tool.js +121 -36
  27. package/lib/govard-audit-lint-tool.js.map +1 -1
  28. package/lib/govard-tool.js +1 -1
  29. package/lib/govard-tool.js.map +1 -1
  30. package/lib/incremental.d.ts.map +1 -1
  31. package/lib/incremental.js +3 -2
  32. package/lib/incremental.js.map +1 -1
  33. package/lib/orchestrator.d.ts +137 -5
  34. package/lib/orchestrator.d.ts.map +1 -1
  35. package/lib/orchestrator.js +439 -70
  36. package/lib/orchestrator.js.map +1 -1
  37. package/lib/providers/ci-trigger.d.ts +43 -0
  38. package/lib/providers/ci-trigger.d.ts.map +1 -0
  39. package/lib/providers/ci-trigger.js +146 -0
  40. package/lib/providers/ci-trigger.js.map +1 -0
  41. package/lib/providers/gitlab.d.ts.map +1 -1
  42. package/lib/providers/gitlab.js +2 -1
  43. package/lib/providers/gitlab.js.map +1 -1
  44. package/lib/review-findings-tool.d.ts +5 -0
  45. package/lib/review-findings-tool.d.ts.map +1 -1
  46. package/lib/review-findings-tool.js +3 -1
  47. package/lib/review-findings-tool.js.map +1 -1
  48. package/lib/review-intake.d.ts.map +1 -1
  49. package/lib/review-intake.js +5 -1
  50. package/lib/review-intake.js.map +1 -1
  51. package/lib/review-marker.d.ts +17 -0
  52. package/lib/review-marker.d.ts.map +1 -0
  53. package/lib/review-marker.js +23 -0
  54. package/lib/review-marker.js.map +1 -0
  55. package/lib/review-signals.d.ts.map +1 -1
  56. package/lib/review-signals.js +4 -8
  57. package/lib/review-signals.js.map +1 -1
  58. package/lib/settings-rpc.js +1 -1
  59. package/lib/settings-rpc.js.map +1 -1
  60. package/package.json +14 -9
  61. package/profiles/reviewer-ci/cordis.patch.yml +61 -0
  62. package/profiles/reviewer-ci/package.json +19 -0
  63. package/profiles/reviewer-ci/pnpm-lock.yaml +62 -0
  64. package/profiles/reviewer-ci/pnpm-workspace.yaml +12 -0
  65. package/src/host/ci-clone.ts +54 -0
  66. package/src/host/ci-coexist.ts +43 -0
  67. package/src/host/config-store.ts +14 -1
  68. package/src/host/events.ts +25 -0
  69. package/src/host/gitlab-auth.ts +13 -0
  70. package/src/host/gitlab-client.ts +142 -17
  71. package/src/host/govard-audit-lint-tool.ts +144 -31
  72. package/src/host/govard-tool.ts +1 -1
  73. package/src/host/incremental.ts +3 -2
  74. package/src/host/orchestrator.ts +462 -50
  75. package/src/host/providers/ci-trigger.ts +164 -0
  76. package/src/host/providers/gitlab.ts +2 -1
  77. package/src/host/review-findings-tool.ts +9 -1
  78. package/src/host/review-intake.ts +5 -1
  79. package/src/host/review-marker.ts +25 -0
  80. package/src/host/review-signals.ts +4 -3
  81. package/src/host/settings-rpc.ts +1 -1
  82. package/templates/reviewer-project.gitlab-ci.yml +90 -0
  83. package/templates/source-project.gitlab-ci.yml +42 -0
@@ -0,0 +1,164 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import z from '@deepseek-ai/schemastery'
3
+ import { join } from 'node:path'
4
+ import type { ReviewRequest, ReviewResult } from '../events.js'
5
+ import { lastCompletedReview } from '../review-history.js'
6
+ import type { ReviewSkillProfile } from '../skills-tool.js'
7
+ import { gitlabAuthHeaders } from '../gitlab-auth.js'
8
+ import { hasCompletedReviewForSha, hasRunningEyes } from '../ci-coexist.js'
9
+
10
+ export const name = 'maestro-review-ci-trigger'
11
+ export const inject = ['reviewRunner'] as const
12
+
13
+ const REVIEW_PROFILES = ['magento2', 'laravel', 'symfony', 'wordpress', 'generic'] as const
14
+
15
+ export interface CiEnvConfig {
16
+ gitlabBaseUrl: string
17
+ gitlabToken: string
18
+ sourceProjectId: number
19
+ mrIid: number
20
+ mode: 'quick' | 'deep'
21
+ dryRun: boolean
22
+ reviewProfile?: ReviewSkillProfile
23
+ /** Mirror of Settings UI autoRereviewOnPush (default off): re-review new pushes. */
24
+ rereviewOnPush: boolean
25
+ }
26
+
27
+ export const CiEnvConfig: z<CiEnvConfig> = z.object({
28
+ gitlabBaseUrl: z.string().required(),
29
+ gitlabToken: z.string().role('secret').required(),
30
+ sourceProjectId: z.number().required(),
31
+ mrIid: z.number().required(),
32
+ mode: z.union([z.const('quick'), z.const('deep')]).default('quick'),
33
+ dryRun: z.boolean().default(false),
34
+ reviewProfile: z.union([z.const('magento2'), z.const('laravel'), z.const('symfony'), z.const('wordpress'), z.const('generic')]),
35
+ rereviewOnPush: z.boolean().default(false),
36
+ })
37
+
38
+ /**
39
+ * Reads the CI contract env vars (spec §5.2) — not Cordis-supplied config, since these are
40
+ * per-run values a static profile config cannot hold. schemastery's `.required()` only rejects
41
+ * `undefined`, not an empty string, so GITLAB_HOST/MAESTRO_GITLAB_TOKEN presence is checked here
42
+ * before defaulting to `''` — otherwise a missing env var would silently pass validation.
43
+ */
44
+ export function parseCiEnvConfig(env: Record<string, string | undefined>): CiEnvConfig {
45
+ if (env.GITLAB_HOST === undefined) throw new Error('missing GITLAB_HOST')
46
+ if (env.MAESTRO_GITLAB_TOKEN === undefined) throw new Error('missing MAESTRO_GITLAB_TOKEN')
47
+ if (env.REVIEW_PROFILE !== undefined && !(REVIEW_PROFILES as readonly string[]).includes(env.REVIEW_PROFILE)) {
48
+ throw new Error(`unsupported REVIEW_PROFILE "${env.REVIEW_PROFILE}" (supported: ${REVIEW_PROFILES.join(', ')})`)
49
+ }
50
+ return CiEnvConfig({
51
+ gitlabBaseUrl: `https://${env.GITLAB_HOST}`,
52
+ gitlabToken: env.MAESTRO_GITLAB_TOKEN,
53
+ sourceProjectId: Number(env.SOURCE_PROJECT_ID),
54
+ mrIid: Number(env.MR_IID),
55
+ mode: env.REVIEW_MODE === 'deep' ? 'deep' : 'quick',
56
+ dryRun: env.REVIEW_DRY_RUN === '1',
57
+ reviewProfile: env.REVIEW_PROFILE as ReviewSkillProfile | undefined,
58
+ rereviewOnPush: env.REVIEW_ON_PUSH === '1',
59
+ })
60
+ }
61
+
62
+ /** The one field a webhook payload carries that env vars don't — orchestrator fetches the diff itself. */
63
+ export async function fetchMrSourceBranch(config: CiEnvConfig, fetcher: typeof fetch = fetch): Promise<string> {
64
+ return (await fetchMrDetail(config, fetcher)).sourceBranch
65
+ }
66
+
67
+ /** MR detail pieces CI needs in one call: source branch (worktree) + head SHA (push-gate). */
68
+ export async function fetchMrDetail(config: CiEnvConfig, fetcher: typeof fetch = fetch): Promise<{ sourceBranch: string; headSha: string }> {
69
+ const url = `${config.gitlabBaseUrl}/api/v4/projects/${config.sourceProjectId}/merge_requests/${config.mrIid}`
70
+ const res = await fetcher(url, { headers: gitlabAuthHeaders(config.gitlabToken) })
71
+ if (!res.ok) throw new Error(`GitLab API error ${res.status}: ${await res.text()}`)
72
+ const body = await res.json() as { source_branch?: string; sha?: string }
73
+ if (typeof body.source_branch !== 'string') throw new Error('GitLab merge request response is missing source_branch')
74
+ if (typeof body.sha !== 'string') throw new Error('GitLab merge request response is missing sha')
75
+ return { sourceBranch: body.source_branch, headSha: body.sha }
76
+ }
77
+
78
+ export async function runCiTrigger(
79
+ ctx: Context,
80
+ config: CiEnvConfig,
81
+ deps: {
82
+ fetcher?: typeof fetch
83
+ writeFile?: typeof import('node:fs/promises').writeFile
84
+ history?: { lastCompletedReview(projectId: number, mrIid: number): Promise<{ headSha?: string } | undefined> }
85
+ } = {},
86
+ ): Promise<ReviewResult> {
87
+ const { sourceBranch, headSha } = await fetchMrDetail(config, deps.fetcher)
88
+ // Push-gate (mirror of the webhook autoRereviewOnPush semantics): the bridge fires
89
+ // on every MR pipeline, so skip when this exact head SHA already completed — and
90
+ // skip new pushes too unless REVIEW_ON_PUSH=1. A re-run then gets the H6 incremental
91
+ // block from history for free. Cache miss (no history) fails open toward running.
92
+ const history = deps.history ?? { lastCompletedReview }
93
+ const prior = await history.lastCompletedReview(config.sourceProjectId, config.mrIid)
94
+ let result: ReviewResult
95
+ if (prior?.headSha === headSha) {
96
+ result = { ok: true, summary: `already reviewed at ${headSha}, skipping`, failures: [], durationMs: 0 }
97
+ } else if (prior !== undefined && !config.rereviewOnPush) {
98
+ result = { ok: true, summary: `new commits since ${prior.headSha} but REVIEW_ON_PUSH is not set, skipping`, failures: [], durationMs: 0 }
99
+ } else {
100
+ // Coexistence (CI yields to webhook): the CI history store is invisible to
101
+ // the webhook flow and vice versa, so check the MR itself for a completed
102
+ // review marker of this SHA or an in-flight eyes marker before booting.
103
+ const coFetcher = deps.fetcher ?? fetch
104
+ if (await hasCompletedReviewForSha(coFetcher, config.gitlabBaseUrl, config.gitlabToken, config.sourceProjectId, config.mrIid, headSha)) {
105
+ result = { ok: true, summary: `webhook already reviewed ${headSha} — skipping`, failures: [], durationMs: 0 }
106
+ } else if (await hasRunningEyes(coFetcher, config.gitlabBaseUrl, config.gitlabToken, config.sourceProjectId, config.mrIid)) {
107
+ result = { ok: true, summary: 'another review is running — skipping', failures: [], durationMs: 0 }
108
+ } else {
109
+ // projectPath is synthetic (no webhook payload to read it from) — harmless: the
110
+ // reviewer-ci profile's own projectMappings is always [], so orchestrator's
111
+ // mapping lookup on projectPath never matches regardless of its exact value
112
+ // (same precedent as the PR #52 runner it replaces).
113
+ const request: ReviewRequest = {
114
+ projectPath: `project/${config.sourceProjectId}`,
115
+ projectId: config.sourceProjectId,
116
+ mrIid: config.mrIid,
117
+ sourceBranch,
118
+ headSha,
119
+ trigger: 'mention',
120
+ mode: config.mode,
121
+ scope: { kind: 'mr' },
122
+ reviewProfile: config.reviewProfile,
123
+ }
124
+ result = await ctx.reviewRunner(request)
125
+ }
126
+ }
127
+ const { writeFile } = deps.writeFile !== undefined ? { writeFile: deps.writeFile } : await import('node:fs/promises')
128
+ // entrypoint.sh cds into deepseek-harness before execing dsh, so a bare relative
129
+ // path would land inside the harness checkout (lost with the container). The
130
+ // entrypoint exports REVIEW_REPORT_DIR=$PWD captured before the cd, so reports land
131
+ // in the CI job's working directory where `artifacts:` picks them up.
132
+ const reportDir = process.env.REVIEW_REPORT_DIR?.trim() || '.'
133
+ const report = { ...result, projectId: config.sourceProjectId, mrIid: config.mrIid, mode: config.mode, generatedAt: new Date().toISOString() }
134
+ await writeFile(join(reportDir, 'review-report.json'), JSON.stringify(report, null, 2), 'utf-8')
135
+ const md = `# Review Report\n\n**Project:** ${config.sourceProjectId} !${config.mrIid}\n**Mode:** ${config.mode}\n**Duration:** ${result.durationMs}ms\n\n${result.summary ?? '(no summary)'}\n${result.failures.length > 0 ? `\nFailures:\n${result.failures.map(f => `- ${f}`).join('\n')}\n` : ''}`
136
+ await writeFile(join(reportDir, 'review-report.md'), md, 'utf-8')
137
+ return result
138
+ }
139
+
140
+ export function apply(ctx: Context): void {
141
+ ctx.effect(() => {
142
+ void (async () => {
143
+ try {
144
+ const config = parseCiEnvConfig(process.env)
145
+ console.log(`[review] ci-trigger starting: project ${config.sourceProjectId} !${config.mrIid} mode=${config.mode} dryRun=${config.dryRun}`)
146
+ const result = config.dryRun
147
+ ? { ok: true, summary: '[dry-run] config valid, skipping review', failures: [], durationMs: 0 }
148
+ : await runCiTrigger(ctx, config)
149
+ console.log(`[review] ci-trigger finished: ok=${result.ok} summary=${JSON.stringify(result.summary)} failures=${result.failures.length}`)
150
+ for (const failure of result.failures) console.error(`[review] ci-trigger failure: ${failure}`)
151
+ const exit = ctx.get('appExit')
152
+ if (exit === undefined) {
153
+ console.error('[review] ctx.get(\'appExit\') is undefined — the launcher did not provide it, process cannot signal its real exit code')
154
+ } else {
155
+ exit(result.ok ? 0 : 1)
156
+ }
157
+ } catch (err) {
158
+ console.error('maestro-review-ci-trigger:', err instanceof Error ? err.message : String(err))
159
+ ctx.get('appExit')?.(1)
160
+ }
161
+ })()
162
+ return () => {}
163
+ }, 'maestro-review-ci-trigger run')
164
+ }
@@ -128,7 +128,8 @@ export function apply(ctx: Context, config: Config): void {
128
128
  let body: GitlabMrWebhookBody; try { body = JSON.parse(raw) } catch { res.writeHead(400).end(); return }
129
129
  const userConfig = await loadUserConfig()
130
130
  if ((body.object_kind === 'merge_request' || body.object_kind === 'note') && !hasValidGitlabMrIdentity(body)) { res.writeHead(400).end(); return }
131
- const request = routeGitlabReviewRequest(body, userConfig.botUsername ?? config.botUsername ?? 'maestro', { pushEnabled: userConfig.autoRereviewOnPush === true })
131
+ // The orchestrator is the sole push gate (resolveReviewTriggers); intake always emits.
132
+ const request = routeGitlabReviewRequest(body, userConfig.botUsername ?? config.botUsername ?? 'maestro', { pushEnabled: true })
132
133
  if (request !== undefined) ctx.emit('maestro/review-request', request)
133
134
  res.writeHead(200).end()
134
135
  })
@@ -4,12 +4,18 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
4
4
  export const name = 'maestro-review-findings-tool'
5
5
  export const inject = ['tools']
6
6
 
7
+ export type FindingSeverity = 'blocking' | 'major' | 'minor' | 'nit'
8
+
7
9
  export interface ReviewFinding {
8
10
  status: 'new' | 'reply'
9
11
  body: string
10
12
  path?: string
11
13
  line?: number
12
14
  discussionId?: string
15
+ /** Assigned by the reviewer; missing values are treated as `minor`. */
16
+ severity?: FindingSeverity
17
+ /** Optional area tag, e.g. security, correctness, perf, style. */
18
+ category?: string
13
19
  }
14
20
 
15
21
  export interface Config {
@@ -19,7 +25,7 @@ export interface Config {
19
25
  export function apply(ctx: Context, config: Config): void {
20
26
  ctx.tools.register(defineTool({
21
27
  name: 'report_review_findings',
22
- description: 'Submit your complete list of review findings exactly once, when done analyzing. Each finding is {status: "new", path, line, body} for a location with no existing thread, or {status: "reply", discussionId, body} to update an existing thread returned by gitlab_list_own_review_threads.',
28
+ description: 'Submit your complete list of review findings exactly once, when done analyzing. Each finding is {status: "new", path, line, body, severity, category?} for a location with no existing thread, or {status: "reply", discussionId, body, severity, category?} to update an existing thread returned by gitlab_list_own_review_threads (reply to a resolved thread reopens it — prefer reply over new when the substance matches). Severity (required in spirit, defaults to minor): blocking = must fix before merge (regression, data loss, real XSS/RCE); major = should fix (correctness bug, security hardening with a realistic path); minor = nice to fix (perf nit with impact, stale docs, fragile coupling); nit = style/micro with no behavior impact.',
23
29
  parameters: {
24
30
  findings: {
25
31
  type: 'array',
@@ -34,6 +40,8 @@ export function apply(ctx: Context, config: Config): void {
34
40
  path: { type: 'string' },
35
41
  line: { type: 'number' },
36
42
  discussionId: { type: 'string' },
43
+ severity: { type: 'string', enum: ['blocking', 'major', 'minor', 'nit'] },
44
+ category: { type: 'string' },
37
45
  },
38
46
  },
39
47
  },
@@ -81,6 +81,10 @@ export function routeGitlabReviewRequest(body: unknown, botUsername: string, opt
81
81
  if (value.object_kind !== 'note' || typeof attributes.note !== 'string' || !isMentioned(attributes.note, botUsername)) return undefined
82
82
  const scope = noteScope(attributes)
83
83
  if (scope === undefined) return undefined
84
- const mode: ReviewMode = /(?:^|\s)\/maestro\s+deep(?:\s|$)/i.test(attributes.note) ? 'deep' : 'quick'
84
+ // Deep runs the auditor (env + perf) and costs far more than quick, so the
85
+ // trigger stays explicit: the `/maestro deep` slash command or the natural
86
+ // phrase "deep review". A bare "deep" alone never triggers (too loose).
87
+ const note = attributes.note
88
+ const mode: ReviewMode = /(?:^|\s)\/maestro\s+deep(?:\s|$)/i.test(note) || /\bdeep\s+review\b/i.test(note) ? 'deep' : 'quick'
85
89
  return { ...payload, trigger: 'mention', mode, scope }
86
90
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Machine-readable review comment identity (spec: coexistence §4-5).
3
+ *
4
+ * Every posted review comment (webhook + CI, quick + deep) ends with an
5
+ * invisible marker so the CI flow can detect "this head SHA already has a
6
+ * completed review" without access to the webhook's history store.
7
+ */
8
+
9
+ export type ReviewFlow = 'ci-quick' | 'ci-deep' | 'webhook'
10
+
11
+ export function reviewMarker(sha: string, flow: ReviewFlow): string {
12
+ return `<!-- maestro-review sha=${sha} flow=${flow} status=completed -->`
13
+ }
14
+
15
+ export function parseReviewMarker(body: string): { sha: string; flow: string; status: string } | undefined {
16
+ const m = /<!-- maestro-review sha=([0-9a-fA-F]+) flow=([a-z-]+) status=([a-z]+) -->/.exec(body)
17
+ if (!m) return undefined
18
+ return { sha: m[1], flow: m[2], status: m[3] }
19
+ }
20
+
21
+ /** CI detection is env-based (established pattern): the reviewer job always sets these. */
22
+ export function resolveFlow(mode: 'quick' | 'deep'): ReviewFlow {
23
+ if (process.env.SOURCE_PROJECT_ID === undefined) return 'webhook'
24
+ return mode === 'deep' ? 'ci-deep' : 'ci-quick'
25
+ }
@@ -1,3 +1,4 @@
1
+ import { gitlabAuthHeaders } from './gitlab-auth.js'
1
2
  /**
2
3
  * MR award-emoji acknowledgements: the MR shows "eyes" while a review runs and
3
4
  * a final marker when it completes or fails. Signalling is best-effort — any
@@ -12,7 +13,7 @@ export interface ReviewSignals {
12
13
  async function award(baseUrl: string, token: string, projectId: number, mrIid: number, name: string): Promise<void> {
13
14
  await fetch(`${baseUrl}/api/v4/projects/${projectId}/merge_requests/${mrIid}/award_emoji`, {
14
15
  method: 'POST',
15
- headers: { 'PRIVATE-TOKEN': token, 'Content-Type': 'application/json' },
16
+ headers: { ...gitlabAuthHeaders(token), 'Content-Type': 'application/json' },
16
17
  body: JSON.stringify({ name }),
17
18
  })
18
19
  }
@@ -20,7 +21,7 @@ async function award(baseUrl: string, token: string, projectId: number, mrIid: n
20
21
  /** Remove only this bot's stale running markers; other users' awards stay untouched. */
21
22
  async function unawardOwn(baseUrl: string, token: string, projectId: number, mrIid: number, botUsername: string): Promise<void> {
22
23
  const response = await fetch(`${baseUrl}/api/v4/projects/${projectId}/merge_requests/${mrIid}/award_emoji`, {
23
- headers: { 'PRIVATE-TOKEN': token },
24
+ headers: gitlabAuthHeaders(token),
24
25
  })
25
26
  if (!response.ok) return
26
27
  const awards = (await response.json()) as Array<{ id?: number; name?: string; user?: { username?: string } }>
@@ -28,7 +29,7 @@ async function unawardOwn(baseUrl: string, token: string, projectId: number, mrI
28
29
  if (awardItem.name !== 'eyes' || awardItem.user?.username !== botUsername || typeof awardItem.id !== 'number') continue
29
30
  await fetch(`${baseUrl}/api/v4/projects/${projectId}/merge_requests/${mrIid}/award_emoji/${awardItem.id}`, {
30
31
  method: 'DELETE',
31
- headers: { 'PRIVATE-TOKEN': token },
32
+ headers: gitlabAuthHeaders(token),
32
33
  }).catch(() => {})
33
34
  }
34
35
  }
@@ -12,7 +12,7 @@ export const inject = ['connection', 'maestroTunnel']
12
12
  /** Keys the Settings card may persist; anything else is a rejected save. */
13
13
  const SAVABLE_KEYS = new Set<keyof MaestroUserConfig>([
14
14
  'gitlabBaseUrl', 'gitlabToken', 'botUsername', 'webhookSecret', 'webhookPort',
15
- 'projectMappings', 'reviewModel', 'autoRereviewOnPush', 'agentTimeoutMs', 'reviewSessionRetentionDays',
15
+ 'projectMappings', 'reviewModel', 'autoRereviewOnPush', 'autoReviewOnAssign', 'agentTimeoutMs', 'reviewSessionRetentionDays',
16
16
  'tunnelMode', 'quickTarget', 'tunnelId', 'tunnelCredentialsFile', 'tunnelHostname',
17
17
  'proxyPort', 'proxyHost', 'lanPinEnabled', 'telegramBotToken', 'telegramChatId',
18
18
  'telegramReviewNotifications',
@@ -0,0 +1,90 @@
1
+ # ddtcorex/maestro-reviewer — dedicated reviewer project
2
+ stages: [review]
3
+
4
+ variables:
5
+ # Image registry — pin the SHA for reproducibility
6
+ REVIEWER_IMAGE: "ddtcorex/maestro-reviewer:latest"
7
+ # Or use $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA when building the image in the same project
8
+
9
+ review:
10
+ stage: review
11
+ image: $REVIEWER_IMAGE
12
+ # Only runs when triggered from the source project (or manually)
13
+ rules:
14
+ - if: $CI_PIPELINE_SOURCE == "pipeline" # bridge trigger
15
+ - if: $CI_PIPELINE_SOURCE == "trigger" # token trigger
16
+ - if: $CI_PIPELINE_SOURCE == "web" # manual retry
17
+ when: manual
18
+ variables:
19
+ GIT_STRATEGY: none # no need to check out the reviewer repo
20
+ # Every parameter comes from env (SOURCE_PROJECT_ID, MR_IID, GITLAB_HOST, REVIEW_MODE) — no CLI args.
21
+ script:
22
+ - /entrypoint.sh
23
+ # History cache — the push-gate (skip already-reviewed head SHAs) and incremental
24
+ # context read $DSH_HOME/dsh-maestro-review/reviews.json, which is ephemeral in a
25
+ # fresh container. entrypoint.sh symlinks it at this cached path (one store, many
26
+ # MRs — entries are keyed per MR inside). Cache miss fails open toward reviewing.
27
+ cache:
28
+ key: maestro-review-history
29
+ paths:
30
+ - .maestro-history/
31
+ artifacts:
32
+ when: always
33
+ expire_in: 1 week
34
+ paths:
35
+ - review-report.json
36
+ - review-report.md
37
+ # Secrets — Protected + Masked, only available on the reviewer project's protected branches.
38
+ # MAESTRO_GITLAB_TOKEN is REQUIRED and must be a user PAT or Group/Project
39
+ # Access Token (api scope). CI_JOB_TOKEN is NOT enough: proven 2026-09-04 on
40
+ # GitLab 18.11 that reads work (GET=200) but posting the review comment fails
41
+ # (POST /notes=401) even with the source project allowlisting this reviewer —
42
+ # the run would "complete" without ever posting. A human-tied PAT needs yearly
43
+ # rotation; prefer a bot/group token where the instance allows creating one.
44
+ # MAESTRO_GITLAB_TOKEN (api scope) — required
45
+ # Route selection is key-based (entrypoint.sh swaps the baked settings variant):
46
+ # OPENCODE_GO_API_KEY → opencode profile (default, host mirror; serves opencode-go
47
+ # AND deepseek-official-via-zen). Set alone, or with DEEPSEEK_API_KEY (opencode wins).
48
+ # DEEPSEEK_API_KEY → deepseek profile ONLY when OPENCODE_GO_API_KEY is absent
49
+ # (deepseek-official straight from api.deepseek.com — needs a LIVE deepseek.com key).
50
+ # TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID (optional)
51
+ # Opencode model (optional, one variable — default muse-spark-1.3-contributor):
52
+ # OPENCODE_MODEL: "muse-spark-1.3-contributor"
53
+ # Model pin (optional, as a pair — unset both for the host-mirrored default
54
+ # = opencode-go/muse-spark-1.3-contributor, baked in docker/ci-settings.yaml):
55
+ # REVIEW_MODEL_PROVIDER: "opencode-go" # or "deepseek-official"
56
+ # REVIEW_MODEL: "muse-spark-1.3-contributor" # e.g. deepseek-v4-flash (deepseek-official via zen)
57
+ # Only the provider's key is needed — no settings mount required. ID without
58
+ # provider fails the review with a provider error (not silently the wrong
59
+ # model); provider without ID is ignored.
60
+ # Review skill profile (optional — unset for the legacy diff-only generic review):
61
+ # REVIEW_PROFILE: "magento2" # magento2 | laravel | symfony | wordpress | generic
62
+ # The reviewer must load the full skill set or the review fails closed
63
+ # (no half-skilled review). Runtime-backed checks still degrade without a
64
+ # checkout — the profile adds framework checklists, not a govard run.
65
+ # Review mode (default quick; deep runs on demand — no new variables):
66
+ # REVIEW_MODE: "deep" # manual downstream run with the same
67
+ # bridge variables (SOURCE_PROJECT_ID, MR_IID, GITLAB_HOST) + REVIEW_ON_PUSH=1
68
+ # to re-review pushes. The job clones the source at the MR head SHA with
69
+ # MAESTRO_GITLAB_TOKEN (api scope, oauth2) and runs reviewer (+auditor
70
+ # degraded to reviewer-only without a runtime). No govard in the container.
71
+ # CI yields to webhook: if the head SHA already has a completed review
72
+ # comment (either flow), the run skips instead of double-posting.
73
+ # Re-review pushes (optional — mirrors Settings UI autoRereviewOnPush, default off):
74
+ # REVIEW_ON_PUSH: "1" # re-review new pushes with incremental context;
75
+ # unset = only the first (opened) review runs; same head SHA always skips.
76
+
77
+ # Build the image inside the same reviewer project (optional):
78
+ #
79
+ # build:image:
80
+ # stage: .pre
81
+ # image: docker:27
82
+ # services: [docker:27-dind]
83
+ # rules:
84
+ # - if: $CI_COMMIT_BRANCH == "main"
85
+ # script:
86
+ # - docker build -f packages/dsh-maestro-review/docker/Dockerfile -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -t $CI_REGISTRY_IMAGE:latest .
87
+ # - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
88
+ # - docker push $CI_REGISTRY_IMAGE:latest
89
+ # # Build context must be the maestro-harness workspace root (contains deepseek-harness/) —
90
+ # # the trailing "." above assumes this job checks out maestro-harness, not this repo alone.
@@ -0,0 +1,42 @@
1
+ # example-group/example-project — source project (copy this template into YOUR source project)
2
+ stages: [review]
3
+
4
+ # Only runs when there is an MR (fork MRs run fine too — this job has no secrets)
5
+ maestro:trigger-review:
6
+ stage: review
7
+ rules:
8
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
9
+ # No image needed, no secrets needed
10
+ trigger:
11
+ project: ddtcorex/maestro-reviewer # ← reviewer project path
12
+ branch: main # ← the reviewer project's default branch (master on some instances)
13
+ strategy: depend # optional: wait for the reviewer pipeline, surface its status on the MR
14
+ variables:
15
+ SOURCE_PROJECT_ID: $CI_PROJECT_ID
16
+ MR_IID: $CI_MERGE_REQUEST_IID
17
+ GITLAB_HOST: $CI_SERVER_HOST
18
+ SOURCE_SHA: $CI_COMMIT_SHA
19
+ REVIEW_MODE: "quick" # or "deep" for a govard audit
20
+ REVIEW_PROFILE: "generic" # per-project skill profile: magento2 | laravel | symfony | wordpress | generic
21
+ # REVIEW_ON_PUSH: "1" # uncomment to re-review every push (default: only the opened review runs)
22
+
23
+ # Trigger Token fallback (when not using `trigger: project:`):
24
+ #
25
+ # maestro:trigger-review:token:
26
+ # stage: review
27
+ # rules:
28
+ # - if: $CI_PIPELINE_SOURCE == "merge_request_event"
29
+ # image: curlimages/curl:8
30
+ # variables:
31
+ # REVIEWER_PROJECT_ID: "123" # id of the maestro-reviewer project
32
+ # script:
33
+ # - |
34
+ # curl --fail --request POST \
35
+ # --form token=$MAESTRO_TRIGGER_TOKEN \
36
+ # --form ref=main \
37
+ # --form variables[SOURCE_PROJECT_ID]=$CI_PROJECT_ID \
38
+ # --form variables[MR_IID]=$CI_MERGE_REQUEST_IID \
39
+ # --form variables[GITLAB_HOST]=$CI_SERVER_HOST \
40
+ # "https://${CI_SERVER_HOST}/api/v4/projects/${REVIEWER_PROJECT_ID}/trigger/pipeline"
41
+ # # MAESTRO_TRIGGER_TOKEN is the reviewer project's Trigger Token, stored in the source project
42
+ # # (can only trigger, never read/write code — safer than GITLAB_TOKEN)