@ddtcorex/dsh-maestro-review 0.7.4 → 0.8.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/src/host/index.ts CHANGED
@@ -1,15 +1,37 @@
1
1
  import type { Context } from '@deepseek-ai/cordis'
2
+ import { makeSkillProvider, resolveSkillsDir } from './skill-provider.js'
2
3
 
3
4
  export const name = '@ddtcorex/dsh-maestro-review'
4
5
 
5
6
  export function apply(ctx: Context): void {
6
- ctx.effect(() => ctx.connection.rpc.handle('/dsh-maestro-review', async (endpoint, payload) => {
7
- if (endpoint === 'status') return { ok: true, value: { provider: 'gitlab' } }
8
- if (endpoint === 'providers') return { ok: true, value: { providers: ['gitlab', 'github'] } }
9
- if (endpoint === 'review') {
10
- const body = payload as Record<string, unknown> | undefined
11
- return { ok: true, value: { received: true, provider: (body as any)?.provider ?? 'gitlab' } }
7
+ // The bundled DSH tool map is served from here, not from the public
8
+ // `maestro-skills` plugin: that repo's catalog test requires its skill content
9
+ // to stay tool-neutral, and this map names harness tools. Wrapped like the
10
+ // rest of apply(): a missing service or a throwing registration must never
11
+ // take down the whole plugin tree.
12
+ try {
13
+ const skills = (ctx as unknown as { get?: (key: string) => any }).get?.('skills')
14
+ if (skills?.registerProvider) {
15
+ ctx.effect(() => {
16
+ let unregister: (() => void) | undefined
17
+ try {
18
+ // Package-root skills/ is resolved at runtime by walking to the
19
+ // nearest package.json (robust to lib/ vs src/host/ layouts).
20
+ unregister = skills.registerProvider(() => makeSkillProvider(resolveSkillsDir(__dirname)))
21
+ } catch (e: any) {
22
+ try { (ctx as any).logger?.warn?.(`[review] skill provider failed: ${e?.message ?? String(e)}`) } catch {}
23
+ }
24
+ return () => { try { unregister?.() } catch {} }
25
+ }, 'maestro-review:skill')
12
26
  }
13
- return { ok: false, error: { code: 'bad-request', message: `Unknown endpoint: ${endpoint}`, details: { issues: [{ message: String(endpoint) }] } as any } }
14
- }, { authority: 'loopback' }), 'maestro-review rpc')
27
+ } catch (e: any) {
28
+ try { (ctx as any).logger?.warn?.(`[review] skill provider effect failed: ${e?.message ?? String(e)}`) } catch {}
29
+ }
30
+
31
+ // No RPC registration here on purpose. The channel belongs to settings-rpc.ts
32
+ // (row `maestro-review-settings-rpc`), which implements the real endpoints. A
33
+ // stub in this file used to claim the same channel; that stayed invisible only
34
+ // because apply() was never mounted. Once the `maestro-review-host` row made
35
+ // apply() run, both registrations collided and the Settings card died with:
36
+ // webserver: duplicate prefix route "/dsh-maestro-review"
15
37
  }
@@ -1120,14 +1120,12 @@ export function apply(ctx: Context, config: Config): void {
1120
1120
  const runOnce = async (agentOptions: ModelSelection): Promise<ReviewOutcome> => {
1121
1121
  let capturedFindings: ReviewFinding[] = []
1122
1122
  let handle: AgentHandle | undefined
1123
- let reviewerContext: Context | undefined
1124
1123
  try {
1125
1124
  handle = await ctx.agents.create({
1126
1125
  sessionId: SessionId(`maestro-reviewer-${payload.mrIid}-${Date.now()}`),
1127
1126
  meta: { cwd: worktreePath ?? tmpdir() },
1128
1127
  agentOptions,
1129
1128
  setup: async (agentCtx) => {
1130
- reviewerContext = agentCtx
1131
1129
  installModelSelection(agentCtx, { current: agentOptions, assembled: undefined })
1132
1130
  await agentCtx.plugin(ReviewToolPolicy)
1133
1131
  await mountAgentPreset(ctx.agentPresets, agentCtx, 'dsh-maestro-reviewer')
@@ -1173,7 +1171,7 @@ export function apply(ctx: Context, config: Config): void {
1173
1171
  }))
1174
1172
  await whenIdleWithTimeout(handle, effectiveAgentTimeoutMs)
1175
1173
  assertTurnSucceededOrSalvage(handle, capturedFindings.length > 0, 'reviewer')
1176
- if (reviewProfile !== undefined && (reviewerContext === undefined || loadedReviewProfile(reviewerContext) !== reviewProfile)) {
1174
+ if (reviewProfile !== undefined && loadedReviewProfile(handle.agent) !== reviewProfile) {
1177
1175
  throw new Error(`reviewer did not successfully load the required ${reviewProfile} review skill profile; no findings were posted`)
1178
1176
  }
1179
1177
 
@@ -210,7 +210,10 @@ export function apply(ctx: Context): void {
210
210
  // The proxy reads lanPinEnabled at boot; a reload applies the new gate
211
211
  // without waiting for a harness restart.
212
212
  await ctx.maestroTunnel.reloadConfig()
213
- return ok({ enabled })
213
+ // The gate lives in the proxy listener, not in the settings store, so a
214
+ // client cannot infer "live" from a successful write. Say so explicitly
215
+ // instead of letting the config card imply the gate is already in force.
216
+ return ok({ enabled, requiresRestart: true })
214
217
  }
215
218
  if (endpoint === MAESTRO_ENDPOINTS.lanPinRotate) {
216
219
  return ok({ pin: await ctx.maestroTunnel.rotateLanPin() })
@@ -0,0 +1,91 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { readFile, stat } from 'node:fs/promises'
3
+ import { dirname, join } from 'node:path'
4
+ import type { SkillCandidate, SkillDefinition, SkillLookupOptions } from '@deepseek-ai/dsh-skill'
5
+
6
+ const SKILL_NAME = 'dsh-native-tools'
7
+
8
+ /**
9
+ * Resolve the package-root `skills/` dir regardless of module layout. The built
10
+ * host lib is flat (`lib/index.js` → `../skills`), but under vitest the same
11
+ * module loads from `src/host/` (→ `../../skills`). Walking to the nearest
12
+ * `package.json` yields the same package-root `skills/` in both layouts.
13
+ */
14
+ export function resolveSkillsDir(fromDir: string): string {
15
+ let dir = fromDir
16
+ for (let i = 0; i < 6; i++) {
17
+ try {
18
+ if (existsSync(join(dir, 'package.json'))) return join(dir, 'skills')
19
+ } catch { /* keep walking */ }
20
+ const parent = dirname(dir)
21
+ if (parent === dir) break
22
+ dir = parent
23
+ }
24
+ return join(fromDir, '..', 'skills')
25
+ }
26
+
27
+ /** Minimal frontmatter reader for our own SKILL.md — enough for the provider contract. */
28
+ function parseFrontmatter(raw: string): { name: string; description: string; body: string } {
29
+ const m = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
30
+ if (!m) return { name: SKILL_NAME, description: '', body: raw }
31
+ const fm = m[1].split('\n').reduce<Record<string, string>>((acc, line) => {
32
+ const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/)
33
+ if (kv) acc[kv[1]] = kv[2].replace(/^["']|["']$/g, '')
34
+ return acc
35
+ }, {})
36
+ return { name: fm.name || SKILL_NAME, description: fm.description || '', body: m[2] }
37
+ }
38
+
39
+ /**
40
+ * Provider for the bundled DSH tool map. Served here rather than in the public
41
+ * `maestro-skills` plugin on purpose: the map names harness tools, and that
42
+ * repo's catalog test requires its skill content to stay tool-neutral.
43
+ */
44
+ export function makeSkillProvider(skillsDir: string) {
45
+ return {
46
+ // The dsh-skill service attributes candidates through the provider object's
47
+ // own `name`; a missing one surfaces at runtime as `skill provider
48
+ // "undefined" returned skill ...` and fails every turn.
49
+ name: 'maestro-review',
50
+ async list(_options: SkillLookupOptions): Promise<SkillCandidate[]> {
51
+ const entry = join(skillsDir, SKILL_NAME)
52
+ const st = await stat(entry).catch(() => null)
53
+ if (!st?.isDirectory()) return []
54
+ const skillFilePath = join(entry, 'SKILL.md')
55
+ const fileSt = await stat(skillFilePath).catch(() => null)
56
+ if (!fileSt?.isFile()) return []
57
+ const raw = await readFile(skillFilePath, 'utf-8').catch(() => null)
58
+ if (raw === null) return []
59
+ const { name, description } = parseFrontmatter(raw)
60
+ return [{
61
+ name,
62
+ description,
63
+ invocation: { modelInvocable: true, userInvocable: true },
64
+ source: 'custom',
65
+ provider: 'maestro-review',
66
+ rank: 360,
67
+ locator: skillFilePath,
68
+ path: skillFilePath,
69
+ resourceBase: { kind: 'directory', path: entry },
70
+ metadata: { name, description },
71
+ }]
72
+ },
73
+ async get(candidate: SkillCandidate, _options: SkillLookupOptions): Promise<SkillDefinition | undefined> {
74
+ try {
75
+ const raw = await readFile(candidate.path as string, 'utf-8')
76
+ const { name, description, body } = parseFrontmatter(raw)
77
+ return {
78
+ name,
79
+ description,
80
+ invocation: candidate.invocation,
81
+ source: candidate.source,
82
+ provider: candidate.provider,
83
+ resourceBase: candidate.resourceBase,
84
+ path: candidate.path,
85
+ content: body,
86
+ metadata: candidate.metadata,
87
+ }
88
+ } catch { return undefined }
89
+ },
90
+ }
91
+ }
@@ -46,26 +46,19 @@ export const REVIEW_PROFILE_SKILLS: Record<ReviewSkillProfile, readonly string[]
46
46
 
47
47
  export const MAESTRO_SKILLS_INSTALL_COMMAND = 'curl -fsSL https://raw.githubusercontent.com/ddtcorex/maestro-skills/master/install.sh | bash -s -- --scope personal --target dsh --skills govard-toolbox,govard-magento,govard-laravel,govard-symfony,govard-wordpress,php-dev-core,magento2-dev-core,magento2-frontend-dev,magento2-hyva-dev,magento2-code-review,magento2-linter,magento2-security-scan,magento2-performance-audit -y'
48
48
 
49
- // This is deliberately process-local and keyed by the reviewer Agent object,
50
- // which every Cordis child context inherits. Preset mounting inserts child
51
- // contexts, so keying on the plugin's context would make a successful load
52
- // invisible to the orchestrator's agent context.
49
+ // Keyed by the reviewer's Agent object itself, not a Cordis context: `ctx.agent`
50
+ // is not an actual injectable Cordis service anywhere in this composition (it
51
+ // throws "cannot get property agent without inject" whenever read, silently
52
+ // making every post-turn profile check report "not loaded" even after the
53
+ // tool above successfully set it — root-caused 2026-09-14 against a real
54
+ // failed review, reproduced deterministically against real
55
+ // @deepseek-ai/cordis). The tool's own `exec.agent` (a plain, non-Cordis
56
+ // property `ToolExecutionInput` always carries, "set by the agent loop") is
57
+ // the reliable handle — callers pass `handle.agent` from the same
58
+ // `AgentHandle` the orchestrator already holds, which is the identical object.
53
59
  const loadedReviewProfiles = new WeakMap<object, ReviewSkillProfile>()
54
60
 
55
- export function loadedReviewProfile(ctx: Context): ReviewSkillProfile | undefined {
56
- // Cordis's Context proxy throws synchronously on any property nobody
57
- // registered/injected ("cannot get property ... without inject") rather
58
- // than returning undefined — so a context where the framework's own
59
- // `agent` service isn't available needs a try/catch, not just an
60
- // `=== undefined` check, to actually get the "no agent yet" outcome this
61
- // function documents. Root-caused 2026-09-14 against a real failed
62
- // review (reproduced directly with @deepseek-ai/cordis, not a mock).
63
- let agent: object | undefined
64
- try {
65
- agent = ctx.agent
66
- } catch {
67
- return undefined
68
- }
61
+ export function loadedReviewProfile(agent: object | undefined): ReviewSkillProfile | undefined {
69
62
  return agent === undefined ? undefined : loadedReviewProfiles.get(agent)
70
63
  }
71
64
 
@@ -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.7.4"
6
+ REVIEWER_IMAGE: "ddtcorex/maestro-reviewer:0.8.0"
7
7
  # Or use $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA when building the image in the same project
8
8
 
9
9
  review: