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.
- package/CHANGELOG.md +85 -0
- package/README.md +60 -8
- package/bin/ocskill.mjs +354 -6
- package/docs/DETERMINISTIC-TOOLS.md +1 -1
- package/docs/ENGINEERING-DESIGN.md +4 -4
- package/docs/EVALS.md +3 -3
- package/docs/GITHUB-RULESET.md +50 -0
- package/docs/NPM-PUBLISH.md +4 -4
- package/docs/OPENCODE-COMPAT.md +3 -3
- package/docs/TRACE-SCHEMA.md +1 -1
- package/docs/V11-PERCEPTION-ADAPTIVE-EXECUTION.md +75 -0
- package/docs/V11-PERCEPTION-ADAPTIVE.md +220 -0
- package/docs/V12-WEAK-MODEL-INTELLIGENCE.md +27 -0
- package/evals/repo-scale/tasks.json +62 -0
- package/evals/router-triggers.json +82 -0
- package/evals/routing.json +76 -0
- package/evals/v11/tasks.json +122 -0
- package/global-config/agents/merge-arbiter.md +12 -0
- package/global-config/agents/visual-verifier.md +12 -0
- package/global-config/plugins/ues-router/index.js +272 -2
- package/global-config/plugins/ues-router/router.js +27 -3
- package/global-config/skills/browser-qa/SKILL.md +14 -0
- package/global-config/skills/browser-qa/references/workflow.md +11 -0
- package/global-config/skills/browser-security/SKILL.md +12 -0
- package/global-config/skills/component-visual-testing/SKILL.md +10 -0
- package/global-config/skills/design-source/SKILL.md +10 -0
- package/global-config/skills/design-source/references/workflow.md +12 -0
- package/global-config/skills/dynamic-workflow/SKILL.md +18 -0
- package/global-config/skills/dynamic-workflow/references/workflow.md +19 -0
- package/global-config/skills/responsive-verification/SKILL.md +10 -0
- package/global-config/skills/skill-authoring/SKILL.md +12 -0
- package/global-config/skills/skill-evaluation/SKILL.md +17 -0
- package/global-config/skills/visual-fidelity/SKILL.md +14 -0
- package/global-config/skills/visual-fidelity/references/workflow.md +14 -0
- package/lib/browser-adapter.mjs +82 -0
- package/lib/browser-runtime.mjs +193 -0
- package/lib/capability-registry.mjs +109 -0
- package/lib/context-engine-v11.mjs +150 -0
- package/lib/context-manifest.mjs +16 -3
- package/lib/context-quality.mjs +59 -0
- package/lib/control-center.mjs +12 -2
- package/lib/decision-policy.mjs +23 -0
- package/lib/dynamic-workflow.mjs +179 -0
- package/lib/eval-ablation.mjs +43 -1
- package/lib/eval-report.mjs +72 -0
- package/lib/eval-telemetry.mjs +61 -0
- package/lib/evidence-budget.mjs +84 -0
- package/lib/evidence-store.mjs +178 -0
- package/lib/hermes-bridge.mjs +45 -1
- package/lib/model-config.mjs +21 -1
- package/lib/model-performance.mjs +113 -0
- package/lib/model-policy.mjs +59 -1
- package/lib/orchestrator-policy.mjs +1 -1
- package/lib/png-diff.mjs +229 -0
- package/lib/prompt-cache.mjs +60 -0
- package/lib/repo-scale-fixture.mjs +45 -0
- package/lib/skill-quality.mjs +72 -0
- package/lib/task-engine.mjs +95 -7
- package/lib/ui-inspector.mjs +152 -0
- package/lib/v11-metrics.mjs +64 -0
- package/lib/visual-spec.mjs +159 -0
- package/lib/work-plan-scope.mjs +49 -0
- package/package.json +13 -5
- package/scripts/check-release-consistency.mjs +228 -0
- package/scripts/eval-ablation.mjs +4 -1
- package/scripts/validate-repo-scale-suite.mjs +27 -0
- package/scripts/validate-v11-suite.mjs +58 -0
- package/scripts/validate-v12-foundation.mjs +24 -0
- package/scripts/validate.mjs +16 -4
package/lib/task-engine.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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,
|
|
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": "
|
|
4
|
-
"description": "
|
|
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"
|