@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.
Files changed (143) hide show
  1. package/lib/config-store.d.ts +2 -0
  2. package/lib/config-store.d.ts.map +1 -1
  3. package/lib/config-store.js.map +1 -1
  4. package/lib/events.d.ts.map +1 -1
  5. package/lib/events.js.map +1 -1
  6. package/lib/git-worktree-tool.d.ts +11 -0
  7. package/lib/git-worktree-tool.d.ts.map +1 -0
  8. package/lib/git-worktree-tool.js +162 -0
  9. package/lib/git-worktree-tool.js.map +1 -0
  10. package/lib/gitlab-client.d.ts.map +1 -1
  11. package/lib/gitlab-client.js.map +1 -1
  12. package/lib/gitlab-webhook.d.ts.map +1 -1
  13. package/lib/gitlab-webhook.js.map +1 -1
  14. package/lib/govard-audit-lint-tool.d.ts +14 -0
  15. package/lib/govard-audit-lint-tool.d.ts.map +1 -0
  16. package/lib/govard-audit-lint-tool.js +177 -0
  17. package/lib/govard-audit-lint-tool.js.map +1 -0
  18. package/lib/govard-tool.d.ts.map +1 -1
  19. package/lib/govard-tool.js.map +1 -1
  20. package/lib/hyva-csp-scan-tool.d.ts +11 -0
  21. package/lib/hyva-csp-scan-tool.d.ts.map +1 -0
  22. package/lib/hyva-csp-scan-tool.js +193 -0
  23. package/lib/hyva-csp-scan-tool.js.map +1 -0
  24. package/lib/hyva-theme-inspect-tool.d.ts +10 -0
  25. package/lib/hyva-theme-inspect-tool.d.ts.map +1 -0
  26. package/lib/hyva-theme-inspect-tool.js +299 -0
  27. package/lib/hyva-theme-inspect-tool.js.map +1 -0
  28. package/lib/incremental.d.ts +28 -0
  29. package/lib/incremental.d.ts.map +1 -0
  30. package/lib/incremental.js +60 -0
  31. package/lib/incremental.js.map +1 -0
  32. package/lib/index.d.ts.map +1 -1
  33. package/lib/index.js.map +1 -1
  34. package/lib/layout-xml-tool.d.ts +11 -0
  35. package/lib/layout-xml-tool.d.ts.map +1 -0
  36. package/lib/layout-xml-tool.js +240 -0
  37. package/lib/layout-xml-tool.js.map +1 -0
  38. package/lib/module-check-tool.d.ts +11 -0
  39. package/lib/module-check-tool.d.ts.map +1 -0
  40. package/lib/module-check-tool.js +304 -0
  41. package/lib/module-check-tool.js.map +1 -0
  42. package/lib/notify.d.ts.map +1 -1
  43. package/lib/notify.js.map +1 -1
  44. package/lib/orchestrator.d.ts +2 -0
  45. package/lib/orchestrator.d.ts.map +1 -1
  46. package/lib/orchestrator.js +220 -88
  47. package/lib/orchestrator.js.map +1 -1
  48. package/lib/perf-log-stats-tool.d.ts +13 -0
  49. package/lib/perf-log-stats-tool.d.ts.map +1 -0
  50. package/lib/perf-log-stats-tool.js +245 -0
  51. package/lib/perf-log-stats-tool.js.map +1 -0
  52. package/lib/phtml-escape-scan-tool.d.ts +11 -0
  53. package/lib/phtml-escape-scan-tool.d.ts.map +1 -0
  54. package/lib/phtml-escape-scan-tool.js +170 -0
  55. package/lib/phtml-escape-scan-tool.js.map +1 -0
  56. package/lib/plan-track-tool.d.ts +11 -0
  57. package/lib/plan-track-tool.d.ts.map +1 -0
  58. package/lib/plan-track-tool.js +143 -0
  59. package/lib/plan-track-tool.js.map +1 -0
  60. package/lib/providers/github.stub.d.ts.map +1 -1
  61. package/lib/providers/github.stub.js.map +1 -1
  62. package/lib/providers/gitlab.d.ts.map +1 -1
  63. package/lib/providers/gitlab.js.map +1 -1
  64. package/lib/providers/interface.d.ts.map +1 -1
  65. package/lib/providers/interface.js.map +1 -1
  66. package/lib/review-findings-tool.d.ts.map +1 -1
  67. package/lib/review-findings-tool.js.map +1 -1
  68. package/lib/review-history.d.ts +4 -0
  69. package/lib/review-history.d.ts.map +1 -1
  70. package/lib/review-history.js +23 -1
  71. package/lib/review-history.js.map +1 -1
  72. package/lib/review-intake.d.ts.map +1 -1
  73. package/lib/review-intake.js.map +1 -1
  74. package/lib/review-signals.d.ts.map +1 -1
  75. package/lib/review-signals.js.map +1 -1
  76. package/lib/scope-split-tool.d.ts +42 -0
  77. package/lib/scope-split-tool.d.ts.map +1 -0
  78. package/lib/scope-split-tool.js +109 -0
  79. package/lib/scope-split-tool.js.map +1 -0
  80. package/lib/search-tool.d.ts +15 -0
  81. package/lib/search-tool.d.ts.map +1 -0
  82. package/lib/search-tool.js +144 -0
  83. package/lib/search-tool.js.map +1 -0
  84. package/lib/secure-compare.d.ts.map +1 -1
  85. package/lib/secure-compare.js.map +1 -1
  86. package/lib/settings-rpc.d.ts.map +1 -1
  87. package/lib/settings-rpc.js +47 -8
  88. package/lib/settings-rpc.js.map +1 -1
  89. package/lib/skills-tool.d.ts +1 -1
  90. package/lib/skills-tool.d.ts.map +1 -1
  91. package/lib/skills-tool.js +5 -1
  92. package/lib/skills-tool.js.map +1 -1
  93. package/lib/tdd-evidence-tool.d.ts +11 -0
  94. package/lib/tdd-evidence-tool.d.ts.map +1 -0
  95. package/lib/tdd-evidence-tool.js +114 -0
  96. package/lib/tdd-evidence-tool.js.map +1 -0
  97. package/lib/tool-policy.d.ts +20 -0
  98. package/lib/tool-policy.d.ts.map +1 -0
  99. package/lib/tool-policy.js +42 -0
  100. package/lib/tool-policy.js.map +1 -0
  101. package/lib/workspace-tool.d.ts.map +1 -1
  102. package/lib/workspace-tool.js +15 -9
  103. package/lib/workspace-tool.js.map +1 -1
  104. package/package.json +4 -4
  105. package/src/{augment.d.ts → host/augment.d.ts} +5 -2
  106. package/src/{config-store.ts → host/config-store.ts} +2 -0
  107. package/src/host/git-worktree-tool.ts +130 -0
  108. package/src/host/govard-audit-lint-tool.ts +153 -0
  109. package/src/host/hyva-csp-scan-tool.ts +165 -0
  110. package/src/host/hyva-theme-inspect-tool.ts +258 -0
  111. package/src/host/incremental.ts +84 -0
  112. package/src/host/layout-xml-tool.ts +191 -0
  113. package/src/host/module-check-tool.ts +188 -0
  114. package/src/{orchestrator.ts → host/orchestrator.ts} +205 -84
  115. package/src/host/perf-log-stats-tool.ts +224 -0
  116. package/src/host/phtml-escape-scan-tool.ts +117 -0
  117. package/src/host/plan-track-tool.ts +118 -0
  118. package/src/{review-history.ts → host/review-history.ts} +27 -1
  119. package/src/host/scope-split-tool.ts +110 -0
  120. package/src/host/search-tool.ts +153 -0
  121. package/src/{settings-rpc.ts → host/settings-rpc.ts} +54 -11
  122. package/src/{skills-tool.ts → host/skills-tool.ts} +5 -1
  123. package/src/host/tdd-evidence-tool.ts +85 -0
  124. package/src/host/tool-policy.ts +49 -0
  125. package/src/{workspace-tool.ts → host/workspace-tool.ts} +27 -9
  126. package/lib/pin-store.d.ts +0 -5
  127. package/lib/pin-store.d.ts.map +0 -1
  128. package/lib/pin-store.js +0 -5
  129. package/lib/pin-store.js.map +0 -1
  130. package/src/pin-store.ts +0 -4
  131. /package/src/{events.ts → host/events.ts} +0 -0
  132. /package/src/{gitlab-client.ts → host/gitlab-client.ts} +0 -0
  133. /package/src/{gitlab-webhook.ts → host/gitlab-webhook.ts} +0 -0
  134. /package/src/{govard-tool.ts → host/govard-tool.ts} +0 -0
  135. /package/src/{index.ts → host/index.ts} +0 -0
  136. /package/src/{notify.ts → host/notify.ts} +0 -0
  137. /package/src/{providers → host/providers}/github.stub.ts +0 -0
  138. /package/src/{providers → host/providers}/gitlab.ts +0 -0
  139. /package/src/{providers → host/providers}/interface.ts +0 -0
  140. /package/src/{review-findings-tool.ts → host/review-findings-tool.ts} +0 -0
  141. /package/src/{review-intake.ts → host/review-intake.ts} +0 -0
  142. /package/src/{review-signals.ts → host/review-signals.ts} +0 -0
  143. /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-host-apiproxy/api'
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 readPin() })
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 readLanPin() })
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
- const llm = ctx.get('llm') as { listProviders?: () => Array<{ id: string; name: string }>; listModels?: (provider: string) => Promise<Array<{ id: string; name?: string }>> } | undefined
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
- groups.push({ provider: p.id, name: p.name, models: models.map(m => m.id) })
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({ groups: [{ provider: fallbackSelection.provider, name: fallbackSelection.provider, models: [fallbackSelection.model] }], current: fallbackSelection })
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 rootPath = config.rootPath ?? process.cwd()
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(rootPath, args.path)
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(rootPath, args.path)
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
  }
@@ -1,5 +0,0 @@
1
- export declare function readPin(): Promise<string>;
2
- export declare function rotatePin(): Promise<string>;
3
- export declare function readLanPin(): Promise<string>;
4
- export declare function rotateLanPin(): Promise<string>;
5
- //# sourceMappingURL=pin-store.d.ts.map
@@ -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
@@ -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