opencode-agent-skill 9.0.0 → 11.0.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 (56) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +742 -675
  3. package/bin/ocskill.mjs +354 -5
  4. package/docs/V11-PERCEPTION-ADAPTIVE-EXECUTION.md +75 -0
  5. package/docs/V11-PERCEPTION-ADAPTIVE.md +220 -0
  6. package/evals/router-triggers.json +82 -0
  7. package/evals/routing.json +76 -0
  8. package/evals/v11/tasks.json +122 -0
  9. package/global-config/AGENTS.md +78 -163
  10. package/global-config/agents/merge-arbiter.md +12 -0
  11. package/global-config/agents/visual-verifier.md +12 -0
  12. package/global-config/plugins/ues-router/index.js +683 -59
  13. package/global-config/plugins/ues-router/router.js +62 -3
  14. package/global-config/plugins/ues-router/runtime-guard.js +265 -0
  15. package/global-config/skills/browser-qa/SKILL.md +14 -0
  16. package/global-config/skills/browser-qa/references/workflow.md +11 -0
  17. package/global-config/skills/browser-security/SKILL.md +12 -0
  18. package/global-config/skills/component-visual-testing/SKILL.md +10 -0
  19. package/global-config/skills/design-source/SKILL.md +10 -0
  20. package/global-config/skills/design-source/references/workflow.md +12 -0
  21. package/global-config/skills/dynamic-workflow/SKILL.md +18 -0
  22. package/global-config/skills/dynamic-workflow/references/workflow.md +19 -0
  23. package/global-config/skills/responsive-verification/SKILL.md +10 -0
  24. package/global-config/skills/skill-authoring/SKILL.md +12 -0
  25. package/global-config/skills/skill-evaluation/SKILL.md +17 -0
  26. package/global-config/skills/visual-fidelity/SKILL.md +14 -0
  27. package/global-config/skills/visual-fidelity/references/workflow.md +14 -0
  28. package/lib/benchmark-confidence.mjs +49 -11
  29. package/lib/browser-adapter.mjs +82 -0
  30. package/lib/browser-runtime.mjs +193 -0
  31. package/lib/capability-registry.mjs +109 -0
  32. package/lib/context-engine-v11.mjs +146 -0
  33. package/lib/context-manifest.mjs +16 -3
  34. package/lib/control-center.mjs +12 -2
  35. package/lib/dynamic-workflow.mjs +179 -0
  36. package/lib/eval-ablation.mjs +146 -0
  37. package/lib/eval-report.mjs +83 -0
  38. package/lib/eval-telemetry.mjs +64 -0
  39. package/lib/evidence-budget.mjs +84 -0
  40. package/lib/evidence-store.mjs +178 -0
  41. package/lib/hermes-bridge.mjs +45 -1
  42. package/lib/model-config.mjs +9 -1
  43. package/lib/model-policy.mjs +58 -1
  44. package/lib/orchestrator-policy.mjs +100 -7
  45. package/lib/png-diff.mjs +229 -0
  46. package/lib/prompt-cache.mjs +60 -0
  47. package/lib/skill-quality.mjs +72 -0
  48. package/lib/task-engine.mjs +223 -12
  49. package/lib/ui-inspector.mjs +152 -0
  50. package/lib/v11-metrics.mjs +64 -0
  51. package/lib/visual-spec.mjs +159 -0
  52. package/package.json +11 -5
  53. package/scripts/eval-ablation.mjs +47 -0
  54. package/scripts/eval-matrix.mjs +13 -2
  55. package/scripts/validate-v11-suite.mjs +58 -0
  56. package/scripts/validate.mjs +16 -4
@@ -0,0 +1,229 @@
1
+ import { inflateSync, deflateSync } from "node:zlib"
2
+ import { readFile, writeFile } from "node:fs/promises"
3
+
4
+ const SIGNATURE = Buffer.from([137,80,78,71,13,10,26,10])
5
+
6
+ function paeth(a, b, c) {
7
+ const p = a + b - c
8
+ const pa = Math.abs(p - a)
9
+ const pb = Math.abs(p - b)
10
+ const pc = Math.abs(p - c)
11
+ return pa <= pb && pa <= pc ? a : pb <= pc ? b : c
12
+ }
13
+
14
+ function bytesPerPixel(colorType) {
15
+ if (colorType === 0 || colorType === 3) return 1
16
+ if (colorType === 2) return 3
17
+ if (colorType === 4) return 2
18
+ if (colorType === 6) return 4
19
+ throw new Error("Unsupported PNG color type: " + colorType)
20
+ }
21
+
22
+ function parseChunks(buffer) {
23
+ if (buffer.length < 8 || !buffer.subarray(0, 8).equals(SIGNATURE)) throw new Error("Invalid PNG signature")
24
+ const chunks = []
25
+ let offset = 8
26
+ while (offset + 12 <= buffer.length) {
27
+ const length = buffer.readUInt32BE(offset)
28
+ const type = buffer.toString("ascii", offset + 4, offset + 8)
29
+ const dataStart = offset + 8
30
+ const dataEnd = dataStart + length
31
+ if (dataEnd + 4 > buffer.length) throw new Error("Truncated PNG chunk")
32
+ chunks.push({ type, data: buffer.subarray(dataStart, dataEnd) })
33
+ offset = dataEnd + 4
34
+ if (type === "IEND") break
35
+ }
36
+ return chunks
37
+ }
38
+
39
+ export function decodePng(buffer) {
40
+ const chunks = parseChunks(buffer)
41
+ const ihdr = chunks.find((chunk) => chunk.type === "IHDR")?.data
42
+ if (!ihdr || ihdr.length < 13) throw new Error("PNG missing IHDR")
43
+ const width = ihdr.readUInt32BE(0)
44
+ const height = ihdr.readUInt32BE(4)
45
+ const bitDepth = ihdr[8]
46
+ const colorType = ihdr[9]
47
+ const interlace = ihdr[12]
48
+ if (bitDepth !== 8) throw new Error("Only 8-bit PNG is supported")
49
+ if (interlace !== 0) throw new Error("Interlaced PNG is not supported")
50
+
51
+ const paletteChunk = chunks.find((chunk) => chunk.type === "PLTE")?.data || null
52
+ const transparency = chunks.find((chunk) => chunk.type === "tRNS")?.data || null
53
+ const idat = Buffer.concat(chunks.filter((chunk) => chunk.type === "IDAT").map((chunk) => chunk.data))
54
+ const bpp = bytesPerPixel(colorType)
55
+ const pixelsCount = width * height
56
+ if (!Number.isSafeInteger(pixelsCount) || pixelsCount <= 0 || pixelsCount > 50_000_000) {
57
+ throw new Error("PNG dimensions exceed the V11 visual safety budget")
58
+ }
59
+ const stride = width * bpp
60
+ const expectedRawBytes = height * (1 + stride)
61
+ if (!Number.isSafeInteger(expectedRawBytes) || expectedRawBytes > 256 * 1024 * 1024) {
62
+ throw new Error("PNG decompressed payload exceeds the V11 visual safety budget")
63
+ }
64
+ const raw = inflateSync(idat, { maxOutputLength: expectedRawBytes })
65
+ if (raw.length !== expectedRawBytes) throw new Error("PNG decompressed payload length does not match IHDR")
66
+ const pixels = Buffer.alloc(stride * height)
67
+ let input = 0
68
+
69
+ for (let y = 0; y < height; y += 1) {
70
+ const filter = raw[input++]
71
+ const row = y * stride
72
+ for (let x = 0; x < stride; x += 1) {
73
+ const byte = raw[input++]
74
+ const left = x >= bpp ? pixels[row + x - bpp] : 0
75
+ const up = y > 0 ? pixels[row - stride + x] : 0
76
+ const upLeft = y > 0 && x >= bpp ? pixels[row - stride + x - bpp] : 0
77
+ let value
78
+ if (filter === 0) value = byte
79
+ else if (filter === 1) value = (byte + left) & 255
80
+ else if (filter === 2) value = (byte + up) & 255
81
+ else if (filter === 3) value = (byte + Math.floor((left + up) / 2)) & 255
82
+ else if (filter === 4) value = (byte + paeth(left, up, upLeft)) & 255
83
+ else throw new Error("Unsupported PNG filter: " + filter)
84
+ pixels[row + x] = value
85
+ }
86
+ }
87
+
88
+ const rgba = Buffer.alloc(width * height * 4)
89
+ for (let i = 0, p = 0; i < pixels.length; i += bpp, p += 4) {
90
+ if (colorType === 6) {
91
+ rgba[p] = pixels[i]; rgba[p+1] = pixels[i+1]; rgba[p+2] = pixels[i+2]; rgba[p+3] = pixels[i+3]
92
+ } else if (colorType === 2) {
93
+ rgba[p] = pixels[i]; rgba[p+1] = pixels[i+1]; rgba[p+2] = pixels[i+2]; rgba[p+3] = 255
94
+ } else if (colorType === 0) {
95
+ rgba[p] = pixels[i]; rgba[p+1] = pixels[i]; rgba[p+2] = pixels[i]; rgba[p+3] = 255
96
+ } else if (colorType === 4) {
97
+ rgba[p] = pixels[i]; rgba[p+1] = pixels[i]; rgba[p+2] = pixels[i]; rgba[p+3] = pixels[i+1]
98
+ } else if (colorType === 3) {
99
+ if (!paletteChunk) throw new Error("Indexed PNG missing PLTE")
100
+ const index = pixels[i]
101
+ rgba[p] = paletteChunk[index * 3] ?? 0
102
+ rgba[p+1] = paletteChunk[index * 3 + 1] ?? 0
103
+ rgba[p+2] = paletteChunk[index * 3 + 2] ?? 0
104
+ rgba[p+3] = transparency?.[index] ?? 255
105
+ }
106
+ }
107
+ return { width, height, rgba }
108
+ }
109
+
110
+ let crcTable = null
111
+ function crc32(buffer) {
112
+ if (!crcTable) {
113
+ crcTable = Array.from({ length: 256 }, (_, n) => {
114
+ let c = n
115
+ for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
116
+ return c >>> 0
117
+ })
118
+ }
119
+ let crc = 0xffffffff
120
+ for (const byte of buffer) crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8)
121
+ return (crc ^ 0xffffffff) >>> 0
122
+ }
123
+
124
+ function chunk(type, data) {
125
+ const typeBytes = Buffer.from(type, "ascii")
126
+ const length = Buffer.alloc(4)
127
+ length.writeUInt32BE(data.length)
128
+ const crc = Buffer.alloc(4)
129
+ crc.writeUInt32BE(crc32(Buffer.concat([typeBytes, data])))
130
+ return Buffer.concat([length, typeBytes, data, crc])
131
+ }
132
+
133
+ export function encodeRgbaPng({ width, height, rgba }) {
134
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) throw new Error("Invalid PNG dimensions")
135
+ if (!Buffer.isBuffer(rgba) || rgba.length !== width * height * 4) throw new Error("Invalid RGBA buffer")
136
+ const ihdr = Buffer.alloc(13)
137
+ ihdr.writeUInt32BE(width, 0)
138
+ ihdr.writeUInt32BE(height, 4)
139
+ ihdr[8] = 8
140
+ ihdr[9] = 6
141
+ const scan = Buffer.alloc(height * (1 + width * 4))
142
+ for (let y = 0; y < height; y += 1) {
143
+ const row = y * (1 + width * 4)
144
+ scan[row] = 0
145
+ rgba.copy(scan, row + 1, y * width * 4, (y + 1) * width * 4)
146
+ }
147
+ return Buffer.concat([SIGNATURE, chunk("IHDR", ihdr), chunk("IDAT", deflateSync(scan)), chunk("IEND", Buffer.alloc(0))])
148
+ }
149
+
150
+ export function comparePngBuffers(expectedBuffer, actualBuffer, options = {}) {
151
+ const expected = decodePng(expectedBuffer)
152
+ const actual = decodePng(actualBuffer)
153
+ if (expected.width !== actual.width || expected.height !== actual.height) {
154
+ return {
155
+ schemaVersion: 1,
156
+ verdict: "FAIL",
157
+ reason: "dimension-mismatch",
158
+ expected: { width: expected.width, height: expected.height },
159
+ actual: { width: actual.width, height: actual.height },
160
+ differentPixels: expected.width * expected.height,
161
+ differentRatio: 1,
162
+ bounds: { x: 0, y: 0, width: Math.max(expected.width, actual.width), height: Math.max(expected.height, actual.height) },
163
+ }
164
+ }
165
+
166
+ const threshold = Math.max(0, Math.min(255, Number(options.threshold ?? 16)))
167
+ const allowedRatio = Math.max(0, Math.min(1, Number(options.maxDiffRatio ?? 0)))
168
+ let differentPixels = 0
169
+ let minX = expected.width
170
+ let minY = expected.height
171
+ let maxX = -1
172
+ let maxY = -1
173
+
174
+ for (let pixel = 0; pixel < expected.width * expected.height; pixel += 1) {
175
+ const offset = pixel * 4
176
+ let delta = 0
177
+ for (let channel = 0; channel < 4; channel += 1) {
178
+ delta = Math.max(delta, Math.abs(expected.rgba[offset + channel] - actual.rgba[offset + channel]))
179
+ }
180
+ if (delta <= threshold) continue
181
+ differentPixels += 1
182
+ const x = pixel % expected.width
183
+ const y = Math.floor(pixel / expected.width)
184
+ minX = Math.min(minX, x); maxX = Math.max(maxX, x)
185
+ minY = Math.min(minY, y); maxY = Math.max(maxY, y)
186
+ }
187
+
188
+ const totalPixels = expected.width * expected.height
189
+ const differentRatio = totalPixels ? differentPixels / totalPixels : 0
190
+ const bounds = differentPixels
191
+ ? { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 }
192
+ : null
193
+
194
+ return {
195
+ schemaVersion: 1,
196
+ verdict: differentRatio <= allowedRatio ? "PASS" : "FAIL",
197
+ width: expected.width,
198
+ height: expected.height,
199
+ differentPixels,
200
+ differentRatio,
201
+ threshold,
202
+ maxDiffRatio: allowedRatio,
203
+ bounds,
204
+ }
205
+ }
206
+
207
+ export async function comparePngFiles(expectedFile, actualFile, options = {}) {
208
+ return comparePngBuffers(await readFile(expectedFile), await readFile(actualFile), options)
209
+ }
210
+
211
+ export function cropDecodedPng(decoded, bounds) {
212
+ const x = Math.max(0, Math.floor(Number(bounds?.x || 0)))
213
+ const y = Math.max(0, Math.floor(Number(bounds?.y || 0)))
214
+ const width = Math.max(1, Math.min(decoded.width - x, Math.floor(Number(bounds?.width || decoded.width))))
215
+ const height = Math.max(1, Math.min(decoded.height - y, Math.floor(Number(bounds?.height || decoded.height))))
216
+ const rgba = Buffer.alloc(width * height * 4)
217
+ for (let row = 0; row < height; row += 1) {
218
+ const sourceStart = ((y + row) * decoded.width + x) * 4
219
+ decoded.rgba.copy(rgba, row * width * 4, sourceStart, sourceStart + width * 4)
220
+ }
221
+ return { width, height, rgba }
222
+ }
223
+
224
+ export async function cropPngFile(inputFile, outputFile, bounds) {
225
+ const decoded = decodePng(await readFile(inputFile))
226
+ const cropped = cropDecodedPng(decoded, bounds)
227
+ await writeFile(outputFile, encodeRgbaPng(cropped))
228
+ return { file: outputFile, width: cropped.width, height: cropped.height }
229
+ }
@@ -0,0 +1,60 @@
1
+ import { createHash } from "node:crypto"
2
+
3
+ function canonical(value) {
4
+ if (Array.isArray(value)) return value.map(canonical)
5
+ if (!value || typeof value !== "object") return value
6
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]))
7
+ }
8
+
9
+ function stringify(value) {
10
+ return typeof value === "string" ? value : JSON.stringify(canonical(value))
11
+ }
12
+
13
+ function digest(value) {
14
+ return createHash("sha256").update(stringify(value)).digest("hex")
15
+ }
16
+
17
+ function chars(value) {
18
+ return stringify(value ?? "").length
19
+ }
20
+
21
+ export function buildPromptEnvelope(input = {}) {
22
+ const stable = {
23
+ invariants: input.invariants || "",
24
+ role: input.role || "",
25
+ skills: input.skills || [],
26
+ projectFacts: input.projectFacts || {},
27
+ toolPolicy: input.toolPolicy || {},
28
+ }
29
+ const dynamic = {
30
+ task: input.task || {},
31
+ evidence: input.evidence || [],
32
+ recentFailure: input.recentFailure || null,
33
+ nextAction: input.nextAction || null,
34
+ recentMessages: input.recentMessages || [],
35
+ }
36
+ const stableChars = chars(stable)
37
+ const dynamicChars = chars(dynamic)
38
+ return {
39
+ schemaVersion: 1,
40
+ stable,
41
+ dynamic,
42
+ stablePrefixHash: digest(stable),
43
+ dynamicHash: digest(dynamic),
44
+ stableChars,
45
+ dynamicChars,
46
+ cacheableRatio: stableChars + dynamicChars ? stableChars / (stableChars + dynamicChars) : 0,
47
+ }
48
+ }
49
+
50
+ export function comparePromptEnvelopes(previous, current) {
51
+ const stableReused = Boolean(previous?.stablePrefixHash && previous.stablePrefixHash === current?.stablePrefixHash)
52
+ const repeatedStableChars = stableReused ? Number(current?.stableChars || 0) : 0
53
+ return {
54
+ schemaVersion: 1,
55
+ stableReused,
56
+ repeatedStableChars,
57
+ avoidableRepeatedChars: stableReused ? 0 : Math.min(Number(previous?.stableChars || 0), Number(current?.stableChars || 0)),
58
+ currentCacheableRatio: Number(current?.cacheableRatio || 0),
59
+ }
60
+ }
@@ -0,0 +1,72 @@
1
+ import { existsSync } from "node:fs"
2
+ import { readFile, readdir } from "node:fs/promises"
3
+ import path from "node:path"
4
+
5
+ function words(value) {
6
+ return new Set(String(value || "").toLowerCase().match(/[a-z0-9-]{3,}/g) || [])
7
+ }
8
+
9
+ function similarity(a, b) {
10
+ const left = words(a)
11
+ const right = words(b)
12
+ const union = new Set([...left, ...right])
13
+ let intersection = 0
14
+ for (const item of left) if (right.has(item)) intersection += 1
15
+ return union.size ? intersection / union.size : 0
16
+ }
17
+
18
+ function frontmatter(source) {
19
+ const name = source.match(/^name:\s*([^\r\n]+)/m)?.[1]?.trim() || null
20
+ const description = source.match(/^description:\s*([^\r\n]+)/m)?.[1]?.trim() || null
21
+ return { name, description }
22
+ }
23
+
24
+ export async function lintSkillCatalog(root = process.cwd(), options = {}) {
25
+ const skillsRoot = path.join(path.resolve(root), "global-config", "skills")
26
+ const entries = await readdir(skillsRoot, { withFileTypes: true })
27
+ const skills = []
28
+ const errors = []
29
+ const warnings = []
30
+
31
+ for (const entry of entries) {
32
+ if (!entry.isDirectory()) continue
33
+ const file = path.join(skillsRoot, entry.name, "SKILL.md")
34
+ if (!existsSync(file)) {
35
+ errors.push({ skill: entry.name, issue: "missing-skill-md" })
36
+ continue
37
+ }
38
+ const source = await readFile(file, "utf8")
39
+ const meta = frontmatter(source)
40
+ const body = source.replace(/^---[\s\S]*?---\s*/m, "")
41
+ const row = {
42
+ id: entry.name,
43
+ description: meta.description,
44
+ chars: source.length,
45
+ bodyChars: body.length,
46
+ estimatedTokens: Math.ceil(source.length / 4),
47
+ }
48
+ skills.push(row)
49
+ if (meta.name !== entry.name) errors.push({ skill: entry.name, issue: "frontmatter-name-mismatch" })
50
+ if (!meta.description) errors.push({ skill: entry.name, issue: "missing-description" })
51
+ if (row.estimatedTokens > Number(options.maxSkillTokens || 1800)) warnings.push({ skill: entry.name, issue: "large-entrypoint", estimatedTokens: row.estimatedTokens })
52
+ }
53
+
54
+ const collisions = []
55
+ const threshold = Number(options.collisionThreshold || 0.58)
56
+ for (let i = 0; i < skills.length; i += 1) {
57
+ for (let j = i + 1; j < skills.length; j += 1) {
58
+ const score = similarity(skills[i].description, skills[j].description)
59
+ if (score >= threshold) collisions.push({ a: skills[i].id, b: skills[j].id, score: Number(score.toFixed(3)) })
60
+ }
61
+ }
62
+
63
+ return {
64
+ schemaVersion: 1,
65
+ valid: errors.length === 0,
66
+ skillCount: skills.length,
67
+ errors,
68
+ warnings,
69
+ collisions: collisions.sort((a, b) => b.score - a.score),
70
+ skills: skills.sort((a, b) => a.id.localeCompare(b.id)),
71
+ }
72
+ }