opencode-agent-skill 12.0.0-beta.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 +37 -0
- package/README.md +110 -11
- package/bin/ocskill.mjs +394 -84
- package/docs/NPM-PUBLISH.md +20 -6
- package/docs/OPENCODE-COMPAT.md +12 -2
- package/docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md +75 -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/installer.mjs +49 -16
- package/lib/task-engine.mjs +19 -9
- package/lib/task-graph.mjs +32 -0
- package/lib/text-encoding.mjs +74 -0
- package/lib/worktree-sandbox.mjs +141 -12
- package/package.json +5 -4
- package/scripts/check-release-consistency.mjs +33 -1
- package/scripts/smoke-packed-install.mjs +39 -3
- package/scripts/smoke-plain-install.mjs +27 -7
- package/scripts/validate.mjs +18 -1
package/lib/installer.mjs
CHANGED
|
@@ -23,6 +23,15 @@ function validManagedPlugin(value) {
|
|
|
23
23
|
return value === ROUTER_PLUGIN_STATE
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function validPromptAlias(value) {
|
|
27
|
+
if (typeof value !== "string" || !value.startsWith(SKILL_PREFIX)) return false
|
|
28
|
+
return validResourceID(value.slice(SKILL_PREFIX.length))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function promptAliasTemplateName(value) {
|
|
32
|
+
return value.slice(SKILL_PREFIX.length) + ".md"
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
function isOwnedPackageName(value) {
|
|
27
36
|
return !value || value === PACKAGE_NAME || value === LEGACY_PACKAGE_NAME
|
|
28
37
|
}
|
|
@@ -94,7 +103,7 @@ async function writeManagedFile(file, content, warnings) {
|
|
|
94
103
|
|
|
95
104
|
async function readManagedState(file, warnings = []) {
|
|
96
105
|
const raw = await readText(file)
|
|
97
|
-
if (!raw) return { kind: "empty", state: { skills: [], commands: [], agents: [], plugins: [] } }
|
|
106
|
+
if (!raw) return { kind: "empty", state: { skills: [], commands: [], promptAliases: [], agents: [], plugins: [] } }
|
|
98
107
|
|
|
99
108
|
try {
|
|
100
109
|
const parsed = JSON.parse(raw)
|
|
@@ -103,7 +112,7 @@ async function readManagedState(file, warnings = []) {
|
|
|
103
112
|
return {
|
|
104
113
|
kind: "foreign",
|
|
105
114
|
package: parsed.package,
|
|
106
|
-
state: { skills: [], commands: [], agents: [], plugins: [] },
|
|
115
|
+
state: { skills: [], commands: [], promptAliases: [], agents: [], plugins: [] },
|
|
107
116
|
}
|
|
108
117
|
}
|
|
109
118
|
return {
|
|
@@ -112,13 +121,14 @@ async function readManagedState(file, warnings = []) {
|
|
|
112
121
|
...parsed,
|
|
113
122
|
skills: Array.isArray(parsed.skills) ? parsed.skills.filter(validSkillID) : [],
|
|
114
123
|
commands: Array.isArray(parsed.commands) ? parsed.commands.filter(validManagedMarkdown) : [],
|
|
124
|
+
promptAliases: Array.isArray(parsed.promptAliases) ? parsed.promptAliases.filter(validPromptAlias) : [],
|
|
115
125
|
agents: Array.isArray(parsed.agents) ? parsed.agents.filter(validManagedMarkdown) : [],
|
|
116
126
|
plugins: Array.isArray(parsed.plugins) ? parsed.plugins.filter(validManagedPlugin) : [],
|
|
117
127
|
},
|
|
118
128
|
}
|
|
119
129
|
} catch {
|
|
120
130
|
warnings.push(`Invalid UES state file; run 'ocskill install' to rebuild it: ${file}`)
|
|
121
|
-
return { kind: "invalid", state: { skills: [], commands: [], agents: [], plugins: [] } }
|
|
131
|
+
return { kind: "invalid", state: { skills: [], commands: [], promptAliases: [], agents: [], plugins: [] } }
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
134
|
|
|
@@ -310,7 +320,7 @@ export async function installResources(options = {}) {
|
|
|
310
320
|
const stateFile = path.join(stateDir, "state.json")
|
|
311
321
|
const warnings = []
|
|
312
322
|
const openCodeMajor = options.openCodeMajor ?? detectOpenCodeMajor()
|
|
313
|
-
const installed = { skills: [], commands: [], agents: [], plugins: [] }
|
|
323
|
+
const installed = { skills: [], commands: [], promptAliases: [], agents: [], plugins: [] }
|
|
314
324
|
const previous = await readManagedState(stateFile, warnings)
|
|
315
325
|
const previousState = previous.state
|
|
316
326
|
const modelPolicy = await readModelPolicy(configDir)
|
|
@@ -366,25 +376,31 @@ export async function installResources(options = {}) {
|
|
|
366
376
|
|
|
367
377
|
const sourceCommands = path.join(sourceRoot, "global-config", "commands")
|
|
368
378
|
const commandEntries = await readdir(sourceCommands, { withFileTypes: true })
|
|
369
|
-
|
|
379
|
+
const validCommandEntries = []
|
|
370
380
|
for (const entry of commandEntries) {
|
|
371
381
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue
|
|
372
|
-
|
|
373
382
|
const id = entry.name.slice(0, -3)
|
|
374
383
|
if (!validResourceID(id)) {
|
|
375
384
|
warnings.push(`Skipped command with invalid resource ID: ${id}`)
|
|
376
385
|
continue
|
|
377
386
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const source = await readFile(path.join(sourceCommands, entry.name), "utf8")
|
|
381
|
-
const normalizedSource = normalizeManagedMarker(source)
|
|
382
|
-
const content = hasManagedMarker(normalizedSource)
|
|
383
|
-
? normalizedSource
|
|
384
|
-
: `${normalizedSource.trimEnd()}\n\n${MANAGED_MARKER}\n`
|
|
387
|
+
validCommandEntries.push(entry)
|
|
388
|
+
}
|
|
385
389
|
|
|
386
|
-
|
|
387
|
-
|
|
390
|
+
if (openCodeMajor < 2) {
|
|
391
|
+
for (const entry of validCommandEntries) {
|
|
392
|
+
const id = entry.name.slice(0, -3)
|
|
393
|
+
const targetName = `${SKILL_PREFIX}${id}.md`
|
|
394
|
+
const targetFile = path.join(commandsTarget, targetName)
|
|
395
|
+
const source = await readFile(path.join(sourceCommands, entry.name), "utf8")
|
|
396
|
+
const normalizedSource = normalizeManagedMarker(source)
|
|
397
|
+
const content = hasManagedMarker(normalizedSource)
|
|
398
|
+
? normalizedSource
|
|
399
|
+
: `${normalizedSource.trimEnd()}\n\n${MANAGED_MARKER}\n`
|
|
400
|
+
|
|
401
|
+
if (await writeManagedFile(targetFile, content, warnings)) {
|
|
402
|
+
installed.commands.push(targetName)
|
|
403
|
+
}
|
|
388
404
|
}
|
|
389
405
|
}
|
|
390
406
|
|
|
@@ -425,6 +441,14 @@ export async function installResources(options = {}) {
|
|
|
425
441
|
if (existsSync(path.join(sourcePluginDir, "index.js"))) {
|
|
426
442
|
if (await installPluginDirectory(sourcePluginDir, targetPluginDir, warnings)) {
|
|
427
443
|
installed.plugins.push(ROUTER_PLUGIN_STATE)
|
|
444
|
+
|
|
445
|
+
const promptAliasDir = path.join(targetPluginDir, "command-templates")
|
|
446
|
+
await mkdir(promptAliasDir, { recursive: true })
|
|
447
|
+
for (const entry of validCommandEntries) {
|
|
448
|
+
const id = entry.name.slice(0, -3)
|
|
449
|
+
await cp(path.join(sourceCommands, entry.name), path.join(promptAliasDir, entry.name))
|
|
450
|
+
installed.promptAliases.push(`${SKILL_PREFIX}${id}`)
|
|
451
|
+
}
|
|
428
452
|
}
|
|
429
453
|
}
|
|
430
454
|
|
|
@@ -457,7 +481,7 @@ export async function installResources(options = {}) {
|
|
|
457
481
|
}
|
|
458
482
|
|
|
459
483
|
const state = {
|
|
460
|
-
schemaVersion:
|
|
484
|
+
schemaVersion: 3,
|
|
461
485
|
package: PACKAGE_NAME,
|
|
462
486
|
version: await packageVersion(),
|
|
463
487
|
configDir,
|
|
@@ -465,6 +489,7 @@ export async function installResources(options = {}) {
|
|
|
465
489
|
openCodeMajor,
|
|
466
490
|
skills: installed.skills.sort(),
|
|
467
491
|
commands: installed.commands.sort(),
|
|
492
|
+
promptAliases: installed.promptAliases.sort(),
|
|
468
493
|
agents: installed.agents.sort(),
|
|
469
494
|
plugins: installed.plugins.sort(),
|
|
470
495
|
}
|
|
@@ -611,11 +636,13 @@ export async function getStatus() {
|
|
|
611
636
|
|
|
612
637
|
state.skills = Array.isArray(state.skills) ? state.skills.filter(validSkillID) : []
|
|
613
638
|
state.commands = Array.isArray(state.commands) ? state.commands.filter(validManagedMarkdown) : []
|
|
639
|
+
state.promptAliases = Array.isArray(state.promptAliases) ? state.promptAliases.filter(validPromptAlias) : []
|
|
614
640
|
state.agents = Array.isArray(state.agents) ? state.agents.filter(validManagedMarkdown) : []
|
|
615
641
|
state.plugins = Array.isArray(state.plugins) ? state.plugins.filter(validManagedPlugin) : []
|
|
616
642
|
|
|
617
643
|
let skillsPresent = 0
|
|
618
644
|
let commandsPresent = 0
|
|
645
|
+
let promptAliasesPresent = 0
|
|
619
646
|
let agentsPresent = 0
|
|
620
647
|
let pluginsPresent = 0
|
|
621
648
|
|
|
@@ -625,6 +652,11 @@ export async function getStatus() {
|
|
|
625
652
|
for (const name of state.commands || []) {
|
|
626
653
|
if (existsSync(path.join(configDir, "commands", name))) commandsPresent += 1
|
|
627
654
|
}
|
|
655
|
+
for (const name of state.promptAliases || []) {
|
|
656
|
+
if (existsSync(path.join(configDir, "plugins", "ues-router", "command-templates", promptAliasTemplateName(name)))) {
|
|
657
|
+
promptAliasesPresent += 1
|
|
658
|
+
}
|
|
659
|
+
}
|
|
628
660
|
for (const name of state.agents || []) {
|
|
629
661
|
if (existsSync(path.join(configDir, "agents", name))) agentsPresent += 1
|
|
630
662
|
}
|
|
@@ -639,6 +671,7 @@ export async function getStatus() {
|
|
|
639
671
|
...state,
|
|
640
672
|
skillsPresent,
|
|
641
673
|
commandsPresent,
|
|
674
|
+
promptAliasesPresent,
|
|
642
675
|
agentsPresent,
|
|
643
676
|
pluginsPresent,
|
|
644
677
|
workflowPresent: agents.includes(AGENTS_BEGIN),
|
package/lib/task-engine.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from "node:fs"
|
|
2
|
-
import { mkdir,
|
|
2
|
+
import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises"
|
|
3
3
|
import { spawnSync } from "node:child_process"
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto"
|
|
5
5
|
import path from "node:path"
|
|
6
|
-
import { analyzePlan, taskFiles, validatePlan } from "./task-graph.mjs"
|
|
6
|
+
import { analyzePlan, taskFiles, taskVerificationCommands, validatePlan } from "./task-graph.mjs"
|
|
7
7
|
import { buildContextManifest } from "./context-manifest.mjs"
|
|
8
8
|
import { relevantAcceptedLearnings } from "./learning-engine.mjs"
|
|
9
9
|
import { validateVerificationReceipt } from "./evidence-receipt.mjs"
|
|
@@ -17,6 +17,7 @@ import { buildPromptEnvelope } from "./prompt-cache.mjs"
|
|
|
17
17
|
import { externalizeContextExcerpts } from "./context-engine-v11.mjs"
|
|
18
18
|
import { assertActivePlanScope, persistPlanScope } from "./work-plan-scope.mjs"
|
|
19
19
|
import { classifyDecisionPolicy } from "./decision-policy.mjs"
|
|
20
|
+
import { readTextAuto } from "./text-encoding.mjs"
|
|
20
21
|
|
|
21
22
|
const WORK_DIR = ".ues-work"
|
|
22
23
|
const STATE_SCHEMA = 4
|
|
@@ -59,7 +60,7 @@ export function workPaths(root, slug) {
|
|
|
59
60
|
|
|
60
61
|
async function readJson(file, fallback = null) {
|
|
61
62
|
try {
|
|
62
|
-
return JSON.parse(
|
|
63
|
+
return JSON.parse(readTextAuto(file))
|
|
63
64
|
} catch (error) {
|
|
64
65
|
if (error?.code === "ENOENT") return fallback
|
|
65
66
|
throw error
|
|
@@ -340,7 +341,7 @@ function currentEvidencePolicy(state, plan) {
|
|
|
340
341
|
|
|
341
342
|
function taskBrief(task) {
|
|
342
343
|
const files = taskFiles(task)
|
|
343
|
-
return `# ${task.id}: ${task.title}\n\n## Summary\n\n${task.summary || "No summary provided."}\n\n## Dependencies\n\n${(task.dependsOn || []).map((id) => `- ${id}`).join("\n") || "- None"}\n\n## Files\n\n${files.map((file) => `- ${file}`).join("\n") || "- Scope must be established before editing."}\n\n## Acceptance criteria\n\n${(task.acceptance || []).map((item) => `- ${item}`).join("\n")}\n\n## Verification\n\n${(task.verification || []).map((item) => `- ${item}`).join("\n")}\n\n## Risk\n\n${task.risk || "medium"}\n`
|
|
344
|
+
return `# ${task.id}: ${task.title}\n\n## Summary\n\n${task.summary || "No summary provided."}\n\n## Dependencies\n\n${(task.dependsOn || []).map((id) => `- ${id}`).join("\n") || "- None"}\n\n## Files\n\n${files.map((file) => `- ${file}`).join("\n") || "- Scope must be established before editing."}\n\n## Acceptance criteria\n\n${(task.acceptance || []).map((item) => `- ${item}`).join("\n")}\n\n## Verification\n\n${(task.verification || []).map((item) => `- ${item}`).join("\n")}\n\n## Structured verification commands\n\n${taskVerificationCommands(task).map((item) => `- ${[item.command, ...item.args].join(" ")}`).join("\n") || "- None declared"}\n\n## Risk\n\n${task.risk || "medium"}\n`
|
|
344
345
|
}
|
|
345
346
|
|
|
346
347
|
function planIsApproved(state, plan) {
|
|
@@ -351,7 +352,7 @@ function planIsApproved(state, plan) {
|
|
|
351
352
|
|
|
352
353
|
export async function importPlan(root, slug, planInput) {
|
|
353
354
|
const plan = typeof planInput === "string"
|
|
354
|
-
? JSON.parse(
|
|
355
|
+
? JSON.parse(readTextAuto(path.resolve(planInput)))
|
|
355
356
|
: planInput
|
|
356
357
|
|
|
357
358
|
const validation = validatePlan(plan)
|
|
@@ -534,7 +535,9 @@ export async function workStatus(root, slug) {
|
|
|
534
535
|
leaseExpiresAt: value.leaseExpiresAt || null,
|
|
535
536
|
}))
|
|
536
537
|
const taskEntries = Object.values(loaded.state.tasks || {})
|
|
537
|
-
const receiptBacked = taskEntries.filter((value) =>
|
|
538
|
+
const receiptBacked = taskEntries.filter((value) =>
|
|
539
|
+
["receipt-backed", "command-receipt-backed", "independent-agent-receipt-backed"].includes(value.evidenceStrength)
|
|
540
|
+
).length
|
|
538
541
|
const completed = taskEntries.filter((value) => value.status === "completed").length
|
|
539
542
|
|
|
540
543
|
return {
|
|
@@ -823,7 +826,13 @@ export async function completeTask(root, slug, taskID, options = {}) {
|
|
|
823
826
|
record.status = "completed"
|
|
824
827
|
record.completedAt = timestamp
|
|
825
828
|
record.report = path.relative(loaded.paths.root, reportPath).replaceAll("\\", "/")
|
|
826
|
-
|
|
829
|
+
const commandReceipts = acceptedReceipts.filter((item) => item.kind !== "agent-verifier")
|
|
830
|
+
const agentVerifierReceipts = acceptedReceipts.filter((item) => item.kind === "agent-verifier")
|
|
831
|
+
record.evidenceStrength = commandReceipts.length
|
|
832
|
+
? "command-receipt-backed"
|
|
833
|
+
: agentVerifierReceipts.length
|
|
834
|
+
? "independent-agent-receipt-backed"
|
|
835
|
+
: "narrative"
|
|
827
836
|
record.runId = null
|
|
828
837
|
record.owner = null
|
|
829
838
|
record.heartbeatAt = null
|
|
@@ -1085,14 +1094,15 @@ export async function finalizeWork(root, slug, evidence) {
|
|
|
1085
1094
|
async function buildContextPack(loaded, taskID) {
|
|
1086
1095
|
if (!loaded.plan) throw new Error("PLAN.json is missing")
|
|
1087
1096
|
const task = taskByID(loaded.plan, taskID)
|
|
1088
|
-
|
|
1097
|
+
let spec = ""
|
|
1098
|
+
try { spec = readTextAuto(loaded.paths.spec) } catch {}
|
|
1089
1099
|
const dependencyReports = {}
|
|
1090
1100
|
const dependencyReportRefs = {}
|
|
1091
1101
|
|
|
1092
1102
|
for (const dep of task.dependsOn || []) {
|
|
1093
1103
|
const file = path.join(loaded.paths.reports, `${dep}.md`)
|
|
1094
1104
|
if (!existsSync(file)) continue
|
|
1095
|
-
const report =
|
|
1105
|
+
const report = readTextAuto(file)
|
|
1096
1106
|
dependencyReports[dep] = report.slice(0, 6000)
|
|
1097
1107
|
const stored = await putEvidence(loaded.paths.root, report, {
|
|
1098
1108
|
kind: "dependency-report",
|
package/lib/task-graph.mjs
CHANGED
|
@@ -48,6 +48,20 @@ export function taskReadFiles(task) {
|
|
|
48
48
|
return [...new Set((files.read || []).map(normalizeFile).filter(Boolean))].sort()
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
|
|
52
|
+
export function taskVerificationCommands(task) {
|
|
53
|
+
if (!Array.isArray(task?.verificationCommands)) return []
|
|
54
|
+
return task.verificationCommands
|
|
55
|
+
.map((item) => {
|
|
56
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null
|
|
57
|
+
const command = String(item.command || "").trim()
|
|
58
|
+
if (!command) return null
|
|
59
|
+
const args = Array.isArray(item.args) ? item.args.map((value) => String(value)) : []
|
|
60
|
+
return { command, args }
|
|
61
|
+
})
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
function taskMap(plan) {
|
|
52
66
|
return new Map((plan?.tasks || []).map((task) => [task.id, task]))
|
|
53
67
|
}
|
|
@@ -97,6 +111,24 @@ export function validatePlan(plan) {
|
|
|
97
111
|
errors.push(`${id || prefix}: verification must contain at least one concrete check`)
|
|
98
112
|
}
|
|
99
113
|
|
|
114
|
+
if (task.verificationCommands !== undefined) {
|
|
115
|
+
if (!Array.isArray(task.verificationCommands)) {
|
|
116
|
+
errors.push(`${id || prefix}.verificationCommands must be an array when provided`)
|
|
117
|
+
} else {
|
|
118
|
+
for (const [commandIndex, commandSpec] of task.verificationCommands.entries()) {
|
|
119
|
+
const commandPrefix = `${id || prefix}.verificationCommands[${commandIndex}]`
|
|
120
|
+
if (!commandSpec || typeof commandSpec !== "object" || Array.isArray(commandSpec)) {
|
|
121
|
+
errors.push(`${commandPrefix} must be an object`)
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
if (!String(commandSpec.command || "").trim()) errors.push(`${commandPrefix}.command is required`)
|
|
125
|
+
if (commandSpec.args !== undefined && !Array.isArray(commandSpec.args)) {
|
|
126
|
+
errors.push(`${commandPrefix}.args must be an array when provided`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
100
132
|
const risk = task.risk ?? "medium"
|
|
101
133
|
if (!VALID_RISKS.has(risk)) errors.push(`${id || prefix}: invalid risk '${risk}'`)
|
|
102
134
|
|
|
@@ -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
|
+
}
|
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 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",
|
|
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",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"evals:v11": "node --test test/*-v11.test.mjs",
|
|
52
52
|
"evals:v11:validate": "node scripts/validate-v11-suite.mjs",
|
|
53
53
|
"evals:repo-scale:validate": "node scripts/validate-repo-scale-suite.mjs",
|
|
54
|
-
"evals:v12:validate": "node scripts/validate-v12-foundation.mjs"
|
|
54
|
+
"evals:v12:validate": "node scripts/validate-v12-foundation.mjs",
|
|
55
|
+
"evals:v13": "node --test test/*-v13.test.mjs"
|
|
55
56
|
},
|
|
56
57
|
"keywords": [
|
|
57
58
|
"opencode",
|
|
@@ -130,6 +130,35 @@ export function checkReleaseConsistency(root) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
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
|
+
|
|
133
162
|
// 5. Verify docs reflect actual counts (skip historical sections)
|
|
134
163
|
const docsToCheck = ["README.md", "docs/ENGINEERING-DESIGN.md", "docs/OPENCODE-COMPAT.md"]
|
|
135
164
|
for (const doc of docsToCheck) {
|
|
@@ -164,7 +193,7 @@ export function checkReleaseConsistency(root) {
|
|
|
164
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")
|
|
165
194
|
if (!/if:\s*always\(\)/m.test(ciYaml.value)) errors.push(".github/workflows/ci.yml: aggregate gate must use if: always()")
|
|
166
195
|
if (!ciYaml.value.includes("docs:check")) errors.push(".github/workflows/ci.yml: missing docs:check job")
|
|
167
|
-
for (const check of ["evals:v12:validate","evals:repo-scale:validate"]) {
|
|
196
|
+
for (const check of ["evals:v12:validate", ...(expectedVersion.startsWith("13.") ? ["evals:v13"] : []), "evals:repo-scale:validate"]) {
|
|
168
197
|
if (!ciYaml.value.includes(check)) errors.push(".github/workflows/ci.yml: missing " + check + " job")
|
|
169
198
|
}
|
|
170
199
|
} else {
|
|
@@ -196,6 +225,9 @@ export function checkReleaseConsistency(root) {
|
|
|
196
225
|
if (!pkgScripts["docs:check"]) {
|
|
197
226
|
warnings.push("package.json: missing docs:check npm script")
|
|
198
227
|
}
|
|
228
|
+
if (expectedVersion.startsWith("13.") && !pkgScripts["evals:v13"]) {
|
|
229
|
+
errors.push("package.json: V13 requires evals:v13 regression script")
|
|
230
|
+
}
|
|
199
231
|
}
|
|
200
232
|
|
|
201
233
|
return {
|