@ludi-uni/ludi-agent-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/AGENTS.md +55 -0
  2. package/LICENSE +21 -0
  3. package/README.md +107 -0
  4. package/adapters/codex/README.md +24 -0
  5. package/adapters/codex/skill-metadata/visual-verification/agents/openai.yaml +7 -0
  6. package/adapters/pi/README.md +88 -0
  7. package/adapters/pi/browser/agent-browser.mjs +193 -0
  8. package/adapters/pi/lib/invoke.mjs +55 -0
  9. package/adapters/pi/lib/list-models.mjs +29 -0
  10. package/adapters/pi/lib/settings-proposal.mjs +34 -0
  11. package/adapters/pi/lib/subagent.mjs +175 -0
  12. package/adapters/pi/loop-guard/index.js +51 -0
  13. package/adapters/pi/maintenance-policy.json +36 -0
  14. package/adapters/pi/mcp.template.json +4 -0
  15. package/adapters/pi/model-catalog.json +97 -0
  16. package/adapters/pi/models.json +13 -0
  17. package/adapters/pi/models.local.example.json +14 -0
  18. package/adapters/pi/orchestrator-ext/command.mjs +14 -0
  19. package/adapters/pi/orchestrator-ext/index.js +150 -0
  20. package/adapters/pi/settings.template.json +7 -0
  21. package/adapters/pi/shell-gate/index.js +70 -0
  22. package/adapters/pi/sync-pi.ps1 +137 -0
  23. package/agents/README.md +26 -0
  24. package/agents/browser.md +64 -0
  25. package/agents/coder.md +31 -0
  26. package/agents/orchestrator.md +37 -0
  27. package/agents/reviewer.md +32 -0
  28. package/agents/scout.md +35 -0
  29. package/agents/tester.md +28 -0
  30. package/agents/visual.md +28 -0
  31. package/context-pack/SPEC.md +101 -0
  32. package/context-pack/context-pack.schema.json +79 -0
  33. package/context-pack/examples/example-fix.md +44 -0
  34. package/docs/architecture.md +55 -0
  35. package/docs/migration-from-codex-setting.md +44 -0
  36. package/docs/model-maintenance.md +401 -0
  37. package/docs/orchestrator.md +155 -0
  38. package/docs/phase2-report.md +39 -0
  39. package/docs/roadmap.md +27 -0
  40. package/docs/third-party.md +15 -0
  41. package/lib/agents.mjs +79 -0
  42. package/lib/context-pack.mjs +215 -0
  43. package/lib/job.mjs +312 -0
  44. package/lib/language-policy.mjs +27 -0
  45. package/lib/maintenance-exec.mjs +377 -0
  46. package/lib/maintenance-runner.mjs +266 -0
  47. package/lib/maintenance.mjs +422 -0
  48. package/lib/normalize.mjs +101 -0
  49. package/lib/observe/differ.mjs +185 -0
  50. package/lib/observe/observation.mjs +147 -0
  51. package/lib/observe/observers.mjs +134 -0
  52. package/lib/observe/sources.mjs +154 -0
  53. package/lib/orchestrator/activity.mjs +249 -0
  54. package/lib/orchestrator/api.mjs +151 -0
  55. package/lib/orchestrator/contract.mjs +68 -0
  56. package/lib/orchestrator/escalation.mjs +84 -0
  57. package/lib/orchestrator/evaluator.mjs +92 -0
  58. package/lib/orchestrator/failures.mjs +88 -0
  59. package/lib/orchestrator/health.mjs +53 -0
  60. package/lib/orchestrator/orchestrator.mjs +483 -0
  61. package/lib/orchestrator/permissions.mjs +64 -0
  62. package/lib/orchestrator/planner.mjs +194 -0
  63. package/lib/orchestrator/policy.mjs +134 -0
  64. package/lib/orchestrator/router.mjs +45 -0
  65. package/lib/orchestrator/runner.mjs +278 -0
  66. package/lib/orchestrator/shell-policy.mjs +52 -0
  67. package/lib/orchestrator/store.mjs +581 -0
  68. package/lib/orchestrator/task-store.mjs +79 -0
  69. package/lib/orchestrator/turn-budget.mjs +63 -0
  70. package/lib/orchestrator/worktree.mjs +72 -0
  71. package/lib/pipeline.mjs +279 -0
  72. package/lib/registry.mjs +63 -0
  73. package/lib/resolve.mjs +35 -0
  74. package/lib/routing.mjs +137 -0
  75. package/lib/telemetry.mjs +222 -0
  76. package/mcp/README.md +11 -0
  77. package/mcp/servers.json +13 -0
  78. package/orchestration/decision-policy.json +66 -0
  79. package/package.json +56 -0
  80. package/routing/README.md +24 -0
  81. package/routing/routing.json +81 -0
  82. package/routing/routing.schema.json +66 -0
  83. package/rules/README.md +10 -0
  84. package/rules/common.md +52 -0
  85. package/rules/loop-prevention.md +15 -0
  86. package/rules/repo-local.md +6 -0
  87. package/scripts/check-environment.ps1 +22 -0
  88. package/scripts/context-pack.mjs +17 -0
  89. package/scripts/e2e-investigate-repro.mjs +66 -0
  90. package/scripts/model-maintenance-job.mjs +59 -0
  91. package/scripts/observe-models.mjs +97 -0
  92. package/scripts/orchestrate.mjs +137 -0
  93. package/scripts/reevaluate-models.mjs +95 -0
  94. package/scripts/report-model-maintenance.mjs +70 -0
  95. package/scripts/resolve-capabilities.mjs +39 -0
  96. package/scripts/run-pipeline.mjs +56 -0
  97. package/scripts/sync-agents-md.ps1 +10 -0
  98. package/scripts/validate.mjs +71 -0
  99. package/skills/README.md +14 -0
  100. package/skills/pi-workflow/SKILL.md +26 -0
  101. package/skills/pi-workflow/references/code-investigation-and-fix.md +16 -0
  102. package/skills/pi-workflow/references/research.md +14 -0
  103. package/skills/pi-workflow/references/review.md +11 -0
  104. package/skills/pi-workflow/references/visual-work.md +14 -0
  105. package/skills/project-management/SKILL.md +106 -0
  106. package/skills/project-management/references/operations.md +52 -0
  107. package/skills/visual-verification/SKILL.md +88 -0
  108. package/skills/visual-verification/scripts/analyze-speech.ps1 +346 -0
  109. package/skills/visual-verification/scripts/backends/whisperx_backend.py +234 -0
  110. package/skills/visual-verification/scripts/common.ps1 +387 -0
  111. package/skills/visual-verification/scripts/contact-sheet.ps1 +121 -0
  112. package/skills/visual-verification/scripts/desktop-discover.ps1 +45 -0
  113. package/skills/visual-verification/scripts/desktop-inspect.ps1 +67 -0
  114. package/skills/visual-verification/scripts/desktop-record.ps1 +97 -0
  115. package/skills/visual-verification/scripts/desktop-screenshot.ps1 +65 -0
  116. package/skills/visual-verification/scripts/evaluate-sync.ps1 +249 -0
  117. package/skills/visual-verification/scripts/extract-frames.ps1 +79 -0
  118. package/skills/visual-verification/scripts/inspect-media.ps1 +138 -0
  119. package/skills/visual-verification/scripts/record-av.ps1 +102 -0
  120. package/skills/visual-verification/scripts/record.ps1 +72 -0
  121. package/skills/visual-verification/scripts/screenshot.ps1 +44 -0
  122. package/skills/visual-verification/scripts/waveform.ps1 +450 -0
  123. package/skills/visual-verification/scripts/winapp-common.ps1 +465 -0
  124. package/tests/activity.test.mjs +252 -0
  125. package/tests/attempt-budget.test.mjs +102 -0
  126. package/tests/browser.test.mjs +121 -0
  127. package/tests/context-pack.test.mjs +98 -0
  128. package/tests/dirty-gate.test.mjs +211 -0
  129. package/tests/e2e-browser.mjs +66 -0
  130. package/tests/e2e-real-orchestrator-resume.mjs +101 -0
  131. package/tests/e2e-real-orchestrator.mjs +41 -0
  132. package/tests/e2e-real-pi.mjs +27 -0
  133. package/tests/e2e-real-tool-orchestrator.mjs +66 -0
  134. package/tests/fixtures/browser-page/index.html +20 -0
  135. package/tests/fixtures/maintenance/availability.txt +5 -0
  136. package/tests/fixtures/maintenance/catalog.json +74 -0
  137. package/tests/fixtures/maintenance/events.json +13 -0
  138. package/tests/fixtures/math-repo/README.md +3 -0
  139. package/tests/fixtures/math-repo/package.json +7 -0
  140. package/tests/fixtures/math-repo/src/math.js +11 -0
  141. package/tests/fixtures/math-repo/test/math.test.js +7 -0
  142. package/tests/fixtures/observe/announcements.json +8 -0
  143. package/tests/fixtures/orch-concurrent-child.mjs +44 -0
  144. package/tests/fixtures/orch-persist-child.mjs +61 -0
  145. package/tests/job.test.mjs +230 -0
  146. package/tests/kit.test.mjs +79 -0
  147. package/tests/language-policy.test.mjs +93 -0
  148. package/tests/loop-guard.test.mjs +60 -0
  149. package/tests/maintenance-exec.test.mjs +218 -0
  150. package/tests/maintenance-runner.test.mjs +222 -0
  151. package/tests/maintenance.test.mjs +195 -0
  152. package/tests/observe.test.mjs +283 -0
  153. package/tests/observer-registry.test.mjs +157 -0
  154. package/tests/orchestrator-cleanup.test.mjs +358 -0
  155. package/tests/orchestrator-command.test.mjs +14 -0
  156. package/tests/orchestrator-persist.test.mjs +375 -0
  157. package/tests/orchestrator-tools.test.mjs +215 -0
  158. package/tests/orchestrator.test.mjs +396 -0
  159. package/tests/package.test.mjs +37 -0
  160. package/tests/pipeline.test.mjs +239 -0
  161. package/tests/planner-classification.test.mjs +81 -0
  162. package/tests/planner-split.test.mjs +67 -0
  163. package/tests/qoder-observer.test.mjs +266 -0
  164. package/tests/reassign-progression.test.mjs +104 -0
  165. package/tests/retry-escalation.test.mjs +120 -0
  166. package/tests/routing.test.mjs +110 -0
  167. package/tests/sqlite-concurrency.test.mjs +178 -0
  168. package/tests/task-global-e2e.test.mjs +63 -0
  169. package/tests/task-global-failed.test.mjs +134 -0
  170. package/tests/telemetry.test.mjs +173 -0
  171. package/tests/test-sync-pi.ps1 +56 -0
  172. package/tests/turn-budget.test.mjs +106 -0
@@ -0,0 +1,387 @@
1
+ Set-StrictMode -Version Latest
2
+
3
+ function Get-FfmpegPath {
4
+ [OutputType([string])]
5
+ [CmdletBinding()]
6
+ param()
7
+
8
+ try {
9
+ $command = Get-Command ffmpeg -ErrorAction Stop
10
+ }
11
+ catch {
12
+ throw 'ffmpeg was not found on PATH.'
13
+ }
14
+
15
+ if ($command.CommandType -ne [System.Management.Automation.CommandTypes]::Application) {
16
+ throw "ffmpeg resolved to '$($command.CommandType)', not an executable application."
17
+ }
18
+
19
+ return [IO.Path]::GetFullPath($command.Source)
20
+ }
21
+
22
+ function New-VisualRun {
23
+ [OutputType([pscustomobject])]
24
+ [CmdletBinding()]
25
+ param(
26
+ [string]$RunDirectory,
27
+ [Parameter(Mandatory)]
28
+ [ValidateNotNullOrEmpty()]
29
+ [string]$Kind
30
+ )
31
+
32
+ if (-not [string]::IsNullOrWhiteSpace($RunDirectory)) {
33
+ if (-not (Test-Path -LiteralPath $RunDirectory -PathType Container)) {
34
+ throw "The explicit run directory does not exist: $RunDirectory"
35
+ }
36
+
37
+ $resolvedRunDirectory = [IO.Path]::GetFullPath($RunDirectory)
38
+ return [pscustomobject]@{
39
+ RunId = Split-Path -Leaf $resolvedRunDirectory
40
+ RunDirectory = $resolvedRunDirectory
41
+ Kind = $Kind
42
+ }
43
+ }
44
+
45
+ $evidenceRoot = Join-Path ([IO.Path]::GetTempPath()) 'agent-verification-lab'
46
+ New-Item -ItemType Directory -Path $evidenceRoot -Force | Out-Null
47
+
48
+ do {
49
+ $candidate = Join-Path $evidenceRoot ([Guid]::NewGuid().ToString('N'))
50
+ } while (Test-Path -LiteralPath $candidate)
51
+
52
+ New-Item -ItemType Directory -Path $candidate -ErrorAction Stop | Out-Null
53
+ return [pscustomobject]@{
54
+ RunId = Split-Path -Leaf $candidate
55
+ RunDirectory = [IO.Path]::GetFullPath($candidate)
56
+ Kind = $Kind
57
+ }
58
+ }
59
+
60
+ function New-VisualEvidenceRun {
61
+ [OutputType([string])]
62
+ [CmdletBinding()]
63
+ param(
64
+ [string]$RunDirectory
65
+ )
66
+
67
+ return (New-VisualRun -RunDirectory $RunDirectory -Kind 'visual').RunDirectory
68
+ }
69
+
70
+ function Assert-OutputAvailable {
71
+ [CmdletBinding()]
72
+ param(
73
+ [Parameter(Mandatory)]
74
+ [string]$Path,
75
+ [switch]$Overwrite
76
+ )
77
+
78
+ if (-not (Test-Path -LiteralPath $Path)) {
79
+ return
80
+ }
81
+
82
+ $item = Get-Item -LiteralPath $Path -Force
83
+ if ($item.PSIsContainer) {
84
+ throw "Output path is a directory: $Path"
85
+ }
86
+
87
+ if (-not $Overwrite) {
88
+ throw "Output already exists; specify -Overwrite to replace it: $Path"
89
+ }
90
+ }
91
+
92
+ function Get-VisualVerificationNativeMethods {
93
+ [OutputType([type])]
94
+ [CmdletBinding()]
95
+ param()
96
+
97
+ $nativeMethods = 'VisualVerification.NativeMethods' -as [type]
98
+ if ($null -eq $nativeMethods) {
99
+ Add-Type -TypeDefinition @'
100
+ using System;
101
+ using System.Runtime.InteropServices;
102
+ using System.Text;
103
+
104
+ namespace VisualVerification
105
+ {
106
+ public static class NativeMethods
107
+ {
108
+ [StructLayout(LayoutKind.Sequential)]
109
+ public struct RECT
110
+ {
111
+ public int Left;
112
+ public int Top;
113
+ public int Right;
114
+ public int Bottom;
115
+ }
116
+
117
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
118
+
119
+ [DllImport("user32.dll")]
120
+ public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
121
+
122
+ [DllImport("user32.dll")]
123
+ public static extern bool IsWindow(IntPtr hWnd);
124
+
125
+ [DllImport("user32.dll")]
126
+ public static extern bool IsWindowVisible(IntPtr hWnd);
127
+
128
+ [DllImport("user32.dll")]
129
+ public static extern int GetWindowTextLength(IntPtr hWnd);
130
+
131
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
132
+ public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
133
+
134
+ [DllImport("user32.dll")]
135
+ public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
136
+ }
137
+ }
138
+ '@
139
+ $nativeMethods = 'VisualVerification.NativeMethods' -as [type]
140
+ }
141
+
142
+ return $nativeMethods
143
+ }
144
+
145
+ function Resolve-CaptureTarget {
146
+ [OutputType([pscustomobject])]
147
+ [CmdletBinding()]
148
+ param(
149
+ [Parameter(Mandatory)]
150
+ [ValidateSet('desktop', 'region', 'window')]
151
+ [string]$Mode,
152
+ [Nullable[int]]$X,
153
+ [Nullable[int]]$Y,
154
+ [Nullable[int]]$Width,
155
+ [Nullable[int]]$Height,
156
+ [string]$WindowTitle,
157
+ [IntPtr]$Hwnd = [IntPtr]::Zero
158
+ )
159
+
160
+ $hasRegionArgument = $null -ne $X -or $null -ne $Y -or $null -ne $Width -or $null -ne $Height
161
+ $hasTitle = -not [string]::IsNullOrWhiteSpace($WindowTitle)
162
+ $hasHwnd = $Hwnd -ne [IntPtr]::Zero
163
+
164
+ switch ($Mode) {
165
+ 'desktop' {
166
+ if ($hasRegionArgument -or $hasTitle -or $hasHwnd) {
167
+ throw 'Desktop capture cannot include region or window target arguments.'
168
+ }
169
+
170
+ return [pscustomobject]@{
171
+ InputName = 'desktop'
172
+ InputOptions = [string[]]@('-i', 'desktop')
173
+ Bounds = $null
174
+ TargetDescription = 'desktop'
175
+ }
176
+ }
177
+
178
+ 'region' {
179
+ if ($hasTitle -or $hasHwnd) {
180
+ throw 'Region capture cannot include window target arguments.'
181
+ }
182
+
183
+ if ($null -eq $X -or $null -eq $Y -or $null -eq $Width -or $null -eq $Height) {
184
+ throw 'Region capture requires X, Y, Width, and Height.'
185
+ }
186
+
187
+ if ($Width -le 0 -or $Height -le 0) {
188
+ throw 'Region capture Width and Height must be positive.'
189
+ }
190
+
191
+ $bounds = [pscustomobject]@{
192
+ X = [int]$X
193
+ Y = [int]$Y
194
+ Width = [int]$Width
195
+ Height = [int]$Height
196
+ }
197
+
198
+ return [pscustomobject]@{
199
+ InputName = 'desktop'
200
+ InputOptions = [string[]]@(
201
+ '-video_size', "$($bounds.Width)x$($bounds.Height)",
202
+ '-offset_x', [string]$bounds.X,
203
+ '-offset_y', [string]$bounds.Y,
204
+ '-i', 'desktop'
205
+ )
206
+ Bounds = $bounds
207
+ TargetDescription = "region x=$($bounds.X) y=$($bounds.Y) width=$($bounds.Width) height=$($bounds.Height)"
208
+ }
209
+ }
210
+
211
+ 'window' {
212
+ if ($hasRegionArgument) {
213
+ throw 'Window capture cannot include region target arguments.'
214
+ }
215
+
216
+ if ($hasTitle -eq $hasHwnd) {
217
+ throw 'Window capture requires exactly one of WindowTitle or Hwnd.'
218
+ }
219
+
220
+ $nativeMethods = Get-VisualVerificationNativeMethods
221
+ if ($hasTitle) {
222
+ $matches = [System.Collections.Generic.List[IntPtr]]::new()
223
+ $callback = [VisualVerification.NativeMethods+EnumWindowsProc]{
224
+ param([IntPtr]$candidate, [IntPtr]$unused)
225
+
226
+ if (-not [VisualVerification.NativeMethods]::IsWindowVisible($candidate)) {
227
+ return $true
228
+ }
229
+
230
+ $titleLength = [VisualVerification.NativeMethods]::GetWindowTextLength($candidate)
231
+ if ($titleLength -le 0) {
232
+ return $true
233
+ }
234
+
235
+ $titleBuilder = [System.Text.StringBuilder]::new($titleLength + 1)
236
+ [void][VisualVerification.NativeMethods]::GetWindowText($candidate, $titleBuilder, $titleBuilder.Capacity)
237
+ if ($titleBuilder.ToString() -ceq $WindowTitle) {
238
+ [void]$matches.Add($candidate)
239
+ }
240
+
241
+ return $true
242
+ }
243
+ [void][VisualVerification.NativeMethods]::EnumWindows($callback, [IntPtr]::Zero)
244
+
245
+ if ($matches.Count -eq 0) {
246
+ throw "No visible window exactly matches title '$WindowTitle'."
247
+ }
248
+
249
+ if ($matches.Count -ne 1) {
250
+ throw "Window title '$WindowTitle' is ambiguous; $($matches.Count) visible windows match exactly."
251
+ }
252
+
253
+ $Hwnd = $matches[0]
254
+ }
255
+
256
+ if (-not [VisualVerification.NativeMethods]::IsWindow($Hwnd)) {
257
+ throw "Hwnd '$Hwnd' is not a valid window."
258
+ }
259
+
260
+ if (-not [VisualVerification.NativeMethods]::IsWindowVisible($Hwnd)) {
261
+ throw "Hwnd '$Hwnd' is not visible."
262
+ }
263
+
264
+ $rect = [VisualVerification.NativeMethods+RECT]::new()
265
+ if (-not [VisualVerification.NativeMethods]::GetWindowRect($Hwnd, [ref]$rect)) {
266
+ throw "GetWindowRect failed for Hwnd '$Hwnd'."
267
+ }
268
+
269
+ $windowWidth = $rect.Right - $rect.Left
270
+ $windowHeight = $rect.Bottom - $rect.Top
271
+ if ($windowWidth -le 0 -or $windowHeight -le 0) {
272
+ throw "Hwnd '$Hwnd' has invalid bounds."
273
+ }
274
+
275
+ $bounds = [pscustomobject]@{
276
+ X = $rect.Left
277
+ Y = $rect.Top
278
+ Width = $windowWidth
279
+ Height = $windowHeight
280
+ }
281
+ $handleText = "0x$($Hwnd.ToInt64().ToString('X'))"
282
+
283
+ return [pscustomobject]@{
284
+ InputName = 'desktop'
285
+ InputOptions = [string[]]@(
286
+ '-video_size', "$($bounds.Width)x$($bounds.Height)",
287
+ '-offset_x', [string]$bounds.X,
288
+ '-offset_y', [string]$bounds.Y,
289
+ '-i', 'desktop'
290
+ )
291
+ Bounds = $bounds
292
+ TargetDescription = "window $handleText x=$($bounds.X) y=$($bounds.Y) width=$($bounds.Width) height=$($bounds.Height)"
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ function Invoke-Ffmpeg {
299
+ [CmdletBinding()]
300
+ param(
301
+ [Parameter(Mandatory)]
302
+ [string]$FfmpegPath,
303
+ [Parameter(Mandatory)]
304
+ [string[]]$Arguments,
305
+ [Parameter(Mandatory)]
306
+ [string]$Operation
307
+ )
308
+
309
+ $ffmpegArguments = [string[]]@('-hide_banner', '-loglevel', 'error', '-nostdin') + $Arguments
310
+ $diagnostics = & $FfmpegPath @ffmpegArguments 2>&1
311
+ $exitCode = $LASTEXITCODE
312
+ if ($exitCode -ne 0) {
313
+ $diagnosticText = ($diagnostics | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine
314
+ throw "FFmpeg $Operation failed with exit code $exitCode.$([Environment]::NewLine)$diagnosticText"
315
+ }
316
+ }
317
+
318
+ function Write-ResultJson {
319
+ [OutputType([string])]
320
+ [CmdletBinding()]
321
+ param(
322
+ [Parameter(Mandatory)]
323
+ [pscustomobject]$Result
324
+ )
325
+
326
+ if ([string]::IsNullOrWhiteSpace($Result.run_directory)) {
327
+ throw 'Result.run_directory is required.'
328
+ }
329
+
330
+ $runDirectory = [IO.Path]::GetFullPath($Result.run_directory)
331
+ if (-not (Test-Path -LiteralPath $runDirectory -PathType Container)) {
332
+ throw "Result run directory does not exist: $runDirectory"
333
+ }
334
+
335
+ $Result.run_directory = $runDirectory
336
+ $resultPath = Join-Path $runDirectory 'result.json'
337
+ if (Test-Path -LiteralPath $resultPath -PathType Leaf) {
338
+ try {
339
+ $previous = Get-Content -LiteralPath $resultPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
340
+ }
341
+ catch {
342
+ throw "Existing result.json cannot be read safely: $resultPath"
343
+ }
344
+
345
+ if ($previous.run_directory -ne $runDirectory) {
346
+ throw "Existing result.json belongs to a different run directory: $resultPath"
347
+ }
348
+
349
+ $artifactKeys = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
350
+ $artifacts = [System.Collections.Generic.List[object]]::new()
351
+ foreach ($artifact in @($previous.artifacts) + @($Result.artifacts)) {
352
+ if ($null -eq $artifact) {
353
+ continue
354
+ }
355
+
356
+ $key = "$($artifact.role)`0$($artifact.path)"
357
+ if ($artifactKeys.Add($key)) {
358
+ [void]$artifacts.Add($artifact)
359
+ }
360
+ }
361
+ $Result.artifacts = @($artifacts)
362
+ }
363
+
364
+ $json = $Result | ConvertTo-Json -Depth 8
365
+ Set-Content -LiteralPath $resultPath -Value $json -Encoding utf8NoBOM -NoNewline -ErrorAction Stop
366
+ return [IO.Path]::GetFullPath($resultPath)
367
+ }
368
+
369
+ function Write-VisualResult {
370
+ [CmdletBinding()]
371
+ param(
372
+ [Parameter(Mandatory)]
373
+ [string]$RunDirectory,
374
+ [Parameter(Mandatory)]
375
+ [string]$OutputPath,
376
+ [Parameter(Mandatory)]
377
+ [string]$Kind,
378
+ [string]$ResultJsonPath
379
+ )
380
+
381
+ Write-Output "RUN_DIRECTORY=$([IO.Path]::GetFullPath($RunDirectory))"
382
+ Write-Output "OUTPUT_PATH=$([IO.Path]::GetFullPath($OutputPath))"
383
+ Write-Output "KIND=$Kind"
384
+ if (-not [string]::IsNullOrWhiteSpace($ResultJsonPath)) {
385
+ Write-Output "RESULT_JSON=$([IO.Path]::GetFullPath($ResultJsonPath))"
386
+ }
387
+ }
@@ -0,0 +1,121 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [Parameter(Mandatory)]
4
+ [ValidateNotNullOrEmpty()]
5
+ [string]$InputDirectory,
6
+ [int]$Columns = 4,
7
+ [double]$Interval = 0.5,
8
+ [int]$CellWidth = 640,
9
+ [string]$OutputPath,
10
+ [string]$RunDirectory,
11
+ [switch]$Overwrite
12
+ )
13
+
14
+ $common = Join-Path $PSScriptRoot 'common.ps1'
15
+ . $common
16
+
17
+ if (-not (Test-Path -LiteralPath $InputDirectory -PathType Container)) {
18
+ throw "Input directory does not exist or is not a directory: $InputDirectory"
19
+ }
20
+
21
+ if ($Columns -lt 1 -or $Columns -gt 10) {
22
+ throw 'Columns must be between 1 and 10.'
23
+ }
24
+
25
+ if ([double]::IsNaN($Interval) -or [double]::IsInfinity($Interval) -or $Interval -le 0) {
26
+ throw 'Interval must be positive.'
27
+ }
28
+
29
+ if ($CellWidth -lt 64 -or $CellWidth -gt 1920) {
30
+ throw 'CellWidth must be between 64 and 1920.'
31
+ }
32
+
33
+ $resolvedInputDirectory = [IO.Path]::GetFullPath($InputDirectory)
34
+ $frames = @(
35
+ Get-ChildItem -LiteralPath $resolvedInputDirectory -Filter 'frame-*.png' -File |
36
+ ForEach-Object {
37
+ if ($_.Name -match '^frame-(\d+)\.png$') {
38
+ [pscustomobject]@{
39
+ Path = $_.FullName
40
+ Index = [long]$Matches[1]
41
+ Name = $_.Name
42
+ }
43
+ }
44
+ } |
45
+ Sort-Object -Property Index, Name
46
+ )
47
+
48
+ if ($frames.Count -eq 0) {
49
+ throw "No frame-*.png files were found in: $resolvedInputDirectory"
50
+ }
51
+
52
+ if ($frames.Count -gt 100) {
53
+ throw 'Contact sheets support at most 100 frames.'
54
+ }
55
+
56
+ $run = New-VisualRun -RunDirectory $RunDirectory -Kind 'visual'
57
+ $resolvedRunDirectory = $run.RunDirectory
58
+ if ([string]::IsNullOrWhiteSpace($OutputPath)) {
59
+ $OutputPath = Join-Path $resolvedRunDirectory 'contact-sheet.png'
60
+ }
61
+
62
+ $resolvedOutputPath = [IO.Path]::GetFullPath($OutputPath)
63
+ Assert-OutputAvailable -Path $resolvedOutputPath -Overwrite:$Overwrite
64
+
65
+ $invariantCulture = [Globalization.CultureInfo]::InvariantCulture
66
+ $intervalText = $Interval.ToString('G17', $invariantCulture)
67
+ $frameRateText = (1 / $Interval).ToString('G17', $invariantCulture)
68
+ $columnsText = $Columns.ToString($invariantCulture)
69
+ $rows = [int][Math]::Ceiling($frames.Count / [double]$Columns)
70
+ $rowsText = $rows.ToString($invariantCulture)
71
+ $cellWidthText = $CellWidth.ToString($invariantCulture)
72
+ $cellHeight = [int][Math]::Ceiling(($CellWidth * 9.0) / 32.0) * 2
73
+ $cellHeightText = $cellHeight.ToString($invariantCulture)
74
+ $fontPath = Join-Path $env:WINDIR 'Fonts\arial.ttf'
75
+ if (-not (Test-Path -LiteralPath $fontPath -PathType Leaf)) {
76
+ throw "Required label font does not exist: $fontPath"
77
+ }
78
+
79
+ $fontForFilter = [IO.Path]::GetFullPath($fontPath).Replace('\', '/').Replace(':', '\:')
80
+
81
+ $ffmpegArguments = [System.Collections.Generic.List[string]]::new()
82
+ foreach ($frame in $frames) {
83
+ $ffmpegArguments.Add('-framerate')
84
+ $ffmpegArguments.Add($frameRateText)
85
+ $ffmpegArguments.Add('-i')
86
+ $ffmpegArguments.Add($frame.Path)
87
+ }
88
+
89
+ $branchFilters = [System.Collections.Generic.List[string]]::new()
90
+ $normalizedLabels = [System.Collections.Generic.List[string]]::new()
91
+ foreach ($index in 0..($frames.Count - 1)) {
92
+ $normalizedLabel = "frame$index"
93
+ $branchFilters.Add("[$index`:v]scale=${cellWidthText}:${cellHeightText}:force_original_aspect_ratio=decrease,pad=${cellWidthText}:${cellHeightText}:(ow-iw)/2:(oh-ih)/2:color=black,setsar=1,setpts=PTS-STARTPTS[$normalizedLabel]")
94
+ $normalizedLabels.Add("[$normalizedLabel]")
95
+ }
96
+
97
+ $filter = ($branchFilters -join ';') + ';' + ($normalizedLabels -join '') + "concat=n=$($frames.Count):v=1:a=0,settb=AVTB,setpts=N*$intervalText/TB,fps=$frameRateText,drawtext=fontfile='$fontForFilter':text='t=%{pts\:hms}':x=12:y=12:fontsize=28:fontcolor=white:box=1:boxcolor=black@0.75:boxborderw=8,tile=${columnsText}x${rowsText}:padding=8:margin=8:color=black"
98
+
99
+ $ffmpegArguments.Add('-filter_complex')
100
+ $ffmpegArguments.Add($filter)
101
+ $ffmpegArguments.Add('-frames:v')
102
+ $ffmpegArguments.Add('1')
103
+ $ffmpegArguments.Add('-c:v')
104
+ $ffmpegArguments.Add('png')
105
+ if ($Overwrite) {
106
+ $ffmpegArguments.Add('-y')
107
+ }
108
+ $ffmpegArguments.Add($resolvedOutputPath)
109
+
110
+ Invoke-Ffmpeg -FfmpegPath (Get-FfmpegPath) -Arguments $ffmpegArguments.ToArray() -Operation "contact sheet from $resolvedInputDirectory"
111
+ $resultJson = Write-ResultJson -Result ([pscustomobject]@{
112
+ schema_version = 'agent-verification-lab.result.v1'
113
+ status = 'ok'
114
+ kind = $run.Kind
115
+ run_id = $run.RunId
116
+ run_directory = $resolvedRunDirectory
117
+ artifacts = @([pscustomobject]@{ role = 'contact-sheet'; path = $resolvedOutputPath; mime = 'image/png' })
118
+ backend = [pscustomobject]@{ name = 'ffmpeg'; version = $null }
119
+ warnings = @()
120
+ })
121
+ Write-VisualResult -RunDirectory $resolvedRunDirectory -OutputPath $resolvedOutputPath -Kind 'contact-sheet' -ResultJsonPath $resultJson
@@ -0,0 +1,45 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [string]$App,
4
+ [string]$ProcessName,
5
+ [string]$WindowTitle,
6
+ [string]$ClassName,
7
+ [Nullable[int]]$ProcessId,
8
+ [Nullable[long]]$Hwnd,
9
+ [Nullable[int]]$Width,
10
+ [Nullable[int]]$Height,
11
+ [string]$RunDirectory
12
+ )
13
+
14
+ Set-StrictMode -Version Latest
15
+ $ErrorActionPreference = 'Stop'
16
+ . (Join-Path $PSScriptRoot 'common.ps1')
17
+ . (Join-Path $PSScriptRoot 'winapp-common.ps1')
18
+
19
+ try {
20
+ $version = Get-WinAppVersionInfo
21
+ Assert-WinAppCaptureReady -VersionInfo $version
22
+ $run = New-VisualRun -RunDirectory $RunDirectory -Kind 'visual'
23
+ $target = Get-WinAppWindowTarget -Executable $version.executable -App $App -ProcessName $ProcessName -WindowTitle $WindowTitle -ClassName $ClassName -ProcessId $ProcessId -Hwnd $Hwnd -Width $Width -Height $Height
24
+ $outputPath = Join-Path $run.RunDirectory 'desktop-window.json'
25
+ $metadata = New-WinAppOperationMetadata -Operation 'discover' -Version $version.actual -VersionClassification $version.classification -Target $target -Width $target.width -Height $target.height
26
+ Write-WinAppTargetDocument -Path $outputPath -Target $target -VersionInfo $version | Out-Null
27
+ $warnings = @()
28
+ if ($version.classification -eq 'UNTESTED_NEWER') { $warnings += 'WINAPP_VERSION_UNTESTED' }
29
+ $resultJson = Write-ResultJson -Result ([pscustomobject]@{
30
+ schema_version = 'agent-verification-lab.result.v1'
31
+ status = 'ok'
32
+ kind = $run.Kind
33
+ run_id = $run.RunId
34
+ run_directory = $run.RunDirectory
35
+ artifacts = @([pscustomobject]@{ role = 'desktop-window'; path = $outputPath; mime = 'application/json' })
36
+ backend = $metadata.backend
37
+ target = $target
38
+ warnings = $warnings
39
+ })
40
+ Write-VisualResult -RunDirectory $run.RunDirectory -OutputPath $outputPath -Kind 'desktop-window' -ResultJsonPath $resultJson
41
+ }
42
+ catch {
43
+ Write-WinAppFailure -ErrorRecord $_ -Operation discover
44
+ exit 1
45
+ }
@@ -0,0 +1,67 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [string]$App,
4
+ [string]$ProcessName,
5
+ [string]$WindowTitle,
6
+ [string]$ClassName,
7
+ [Nullable[int]]$ProcessId,
8
+ [Nullable[long]]$Hwnd,
9
+ [Nullable[int]]$Width,
10
+ [Nullable[int]]$Height,
11
+ [ValidateRange(1, 20)][int]$Depth = 6,
12
+ [switch]$Interactive,
13
+ [string]$RunDirectory
14
+ )
15
+
16
+ Set-StrictMode -Version Latest
17
+ $ErrorActionPreference = 'Stop'
18
+ . (Join-Path $PSScriptRoot 'common.ps1')
19
+ . (Join-Path $PSScriptRoot 'winapp-common.ps1')
20
+
21
+ try {
22
+ $version = Get-WinAppVersionInfo
23
+ Assert-WinAppCaptureReady -VersionInfo $version
24
+ $run = New-VisualRun -RunDirectory $RunDirectory -Kind 'visual'
25
+ $target = Get-WinAppWindowTarget -Executable $version.executable -App $App -ProcessName $ProcessName -WindowTitle $WindowTitle -ClassName $ClassName -ProcessId $ProcessId -Hwnd $Hwnd -Width $Width -Height $Height
26
+ $outputPath = Join-Path $run.RunDirectory 'desktop-ui.json'
27
+ Assert-OutputAvailable -Path $outputPath
28
+ $arguments = New-WinAppInspectArguments -Hwnd $target.hwnd -Depth $Depth -Interactive:$Interactive
29
+ $response = Invoke-WinAppJson -Executable $version.executable -Arguments $arguments -Operation inspect
30
+ $elementCount = 0
31
+ foreach ($window in @(Get-WinAppPropertyValue $response.data 'windows' @())) { $elementCount += [int](Get-WinAppPropertyValue $window 'elementCount' 0) }
32
+ $capability = if ($elementCount -eq 0) { 'UIA_UNAVAILABLE' } elseif ($elementCount -le 1) { 'UIA_LIMITED' } else { 'UIA_RICH' }
33
+ $document = [pscustomobject]@{
34
+ schema_version = 'agent-verification-lab.desktop-ui.v1'
35
+ backend = [pscustomobject]@{ name = 'winapp-cli'; version = $version.actual; tested_version = $version.tested; version_classification = $version.classification }
36
+ target = $target
37
+ capability = $capability
38
+ element_count = $elementCount
39
+ upstream_tree = $response.data
40
+ interaction = [pscustomobject]@{ automatic = $false; background_guaranteed = $false; may_foreground_target = $true }
41
+ }
42
+ $targetPath = Write-WinAppTargetDocument -Path (Join-Path $run.RunDirectory 'desktop-window.json') -Target $target -VersionInfo $version
43
+ Write-WinAppJsonFile -Value $document -Path $outputPath | Out-Null
44
+ $warnings = @()
45
+ if ($version.classification -eq 'UNTESTED_NEWER') { $warnings += 'WINAPP_VERSION_UNTESTED' }
46
+ if ($capability -ne 'UIA_RICH') { $warnings += $capability }
47
+ $resultJson = Write-ResultJson -Result ([pscustomobject]@{
48
+ schema_version = 'agent-verification-lab.result.v1'
49
+ status = 'ok'
50
+ kind = $run.Kind
51
+ run_id = $run.RunId
52
+ run_directory = $run.RunDirectory
53
+ artifacts = @(
54
+ [pscustomobject]@{ role = 'desktop-window'; path = $targetPath; mime = 'application/json' },
55
+ [pscustomobject]@{ role = 'desktop-ui'; path = $outputPath; mime = 'application/json' }
56
+ )
57
+ backend = $document.backend
58
+ target = $target
59
+ uia = [pscustomobject]@{ capability = $capability; element_count = $elementCount; optional = $true }
60
+ warnings = $warnings
61
+ })
62
+ Write-VisualResult -RunDirectory $run.RunDirectory -OutputPath $outputPath -Kind 'desktop-ui' -ResultJsonPath $resultJson
63
+ }
64
+ catch {
65
+ Write-WinAppFailure -ErrorRecord $_ -Operation inspect
66
+ exit 1
67
+ }