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/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
- const targetName = `${SKILL_PREFIX}${id}.md`
379
- const targetFile = path.join(commandsTarget, targetName)
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
- if (await writeManagedFile(targetFile, content, warnings)) {
387
- installed.commands.push(targetName)
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: 2,
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),
@@ -1,9 +1,9 @@
1
1
  import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from "node:fs"
2
- import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"
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(await readFile(file, "utf8"))
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(await readFile(path.resolve(planInput), "utf8"))
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) => value.evidenceStrength === "receipt-backed").length
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
- record.evidenceStrength = acceptedReceipts.length ? "receipt-backed" : "narrative"
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
- const spec = await readFile(loaded.paths.spec, "utf8").catch(() => "")
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 = await readFile(file, "utf8")
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",
@@ -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
+ }
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs"
2
- import { mkdir, rm } from "node:fs/promises"
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
- if (status.stdout.trim() && options.allowDirtyRoot !== true) {
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
- return { schemaVersion: 1, root, dir, branch, slug, taskID, startPoint }
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
- // Resolve the merge base against the canonical root HEAD explicitly.
65
- const rootHead = git(root, ["rev-parse", "HEAD"])
66
- const sandboxHead = git(dir, ["rev-parse", "HEAD"])
67
- if (rootHead.status !== 0 || sandboxHead.status !== 0) throw new Error("could not resolve worktree HEADs")
68
- const base = git(dir, ["merge-base", rootHead.stdout.trim(), sandboxHead.stdout.trim()])
69
- if (base.status !== 0) throw new Error((base.stderr || base.stdout || "git merge-base failed").trim())
70
- const baseSha = base.stdout.trim()
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
- if (overlap.length) {
87
- throw new Error("sandbox integration conflicts with existing root changes: " + overlap.join(", "))
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": "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",
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 {