opencode-agent-skill 11.0.0 → 13.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 +55 -0
- package/README.md +119 -14
- package/bin/ocskill.mjs +416 -83
- 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 +22 -8
- package/docs/OPENCODE-COMPAT.md +14 -4
- package/docs/TRACE-SCHEMA.md +1 -1
- package/docs/V11-PERCEPTION-ADAPTIVE.md +2 -2
- package/docs/V12-WEAK-MODEL-INTELLIGENCE.md +27 -0
- package/docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md +75 -0
- package/evals/repo-scale/tasks.json +62 -0
- package/global-config/AGENTS.md +4 -0
- package/global-config/commands/resume.md +4 -1
- package/global-config/commands/run.md +9 -7
- package/global-config/plugins/ues-router/capabilities.js +1 -0
- package/global-config/plugins/ues-router/command-runtime.js +79 -0
- package/global-config/plugins/ues-router/index.js +364 -43
- package/global-config/plugins/ues-router/parallel-runtime.js +271 -0
- package/global-config/plugins/ues-router/text-runtime.js +115 -0
- package/global-config/plugins/ues-router/verifier-runtime.js +33 -0
- package/global-config/skills/dynamic-workflow/SKILL.md +7 -6
- package/global-config/skills/dynamic-workflow/references/workflow.md +8 -6
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +7 -5
- package/lib/cli-utils.mjs +17 -4
- package/lib/context-engine-v11.mjs +4 -0
- package/lib/context-quality.mjs +59 -0
- package/lib/decision-policy.mjs +23 -0
- package/lib/installer.mjs +49 -16
- package/lib/model-config.mjs +13 -1
- package/lib/model-performance.mjs +113 -0
- package/lib/model-policy.mjs +9 -2
- package/lib/repo-scale-fixture.mjs +45 -0
- package/lib/task-engine.mjs +36 -12
- package/lib/task-graph.mjs +32 -0
- package/lib/text-encoding.mjs +74 -0
- package/lib/work-plan-scope.mjs +49 -0
- package/lib/worktree-sandbox.mjs +141 -12
- package/package.json +8 -4
- package/scripts/check-release-consistency.mjs +260 -0
- package/scripts/smoke-packed-install.mjs +39 -3
- package/scripts/smoke-plain-install.mjs +27 -7
- package/scripts/validate-repo-scale-suite.mjs +27 -0
- package/scripts/validate-v12-foundation.mjs +24 -0
- package/scripts/validate.mjs +18 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
function swappedUtf16(buffer) {
|
|
5
|
+
const evenLength = buffer.length - (buffer.length % 2)
|
|
6
|
+
const swapped = Buffer.allocUnsafe(evenLength)
|
|
7
|
+
for (let index = 0; index < evenLength; index += 2) {
|
|
8
|
+
swapped[index] = buffer[index + 1]
|
|
9
|
+
swapped[index + 1] = buffer[index]
|
|
10
|
+
}
|
|
11
|
+
return swapped.toString("utf16le")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function utf16Heuristic(buffer) {
|
|
15
|
+
if (buffer.length < 4) return null
|
|
16
|
+
let evenNulls = 0
|
|
17
|
+
let oddNulls = 0
|
|
18
|
+
let evenCount = 0
|
|
19
|
+
let oddCount = 0
|
|
20
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
21
|
+
if (index % 2 === 0) {
|
|
22
|
+
evenCount += 1
|
|
23
|
+
if (buffer[index] === 0) evenNulls += 1
|
|
24
|
+
} else {
|
|
25
|
+
oddCount += 1
|
|
26
|
+
if (buffer[index] === 0) oddNulls += 1
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const evenRatio = evenCount ? evenNulls / evenCount : 0
|
|
30
|
+
const oddRatio = oddCount ? oddNulls / oddCount : 0
|
|
31
|
+
if (oddRatio >= 0.25 && evenRatio <= 0.05) return "utf16le"
|
|
32
|
+
if (evenRatio >= 0.25 && oddRatio <= 0.05) return "utf16be"
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function decodeTextBuffer(value, options = {}) {
|
|
37
|
+
const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value || "")
|
|
38
|
+
if (buffer.length === 0) return ""
|
|
39
|
+
|
|
40
|
+
if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
|
|
41
|
+
return buffer.subarray(3).toString("utf8")
|
|
42
|
+
}
|
|
43
|
+
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
|
|
44
|
+
return buffer.subarray(2).toString("utf16le")
|
|
45
|
+
}
|
|
46
|
+
if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
|
|
47
|
+
return swappedUtf16(buffer.subarray(2))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const guessed = utf16Heuristic(buffer)
|
|
51
|
+
if (guessed === "utf16le") return buffer.toString("utf16le")
|
|
52
|
+
if (guessed === "utf16be") return swappedUtf16(buffer)
|
|
53
|
+
|
|
54
|
+
let nulBytes = 0
|
|
55
|
+
for (const byte of buffer) if (byte === 0) nulBytes += 1
|
|
56
|
+
if (nulBytes > Math.max(2, Math.floor(buffer.length * 0.01)) && options.allowBinary !== true) {
|
|
57
|
+
const error = new Error("file appears to be binary; refusing lossy text decoding")
|
|
58
|
+
error.code = "UES_BINARY_TEXT"
|
|
59
|
+
error.exitCode = 2
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return buffer.toString("utf8")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function readTextAuto(file, options = {}) {
|
|
67
|
+
return decodeTextBuffer(readFileSync(path.resolve(file)), options)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function writeUtf8Text(file, text) {
|
|
71
|
+
const resolved = path.resolve(file)
|
|
72
|
+
writeFileSync(resolved, String(text ?? ""), "utf8")
|
|
73
|
+
return resolved
|
|
74
|
+
}
|
|
@@ -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/lib/worktree-sandbox.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs"
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto"
|
|
3
|
+
import { copyFile, mkdir, readFile, rm, unlink, writeFile } from "node:fs/promises"
|
|
3
4
|
import { spawnSync } from "node:child_process"
|
|
4
5
|
import path from "node:path"
|
|
5
6
|
|
|
@@ -27,11 +28,69 @@ function safeName(value) {
|
|
|
27
28
|
return String(value || "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "")
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
function metadataFile(dir) {
|
|
32
|
+
return path.resolve(dir) + ".ues-meta.json"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readSandboxMetadata(dir) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(await readFile(metadataFile(dir), "utf8"))
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function writeSandboxMetadata(dir, value) {
|
|
44
|
+
await writeFile(metadataFile(dir), JSON.stringify(value, null, 2) + "\n", "utf8")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function inheritDirtySnapshot(root, dir) {
|
|
48
|
+
const inheritedUntrackedHashes = {}
|
|
49
|
+
const diff = git(root, ["diff", "--binary", "--no-ext-diff", "HEAD", "--"])
|
|
50
|
+
if (diff.status !== 0) throw new Error((diff.stderr || diff.stdout || "git diff HEAD failed").trim())
|
|
51
|
+
if (diff.stdout) {
|
|
52
|
+
const applied = git(dir, ["apply", "--whitespace=nowarn", "-"], { input: diff.stdout })
|
|
53
|
+
if (applied.status !== 0) throw new Error((applied.stderr || applied.stdout || "git apply inherited root diff failed").trim())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const untracked = git(root, ["ls-files", "--others", "--exclude-standard", "-z"])
|
|
57
|
+
if (untracked.status !== 0) throw new Error((untracked.stderr || untracked.stdout || "git ls-files failed").trim())
|
|
58
|
+
for (const relative of untracked.stdout.split("\0").filter(Boolean)) {
|
|
59
|
+
const source = path.join(root, relative)
|
|
60
|
+
const target = path.join(dir, relative)
|
|
61
|
+
const bytes = await readFile(source)
|
|
62
|
+
inheritedUntrackedHashes[relative.replaceAll("\\", "/")] = createHash("sha256").update(bytes).digest("hex")
|
|
63
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
64
|
+
await copyFile(source, target)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const add = git(dir, ["add", "-A"])
|
|
68
|
+
if (add.status !== 0) throw new Error((add.stderr || add.stdout || "git add inherited snapshot failed").trim())
|
|
69
|
+
const commit = git(dir, [
|
|
70
|
+
"-c", "user.name=UES Parallel Runtime",
|
|
71
|
+
"-c", "user.email=ues-parallel@example.invalid",
|
|
72
|
+
"commit", "-m", "chore(ues): inherited parallel integration snapshot",
|
|
73
|
+
])
|
|
74
|
+
if (commit.status !== 0) {
|
|
75
|
+
const status = git(dir, ["status", "--porcelain=v1", "--untracked-files=all"])
|
|
76
|
+
if (status.status !== 0 || status.stdout.trim()) {
|
|
77
|
+
throw new Error((commit.stderr || commit.stdout || "git commit inherited snapshot failed").trim())
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const head = git(dir, ["rev-parse", "HEAD"])
|
|
81
|
+
if (head.status !== 0) throw new Error((head.stderr || head.stdout || "git rev-parse inherited snapshot failed").trim())
|
|
82
|
+
return {
|
|
83
|
+
head: head.stdout.trim(),
|
|
84
|
+
inheritedUntrackedHashes,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
30
88
|
export async function createTaskSandbox(root, slug, taskID, options = {}) {
|
|
31
89
|
root = path.resolve(root)
|
|
32
90
|
const status = git(root, ["status", "--porcelain=v1", "--untracked-files=all"])
|
|
33
91
|
if (status.status !== 0) throw new Error((status.stderr || status.stdout || "git status failed").trim())
|
|
34
|
-
|
|
92
|
+
const rootDirty = Boolean(status.stdout.trim())
|
|
93
|
+
if (rootDirty && options.allowDirtyRoot !== true && options.inheritDirtyRoot !== true) {
|
|
35
94
|
throw new Error("sandbox creation requires a clean root working tree so the isolated executor cannot miss existing uncommitted changes")
|
|
36
95
|
}
|
|
37
96
|
const base = options.baseDir
|
|
@@ -48,7 +107,31 @@ export async function createTaskSandbox(root, slug, taskID, options = {}) {
|
|
|
48
107
|
const result = git(root, ["worktree", "add", "-b", branch, dir, startPoint])
|
|
49
108
|
if (result.status !== 0) throw new Error((result.stderr || result.stdout || "git worktree add failed").trim())
|
|
50
109
|
|
|
51
|
-
|
|
110
|
+
let integrationBase = git(dir, ["rev-parse", "HEAD"]).stdout.trim()
|
|
111
|
+
let inheritedDirtyRoot = false
|
|
112
|
+
let inheritedUntrackedHashes = {}
|
|
113
|
+
if (rootDirty && options.inheritDirtyRoot === true) {
|
|
114
|
+
const inherited = await inheritDirtySnapshot(root, dir)
|
|
115
|
+
integrationBase = inherited.head
|
|
116
|
+
inheritedUntrackedHashes = inherited.inheritedUntrackedHashes
|
|
117
|
+
inheritedDirtyRoot = true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const metadata = {
|
|
121
|
+
schemaVersion: 2,
|
|
122
|
+
root,
|
|
123
|
+
dir,
|
|
124
|
+
branch,
|
|
125
|
+
slug,
|
|
126
|
+
taskID,
|
|
127
|
+
startPoint,
|
|
128
|
+
integrationBase,
|
|
129
|
+
inheritedDirtyRoot,
|
|
130
|
+
inheritedUntrackedHashes,
|
|
131
|
+
createdAt: new Date().toISOString(),
|
|
132
|
+
}
|
|
133
|
+
await writeSandboxMetadata(dir, metadata)
|
|
134
|
+
return metadata
|
|
52
135
|
}
|
|
53
136
|
|
|
54
137
|
export async function integrateTaskSandbox(root, dir, options = {}) {
|
|
@@ -61,13 +144,16 @@ export async function integrateTaskSandbox(root, dir, options = {}) {
|
|
|
61
144
|
if (rootTop.status !== 0 || sandboxTop.status !== 0) throw new Error("root and sandbox must be Git worktrees")
|
|
62
145
|
if (path.resolve(sandboxTop.stdout.trim()) !== dir) throw new Error("sandbox path is not the worktree root")
|
|
63
146
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
147
|
+
const metadata = await readSandboxMetadata(dir)
|
|
148
|
+
let baseSha = metadata?.integrationBase || null
|
|
149
|
+
if (!baseSha) {
|
|
150
|
+
const rootHead = git(root, ["rev-parse", "HEAD"])
|
|
151
|
+
const sandboxHead = git(dir, ["rev-parse", "HEAD"])
|
|
152
|
+
if (rootHead.status !== 0 || sandboxHead.status !== 0) throw new Error("could not resolve worktree HEADs")
|
|
153
|
+
const base = git(dir, ["merge-base", rootHead.stdout.trim(), sandboxHead.stdout.trim()])
|
|
154
|
+
if (base.status !== 0) throw new Error((base.stderr || base.stdout || "git merge-base failed").trim())
|
|
155
|
+
baseSha = base.stdout.trim()
|
|
156
|
+
}
|
|
71
157
|
|
|
72
158
|
// Intent-to-add exposes untracked files in a binary diff without committing them.
|
|
73
159
|
const addIntent = git(dir, ["add", "-N", "."])
|
|
@@ -83,8 +169,30 @@ export async function integrateTaskSandbox(root, dir, options = {}) {
|
|
|
83
169
|
|
|
84
170
|
const rootDirty = new Set(statusFiles(root))
|
|
85
171
|
const overlap = changed.filter((file) => rootDirty.has(file))
|
|
86
|
-
|
|
87
|
-
|
|
172
|
+
const unsafeOverlap = []
|
|
173
|
+
for (const file of overlap) {
|
|
174
|
+
if (!metadata?.inheritedDirtyRoot) {
|
|
175
|
+
unsafeOverlap.push(file)
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const inheritedUntrackedHash = metadata?.inheritedUntrackedHashes?.[file]
|
|
180
|
+
if (inheritedUntrackedHash) {
|
|
181
|
+
try {
|
|
182
|
+
const current = await readFile(path.join(root, file))
|
|
183
|
+
const currentHash = createHash("sha256").update(current).digest("hex")
|
|
184
|
+
if (currentHash !== inheritedUntrackedHash) unsafeOverlap.push(file)
|
|
185
|
+
} catch {
|
|
186
|
+
unsafeOverlap.push(file)
|
|
187
|
+
}
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const check = git(root, ["diff", "--quiet", baseSha, "--", file])
|
|
192
|
+
if (check.status !== 0) unsafeOverlap.push(file)
|
|
193
|
+
}
|
|
194
|
+
if (unsafeOverlap.length) {
|
|
195
|
+
throw new Error("sandbox integration conflicts with existing root changes: " + unsafeOverlap.join(", "))
|
|
88
196
|
}
|
|
89
197
|
|
|
90
198
|
const patch = git(dir, ["diff", "--binary", "--no-ext-diff", baseSha, "--"])
|
|
@@ -121,9 +229,30 @@ export async function removeTaskSandbox(root, dir, options = {}) {
|
|
|
121
229
|
throw new Error((deleted.stderr || deleted.stdout || "git branch delete failed").trim())
|
|
122
230
|
}
|
|
123
231
|
}
|
|
232
|
+
await unlink(metadataFile(dir)).catch(() => {})
|
|
124
233
|
return { removed: !existsSync(dir), dir, branch: branch || null }
|
|
125
234
|
}
|
|
126
235
|
|
|
236
|
+
export async function rollbackTaskSandbox(root, dir, options = {}) {
|
|
237
|
+
root = path.resolve(root)
|
|
238
|
+
dir = path.resolve(dir)
|
|
239
|
+
if (!existsSync(dir)) throw new Error("sandbox does not exist: " + dir)
|
|
240
|
+
const metadata = await readSandboxMetadata(dir)
|
|
241
|
+
if (!metadata?.integrationBase) throw new Error("sandbox rollback metadata is missing")
|
|
242
|
+
|
|
243
|
+
const patch = git(dir, ["diff", "--binary", "--no-ext-diff", metadata.integrationBase, "--"])
|
|
244
|
+
if (patch.status !== 0) throw new Error((patch.stderr || patch.stdout || "git diff for rollback failed").trim())
|
|
245
|
+
if (patch.stdout) {
|
|
246
|
+
const reversed = git(root, ["apply", "--reverse", "--whitespace=nowarn", "-"], { input: patch.stdout })
|
|
247
|
+
if (reversed.status !== 0) throw new Error((reversed.stderr || reversed.stdout || "git rollback apply failed").trim())
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!options.keep) {
|
|
251
|
+
await removeTaskSandbox(root, dir, { force: true, deleteBranch: true })
|
|
252
|
+
}
|
|
253
|
+
return { rolledBack: true, dir, base: metadata.integrationBase }
|
|
254
|
+
}
|
|
255
|
+
|
|
127
256
|
export function listTaskSandboxes(root) {
|
|
128
257
|
const result = git(path.resolve(root), ["worktree", "list", "--porcelain"])
|
|
129
258
|
if (result.status !== 0) throw new Error((result.stderr || result.stdout || "git worktree list failed").trim())
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-agent-skill",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "13.0.0-beta.0",
|
|
4
|
+
"description": "Parallel weak-model engineering runtime with isolated same-model workers, event-driven DAG execution, transactional integration, adaptive context and evidence-gated verification 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:v11: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",
|
|
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:v13 && 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,13 +42,17 @@
|
|
|
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
50
|
"evals:ablation": "node scripts/eval-ablation.mjs",
|
|
50
51
|
"evals:v11": "node --test test/*-v11.test.mjs",
|
|
51
|
-
"evals:v11:validate": "node scripts/validate-v11-suite.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",
|
|
55
|
+
"evals:v13": "node --test test/*-v13.test.mjs"
|
|
52
56
|
},
|
|
53
57
|
"keywords": [
|
|
54
58
|
"opencode",
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
|
|
7
|
+
const DEFAULT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
8
|
+
|
|
9
|
+
function readJson(root, relative) {
|
|
10
|
+
const file = path.join(root, relative)
|
|
11
|
+
if (!existsSync(file)) {
|
|
12
|
+
return { ok: false, error: `${relative}: file not found`, value: null }
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return { ok: true, error: null, value: JSON.parse(readFileSync(file, "utf8")) }
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return { ok: false, error: `${relative}: invalid JSON (${e.message})`, value: null }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readText(root, relative) {
|
|
22
|
+
const file = path.join(root, relative)
|
|
23
|
+
if (!existsSync(file)) {
|
|
24
|
+
return { ok: false, error: `${relative}: file not found`, value: null }
|
|
25
|
+
}
|
|
26
|
+
return { ok: true, error: null, value: readFileSync(file, "utf8") }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function countSkillDirs(root) {
|
|
30
|
+
const fullPath = path.join(root, "global-config", "skills")
|
|
31
|
+
if (!existsSync(fullPath)) return { ok: false, error: "global-config/skills: directory not found", value: 0 }
|
|
32
|
+
const entries = readdirSync(fullPath, { withFileTypes: true })
|
|
33
|
+
const count = entries.filter((entry) => entry.isDirectory() && existsSync(path.join(fullPath, entry.name, "SKILL.md"))).length
|
|
34
|
+
return { ok: true, error: null, value: count }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function countDir(root, dirPath, filter) {
|
|
38
|
+
const fullPath = path.join(root, dirPath)
|
|
39
|
+
if (!existsSync(fullPath)) {
|
|
40
|
+
return { ok: false, error: `${dirPath}: directory not found`, value: 0 }
|
|
41
|
+
}
|
|
42
|
+
const entries = readdirSync(fullPath, { withFileTypes: true })
|
|
43
|
+
const count = filter ? entries.filter(filter).length : entries.length
|
|
44
|
+
return { ok: true, error: null, value: count }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function checkReleaseConsistency(root) {
|
|
48
|
+
root = root || DEFAULT_ROOT
|
|
49
|
+
const errors = []
|
|
50
|
+
const warnings = []
|
|
51
|
+
|
|
52
|
+
function rJson(relative) {
|
|
53
|
+
return readJson(root, relative)
|
|
54
|
+
}
|
|
55
|
+
function rText(relative) {
|
|
56
|
+
return readText(root, relative)
|
|
57
|
+
}
|
|
58
|
+
function rCount(dirPath, filter) {
|
|
59
|
+
return countDir(root, dirPath, filter)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 1. package.json version == package-lock.json version
|
|
63
|
+
const pkg = rJson("package.json")
|
|
64
|
+
const lock = rJson("package-lock.json")
|
|
65
|
+
if (pkg.ok && lock.ok) {
|
|
66
|
+
const pkgVersion = pkg.value.version
|
|
67
|
+
const lockVersion = lock.value.version
|
|
68
|
+
if (pkgVersion !== lockVersion) {
|
|
69
|
+
errors.push(`package.json version (${pkgVersion}) != package-lock.json version (${lockVersion})`)
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
if (!pkg.ok) errors.push(pkg.error)
|
|
73
|
+
if (!lock.ok) errors.push(lock.error)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 2. Count skills, commands, subagents
|
|
77
|
+
const skillRes = countSkillDirs(root)
|
|
78
|
+
const commandRes = rCount("global-config/commands", (e) => e.isFile() && e.name.endsWith(".md"))
|
|
79
|
+
const agentRes = rCount("global-config/agents", (e) => e.isFile() && e.name.endsWith(".md"))
|
|
80
|
+
const skillCount = skillRes.ok ? skillRes.value : 0
|
|
81
|
+
const commandCount = commandRes.ok ? commandRes.value : 0
|
|
82
|
+
const agentCount = agentRes.ok ? agentRes.value : 0
|
|
83
|
+
if (!skillRes.ok) errors.push(skillRes.error)
|
|
84
|
+
if (!commandRes.ok) errors.push(commandRes.error)
|
|
85
|
+
if (!agentRes.ok) errors.push(agentRes.error)
|
|
86
|
+
|
|
87
|
+
const expectedVersion = pkg.ok ? pkg.value.version : "unknown"
|
|
88
|
+
const routing = rJson("evals/routing.json")
|
|
89
|
+
const routerTriggers = rJson("evals/router-triggers.json")
|
|
90
|
+
const staticScenarioCount = routing.ok && Array.isArray(routing.value.scenarios) ? routing.value.scenarios.length : 0
|
|
91
|
+
const routerCaseCount = routerTriggers.ok && Array.isArray(routerTriggers.value.cases) ? routerTriggers.value.cases.length : 0
|
|
92
|
+
if (!routing.ok) errors.push(routing.error)
|
|
93
|
+
if (!routerTriggers.ok) errors.push(routerTriggers.error)
|
|
94
|
+
|
|
95
|
+
// 3. Check README current version block
|
|
96
|
+
const readme = rText("README.md")
|
|
97
|
+
if (readme.ok) {
|
|
98
|
+
const versionMatch = readme.value.match(/Phiên bản hiện tại:\s*\n```text\n(\S+)/)
|
|
99
|
+
if (versionMatch && versionMatch[1] !== expectedVersion) {
|
|
100
|
+
errors.push(`README.md: current version says ${versionMatch[1]}, expected ${expectedVersion}`)
|
|
101
|
+
} else if (!versionMatch) {
|
|
102
|
+
errors.push("README.md: could not find current version block")
|
|
103
|
+
}
|
|
104
|
+
if (staticScenarioCount && !readme.value.includes("**" + staticScenarioCount + " static skill-routing scenarios**")) {
|
|
105
|
+
errors.push("README.md: static routing scenario count drift (actual: " + staticScenarioCount + ")")
|
|
106
|
+
}
|
|
107
|
+
if (routerCaseCount && !readme.value.includes("**" + routerCaseCount + " V2 router cases**")) {
|
|
108
|
+
errors.push("README.md: router case count drift (actual: " + routerCaseCount + ")")
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
errors.push(readme.error)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 4. Check V11 docs for stale status markers
|
|
115
|
+
const v11Docs = ["docs/V11-PERCEPTION-ADAPTIVE.md", "docs/V11-PERCEPTION-ADAPTIVE-EXECUTION.md"]
|
|
116
|
+
for (const doc of v11Docs) {
|
|
117
|
+
const content = rText(doc)
|
|
118
|
+
if (!content.ok) {
|
|
119
|
+
errors.push(content.error)
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
if (content.value.includes("Status: development")) {
|
|
123
|
+
errors.push(`${doc}: still marked as development`)
|
|
124
|
+
}
|
|
125
|
+
if (content.value.includes("11.0.0-dev.")) {
|
|
126
|
+
errors.push(`${doc}: still references dev version 11.0.0-dev.*`)
|
|
127
|
+
}
|
|
128
|
+
if (content.value.includes("npm `latest` remains V10")) {
|
|
129
|
+
errors.push(`${doc}: still says V10 remains npm latest`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 4b. V13 prerelease contract must stay synchronized with compatibility docs.
|
|
134
|
+
if (expectedVersion.startsWith("13.")) {
|
|
135
|
+
const v13 = rText("docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md")
|
|
136
|
+
if (!v13.ok) {
|
|
137
|
+
errors.push(v13.error)
|
|
138
|
+
} else {
|
|
139
|
+
if (!v13.value.includes("Status: beta prerelease (`" + expectedVersion + "`).")) {
|
|
140
|
+
errors.push("docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md: prerelease version/status drift")
|
|
141
|
+
}
|
|
142
|
+
for (const required of ["ues.dispatch_parallel", "same configured model", "OpenCode"]) {
|
|
143
|
+
if (!v13.value.includes(required)) {
|
|
144
|
+
errors.push("docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md: missing V13 contract marker " + required)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const compat = rText("docs/OPENCODE-COMPAT.md")
|
|
150
|
+
if (!compat.ok) {
|
|
151
|
+
errors.push(compat.error)
|
|
152
|
+
} else {
|
|
153
|
+
if (!compat.value.includes("V13 ships one npm package")) {
|
|
154
|
+
errors.push("docs/OPENCODE-COMPAT.md: current compatibility header is stale for V13")
|
|
155
|
+
}
|
|
156
|
+
if (!compat.value.includes("ues.dispatch_parallel")) {
|
|
157
|
+
errors.push("docs/OPENCODE-COMPAT.md: missing V13 parallel capability boundary")
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 5. Verify docs reflect actual counts (skip historical sections)
|
|
163
|
+
const docsToCheck = ["README.md", "docs/ENGINEERING-DESIGN.md", "docs/OPENCODE-COMPAT.md"]
|
|
164
|
+
for (const doc of docsToCheck) {
|
|
165
|
+
const content = rText(doc)
|
|
166
|
+
if (!content.ok) {
|
|
167
|
+
errors.push(content.error)
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
const sections = content.value.split(/\n(?=#{1,6}\s)/)
|
|
171
|
+
for (const section of sections) {
|
|
172
|
+
const isHistorical = /^\s*#{1,6}\s+.*(V\d+\.|UES\s+\d+\.|historical|Historical)/im.test(section)
|
|
173
|
+
if (isHistorical) continue
|
|
174
|
+
if (section.includes("39 skills") || section.includes("39 namespaced skills")) {
|
|
175
|
+
errors.push(`${doc}: current section still references 39 skills (actual: ${skillCount})`)
|
|
176
|
+
}
|
|
177
|
+
if (section.includes("10 namespaced subagents") || section.includes("10 subagents")) {
|
|
178
|
+
errors.push(`${doc}: current section still references 10 subagents (actual: ${agentCount})`)
|
|
179
|
+
}
|
|
180
|
+
if (section.includes("34 static") && section.includes("scenarios")) {
|
|
181
|
+
errors.push(`${doc}: current section still references 34 static scenarios (actual: ${staticScenarioCount})`)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 6. Check CI workflow
|
|
187
|
+
const ciYaml = rText(".github/workflows/ci.yml")
|
|
188
|
+
if (ciYaml.ok) {
|
|
189
|
+
if (!ciYaml.value.includes("evals:v11:validate")) {
|
|
190
|
+
errors.push(".github/workflows/ci.yml: missing evals:v11:validate job")
|
|
191
|
+
}
|
|
192
|
+
if (!/^\s*name:\s*CI Gate\s*$/m.test(ciYaml.value)) errors.push(".github/workflows/ci.yml: missing CI Gate aggregate job name")
|
|
193
|
+
if (!/needs:\s*\[\s*static\s*,\s*unit\s*,\s*package\s*\]/m.test(ciYaml.value)) errors.push(".github/workflows/ci.yml: CI Gate must depend on static, unit and package")
|
|
194
|
+
if (!/if:\s*always\(\)/m.test(ciYaml.value)) errors.push(".github/workflows/ci.yml: aggregate gate must use if: always()")
|
|
195
|
+
if (!ciYaml.value.includes("docs:check")) errors.push(".github/workflows/ci.yml: missing docs:check job")
|
|
196
|
+
for (const check of ["evals:v12:validate", ...(expectedVersion.startsWith("13.") ? ["evals:v13"] : []), "evals:repo-scale:validate"]) {
|
|
197
|
+
if (!ciYaml.value.includes(check)) errors.push(".github/workflows/ci.yml: missing " + check + " job")
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
errors.push(ciYaml.error)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 7. Check Security workflow
|
|
204
|
+
const securityYaml = rText(".github/workflows/security.yml")
|
|
205
|
+
if (securityYaml.ok) {
|
|
206
|
+
if (!/^\s*name:\s*Security Gate\s*$/m.test(securityYaml.value)) errors.push(".github/workflows/security.yml: missing Security Gate aggregate job name")
|
|
207
|
+
if (!/needs:\s*\[\s*codeql\s*,\s*dependency-review\s*\]/m.test(securityYaml.value)) errors.push(".github/workflows/security.yml: Security Gate must depend on codeql and dependency-review")
|
|
208
|
+
} else {
|
|
209
|
+
errors.push(securityYaml.error)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 8. Check publish workflow idempotency
|
|
213
|
+
const publishYaml = rText(".github/workflows/publish.yml")
|
|
214
|
+
if (publishYaml.ok) {
|
|
215
|
+
if (!publishYaml.value.includes("already published")) {
|
|
216
|
+
errors.push(".github/workflows/publish.yml: missing idempotency check for existing versions")
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
errors.push(publishYaml.error)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 9. Check npm script docs:check exists
|
|
223
|
+
if (pkg.ok) {
|
|
224
|
+
const pkgScripts = pkg.value.scripts || {}
|
|
225
|
+
if (!pkgScripts["docs:check"]) {
|
|
226
|
+
warnings.push("package.json: missing docs:check npm script")
|
|
227
|
+
}
|
|
228
|
+
if (expectedVersion.startsWith("13.") && !pkgScripts["evals:v13"]) {
|
|
229
|
+
errors.push("package.json: V13 requires evals:v13 regression script")
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
pass: errors.length === 0,
|
|
235
|
+
errors,
|
|
236
|
+
warnings,
|
|
237
|
+
version: expectedVersion,
|
|
238
|
+
skillCount,
|
|
239
|
+
commandCount,
|
|
240
|
+
subagentCount: agentCount,
|
|
241
|
+
staticScenarioCount,
|
|
242
|
+
routerCaseCount,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
247
|
+
const result = checkReleaseConsistency(process.env.UES_BUNDLE_ROOT || DEFAULT_ROOT)
|
|
248
|
+
if (result.warnings.length) {
|
|
249
|
+
console.warn("Warnings:")
|
|
250
|
+
for (const w of result.warnings) console.warn(` WARN: ${w}`)
|
|
251
|
+
}
|
|
252
|
+
if (result.errors.length) {
|
|
253
|
+
console.error("Release consistency check FAILED:")
|
|
254
|
+
for (const e of result.errors) console.error(` - ${e}`)
|
|
255
|
+
process.exit(1)
|
|
256
|
+
}
|
|
257
|
+
console.log(
|
|
258
|
+
`Release consistency check PASS: package=${result.version}, skills=${result.skillCount}, commands=${result.commandCount}, subagents=${result.subagentCount}`,
|
|
259
|
+
)
|
|
260
|
+
}
|