opencode-agent-skill 10.0.0 → 12.0.0-beta.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 (69) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +60 -8
  3. package/bin/ocskill.mjs +354 -6
  4. package/docs/DETERMINISTIC-TOOLS.md +1 -1
  5. package/docs/ENGINEERING-DESIGN.md +4 -4
  6. package/docs/EVALS.md +3 -3
  7. package/docs/GITHUB-RULESET.md +50 -0
  8. package/docs/NPM-PUBLISH.md +4 -4
  9. package/docs/OPENCODE-COMPAT.md +3 -3
  10. package/docs/TRACE-SCHEMA.md +1 -1
  11. package/docs/V11-PERCEPTION-ADAPTIVE-EXECUTION.md +75 -0
  12. package/docs/V11-PERCEPTION-ADAPTIVE.md +220 -0
  13. package/docs/V12-WEAK-MODEL-INTELLIGENCE.md +27 -0
  14. package/evals/repo-scale/tasks.json +62 -0
  15. package/evals/router-triggers.json +82 -0
  16. package/evals/routing.json +76 -0
  17. package/evals/v11/tasks.json +122 -0
  18. package/global-config/agents/merge-arbiter.md +12 -0
  19. package/global-config/agents/visual-verifier.md +12 -0
  20. package/global-config/plugins/ues-router/index.js +272 -2
  21. package/global-config/plugins/ues-router/router.js +27 -3
  22. package/global-config/skills/browser-qa/SKILL.md +14 -0
  23. package/global-config/skills/browser-qa/references/workflow.md +11 -0
  24. package/global-config/skills/browser-security/SKILL.md +12 -0
  25. package/global-config/skills/component-visual-testing/SKILL.md +10 -0
  26. package/global-config/skills/design-source/SKILL.md +10 -0
  27. package/global-config/skills/design-source/references/workflow.md +12 -0
  28. package/global-config/skills/dynamic-workflow/SKILL.md +18 -0
  29. package/global-config/skills/dynamic-workflow/references/workflow.md +19 -0
  30. package/global-config/skills/responsive-verification/SKILL.md +10 -0
  31. package/global-config/skills/skill-authoring/SKILL.md +12 -0
  32. package/global-config/skills/skill-evaluation/SKILL.md +17 -0
  33. package/global-config/skills/visual-fidelity/SKILL.md +14 -0
  34. package/global-config/skills/visual-fidelity/references/workflow.md +14 -0
  35. package/lib/browser-adapter.mjs +82 -0
  36. package/lib/browser-runtime.mjs +193 -0
  37. package/lib/capability-registry.mjs +109 -0
  38. package/lib/context-engine-v11.mjs +150 -0
  39. package/lib/context-manifest.mjs +16 -3
  40. package/lib/context-quality.mjs +59 -0
  41. package/lib/control-center.mjs +12 -2
  42. package/lib/decision-policy.mjs +23 -0
  43. package/lib/dynamic-workflow.mjs +179 -0
  44. package/lib/eval-ablation.mjs +43 -1
  45. package/lib/eval-report.mjs +72 -0
  46. package/lib/eval-telemetry.mjs +61 -0
  47. package/lib/evidence-budget.mjs +84 -0
  48. package/lib/evidence-store.mjs +178 -0
  49. package/lib/hermes-bridge.mjs +45 -1
  50. package/lib/model-config.mjs +21 -1
  51. package/lib/model-performance.mjs +113 -0
  52. package/lib/model-policy.mjs +59 -1
  53. package/lib/orchestrator-policy.mjs +1 -1
  54. package/lib/png-diff.mjs +229 -0
  55. package/lib/prompt-cache.mjs +60 -0
  56. package/lib/repo-scale-fixture.mjs +45 -0
  57. package/lib/skill-quality.mjs +72 -0
  58. package/lib/task-engine.mjs +95 -7
  59. package/lib/ui-inspector.mjs +152 -0
  60. package/lib/v11-metrics.mjs +64 -0
  61. package/lib/visual-spec.mjs +159 -0
  62. package/lib/work-plan-scope.mjs +49 -0
  63. package/package.json +13 -5
  64. package/scripts/check-release-consistency.mjs +228 -0
  65. package/scripts/eval-ablation.mjs +4 -1
  66. package/scripts/validate-repo-scale-suite.mjs +27 -0
  67. package/scripts/validate-v11-suite.mjs +58 -0
  68. package/scripts/validate-v12-foundation.mjs +24 -0
  69. package/scripts/validate.mjs +16 -4
@@ -10,6 +10,13 @@ import { validateVerificationReceipt } from "./evidence-receipt.mjs"
10
10
  import { appendRuntimeEvent, readRuntimeEvents } from "./runtime-events.mjs"
11
11
  import { createGateReceipt, validateGateReceipt } from "./gate-receipt.mjs"
12
12
  import { classifyEngineeringTask, recoveryPolicyForAttempt } from "./orchestrator-policy.mjs"
13
+ import { planEvidenceBudget } from "./evidence-budget.mjs"
14
+ import { putEvidence } from "./evidence-store.mjs"
15
+ import { inferTaskCapabilities } from "./capability-registry.mjs"
16
+ import { buildPromptEnvelope } from "./prompt-cache.mjs"
17
+ import { externalizeContextExcerpts } from "./context-engine-v11.mjs"
18
+ import { assertActivePlanScope, persistPlanScope } from "./work-plan-scope.mjs"
19
+ import { classifyDecisionPolicy } from "./decision-policy.mjs"
13
20
 
14
21
  const WORK_DIR = ".ues-work"
15
22
  const STATE_SCHEMA = 4
@@ -44,6 +51,8 @@ export function workPaths(root, slug) {
44
51
  events: path.join(dir, "EVENTS.jsonl"),
45
52
  tasks: path.join(dir, "tasks"),
46
53
  reports: path.join(dir, "reports"),
54
+ plans: path.join(dir, "plans"),
55
+ activePlan: path.join(dir, "ACTIVE_PLAN.json"),
47
56
  lock: path.join(dir, ".state-lock"),
48
57
  }
49
58
  }
@@ -263,6 +272,7 @@ export async function initWork(root, slug, goal) {
263
272
 
264
273
  await mkdir(paths.tasks, { recursive: true })
265
274
  await mkdir(paths.reports, { recursive: true })
275
+ await mkdir(paths.plans, { recursive: true })
266
276
  const createdAt = now()
267
277
  const cleanGoal = String(goal || "").trim() || "Define the requested engineering outcome."
268
278
 
@@ -280,6 +290,7 @@ export async function initWork(root, slug, goal) {
280
290
  planImportedAt: null,
281
291
  planHash: null,
282
292
  planApproval: null,
293
+ planScope: null,
283
294
  integrationVerification: null,
284
295
  tasks: {},
285
296
  decisions: [],
@@ -378,6 +389,7 @@ export async function importPlan(root, slug, planInput) {
378
389
  }
379
390
 
380
391
  const updatedAt = now()
392
+ const planScope = await persistPlanScope(loaded.paths, hash, plan, { importedAt: updatedAt, previousPlanHash: loaded.state.planHash || null })
381
393
  const state = {
382
394
  ...loaded.state,
383
395
  schemaVersion: STATE_SCHEMA,
@@ -387,6 +399,7 @@ export async function importPlan(root, slug, planInput) {
387
399
  planImportedAt: updatedAt,
388
400
  planHash: hash,
389
401
  planApproval: { status: "pending", planHash: hash, at: updatedAt, evidence: null },
402
+ planScope,
390
403
  integrationVerification: null,
391
404
  checkpoint: null,
392
405
  evidencePolicy: evidencePolicyForPlan(plan),
@@ -409,6 +422,7 @@ export async function approvePlan(root, slug, evidence, options = {}) {
409
422
  const loaded = await loadWork(root, slug)
410
423
  if (!loaded.plan) throw new Error("PLAN.json is missing")
411
424
  const hash = planHash(loaded.plan)
425
+ await assertActivePlanScope(loaded.paths, loaded.state.planHash || hash)
412
426
  if (loaded.state.planHash && loaded.state.planHash !== hash) {
413
427
  throw new Error("PLAN.json changed after import; re-import it before approval")
414
428
  }
@@ -535,6 +549,7 @@ export async function workStatus(root, slug) {
535
549
  blockers: loaded.state.blockers || [],
536
550
  planApproval: loaded.state.planApproval || null,
537
551
  integrationVerification: loaded.state.integrationVerification || null,
552
+ planScope: loaded.state.planScope || null,
538
553
  evidence: {
539
554
  receiptBacked,
540
555
  completed,
@@ -552,6 +567,7 @@ export async function startTask(root, slug, taskID, options = {}) {
552
567
  const result = await withWorkLock(root, slug, async () => {
553
568
  const loaded = await loadWork(root, slug)
554
569
  if (!loaded.plan) throw new Error("PLAN.json is missing; import a valid plan first")
570
+ await assertActivePlanScope(loaded.paths, loaded.state.planHash || planHash(loaded.plan))
555
571
  if (!planIsApproved(loaded.state, loaded.plan)) {
556
572
  throw new Error("plan is not approved; run ues-plan-checker and 'ocskill work approve-plan' first")
557
573
  }
@@ -883,16 +899,19 @@ export async function failTask(root, slug, taskID, reason, options = {}) {
883
899
  }
884
900
 
885
901
  export async function addDecision(root, slug, decision) {
886
- const text = String(decision || "").trim()
902
+ const input = decision && typeof decision === "object" ? decision : { text: decision }
903
+ const text = String(input.text || "").trim()
887
904
  if (!text) throw new Error("decision text is required")
888
-
905
+ const policy = classifyDecisionPolicy(text, input.facts || input)
906
+ if (input.auto === true && !policy.autoResolvable) throw new Error("decision requires human approval before automatic resolution")
889
907
  return withWorkLock(root, slug, async () => {
890
908
  const loaded = await loadWork(root, slug)
891
909
  loaded.state.decisions ??= []
892
910
  const timestamp = now()
893
- loaded.state.decisions.push({ at: timestamp, text })
911
+ loaded.state.decisions.push({ at: timestamp, text, policy, source: input.source || (input.auto === true ? "auto-ruling" : "human-or-agent") })
894
912
  loaded.state.updatedAt = timestamp
895
913
  await writeJson(loaded.paths.state, loaded.state)
914
+ await journal(loaded.paths, "decision.recorded", { text, policy })
896
915
  return loaded.state
897
916
  })
898
917
  }
@@ -1068,11 +1087,19 @@ async function buildContextPack(loaded, taskID) {
1068
1087
  const task = taskByID(loaded.plan, taskID)
1069
1088
  const spec = await readFile(loaded.paths.spec, "utf8").catch(() => "")
1070
1089
  const dependencyReports = {}
1090
+ const dependencyReportRefs = {}
1071
1091
 
1072
1092
  for (const dep of task.dependsOn || []) {
1073
1093
  const file = path.join(loaded.paths.reports, `${dep}.md`)
1074
1094
  if (!existsSync(file)) continue
1075
- dependencyReports[dep] = (await readFile(file, "utf8")).slice(0, 12000)
1095
+ const report = await readFile(file, "utf8")
1096
+ dependencyReports[dep] = report.slice(0, 6000)
1097
+ const stored = await putEvidence(loaded.paths.root, report, {
1098
+ kind: "dependency-report",
1099
+ source: path.relative(loaded.paths.root, file).replaceAll("\\", "/"),
1100
+ summary: `Dependency report for ${dep}`,
1101
+ }).catch(() => null)
1102
+ if (stored?.ref) dependencyReportRefs[dep] = stored.ref
1076
1103
  }
1077
1104
 
1078
1105
  const taskText = [
@@ -1095,27 +1122,88 @@ async function buildContextPack(loaded, taskID) {
1095
1122
  effectiveMaxSkills: recovery.maxSkills,
1096
1123
  recovery,
1097
1124
  }
1098
- const contextManifest = await buildContextManifest(
1125
+ const capabilities = inferTaskCapabilities(taskText, {
1126
+ longContext: contextPolicy.mode === "long-horizon",
1127
+ coding: true,
1128
+ toolCalling: true,
1129
+ filesystem: true,
1130
+ })
1131
+ const evidenceBudget = planEvidenceBudget(effectiveContextPolicy, task, capabilities.required)
1132
+ const rawContextManifest = await buildContextManifest(
1099
1133
  loaded.paths.root,
1100
1134
  task,
1101
1135
  {
1102
- budget: recovery.contextBudget,
1136
+ budget: evidenceBudget.total,
1103
1137
  strategy: recovery.contextStrategy,
1138
+ evidenceBudget,
1104
1139
  },
1105
1140
  ).catch(() => null)
1141
+ const externalizedContext = rawContextManifest
1142
+ ? await externalizeContextExcerpts(loaded.paths.root, rawContextManifest, {
1143
+ task,
1144
+ threshold: Math.max(1800, Math.round(evidenceBudget.total * 0.12)),
1145
+ inlineChars: Math.max(600, Math.min(1600, Math.round(evidenceBudget.total * 0.08))),
1146
+ }).catch(() => ({ manifest: rawContextManifest, externalized: [], externalizedBytes: 0 }))
1147
+ : { manifest: null, externalized: [], externalizedBytes: 0 }
1148
+ const contextManifest = externalizedContext.manifest
1106
1149
  const learnings = await relevantAcceptedLearnings(
1107
1150
  loaded.paths.root,
1108
1151
  [task.title, task.summary, ...(task.acceptance || [])].join(" "),
1109
1152
  ).catch(() => [])
1110
1153
 
1154
+ const specStored = spec
1155
+ ? await putEvidence(loaded.paths.root, spec, {
1156
+ kind: "work-spec",
1157
+ source: path.relative(loaded.paths.root, loaded.paths.spec).replaceAll("\\", "/"),
1158
+ summary: "Durable work specification",
1159
+ }).catch(() => null)
1160
+ : null
1161
+ const promptEnvelope = buildPromptEnvelope({
1162
+ role: "executor",
1163
+ invariants: "evidence-first; scoped edits; fresh verification; no unsupported completion claims",
1164
+ skills: contextPolicy.domains || [],
1165
+ projectFacts: {
1166
+ instructions: contextManifest?.instructions || [],
1167
+ strategy: recovery.contextStrategy,
1168
+ },
1169
+ task,
1170
+ evidence: [
1171
+ ...(specStored?.ref ? [specStored.ref] : []),
1172
+ ...Object.values(dependencyReportRefs),
1173
+ ...(contextManifest?.evidencePointers || []).map((item) => item.ref),
1174
+ ...(contextManifest?.rankedReferences || []).slice(0, 12).map((item) => item.path),
1175
+ ],
1176
+ recentFailure: recovery.stage === "initial" ? null : loaded.state.tasks?.[taskID]?.lastError || null,
1177
+ nextAction: loaded.state.checkpoint?.nextAction || loaded.state.nextAction || null,
1178
+ })
1179
+
1111
1180
  return {
1112
1181
  schemaVersion: STATE_SCHEMA,
1182
+ contextSchemaVersion: 6,
1113
1183
  contextPolicy: effectiveContextPolicy,
1184
+ capabilities,
1185
+ evidenceBudget,
1186
+ promptCache: {
1187
+ stablePrefixHash: promptEnvelope.stablePrefixHash,
1188
+ dynamicHash: promptEnvelope.dynamicHash,
1189
+ stableChars: promptEnvelope.stableChars,
1190
+ dynamicChars: promptEnvelope.dynamicChars,
1191
+ cacheableRatio: promptEnvelope.cacheableRatio,
1192
+ },
1114
1193
  attempt,
1115
1194
  slug: loaded.state.slug,
1116
1195
  task,
1117
1196
  taskBrief: path.relative(loaded.paths.root, path.join(loaded.paths.tasks, `${taskID}.md`)).replaceAll("\\", "/"),
1118
- spec: spec.slice(0, 24000),
1197
+ spec: spec.slice(0, Math.min(12000, evidenceBudget.buckets.instructions + evidenceBudget.buckets.task)),
1198
+ evidencePointers: {
1199
+ spec: specStored?.ref || null,
1200
+ dependencyReports: dependencyReportRefs,
1201
+ context: (contextManifest?.evidencePointers || []).map((item) => item.ref),
1202
+ },
1203
+ evidenceStore: {
1204
+ refs: externalizedContext.externalized.length,
1205
+ externalizedBytes: externalizedContext.externalizedBytes,
1206
+ },
1119
1207
  dependencyReports,
1120
1208
  decisions: loaded.state.decisions || [],
1121
1209
  blockers: loaded.state.blockers || [],
@@ -0,0 +1,152 @@
1
+ function uniq(values) {
2
+ return [...new Set(values.filter((value) => value !== null && value !== undefined && value !== ""))]
3
+ }
4
+
5
+ function round(value, digits = 3) {
6
+ const factor = 10 ** digits
7
+ return Math.round(Number(value) * factor) / factor
8
+ }
9
+
10
+ function normalizeBox(item = {}) {
11
+ const box = item.box && typeof item.box === "object" ? item.box : item
12
+ const x = Number(box.x)
13
+ const y = Number(box.y)
14
+ const width = Number(box.width)
15
+ const height = Number(box.height)
16
+ if (![x,y,width,height].every(Number.isFinite)) return null
17
+ return {
18
+ id: String(item.id || item.name || "").trim() || null,
19
+ role: item.role || null,
20
+ x, y, width, height,
21
+ right: x + width,
22
+ bottom: y + height,
23
+ }
24
+ }
25
+
26
+ function containsBox(a, b) {
27
+ return a.x <= b.x && a.y <= b.y && a.right >= b.right && a.bottom >= b.bottom
28
+ }
29
+
30
+ function intersectionArea(a, b) {
31
+ const width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.x, b.x))
32
+ const height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.y, b.y))
33
+ return width * height
34
+ }
35
+
36
+ export function inspectResponsiveLayout(items = [], viewport = {}, options = {}) {
37
+ const width = Math.max(1, Number(viewport.width || 0))
38
+ const height = Math.max(1, Number(viewport.height || 0))
39
+ const boxes = items.map(normalizeBox).filter(Boolean)
40
+ const issues = []
41
+ const minTouch = Math.max(1, Number(options.minTouchTarget || 44))
42
+ const touchRoles = new Set(["button","link","checkbox","radio","switch","tab","menuitem"])
43
+
44
+ for (const box of boxes) {
45
+ if (box.x < 0 || box.y < 0 || box.right > width || box.bottom > height) {
46
+ issues.push({
47
+ kind: "viewport-overflow",
48
+ id: box.id,
49
+ box,
50
+ viewport: { width, height },
51
+ })
52
+ }
53
+ if (touchRoles.has(String(box.role || "").toLowerCase()) && (box.width < minTouch || box.height < minTouch)) {
54
+ issues.push({
55
+ kind: "small-touch-target",
56
+ id: box.id,
57
+ role: box.role,
58
+ actual: { width: box.width, height: box.height },
59
+ minimum: minTouch,
60
+ })
61
+ }
62
+ }
63
+
64
+ const maxPairChecks = Math.max(0, Math.min(10000, Number(options.maxPairChecks || 3000)))
65
+ let checks = 0
66
+ for (let i = 0; i < boxes.length; i += 1) {
67
+ for (let j = i + 1; j < boxes.length && checks < maxPairChecks; j += 1) {
68
+ checks += 1
69
+ const a = boxes[i]
70
+ const b = boxes[j]
71
+ if (containsBox(a,b) || containsBox(b,a)) continue
72
+ const area = intersectionArea(a,b)
73
+ if (!area) continue
74
+ const smaller = Math.max(1, Math.min(a.width*a.height, b.width*b.height))
75
+ const ratio = area / smaller
76
+ if (ratio >= Number(options.overlapRatio ?? 0.15)) {
77
+ issues.push({
78
+ kind: "element-overlap",
79
+ a: a.id,
80
+ b: b.id,
81
+ intersectionArea: area,
82
+ smallerElementRatio: round(ratio),
83
+ })
84
+ }
85
+ }
86
+ }
87
+
88
+ return {
89
+ schemaVersion: 1,
90
+ viewport: { width, height },
91
+ elementCount: boxes.length,
92
+ pairChecks: checks,
93
+ verdict: issues.length ? "FAIL" : "PASS",
94
+ issues,
95
+ }
96
+ }
97
+
98
+ function normalizeCssValue(value) {
99
+ return String(value || "").trim().replace(/\s+/g, " ")
100
+ }
101
+
102
+ export function extractDesignTokens(css = "") {
103
+ const source = String(css || "")
104
+ const variables = {}
105
+ for (const match of source.matchAll(/--([a-zA-Z0-9_-]+)\s*:\s*([^;}{]+)\s*;/g)) {
106
+ variables["--" + match[1]] = normalizeCssValue(match[2])
107
+ }
108
+
109
+ const colors = uniq([
110
+ ...source.matchAll(/#[0-9a-fA-F]{3,8}\b/g),
111
+ ...source.matchAll(/\b(?:rgb|rgba|hsl|hsla)\([^)]*\)/g),
112
+ ].map((match) => match[0].toLowerCase())).slice(0, 128)
113
+
114
+ const lengths = uniq([...source.matchAll(/-?\d*\.?\d+(?:px|rem|em)\b/g)].map((m)=>m[0]))
115
+ const px = lengths.filter((value)=>value.endsWith("px")).map((value)=>Number.parseFloat(value)).filter((value)=>Number.isFinite(value) && value >= 0)
116
+ const radii = uniq([...source.matchAll(/border-radius\s*:\s*([^;}{]+)/g)].map((m)=>normalizeCssValue(m[1]))).slice(0,64)
117
+ const fontSizes = uniq([...source.matchAll(/font-size\s*:\s*([^;}{]+)/g)].map((m)=>normalizeCssValue(m[1]))).slice(0,64)
118
+ const shadows = uniq([...source.matchAll(/box-shadow\s*:\s*([^;}{]+)/g)].map((m)=>normalizeCssValue(m[1]))).slice(0,64)
119
+
120
+ const spacingCandidates = uniq(px.filter((value)=>value <= 128).sort((a,b)=>a-b)).slice(0,32)
121
+
122
+ return {
123
+ schemaVersion: 1,
124
+ variables,
125
+ colors,
126
+ spacingCandidates,
127
+ radii,
128
+ fontSizes,
129
+ shadows,
130
+ stats: {
131
+ variableCount: Object.keys(variables).length,
132
+ colorCount: colors.length,
133
+ lengthCount: lengths.length,
134
+ },
135
+ }
136
+ }
137
+
138
+ export function designTokenEvidence(tokens = {}) {
139
+ const variableEntries = Object.entries(tokens.variables || {})
140
+ return {
141
+ schemaVersion: 1,
142
+ summary: {
143
+ variables: variableEntries.slice(0,40),
144
+ colors: (tokens.colors || []).slice(0,24),
145
+ spacingCandidates: (tokens.spacingCandidates || []).slice(0,20),
146
+ radii: (tokens.radii || []).slice(0,16),
147
+ fontSizes: (tokens.fontSizes || []).slice(0,16),
148
+ shadows: (tokens.shadows || []).slice(0,12),
149
+ },
150
+ stats: tokens.stats || {},
151
+ }
152
+ }
@@ -0,0 +1,64 @@
1
+ function finite(value) {
2
+ const n = Number(value)
3
+ return Number.isFinite(n) ? n : 0
4
+ }
5
+
6
+ function ratio(numerator, denominator) {
7
+ return denominator > 0 ? numerator / denominator : null
8
+ }
9
+
10
+ export function summarizeV11Efficiency(samples = []) {
11
+ const totals = {
12
+ samples: samples.length,
13
+ stableChars: 0,
14
+ dynamicChars: 0,
15
+ repeatedStableChars: 0,
16
+ externalizedEvidenceBytes: 0,
17
+ evidenceRefs: 0,
18
+ visualRepairAttempts: 0,
19
+ contextExpansions: 0,
20
+ modelEscalations: 0,
21
+ duplicateToolBlocks: 0,
22
+ loopBlocks: 0,
23
+ }
24
+
25
+ for (const sample of samples) {
26
+ const cache = sample.promptCache || sample.contextPack?.promptCache || {}
27
+ totals.stableChars += finite(cache.stableChars)
28
+ totals.dynamicChars += finite(cache.dynamicChars)
29
+ totals.repeatedStableChars += finite(sample.repeatedStableChars ?? cache.repeatedStableChars)
30
+
31
+ const evidence = sample.evidenceStore || sample.evidence || {}
32
+ totals.externalizedEvidenceBytes += finite(evidence.externalizedBytes ?? evidence.bytes)
33
+ totals.evidenceRefs += finite(evidence.refs ?? sample.evidenceRefs)
34
+
35
+ totals.visualRepairAttempts += finite(sample.visualRepairAttempts)
36
+ totals.contextExpansions += finite(sample.contextExpansions)
37
+ totals.modelEscalations += finite(sample.modelEscalations)
38
+ totals.duplicateToolBlocks += finite(sample.duplicateToolBlocks)
39
+ totals.loopBlocks += finite(sample.loopBlocks)
40
+ }
41
+
42
+ const inputChars = totals.stableChars + totals.dynamicChars
43
+ return {
44
+ schemaVersion: 1,
45
+ totals,
46
+ cacheablePrefixRatio: ratio(totals.stableChars, inputChars),
47
+ repeatedStableRatio: ratio(totals.repeatedStableChars, totals.stableChars),
48
+ externalizedEvidenceBytesPerSample: ratio(totals.externalizedEvidenceBytes, totals.samples),
49
+ evidenceRefsPerSample: ratio(totals.evidenceRefs, totals.samples),
50
+ visualRepairsPerSample: ratio(totals.visualRepairAttempts, totals.samples),
51
+ contextExpansionsPerSample: ratio(totals.contextExpansions, totals.samples),
52
+ modelEscalationsPerSample: ratio(totals.modelEscalations, totals.samples),
53
+ }
54
+ }
55
+
56
+ export function verifiedSuccessPer100kTokens(results = []) {
57
+ let success = 0
58
+ let tokens = 0
59
+ for (const item of results) {
60
+ if (item.passed === true && item.verified !== false) success += 1
61
+ tokens += finite(item.tokens ?? item.telemetry?.tokens?.total)
62
+ }
63
+ return tokens > 0 ? success / (tokens / 100_000) : null
64
+ }
@@ -0,0 +1,159 @@
1
+ function number(value, fallback = 0) {
2
+ const parsed = Number(value)
3
+ return Number.isFinite(parsed) ? parsed : fallback
4
+ }
5
+
6
+ function range(value, tolerance = 0) {
7
+ if (Array.isArray(value) && value.length >= 2) return [number(value[0]), number(value[1])]
8
+ const exact = number(value)
9
+ return [exact - tolerance, exact + tolerance]
10
+ }
11
+
12
+ function within(actual, expected, tolerance) {
13
+ const [min, max] = range(expected, tolerance)
14
+ return actual >= Math.min(min, max) && actual <= Math.max(min, max)
15
+ }
16
+
17
+ function normalizeElement(item = {}) {
18
+ return {
19
+ id: String(item.id || "").trim(),
20
+ role: item.role || null,
21
+ region: item.region || null,
22
+ expected: {
23
+ ...(item.expected || {}),
24
+ ...(item.x !== undefined ? { x: item.x } : {}),
25
+ ...(item.y !== undefined ? { y: item.y } : {}),
26
+ ...(item.width !== undefined ? { width: item.width } : {}),
27
+ ...(item.height !== undefined ? { height: item.height } : {}),
28
+ },
29
+ tolerance: {
30
+ position: number(item.tolerance?.position, 8),
31
+ size: number(item.tolerance?.size, 8),
32
+ },
33
+ required: item.required !== false,
34
+ }
35
+ }
36
+
37
+ export function normalizeVisualSpec(spec = {}) {
38
+ const viewport = {
39
+ width: Math.max(1, number(spec.viewport?.width, 1440)),
40
+ height: Math.max(1, number(spec.viewport?.height, 900)),
41
+ deviceScaleFactor: Math.max(0.1, number(spec.viewport?.deviceScaleFactor, 1)),
42
+ }
43
+ return {
44
+ schemaVersion: 1,
45
+ name: spec.name || null,
46
+ viewport,
47
+ elements: (spec.elements || []).map(normalizeElement).filter((item) => item.id),
48
+ regions: Array.isArray(spec.regions) ? spec.regions : [],
49
+ tokens: spec.tokens || {},
50
+ metadata: spec.metadata || {},
51
+ }
52
+ }
53
+
54
+ export function validateVisualSpec(spec = {}) {
55
+ const normalized = normalizeVisualSpec(spec)
56
+ const errors = []
57
+ const ids = new Set()
58
+ for (const item of normalized.elements) {
59
+ if (ids.has(item.id)) errors.push("duplicate element id: " + item.id)
60
+ ids.add(item.id)
61
+ for (const key of ["x", "y", "width", "height"]) {
62
+ if (item.expected[key] === undefined) continue
63
+ const values = Array.isArray(item.expected[key]) ? item.expected[key] : [item.expected[key]]
64
+ if (values.some((value) => !Number.isFinite(Number(value)))) errors.push(item.id + ": invalid " + key)
65
+ }
66
+ }
67
+ return { valid: errors.length === 0, errors, spec: normalized }
68
+ }
69
+
70
+ function actualMap(actual = []) {
71
+ const values = Array.isArray(actual) ? actual : Object.values(actual || {})
72
+ return new Map(values.filter((item) => item?.id).map((item) => [String(item.id), item]))
73
+ }
74
+
75
+ export function createGeometryReceipt(specInput, actualInput, options = {}) {
76
+ const checked = validateVisualSpec(specInput)
77
+ if (!checked.valid) throw new Error("Invalid visual spec: " + checked.errors.join("; "))
78
+ const actual = actualMap(actualInput)
79
+ const elements = []
80
+ let failed = 0
81
+
82
+ for (const expected of checked.spec.elements) {
83
+ const found = actual.get(expected.id)
84
+ if (!found) {
85
+ const pass = !expected.required
86
+ if (!pass) failed += 1
87
+ elements.push({ id: expected.id, verdict: pass ? "PASS" : "FAIL", reason: "missing", expected: expected.expected, actual: null })
88
+ continue
89
+ }
90
+
91
+ const failures = []
92
+ const checks = {}
93
+ for (const key of ["x", "y"]) {
94
+ if (expected.expected[key] === undefined) continue
95
+ const ok = within(number(found[key]), expected.expected[key], expected.tolerance.position)
96
+ checks[key] = ok
97
+ if (!ok) failures.push(key)
98
+ }
99
+ for (const key of ["width", "height"]) {
100
+ if (expected.expected[key] === undefined) continue
101
+ const ok = within(number(found[key]), expected.expected[key], expected.tolerance.size)
102
+ checks[key] = ok
103
+ if (!ok) failures.push(key)
104
+ }
105
+ const pass = failures.length === 0
106
+ if (!pass) failed += 1
107
+ elements.push({
108
+ id: expected.id,
109
+ verdict: pass ? "PASS" : "FAIL",
110
+ failures,
111
+ checks,
112
+ expected: expected.expected,
113
+ actual: {
114
+ x: number(found.x),
115
+ y: number(found.y),
116
+ width: number(found.width),
117
+ height: number(found.height),
118
+ },
119
+ })
120
+ }
121
+
122
+ return {
123
+ schemaVersion: 1,
124
+ kind: "visual-geometry",
125
+ verdict: failed === 0 ? "PASS" : "FAIL",
126
+ viewport: checked.spec.viewport,
127
+ total: elements.length,
128
+ passed: elements.length - failed,
129
+ failed,
130
+ elements,
131
+ generatedAt: options.generatedAt || new Date().toISOString(),
132
+ }
133
+ }
134
+
135
+ export function buildVisualRepairPlan(receipt, pixelDiff = null) {
136
+ const failedElements = (receipt?.elements || []).filter((item) => item.verdict !== "PASS")
137
+ return {
138
+ schemaVersion: 1,
139
+ action: failedElements.length || (pixelDiff && pixelDiff.differentPixels > 0) ? "repair" : "accept",
140
+ failedElementIDs: failedElements.map((item) => item.id),
141
+ geometryFailures: failedElements.map((item) => ({ id: item.id, failures: item.failures || [item.reason] })),
142
+ pixelRegion: pixelDiff?.bounds || null,
143
+ directives: [
144
+ ...(failedElements.length ? ["edit only the owning component/style for failed geometry unless evidence shows a shared token defect"] : []),
145
+ ...(pixelDiff?.bounds ? ["inspect and, if vision is available, crop only the largest changed pixel region before another edit"] : []),
146
+ "re-render the affected viewport and produce a fresh receipt before declaring success",
147
+ ],
148
+ }
149
+ }
150
+
151
+ export function responsiveViewportMatrix(input = null) {
152
+ if (Array.isArray(input) && input.length) return input
153
+ return [
154
+ { id: "mobile", width: 390, height: 844 },
155
+ { id: "tablet", width: 768, height: 1024 },
156
+ { id: "desktop", width: 1440, height: 900 },
157
+ { id: "wide", width: 1920, height: 1080 },
158
+ ]
159
+ }
@@ -0,0 +1,49 @@
1
+ import { existsSync } from "node:fs"
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises"
3
+ import path from "node:path"
4
+
5
+ function validPlanHash(value) {
6
+ return /^[a-f0-9]{64}$/i.test(String(value || ""))
7
+ }
8
+
9
+ export function planScopePaths(paths, planHash) {
10
+ if (!validPlanHash(planHash)) throw new Error("plan scope requires a SHA-256 plan hash")
11
+ const dir = path.join(paths.plans, planHash)
12
+ return { dir, plan: path.join(dir, "PLAN.json"), metadata: path.join(dir, "SCOPE.json") }
13
+ }
14
+
15
+ export async function persistPlanScope(paths, planHash, plan, metadata = {}) {
16
+ const scoped = planScopePaths(paths, planHash)
17
+ await mkdir(scoped.dir, { recursive: true })
18
+ await writeFile(scoped.plan, JSON.stringify(plan, null, 2) + "\n", "utf8")
19
+ const record = {
20
+ schemaVersion: 1,
21
+ planHash,
22
+ importedAt: metadata.importedAt || new Date().toISOString(),
23
+ previousPlanHash: metadata.previousPlanHash || null,
24
+ relativeDir: path.relative(paths.root, scoped.dir).replaceAll("\\", "/"),
25
+ }
26
+ await writeFile(scoped.metadata, JSON.stringify(record, null, 2) + "\n", "utf8")
27
+ await writeFile(paths.activePlan, JSON.stringify(record, null, 2) + "\n", "utf8")
28
+ return record
29
+ }
30
+
31
+ export async function readActivePlanScope(paths) {
32
+ if (!existsSync(paths.activePlan)) return null
33
+ try { return JSON.parse(await readFile(paths.activePlan, "utf8")) }
34
+ catch { throw new Error("ACTIVE_PLAN.json is invalid; re-import the plan before execution") }
35
+ }
36
+
37
+ export async function assertActivePlanScope(paths, expectedPlanHash) {
38
+ const active = await readActivePlanScope(paths)
39
+ if (!active) return { legacy: true, planHash: expectedPlanHash || null }
40
+ if (!validPlanHash(active.planHash)) throw new Error("ACTIVE_PLAN.json contains an invalid plan hash")
41
+ if (expectedPlanHash && active.planHash !== expectedPlanHash) {
42
+ throw new Error("active plan scope mismatch; re-import PLAN.json before executing tasks")
43
+ }
44
+ const scoped = planScopePaths(paths, active.planHash)
45
+ if (!existsSync(scoped.plan) || !existsSync(scoped.metadata)) {
46
+ throw new Error("active plan scope snapshot is incomplete; re-import PLAN.json")
47
+ }
48
+ return { ...active, legacy: false }
49
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-agent-skill",
3
- "version": "10.0.0",
4
- "description": "Evidence-first engineering runtime with adaptive context, selective routing, weak-model recovery, bounded ACI and benchmark-gated verification for OpenCode",
3
+ "version": "12.0.0-beta.0",
4
+ "description": "Perception-aware evidence-first engineering runtime with adaptive context, capability routing, visual/browser verification, weak-model recovery and benchmark-gated execution for OpenCode",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ocskill": "bin/ocskill.mjs"
@@ -23,7 +23,7 @@
23
23
  "evals": "node scripts/eval-skills.mjs",
24
24
  "evals:live:validate": "node scripts/validate-live-suite.mjs",
25
25
  "test": "node --test test/*.test.mjs",
26
- "ci": "npm run syntax && npm run validate && npm run evals && npm run evals:router && npm run evals:live:validate && npm run evals:long:validate && npm run evals:polyglot:validate && npm test && npm pack --dry-run && npm run smoke:pack && npm run smoke:plain-install",
26
+ "ci": "npm run syntax && npm run validate && npm run docs:check && npm run evals && npm run evals:router && npm run evals:v11:validate && npm run evals:v12:validate && npm run evals:repo-scale:validate && npm run evals:live:validate && npm run evals:long:validate && npm run evals:polyglot:validate && npm test && npm pack --dry-run && npm run smoke:pack && npm run smoke:plain-install",
27
27
  "postinstall": "node scripts/install.mjs",
28
28
  "preuninstall": "node scripts/uninstall.mjs",
29
29
  "prepublishOnly": "npm run ci",
@@ -42,11 +42,16 @@
42
42
  "dashboard": "node scripts/control-center.mjs",
43
43
  "evals:matrix": "node scripts/eval-matrix.mjs",
44
44
  "smoke:plain-install": "node scripts/smoke-plain-install.mjs",
45
+ "docs:check": "node scripts/check-release-consistency.mjs",
45
46
  "release:check-tag": "node scripts/check-release-tag.mjs",
46
47
  "evals:polyglot:validate": "node scripts/validate-live-suite.mjs --suite polyglot",
47
48
  "evals:polyglot": "node scripts/eval-live.mjs --suite polyglot",
48
49
  "evals:matrix:gate": "node scripts/eval-matrix.mjs --require-confidence",
49
- "evals:ablation": "node scripts/eval-ablation.mjs"
50
+ "evals:ablation": "node scripts/eval-ablation.mjs",
51
+ "evals:v11": "node --test test/*-v11.test.mjs",
52
+ "evals:v11:validate": "node scripts/validate-v11-suite.mjs",
53
+ "evals:repo-scale:validate": "node scripts/validate-repo-scale-suite.mjs",
54
+ "evals:v12:validate": "node scripts/validate-v12-foundation.mjs"
50
55
  },
51
56
  "keywords": [
52
57
  "opencode",
@@ -56,7 +61,10 @@
56
61
  "engineering-workflow",
57
62
  "code-review",
58
63
  "debugging",
59
- "verification"
64
+ "verification",
65
+ "visual-testing",
66
+ "browser-automation",
67
+ "multimodal-agents"
60
68
  ],
61
69
  "engines": {
62
70
  "node": ">=20"