@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/cordis.patch.yml +8 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +40 -10
- package/lib/index.js.map +1 -1
- package/lib/orchestrator.d.ts +1 -1
- package/lib/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator.js +1 -3
- package/lib/orchestrator.js.map +1 -1
- package/lib/settings-rpc.d.ts.map +1 -1
- package/lib/settings-rpc.js +4 -1
- package/lib/settings-rpc.js.map +1 -1
- package/lib/skill-provider.d.ts +19 -0
- package/lib/skill-provider.d.ts.map +1 -0
- package/lib/skill-provider.js +98 -0
- package/lib/skill-provider.js.map +1 -0
- package/lib/skills-tool.d.ts +1 -1
- package/lib/skills-tool.d.ts.map +1 -1
- package/lib/skills-tool.js +11 -19
- package/lib/skills-tool.js.map +1 -1
- package/package.json +14 -13
- package/presets/maestro-coder/agent.cordis.yml +2 -2
- package/profiles/reviewer-ci/package.json +3 -4
- package/profiles/reviewer-ci/pnpm-lock.yaml +93 -56
- package/skills/dsh-native-tools/SKILL.md +83 -0
- package/src/host/dsh-skill.d.ts +72 -0
- package/src/host/index.ts +30 -8
- package/src/host/orchestrator.ts +1 -3
- package/src/host/settings-rpc.ts +4 -1
- package/src/host/skill-provider.ts +91 -0
- package/src/host/skills-tool.ts +11 -18
- package/templates/reviewer-project.gitlab-ci.yml +1 -1
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
}
|
package/src/host/orchestrator.ts
CHANGED
|
@@ -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 &&
|
|
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
|
|
package/src/host/settings-rpc.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/host/skills-tool.ts
CHANGED
|
@@ -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
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
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(
|
|
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.
|
|
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:
|