@ddtcorex/dsh-maestro-review 0.1.0 → 0.1.2
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/lib/config-store.d.ts +2 -0
- package/lib/config-store.d.ts.map +1 -1
- package/lib/config-store.js.map +1 -1
- package/lib/events.d.ts.map +1 -1
- package/lib/events.js.map +1 -1
- package/lib/git-worktree-tool.d.ts +11 -0
- package/lib/git-worktree-tool.d.ts.map +1 -0
- package/lib/git-worktree-tool.js +162 -0
- package/lib/git-worktree-tool.js.map +1 -0
- package/lib/gitlab-client.d.ts.map +1 -1
- package/lib/gitlab-client.js.map +1 -1
- package/lib/gitlab-webhook.d.ts.map +1 -1
- package/lib/gitlab-webhook.js.map +1 -1
- package/lib/govard-audit-lint-tool.d.ts +14 -0
- package/lib/govard-audit-lint-tool.d.ts.map +1 -0
- package/lib/govard-audit-lint-tool.js +177 -0
- package/lib/govard-audit-lint-tool.js.map +1 -0
- package/lib/govard-tool.d.ts.map +1 -1
- package/lib/govard-tool.js.map +1 -1
- package/lib/hyva-csp-scan-tool.d.ts +11 -0
- package/lib/hyva-csp-scan-tool.d.ts.map +1 -0
- package/lib/hyva-csp-scan-tool.js +193 -0
- package/lib/hyva-csp-scan-tool.js.map +1 -0
- package/lib/hyva-theme-inspect-tool.d.ts +10 -0
- package/lib/hyva-theme-inspect-tool.d.ts.map +1 -0
- package/lib/hyva-theme-inspect-tool.js +299 -0
- package/lib/hyva-theme-inspect-tool.js.map +1 -0
- package/lib/incremental.d.ts +28 -0
- package/lib/incremental.d.ts.map +1 -0
- package/lib/incremental.js +60 -0
- package/lib/incremental.js.map +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/layout-xml-tool.d.ts +11 -0
- package/lib/layout-xml-tool.d.ts.map +1 -0
- package/lib/layout-xml-tool.js +240 -0
- package/lib/layout-xml-tool.js.map +1 -0
- package/lib/module-check-tool.d.ts +11 -0
- package/lib/module-check-tool.d.ts.map +1 -0
- package/lib/module-check-tool.js +304 -0
- package/lib/module-check-tool.js.map +1 -0
- package/lib/notify.d.ts.map +1 -1
- package/lib/notify.js.map +1 -1
- package/lib/orchestrator.d.ts +2 -0
- package/lib/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator.js +220 -88
- package/lib/orchestrator.js.map +1 -1
- package/lib/perf-log-stats-tool.d.ts +13 -0
- package/lib/perf-log-stats-tool.d.ts.map +1 -0
- package/lib/perf-log-stats-tool.js +245 -0
- package/lib/perf-log-stats-tool.js.map +1 -0
- package/lib/phtml-escape-scan-tool.d.ts +11 -0
- package/lib/phtml-escape-scan-tool.d.ts.map +1 -0
- package/lib/phtml-escape-scan-tool.js +170 -0
- package/lib/phtml-escape-scan-tool.js.map +1 -0
- package/lib/plan-track-tool.d.ts +11 -0
- package/lib/plan-track-tool.d.ts.map +1 -0
- package/lib/plan-track-tool.js +143 -0
- package/lib/plan-track-tool.js.map +1 -0
- package/lib/providers/github.stub.d.ts.map +1 -1
- package/lib/providers/github.stub.js.map +1 -1
- package/lib/providers/gitlab.d.ts.map +1 -1
- package/lib/providers/gitlab.js.map +1 -1
- package/lib/providers/interface.d.ts.map +1 -1
- package/lib/providers/interface.js.map +1 -1
- package/lib/review-findings-tool.d.ts.map +1 -1
- package/lib/review-findings-tool.js.map +1 -1
- package/lib/review-history.d.ts +4 -0
- package/lib/review-history.d.ts.map +1 -1
- package/lib/review-history.js +23 -1
- package/lib/review-history.js.map +1 -1
- package/lib/review-intake.d.ts.map +1 -1
- package/lib/review-intake.js.map +1 -1
- package/lib/review-signals.d.ts.map +1 -1
- package/lib/review-signals.js.map +1 -1
- package/lib/scope-split-tool.d.ts +42 -0
- package/lib/scope-split-tool.d.ts.map +1 -0
- package/lib/scope-split-tool.js +109 -0
- package/lib/scope-split-tool.js.map +1 -0
- package/lib/search-tool.d.ts +15 -0
- package/lib/search-tool.d.ts.map +1 -0
- package/lib/search-tool.js +144 -0
- package/lib/search-tool.js.map +1 -0
- package/lib/secure-compare.d.ts.map +1 -1
- package/lib/secure-compare.js.map +1 -1
- package/lib/settings-rpc.d.ts.map +1 -1
- package/lib/settings-rpc.js +47 -8
- package/lib/settings-rpc.js.map +1 -1
- package/lib/skills-tool.d.ts +1 -1
- package/lib/skills-tool.d.ts.map +1 -1
- package/lib/skills-tool.js +5 -1
- package/lib/skills-tool.js.map +1 -1
- package/lib/tdd-evidence-tool.d.ts +11 -0
- package/lib/tdd-evidence-tool.d.ts.map +1 -0
- package/lib/tdd-evidence-tool.js +114 -0
- package/lib/tdd-evidence-tool.js.map +1 -0
- package/lib/tool-policy.d.ts +20 -0
- package/lib/tool-policy.d.ts.map +1 -0
- package/lib/tool-policy.js +42 -0
- package/lib/tool-policy.js.map +1 -0
- package/lib/workspace-tool.d.ts.map +1 -1
- package/lib/workspace-tool.js +15 -9
- package/lib/workspace-tool.js.map +1 -1
- package/package.json +4 -4
- package/src/{augment.d.ts → host/augment.d.ts} +5 -2
- package/src/{config-store.ts → host/config-store.ts} +2 -0
- package/src/host/git-worktree-tool.ts +130 -0
- package/src/host/govard-audit-lint-tool.ts +153 -0
- package/src/host/hyva-csp-scan-tool.ts +165 -0
- package/src/host/hyva-theme-inspect-tool.ts +258 -0
- package/src/host/incremental.ts +84 -0
- package/src/host/layout-xml-tool.ts +191 -0
- package/src/host/module-check-tool.ts +188 -0
- package/src/{orchestrator.ts → host/orchestrator.ts} +205 -84
- package/src/host/perf-log-stats-tool.ts +224 -0
- package/src/host/phtml-escape-scan-tool.ts +117 -0
- package/src/host/plan-track-tool.ts +118 -0
- package/src/{review-history.ts → host/review-history.ts} +27 -1
- package/src/host/scope-split-tool.ts +110 -0
- package/src/host/search-tool.ts +153 -0
- package/src/{settings-rpc.ts → host/settings-rpc.ts} +54 -11
- package/src/{skills-tool.ts → host/skills-tool.ts} +5 -1
- package/src/host/tdd-evidence-tool.ts +85 -0
- package/src/host/tool-policy.ts +49 -0
- package/src/{workspace-tool.ts → host/workspace-tool.ts} +27 -9
- package/lib/pin-store.d.ts +0 -5
- package/lib/pin-store.d.ts.map +0 -1
- package/lib/pin-store.js +0 -5
- package/lib/pin-store.js.map +0 -1
- package/src/pin-store.ts +0 -4
- /package/src/{events.ts → host/events.ts} +0 -0
- /package/src/{gitlab-client.ts → host/gitlab-client.ts} +0 -0
- /package/src/{gitlab-webhook.ts → host/gitlab-webhook.ts} +0 -0
- /package/src/{govard-tool.ts → host/govard-tool.ts} +0 -0
- /package/src/{index.ts → host/index.ts} +0 -0
- /package/src/{notify.ts → host/notify.ts} +0 -0
- /package/src/{providers → host/providers}/github.stub.ts +0 -0
- /package/src/{providers → host/providers}/gitlab.ts +0 -0
- /package/src/{providers → host/providers}/interface.ts +0 -0
- /package/src/{review-findings-tool.ts → host/review-findings-tool.ts} +0 -0
- /package/src/{review-intake.ts → host/review-intake.ts} +0 -0
- /package/src/{review-signals.ts → host/review-signals.ts} +0 -0
- /package/src/{secure-compare.ts → host/secure-compare.ts} +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { basename, extname, join, resolve, sep } from 'node:path'
|
|
2
|
+
import { readdir, readFile, realpath } from 'node:fs/promises'
|
|
3
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
+
import z from '@deepseek-ai/schemastery'
|
|
5
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
6
|
+
|
|
7
|
+
export const name = 'maestro-search-tool'
|
|
8
|
+
export const inject = ['tools']
|
|
9
|
+
|
|
10
|
+
export interface Config {
|
|
11
|
+
rootPath?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Config: z<Config> = z.object({
|
|
15
|
+
rootPath: z.string(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Same root-resolution rule as the workspace tools (parity with
|
|
20
|
+
* @ddtcorex/dsh-maestro-govard/src/workspace-tool.ts): explicit config wins,
|
|
21
|
+
* otherwise the calling agent's per-session workspace so review agents search
|
|
22
|
+
* their own worktree instead of the harness process cwd.
|
|
23
|
+
*/
|
|
24
|
+
interface SessionCwdSource {
|
|
25
|
+
agent?: { session?: { header?: { cwd?: string } } }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function workspaceRootFor(configuredRoot: string | undefined, exec: unknown): string {
|
|
29
|
+
if (configuredRoot !== undefined) return configuredRoot
|
|
30
|
+
const cwd = (exec as SessionCwdSource | undefined)?.agent?.session?.header?.cwd
|
|
31
|
+
return typeof cwd === 'string' && cwd !== '' ? cwd : process.cwd()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function isInsideRoot(rootPath: string, target: string): Promise<boolean> {
|
|
35
|
+
const absoluteRoot = resolve(rootPath)
|
|
36
|
+
const resolved = resolve(absoluteRoot, target)
|
|
37
|
+
if (resolved !== absoluteRoot && !resolved.startsWith(absoluteRoot + sep)) return false
|
|
38
|
+
try {
|
|
39
|
+
const realRoot = await realpath(absoluteRoot)
|
|
40
|
+
const realTarget = await realpath(resolved)
|
|
41
|
+
return realTarget === realRoot || realTarget.startsWith(realRoot + sep)
|
|
42
|
+
} catch {
|
|
43
|
+
// Unresolvable paths (broken symlinks etc.) cannot be proven contained.
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules'])
|
|
49
|
+
const MAX_FILE_BYTES = 1024 * 1024
|
|
50
|
+
const MAX_FILES = 2000
|
|
51
|
+
const MAX_MATCHES = 50
|
|
52
|
+
|
|
53
|
+
function globToRegExp(glob: string): RegExp | undefined {
|
|
54
|
+
if (glob === '' || glob === '*') return undefined
|
|
55
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*\*/g, '\u0000').replace(/\*/g, '[^/]*').replace(/\u0000/g, '.*')
|
|
56
|
+
return new RegExp(`^${escaped}$`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Bounded recursive walk yielding file paths relative to `dir`. */
|
|
60
|
+
async function* walkFiles(dir: string, base: string, budget: { files: number }): AsyncGenerator<string> {
|
|
61
|
+
if (budget.files <= 0) return
|
|
62
|
+
let entries
|
|
63
|
+
try {
|
|
64
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
65
|
+
} catch {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (budget.files <= 0) return
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
if (SKIP_DIRS.has(entry.name)) continue
|
|
72
|
+
yield* walkFiles(join(dir, entry.name), base, budget)
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
if (!entry.isFile()) continue
|
|
76
|
+
budget.files -= 1
|
|
77
|
+
yield join(dir, entry.name)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Grep-style content search over the review workspace. Reviewers have no
|
|
83
|
+
* shell; this is how they verify claims against the actual project sources
|
|
84
|
+
* (tailwind.config.js extensions/safelist, class usages, block wiring).
|
|
85
|
+
*/
|
|
86
|
+
export function apply(ctx: Context, config: Config = {}): void {
|
|
87
|
+
const configuredRoot = config.rootPath
|
|
88
|
+
|
|
89
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
90
|
+
name: 'maestro_search_files',
|
|
91
|
+
description: 'Search file contents under the current workspace root with a regular expression. Returns matching lines as "path:line: text". Use this before claiming something does or does not exist in the codebase.',
|
|
92
|
+
parameters: {
|
|
93
|
+
pattern: { type: 'string', required: true, description: 'Regular expression (JavaScript syntax) matched against each line.' },
|
|
94
|
+
path: { type: 'string', description: 'Optional directory or file prefix, relative to the workspace root.' },
|
|
95
|
+
glob: { type: 'string', description: 'Optional filename filter, e.g. "*.phtml" or "**/*.css".' },
|
|
96
|
+
},
|
|
97
|
+
output: {
|
|
98
|
+
schema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
additionalProperties: false,
|
|
101
|
+
properties: {
|
|
102
|
+
text: { type: 'string', required: true },
|
|
103
|
+
truncated: { type: 'boolean', required: true },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
render: (_args, value) => [{ type: 'text', text: value.text }],
|
|
107
|
+
},
|
|
108
|
+
async execute(args, exec) {
|
|
109
|
+
const root = workspaceRootFor(configuredRoot, exec)
|
|
110
|
+
const requested = args.path ?? ''
|
|
111
|
+
if (!(await isInsideRoot(root, requested))) {
|
|
112
|
+
return { text: `Path "${requested}" escapes the workspace root.`, truncated: false } as never
|
|
113
|
+
}
|
|
114
|
+
const searchRoot = requested === '' ? root : resolve(root, requested)
|
|
115
|
+
|
|
116
|
+
let pattern: RegExp
|
|
117
|
+
try {
|
|
118
|
+
pattern = new RegExp(args.pattern)
|
|
119
|
+
} catch (err) {
|
|
120
|
+
return { text: `Invalid regular expression: ${err instanceof Error ? err.message : String(err)}`, truncated: false } as never
|
|
121
|
+
}
|
|
122
|
+
const nameFilter = args.glob === undefined ? undefined : globToRegExp(args.glob)
|
|
123
|
+
|
|
124
|
+
const budget = { files: MAX_FILES }
|
|
125
|
+
const lines: string[] = []
|
|
126
|
+
let totalMatched = 0
|
|
127
|
+
let truncated = false
|
|
128
|
+
for await (const file of walkFiles(searchRoot, root, budget)) {
|
|
129
|
+
if (nameFilter !== undefined && !nameFilter.test(basename(file))) continue
|
|
130
|
+
if (extname(file) === '.zip') continue
|
|
131
|
+
let content: string
|
|
132
|
+
try {
|
|
133
|
+
const stat = await readFile(file)
|
|
134
|
+
if (stat.byteLength > MAX_FILE_BYTES) continue
|
|
135
|
+
content = stat.toString('utf-8')
|
|
136
|
+
} catch {
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
const relative = file.startsWith(root + sep) ? file.slice(root.length + 1) : basename(file)
|
|
140
|
+
content.split('\n').forEach((row, index) => {
|
|
141
|
+
if (!pattern.test(row)) return
|
|
142
|
+
totalMatched += 1
|
|
143
|
+
if (totalMatched <= MAX_MATCHES) {
|
|
144
|
+
lines.push(`${relative}:${index + 1}: ${row.trim().slice(0, 240)}`)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
if (budget.files <= 0 || totalMatched > MAX_MATCHES) truncated = true
|
|
149
|
+
const header = lines.length === 0 ? `No matches for /${args.pattern}/` : `${lines.length} match(es)${truncated ? ' (truncated)' : ''}:`
|
|
150
|
+
return { text: [header, ...lines].join('\n'), truncated } as never
|
|
151
|
+
},
|
|
152
|
+
}), 'maestro search tool'))
|
|
153
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis'
|
|
2
2
|
import type {} from '@deepseek-ai/dsh-client-connection'
|
|
3
|
-
import type { RpcErrorDetailsMap, RpcResult } from '@deepseek-ai/dsh-
|
|
3
|
+
import type { RpcErrorDetailsMap, RpcResult } from '@deepseek-ai/dsh-client-connection'
|
|
4
4
|
import { existsSync, statSync } from 'node:fs'
|
|
5
5
|
import { loadUserConfig, saveUserConfig, type MaestroUserConfig } from './config-store.js'
|
|
6
6
|
import { listReviews } from './review-history.js'
|
|
7
|
-
import { readPin, rotatePin, readLanPin, rotateLanPin } from './pin-store.js'
|
|
8
7
|
import { pinRotationText, type NotifierLike } from './notify.js'
|
|
9
8
|
|
|
10
9
|
export const name = 'maestro-settings-rpc'
|
|
@@ -13,7 +12,7 @@ export const inject = ['connection', 'maestroTunnel']
|
|
|
13
12
|
/** Keys the Settings card may persist; anything else is a rejected save. */
|
|
14
13
|
const SAVABLE_KEYS = new Set<keyof MaestroUserConfig>([
|
|
15
14
|
'gitlabBaseUrl', 'gitlabToken', 'botUsername', 'webhookSecret', 'webhookPort',
|
|
16
|
-
'projectMappings', 'reviewModel', 'autoRereviewOnPush', 'agentTimeoutMs', 'reviewSessionRetentionDays',
|
|
15
|
+
'projectMappings', 'reviewModel', 'supervisorModel', 'autoRereviewOnPush', 'agentTimeoutMs', 'reviewSessionRetentionDays',
|
|
17
16
|
'tunnelMode', 'quickTarget', 'tunnelId', 'tunnelCredentialsFile', 'tunnelHostname',
|
|
18
17
|
'proxyPort', 'proxyHost', 'lanPinEnabled', 'telegramBotToken', 'telegramChatId',
|
|
19
18
|
'telegramReviewNotifications',
|
|
@@ -87,6 +86,14 @@ function validateSavePayload(payload: unknown): { ok: true; patch: Partial<Maest
|
|
|
87
86
|
const err = validateReviewModel(value)
|
|
88
87
|
if (err !== null) return { ok: false, message: err }
|
|
89
88
|
}
|
|
89
|
+
if (key === 'supervisorModel') {
|
|
90
|
+
if (value === null) {
|
|
91
|
+
patch[key as keyof MaestroUserConfig] = undefined as never
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
const err = validateReviewModel(value)
|
|
95
|
+
if (err !== null) return { ok: false, message: err.replace('reviewModel', 'supervisorModel') }
|
|
96
|
+
}
|
|
90
97
|
if (key === 'webhookPort' && value !== undefined && (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 65535)) {
|
|
91
98
|
return { ok: false, message: 'webhookPort must be an integer between 1 and 65535.' }
|
|
92
99
|
}
|
|
@@ -165,10 +172,10 @@ export function apply(ctx: Context): void {
|
|
|
165
172
|
return ok(ctx.maestroTunnel.proxyStatus())
|
|
166
173
|
}
|
|
167
174
|
if (endpoint === MAESTRO_ENDPOINTS.getPin) {
|
|
168
|
-
return ok({ pin: await
|
|
175
|
+
return ok({ pin: await ctx.maestroTunnel.getPin() })
|
|
169
176
|
}
|
|
170
177
|
if (endpoint === MAESTRO_ENDPOINTS.rotatePin) {
|
|
171
|
-
const pin = await rotatePin()
|
|
178
|
+
const pin = await ctx.maestroTunnel.rotatePin()
|
|
172
179
|
// Delivery is deliberately detached: a slow/unavailable notifier cannot make the
|
|
173
180
|
// explicit security operation appear to fail or hold the Settings UI open.
|
|
174
181
|
void loadUserConfig().then((config) => {
|
|
@@ -196,7 +203,7 @@ export function apply(ctx: Context): void {
|
|
|
196
203
|
// Disabled by default; the LAN PIN is only read (and generated) once the
|
|
197
204
|
// user opts in, so an untouched install keeps LAN access open.
|
|
198
205
|
if (config.lanPinEnabled !== true) return ok({ enabled: false })
|
|
199
|
-
return ok({ enabled: true, pin: await
|
|
206
|
+
return ok({ enabled: true, pin: await ctx.maestroTunnel.getLanPin() })
|
|
200
207
|
}
|
|
201
208
|
if (endpoint === MAESTRO_ENDPOINTS.lanPinSetEnabled) {
|
|
202
209
|
const enabled = (payload as { enabled?: unknown } | undefined)?.enabled === true
|
|
@@ -207,7 +214,7 @@ export function apply(ctx: Context): void {
|
|
|
207
214
|
return ok({ enabled })
|
|
208
215
|
}
|
|
209
216
|
if (endpoint === MAESTRO_ENDPOINTS.lanPinRotate) {
|
|
210
|
-
return ok({ pin: await rotateLanPin() })
|
|
217
|
+
return ok({ pin: await ctx.maestroTunnel.rotateLanPin() })
|
|
211
218
|
}
|
|
212
219
|
if (endpoint === MAESTRO_ENDPOINTS.reviewsList) {
|
|
213
220
|
return ok(await listReviews(20))
|
|
@@ -220,15 +227,44 @@ export function apply(ctx: Context): void {
|
|
|
220
227
|
if (endpoint === MAESTRO_ENDPOINTS.modelsList) {
|
|
221
228
|
const agentDefaultModel = ctx.get('agentDefaultModel') as { currentSelection(): { provider: string; model: string; reasoningEffort?: string } } | undefined
|
|
222
229
|
const fallbackSelection = agentDefaultModel?.currentSelection() ?? { provider: 'deepseek-official', model: 'deepseek-chat' }
|
|
223
|
-
|
|
230
|
+
type LlmReasoningEffort = { id: string; name: string }
|
|
231
|
+
type LlmModelInfoWithReasoning = { id: string; name?: string; reasoning?: { efforts: LlmReasoningEffort[] } }
|
|
232
|
+
const llm = ctx.get('llm') as {
|
|
233
|
+
listProviders?: () => Array<{ id: string; name: string }>
|
|
234
|
+
listModels?: (provider: string) => Promise<readonly LlmModelInfoWithReasoning[]>
|
|
235
|
+
resolveModelInfo?: (provider: string, model: string) => Promise<LlmModelInfoWithReasoning & { reasoning?: { efforts: LlmReasoningEffort[] } }>
|
|
236
|
+
} | undefined
|
|
224
237
|
if (llm?.listProviders !== undefined && llm?.listModels !== undefined) {
|
|
225
238
|
try {
|
|
226
239
|
const providers = llm.listProviders()
|
|
227
|
-
const groups: Array<{ provider: string; name: string; models: string[] }> = []
|
|
240
|
+
const groups: Array<{ provider: string; name: string; models: Array<{ id: string; name?: string; supportsReasoning: boolean; reasoningEfforts: string[]; reasoning?: { efforts: LlmReasoningEffort[] } }> }> = []
|
|
228
241
|
for (const p of providers) {
|
|
229
242
|
try {
|
|
230
243
|
const models = await llm.listModels(p.id)
|
|
231
|
-
|
|
244
|
+
const enriched = await Promise.all(models.map(async (m) => {
|
|
245
|
+
let efforts: string[] = (m as LlmModelInfoWithReasoning).reasoning?.efforts?.map(e => e.id) ?? []
|
|
246
|
+
let reasoningObj = (m as LlmModelInfoWithReasoning).reasoning
|
|
247
|
+
if (efforts.length === 0 && typeof llm.resolveModelInfo === 'function') {
|
|
248
|
+
try {
|
|
249
|
+
const resolved = await llm.resolveModelInfo(p.id, m.id)
|
|
250
|
+
if (resolved?.reasoning?.efforts) {
|
|
251
|
+
reasoningObj = resolved.reasoning
|
|
252
|
+
efforts = resolved.reasoning.efforts.map(e => e.id)
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
// ignore resolve failures, keep original
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const supportsReasoning = efforts.filter(e => e !== 'off').length > 0
|
|
259
|
+
return {
|
|
260
|
+
id: m.id,
|
|
261
|
+
name: m.name,
|
|
262
|
+
supportsReasoning,
|
|
263
|
+
reasoningEfforts: efforts,
|
|
264
|
+
...(reasoningObj ? { reasoning: reasoningObj } : {}),
|
|
265
|
+
}
|
|
266
|
+
}))
|
|
267
|
+
groups.push({ provider: p.id, name: p.name, models: enriched })
|
|
232
268
|
} catch {
|
|
233
269
|
groups.push({ provider: p.id, name: p.name, models: [] })
|
|
234
270
|
}
|
|
@@ -239,7 +275,14 @@ export function apply(ctx: Context): void {
|
|
|
239
275
|
}
|
|
240
276
|
}
|
|
241
277
|
// Fallback: single group containing the current selection so dropdown still works
|
|
242
|
-
return ok({
|
|
278
|
+
return ok({
|
|
279
|
+
groups: [{
|
|
280
|
+
provider: fallbackSelection.provider,
|
|
281
|
+
name: fallbackSelection.provider,
|
|
282
|
+
models: [{ id: fallbackSelection.model, name: fallbackSelection.model, supportsReasoning: false, reasoningEfforts: [] }],
|
|
283
|
+
}],
|
|
284
|
+
current: fallbackSelection,
|
|
285
|
+
})
|
|
243
286
|
}
|
|
244
287
|
return fail(`Unknown endpoint: ${endpoint}`)
|
|
245
288
|
}
|
|
@@ -16,6 +16,10 @@ export const REVIEW_PROFILE_SKILLS: Record<ReviewSkillProfile, readonly string[]
|
|
|
16
16
|
'govard-toolbox',
|
|
17
17
|
'govard-magento',
|
|
18
18
|
'magento2-dev-core',
|
|
19
|
+
// Frontend passes: without these a Hyvä/Tailwind MR is reviewed on model
|
|
20
|
+
// priors alone (utility-class claims were unverifiable and wrong in practice).
|
|
21
|
+
'magento2-frontend-dev',
|
|
22
|
+
'magento2-hyva-dev',
|
|
19
23
|
'magento2-code-review',
|
|
20
24
|
'magento2-linter',
|
|
21
25
|
'magento2-security-scan',
|
|
@@ -25,7 +29,7 @@ export const REVIEW_PROFILE_SKILLS: Record<ReviewSkillProfile, readonly string[]
|
|
|
25
29
|
generic: [],
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
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,magento2-dev-core,magento2-code-review,magento2-linter,magento2-security-scan,magento2-performance-audit -y'
|
|
32
|
+
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,magento2-dev-core,magento2-frontend-dev,magento2-hyva-dev,magento2-code-review,magento2-linter,magento2-security-scan,magento2-performance-audit -y'
|
|
29
33
|
|
|
30
34
|
// This is deliberately process-local and keyed by the reviewer Agent object,
|
|
31
35
|
// which every Cordis child context inherits. Preset mounting inserts child
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolve, sep } from 'node:path'
|
|
2
|
+
import { readFile, stat, realpath } from 'node:fs/promises'
|
|
3
|
+
import { execFile } from 'node:child_process'
|
|
4
|
+
import { promisify } from 'node:util'
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
6
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
7
|
+
import z from '@deepseek-ai/schemastery'
|
|
8
|
+
|
|
9
|
+
export const name='maestro-tdd-evidence-tool'
|
|
10
|
+
export const inject=['tools']
|
|
11
|
+
export const Config:z<{rootPath?:string}> = z.object({rootPath:z.string()})
|
|
12
|
+
interface SC{agent?:{session?:{header?:{cwd?:string}}}}
|
|
13
|
+
function workspaceRootFor(c:string|undefined,e:unknown):string{ if(c!==undefined) return c; const cwd=(e as SC|undefined)?.agent?.session?.header?.cwd; return typeof cwd==='string'&&cwd!==''?cwd:process.cwd()}
|
|
14
|
+
async function isInsideRoot(r:string,t:string):Promise<boolean>{
|
|
15
|
+
const ar=resolve(r); const rs=resolve(ar,t)
|
|
16
|
+
if(rs!==ar && !rs.startsWith(ar+sep)) return false
|
|
17
|
+
try{ const rr=await realpath(ar); const rt=await realpath(rs); return rt===rr||rt.startsWith(rr+sep)}catch{
|
|
18
|
+
try{ const rr=await realpath(ar); const parent=resolve(rs,'..'); const rp=await realpath(parent); return rp===rr||rp.startsWith(rr+sep)}catch{ return rs===ar || rs.startsWith(ar+sep) }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const execFileAsync=promisify(execFile)
|
|
22
|
+
|
|
23
|
+
export function apply(ctx:Context, config:{rootPath?:string}={}):void{
|
|
24
|
+
const configuredRoot=config.rootPath
|
|
25
|
+
// simple in-memory red evidence store per process
|
|
26
|
+
const redEvidence=new Set<string>()
|
|
27
|
+
ctx.effect(()=>ctx.tools.register(defineTool({
|
|
28
|
+
name:'maestro_tdd_evidence',
|
|
29
|
+
description:'Capture TDD evidence: red (failing test), green (passing), verify (pnpm verify). Enforces red-before-green.',
|
|
30
|
+
parameters:{
|
|
31
|
+
op:{type:'string', enum:['red','green','verify'], required:true},
|
|
32
|
+
testFile:{type:'string'},
|
|
33
|
+
evidence:{type:'string'},
|
|
34
|
+
},
|
|
35
|
+
output:{
|
|
36
|
+
schema:{
|
|
37
|
+
type:'object', additionalProperties:true,
|
|
38
|
+
properties:{
|
|
39
|
+
ok:{type:'boolean', required:true},
|
|
40
|
+
phase:{type:'string'},
|
|
41
|
+
error:{type:'string'},
|
|
42
|
+
verify:{type:'object', additionalProperties:true},
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
render:(_a,v:{ok:boolean, error?:string})=>[{type:'text', text: v.ok? 'ok' : (v.error ?? 'error')}],
|
|
46
|
+
},
|
|
47
|
+
async execute(args, exec){
|
|
48
|
+
const op=(args as {op:string}).op
|
|
49
|
+
const rawTest=(args as {testFile?:string}).testFile
|
|
50
|
+
const root=workspaceRootFor(configuredRoot, exec)
|
|
51
|
+
if(rawTest && !(await isInsideRoot(root, rawTest))) return {text:`Path "${rawTest}" escapes the workspace root.`, truncated:false} as never
|
|
52
|
+
if(op==='red'){
|
|
53
|
+
if(!rawTest) return {text:'testFile required for red', truncated:false} as never
|
|
54
|
+
const abs=resolve(root, rawTest)
|
|
55
|
+
try{ const s=await stat(abs); if(!s.isFile()) return {ok:false, phase:'red', error:'test file not found'} as never }catch{ return {ok:false, phase:'red', error:'test file not found'} as never }
|
|
56
|
+
const content=await readFile(abs,'utf-8').catch(()=>'')
|
|
57
|
+
// red evidence is that file contains failing expectation (throw, expect fail, etc.)
|
|
58
|
+
redEvidence.add(rawTest)
|
|
59
|
+
return {ok:true, phase:'red', testFile: rawTest, evidence: content.slice(0,500), redEvidenceSize: redEvidence.size} as never
|
|
60
|
+
}
|
|
61
|
+
if(op==='green'){
|
|
62
|
+
if(!rawTest) return {text:'testFile required for green', truncated:false} as never
|
|
63
|
+
if(!redEvidence.has(rawTest)){
|
|
64
|
+
// also check file exists? If not in redEvidence, error must contain 'red'
|
|
65
|
+
return {ok:false, phase:'green', error:'red evidence required before green — run op:red first'} as never
|
|
66
|
+
}
|
|
67
|
+
const abs=resolve(root, rawTest)
|
|
68
|
+
try{ await stat(abs) }catch{ return {ok:false, phase:'green', error:'test file not found'} as never }
|
|
69
|
+
return {ok:true, phase:'green', testFile: rawTest} as never
|
|
70
|
+
}
|
|
71
|
+
if(op==='verify'){
|
|
72
|
+
// try to run pnpm verify -- if not available, return ok false but with verify object
|
|
73
|
+
try{
|
|
74
|
+
const {stdout}=await execFileAsync('pnpm', ['verify'], {cwd:root, timeout:30000}) as any
|
|
75
|
+
return {ok:true, phase:'verify', verify:{passed:true, output: String(stdout).slice(0,2000)}} as never
|
|
76
|
+
}catch(e:any){
|
|
77
|
+
const out=(e.stdout ?? '') + (e.stderr ?? '') + String(e.message)
|
|
78
|
+
const passed=out.includes('Tests') && !out.includes('FAIL')
|
|
79
|
+
return {ok:passed, phase:'verify', verify:{passed, output: out.slice(0,2000)}, error: passed? undefined : 'verify failed'} as never
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {text:`unknown op ${op}`, truncated:false} as never
|
|
83
|
+
}
|
|
84
|
+
}), 'tdd evidence'))
|
|
85
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
+
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
|
|
3
|
+
|
|
4
|
+
export const name = 'maestro-review-tool-policy'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Host-level tools that leak into every agent but make no sense for a review
|
|
8
|
+
* agent: review sessions must stay read-only on the worktree and side-effect
|
|
9
|
+
* free on the harness (no personal memory writes, no todo lists, no plugin
|
|
10
|
+
* marketplace suggestions, no sidebar UI, no nested subagents).
|
|
11
|
+
*
|
|
12
|
+
* Deny-listed rather than allow-listed on purpose: DSH has no per-agent tool
|
|
13
|
+
* scoping yet, and a reviewer genuinely needs the shared search/read/gitlab
|
|
14
|
+
* surface — an allow-list would break on every new host capability.
|
|
15
|
+
*/
|
|
16
|
+
export const REVIEW_DENIED_TOOLS: readonly string[] = [
|
|
17
|
+
'memory',
|
|
18
|
+
'memory_suggest',
|
|
19
|
+
'dtodo',
|
|
20
|
+
'find_dsh_plugin',
|
|
21
|
+
'sidebar_open',
|
|
22
|
+
'subagent_claude_code',
|
|
23
|
+
'subagent_codex',
|
|
24
|
+
'maestro_write_file',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
/** Pure predicate: returns the denial reason for denied tools, else undefined. */
|
|
28
|
+
export function deniedToolForReview(toolName: string): string | undefined {
|
|
29
|
+
if (!REVIEW_DENIED_TOOLS.includes(toolName)) return undefined
|
|
30
|
+
return `Tool "${toolName}" is not available to Maestro review agents: reviews stay read-only on the worktree and side-effect free on the harness.`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createReviewToolPolicyHandler(): (
|
|
34
|
+
exec: ToolExecution,
|
|
35
|
+
next: () => Promise<PreToolDecision>,
|
|
36
|
+
) => Promise<PreToolDecision> {
|
|
37
|
+
return async (exec, next) => {
|
|
38
|
+
const toolName = exec.name ?? ''
|
|
39
|
+
const reason = deniedToolForReview(toolName)
|
|
40
|
+
if (reason !== undefined) return { kind: 'deny', reason }
|
|
41
|
+
return next()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Agent-scoped: register inside an agent setup fiber so the policy dies with that agent. */
|
|
46
|
+
export function apply(ctx: Context): void {
|
|
47
|
+
const handler = createReviewToolPolicyHandler()
|
|
48
|
+
ctx.effect(() => ctx.on('tools/pre-execute', handler))
|
|
49
|
+
}
|
|
@@ -47,10 +47,28 @@ async function resolveInRoot(rootPath: string, requestedPath: string): Promise<s
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Structural view of the tool run-context fields this tool needs. Mirrors the
|
|
52
|
+
* `@deepseek-ai/dsh-tool-fs` session-cwd semantics: without an explicit pinned
|
|
53
|
+
* root, relative paths resolve against the CALLING agent's per-session
|
|
54
|
+
* workspace (`exec.agent.session.header.cwd`), so subagents whose session
|
|
55
|
+
* lives in a checkout or review worktree read their own tree instead of the
|
|
56
|
+
* harness host's `process.cwd()`.
|
|
57
|
+
*/
|
|
58
|
+
interface SessionCwdSource {
|
|
59
|
+
agent?: { session?: { header?: { cwd?: string } } }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function workspaceRootFor(configuredRoot: string | undefined, exec: unknown): string {
|
|
63
|
+
if (configuredRoot !== undefined) return configuredRoot
|
|
64
|
+
const cwd = (exec as SessionCwdSource | undefined)?.agent?.session?.header?.cwd
|
|
65
|
+
return typeof cwd === 'string' && cwd !== '' ? cwd : process.cwd()
|
|
66
|
+
}
|
|
67
|
+
|
|
50
68
|
export function apply(ctx: Context, config: Config = {}): void {
|
|
51
|
-
const
|
|
69
|
+
const configuredRoot = config.rootPath
|
|
52
70
|
|
|
53
|
-
ctx.tools.register(defineTool({
|
|
71
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
54
72
|
name: 'maestro_read_file',
|
|
55
73
|
description: 'Read a text file relative to the current workspace root.',
|
|
56
74
|
parameters: {
|
|
@@ -60,15 +78,15 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
60
78
|
schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', required: true } } },
|
|
61
79
|
render: (_args, value) => [{ type: 'text', text: value.text }],
|
|
62
80
|
},
|
|
63
|
-
async execute(args) {
|
|
64
|
-
const target = await resolveInRoot(
|
|
81
|
+
async execute(args, exec) {
|
|
82
|
+
const target = await resolveInRoot(workspaceRootFor(configuredRoot, exec), args.path)
|
|
65
83
|
if (target === undefined) return { isError: true, text: `Path "${args.path}" escapes the workspace root.` } as never
|
|
66
84
|
const text = await readFile(target, 'utf-8')
|
|
67
85
|
return { text }
|
|
68
86
|
},
|
|
69
|
-
}))
|
|
87
|
+
})))
|
|
70
88
|
|
|
71
|
-
ctx.tools.register(defineTool({
|
|
89
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
72
90
|
name: 'maestro_write_file',
|
|
73
91
|
description: 'Write a text file relative to the current workspace root, creating parent directories as needed.',
|
|
74
92
|
parameters: {
|
|
@@ -79,12 +97,12 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
79
97
|
schema: { type: 'object', additionalProperties: false, properties: { written: { type: 'boolean', required: true } } },
|
|
80
98
|
render: (args) => [{ type: 'text', text: `Wrote ${args.path}` }],
|
|
81
99
|
},
|
|
82
|
-
async execute(args) {
|
|
83
|
-
const target = await resolveInRoot(
|
|
100
|
+
async execute(args, exec) {
|
|
101
|
+
const target = await resolveInRoot(workspaceRootFor(configuredRoot, exec), args.path)
|
|
84
102
|
if (target === undefined) return { isError: true, text: `Path "${args.path}" escapes the workspace root.` } as never
|
|
85
103
|
await mkdir(dirname(target), { recursive: true })
|
|
86
104
|
await writeFile(target, args.content, 'utf-8')
|
|
87
105
|
return { written: true }
|
|
88
106
|
},
|
|
89
|
-
}))
|
|
107
|
+
})))
|
|
90
108
|
}
|
package/lib/pin-store.d.ts
DELETED
package/lib/pin-store.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pin-store.d.ts","sourceRoot":"","sources":["../src/pin-store.ts"],"names":[],"mappings":"AAAA,wBAAsB,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAsB;AACtE,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAAsB;AACxE,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAAsB;AACzE,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAsB"}
|
package/lib/pin-store.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export async function readPin() { return '00000000'; }
|
|
2
|
-
export async function rotatePin() { return '00000000'; }
|
|
3
|
-
export async function readLanPin() { return '00000000'; }
|
|
4
|
-
export async function rotateLanPin() { return '00000000'; }
|
|
5
|
-
//# sourceMappingURL=pin-store.js.map
|
package/lib/pin-store.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pin-store.js","sourceRoot":"","sources":["../src/pin-store.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,KAAK,UAAU,OAAO,KAAsB,OAAO,UAAU,CAAA,CAAC,CAAC;AACtE,MAAM,CAAC,KAAK,UAAU,SAAS,KAAsB,OAAO,UAAU,CAAA,CAAC,CAAC;AACxE,MAAM,CAAC,KAAK,UAAU,UAAU,KAAsB,OAAO,UAAU,CAAA,CAAC,CAAC;AACzE,MAAM,CAAC,KAAK,UAAU,YAAY,KAAsB,OAAO,UAAU,CAAA,CAAC,CAAC"}
|
package/src/pin-store.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export async function readPin(): Promise<string> { return '00000000' }
|
|
2
|
-
export async function rotatePin(): Promise<string> { return '00000000' }
|
|
3
|
-
export async function readLanPin(): Promise<string> { return '00000000' }
|
|
4
|
-
export async function rotateLanPin(): Promise<string> { return '00000000' }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|