opencode-agent-skill 12.0.0-beta.0 → 13.0.0-beta.1

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/bin/ocskill.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { writeFileSync } from "node:fs"
2
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
3
3
  import { spawnSync } from "node:child_process"
4
4
  import { fileURLToPath } from "node:url"
5
5
  import path from "node:path"
@@ -62,7 +62,7 @@ import { buildVerificationPlan } from "../lib/verification-plan.mjs"
62
62
  import { resolveAdaptiveModel, resolveCapabilityModel, resolveModel } from "../lib/model-policy.mjs"
63
63
  import { createVerificationReceipt } from "../lib/evidence-receipt.mjs"
64
64
  import { classifyEngineeringTask } from "../lib/orchestrator-policy.mjs"
65
- import { createTaskSandbox, integrateTaskSandbox, listTaskSandboxes, removeTaskSandbox } from "../lib/worktree-sandbox.mjs"
65
+ import { createTaskSandbox, integrateTaskSandbox, listTaskSandboxes, removeTaskSandbox, rollbackTaskSandbox } from "../lib/worktree-sandbox.mjs"
66
66
  import { analyzeEvalTraces, saveLearningAnalysis, readLearningState, acceptLearning, promoteLearning } from "../lib/learning-engine.mjs"
67
67
  import { hermesStatus, buildHermesDelegationPrompt, buildHermesWorkflowPrompt, hermesOneShotArgs, hermesSidecarPlan } from "../lib/hermes-bridge.mjs"
68
68
  import { readModelPolicy, recordModelPerformance, validateModelID, writeModelPolicy } from "../lib/model-config.mjs"
@@ -77,6 +77,7 @@ import { planDynamicWorkflow } from "../lib/dynamic-workflow.mjs"
77
77
  import { lintSkillCatalog } from "../lib/skill-quality.mjs"
78
78
  import { designTokenEvidence, extractDesignTokens, inspectResponsiveLayout } from "../lib/ui-inspector.mjs"
79
79
  import {
80
+ cliErrorPayload,
80
81
  clipOutput,
81
82
  errorMessage,
82
83
  optionInt,
@@ -85,10 +86,22 @@ import {
85
86
  positionalArg,
86
87
  readJsonFile,
87
88
  readTextFile,
89
+ usageError,
88
90
  } from "../lib/cli-utils.mjs"
89
91
 
90
- const args = process.argv.slice(2)
92
+ const rawArgs = process.argv.slice(2)
93
+ const rawSeparator = rawArgs.indexOf("--")
94
+ const jsonOutput = rawArgs.some((value, index) =>
95
+ value === "--json" && (rawSeparator < 0 || index < rawSeparator),
96
+ )
97
+ const args = rawArgs.filter((value, index) =>
98
+ !(value === "--json" && (rawSeparator < 0 || index < rawSeparator)),
99
+ )
91
100
  const command = args[0] || "help"
101
+ const argsSeparator = args.indexOf("--")
102
+ const helpRequested = args.some((value, index) =>
103
+ (value === "--help" || value === "-h") && (argsSeparator < 0 || index < argsSeparator),
104
+ )
92
105
  const force = args.includes("--force")
93
106
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
94
107
 
@@ -107,7 +120,11 @@ Usage:
107
120
  ocskill impact <query> [dir] Search likely impact paths and matching lines
108
121
  ocskill evidence [dir] Collect stack, verification and Git evidence
109
122
  ocskill working-tree [dir] Report Git branch/HEAD/dirty state
110
- ocskill repo-graph [dir] Build a bounded source import/dependency graph
123
+ ocskill diff [dir] [--out file] Emit Git diff safely as UTF-8 (Windows-safe)
124
+ ocskill text-read <file> [--start N] [--max N] Read UTF-8/UTF-16 text safely with bounded output
125
+ ocskill normalize-text <file> Convert UTF-8/UTF-16 text files to UTF-8
126
+ ocskill repo-graph [dir] [--compact] [--max-files N]
127
+ Build a bounded source import/dependency graph; compact omits full node/edge payloads
111
128
  ocskill index <status|build|rebuild> [dir]
112
129
  Build/reuse the persistent incremental semantic index
113
130
  ocskill aci <search|refs|view|text> ...
@@ -124,7 +141,7 @@ Usage:
124
141
  ocskill model-policy <role> [--attempt N]
125
142
  Resolve light/standard/heavy escalation tier
126
143
  ocskill task-policy <text> Classify task mode/risk/context/model tier
127
- ocskill sandbox <action> ... Create, integrate and clean isolated Git worktree sandboxes
144
+ ocskill sandbox <action> ... Create, integrate, rollback and clean isolated Git worktree sandboxes
128
145
  Also supports capability/exec for fail-closed container verification
129
146
  ocskill learn <action> ... Analyze eval traces and promote benchmark-validated lessons
130
147
  ocskill hermes <action> ... Optional Hermes sidecar/status/task/workflow planning
@@ -151,12 +168,15 @@ Usage:
151
168
  ocskill work plan <slug> <plan.json> [dir]
152
169
  ocskill work status|resume <slug> [dir]
153
170
  ocskill work gate-receipt <slug> <plan|integration> [dir] --evidence <text> [--verdict PASS|FAIL|PARTIAL] [--verifier <role>] [--session-id <id>] [--report-file <file>] [--out <file>]
171
+ ocskill work agent-receipt <slug> <task-id> [dir] --run-id <id> --verdict PASS|FAIL --evidence <text>
154
172
  ocskill work approve-plan <slug> [dir] --evidence <plan-checker-evidence> [--receipt-file <file>]
155
173
  ocskill work start <slug> <task-id> [dir] [--lease-ms N]
156
174
  ocskill work attach-session <slug> <task-id> [dir] --run-id <id> --session-id <id> [--execution-dir <dir>] [--sandbox-dir <dir>]
157
175
  ocskill work heartbeat <slug> <task-id> [dir] --run-id <id>
158
176
  ocskill work recover <slug> [dir] [--force]
159
177
  ocskill work recover-task <slug> <task-id> [dir] [--force] [--reason <text>]
178
+ ocskill work checkpoint <slug> <task-id> [dir] --run-id <id> [--reason <text>]
179
+ ocskill work checkpoint-resumed <slug> <task-id> [dir] --run-id <id> [--reason <text>]
160
180
  ocskill work events <slug> [dir] [--limit N]
161
181
  ocskill work verify-command <slug> <task-id> [dir] --run-id <id> -- <command> [args...]
162
182
  ocskill work complete <slug> <task-id> [dir] --run-id <id> --evidence <text> [--report-file <file>]
@@ -178,6 +198,95 @@ Usage:
178
198
  `)
179
199
  }
180
200
 
201
+ function printCommandHelp(commandName, subcommand) {
202
+ const workUsage = {
203
+ init: "ocskill work init <slug> [dir] --goal <text>",
204
+ plan: "ocskill work plan <slug> <plan.json> [dir]",
205
+ status: "ocskill work status [slug|dir]",
206
+ resume: "ocskill work resume <slug> [dir]",
207
+ "gate-receipt": "ocskill work gate-receipt <slug> <plan|integration> [dir] --evidence <text>",
208
+ "agent-receipt": "ocskill work agent-receipt <slug> <task-id> [dir] --run-id <id> --verdict PASS|FAIL --evidence <text>",
209
+ "approve-plan": "ocskill work approve-plan <slug> [dir] --evidence <text>",
210
+ start: "ocskill work start <slug> <task-id> [dir]",
211
+ "attach-session": "ocskill work attach-session <slug> <task-id> [dir] --run-id <id> --session-id <id>",
212
+ heartbeat: "ocskill work heartbeat <slug> <task-id> [dir] --run-id <id>",
213
+ recover: "ocskill work recover <slug> [dir]",
214
+ "recover-task": "ocskill work recover-task <slug> <task-id> [dir]",
215
+ checkpoint: "ocskill work checkpoint <slug> <task-id> [dir] --run-id <id> [--reason <text>]",
216
+ "checkpoint-resumed": "ocskill work checkpoint-resumed <slug> <task-id> [dir] --run-id <id> [--reason <text>]",
217
+ events: "ocskill work events <slug> [dir]",
218
+ "verify-command": "ocskill work verify-command <slug> <task-id> [dir] --run-id <id> -- <command> [args...]",
219
+ complete: "ocskill work complete <slug> <task-id> [dir] --run-id <id> --evidence <text>",
220
+ fail: "ocskill work fail <slug> <task-id> [dir] --run-id <id> --reason <text>",
221
+ decision: "ocskill work decision <slug> [dir] --text <decision>",
222
+ block: "ocskill work block <slug> [dir] --text <blocker>",
223
+ unblock: "ocskill work unblock <slug> [dir] --text <blocker>",
224
+ "verify-integration": "ocskill work verify-integration <slug> [dir] --verdict PASS|FAIL|PARTIAL --evidence <text>",
225
+ finalize: "ocskill work finalize <slug> [dir] --evidence <text>",
226
+ }
227
+ if (commandName === "work") {
228
+ if (subcommand && workUsage[subcommand]) console.log("Usage: " + workUsage[subcommand] + "\n")
229
+ else console.log("Usage: ocskill work <action> ...\n\nActions:\n " + Object.keys(workUsage).join("\n "))
230
+ return
231
+ }
232
+ if (commandName === "sandbox") {
233
+ console.log("Usage: ocskill sandbox <capability|exec|list|create|integrate|rollback|remove> ...\n")
234
+ return
235
+ }
236
+ if (commandName === "diff") {
237
+ console.log("Usage: ocskill diff [dir] [--base <ref>] [--out <utf8-file>]\n")
238
+ return
239
+ }
240
+ if (commandName === "text-read") {
241
+ console.log("Usage: ocskill text-read <file> [--start N] [--max N] [--json]\n")
242
+ return
243
+ }
244
+ if (commandName === "normalize-text") {
245
+ console.log("Usage: ocskill normalize-text <file> [--out <utf8-file>]\n")
246
+ return
247
+ }
248
+ printHelp()
249
+ }
250
+
251
+ function printCliError(error, options = {}) {
252
+ const payload = cliErrorPayload(error, {
253
+ command: options.command || args.filter((value) => value !== "--help" && value !== "-h").join(" "),
254
+ usage: options.usage,
255
+ hint: options.hint,
256
+ recoverable: options.recoverable,
257
+ })
258
+ if (jsonOutput) console.error(JSON.stringify(payload))
259
+ else {
260
+ console.error("[ocskill] " + payload.error.code + ": " + payload.error.message)
261
+ if (payload.error.hint) console.error("[ocskill] Hint: " + payload.error.hint)
262
+ }
263
+ process.exitCode = payload.exitCode
264
+ }
265
+
266
+ function pathLikeWorkRoot(value) {
267
+ return !value || value === "." || value === ".." || value.includes("/") || value.includes("\\")
268
+ }
269
+
270
+ function listWorkspaces(root) {
271
+ root = path.resolve(root)
272
+ const base = path.join(root, ".ues-work")
273
+ if (!existsSync(base)) return []
274
+ const result = []
275
+ for (const entry of readdirSync(base, { withFileTypes: true })) {
276
+ if (!entry.isDirectory()) continue
277
+ try {
278
+ const state = JSON.parse(readTextFile(path.join(base, entry.name, "STATE.json")))
279
+ result.push({
280
+ slug: entry.name,
281
+ status: state.status || null,
282
+ goal: state.goal || null,
283
+ updatedAt: state.updatedAt || null,
284
+ nextAction: state.nextAction || null,
285
+ })
286
+ } catch {}
287
+ }
288
+ return result.sort((a, b) => a.slug.localeCompare(b.slug))
289
+ }
181
290
 
182
291
  function run(executable, commandArgs, options = {}) {
183
292
  const common = { stdio: "inherit", ...options }
@@ -241,9 +350,18 @@ async function install() {
241
350
  console.log(`[ocskill] Installed resources for v${result.version}`)
242
351
  console.log(`[ocskill] OpenCode config: ${result.configDir}`)
243
352
  console.log(`[ocskill] Skills: ${result.skills.length}`)
244
- console.log(`[ocskill] Commands: ${result.commands.length}`)
353
+ if (Number(result.openCodeMajor) >= 2) {
354
+ console.log(`[ocskill] Prompt aliases: ${(result.promptAliases || []).length} (session.prompt compatibility path)`)
355
+ } else {
356
+ console.log(`[ocskill] Commands: ${result.commands.length}`)
357
+ }
245
358
  console.log(`[ocskill] Subagents: ${result.agents.length}`)
246
359
  console.log(`[ocskill] OpenCode major: ${result.openCodeMajor}`)
360
+ console.log(
361
+ Number(result.openCodeMajor) >= 2
362
+ ? "[ocskill] V13 parallel: runtime capability-gated; open a new OpenCode session and check ues.capabilities."
363
+ : "[ocskill] V13 parallel: unavailable on OpenCode 1.x; CLI/durable workflow remains available.",
364
+ )
247
365
  if ((result.plugins || []).length) console.log(`[ocskill] Router plugins: ${result.plugins.length}`)
248
366
  for (const warning of result.warnings) console.warn(`[ocskill] WARNING: ${warning}`)
249
367
  console.log("[ocskill] Start a new OpenCode session to pick up workflow changes.")
@@ -268,15 +386,25 @@ async function status() {
268
386
  console.log(`[ocskill] Sync: ${synced ? "OK" : "OUTDATED - run ocskill install"}`)
269
387
  console.log(`[ocskill] Config: ${result.configDir}`)
270
388
  console.log(`[ocskill] Skills: ${result.skillsPresent}/${result.skills.length}`)
271
- console.log(`[ocskill] Commands: ${result.commandsPresent}/${result.commands.length}`)
389
+ if (Number(result.openCodeMajor) >= 2) {
390
+ console.log(`[ocskill] Prompt aliases: ${result.promptAliasesPresent || 0}/${(result.promptAliases || []).length}`)
391
+ } else {
392
+ console.log(`[ocskill] Commands: ${result.commandsPresent}/${result.commands.length}`)
393
+ }
272
394
  console.log(`[ocskill] Subagents: ${result.agentsPresent}/${(result.agents || []).length}`)
273
395
  console.log(`[ocskill] OpenCode major: ${result.openCodeMajor ?? "legacy/unknown"}`)
396
+ console.log(
397
+ Number(result.openCodeMajor) >= 2
398
+ ? "[ocskill] V13 parallel: runtime capability-gated; verify freshDispatch with ues.capabilities in a new session."
399
+ : "[ocskill] V13 parallel: unavailable on OpenCode 1.x; use serial durable execution or upgrade the runtime.",
400
+ )
274
401
  if ((result.plugins || []).length) console.log(`[ocskill] Router plugins: ${result.pluginsPresent}/${result.plugins.length}`)
275
402
  console.log(`[ocskill] Workflow: ${result.workflowPresent ? "OK" : "MISSING"}`)
276
403
 
277
404
  if (!synced ||
278
405
  result.skillsPresent !== result.skills.length ||
279
406
  result.commandsPresent !== result.commands.length ||
407
+ (result.promptAliasesPresent || 0) !== (result.promptAliases || []).length ||
280
408
  result.agentsPresent !== (result.agents || []).length ||
281
409
  result.pluginsPresent !== (result.plugins || []).length ||
282
410
  !result.workflowPresent) {
@@ -332,8 +460,7 @@ async function inspectRepository() {
332
460
  async function inspectImpact() {
333
461
  const query = args[1]
334
462
  if (!query) {
335
- console.error("Usage: ocskill impact <query> [dir]")
336
- process.exitCode = 2
463
+ printCliError(Object.assign(new Error("Usage: ocskill impact <query> [dir]"), { code: "UES_USAGE", exitCode: 2 }))
337
464
  return
338
465
  }
339
466
  printJson(await impactMap(positionalArg(args, 2) || process.cwd(), query))
@@ -356,7 +483,26 @@ async function inspectTests() {
356
483
  }
357
484
 
358
485
  async function inspectRepoGraph() {
359
- printJson(await buildRepoGraph(positionalArg(args, 1) || process.cwd()))
486
+ const root = positionalArg(args, 1) || process.cwd()
487
+ const parsedMaxFiles = optionInt(args, "--max-files", 2500)
488
+ const maxFiles = Number.isFinite(parsedMaxFiles)
489
+ ? Math.max(100, Math.min(parsedMaxFiles, 10000))
490
+ : 2500
491
+ const graph = await buildRepoGraph(root, { maxFiles })
492
+ if (args.includes("--compact")) {
493
+ printJson({
494
+ schemaVersion: graph.schemaVersion,
495
+ root: graph.root,
496
+ scannedFiles: graph.scannedFiles,
497
+ truncated: graph.truncated,
498
+ nodeCount: graph.nodes.length,
499
+ edgeCount: graph.edges.length,
500
+ hotspots: graph.hotspots,
501
+ externalImports: graph.externalImports,
502
+ })
503
+ return
504
+ }
505
+ printJson(graph)
360
506
  }
361
507
 
362
508
  async function semanticIndexControl() {
@@ -374,8 +520,7 @@ async function semanticIndexControl() {
374
520
  }
375
521
  throw new Error("Usage: ocskill index <status|build|rebuild> [dir]")
376
522
  } catch (error) {
377
- console.error(errorMessage(error))
378
- process.exitCode = 1
523
+ printCliError(error)
379
524
  }
380
525
  }
381
526
 
@@ -416,8 +561,7 @@ async function aciControl() {
416
561
  }
417
562
  throw new Error("Usage: ocskill aci <search|refs|view|text> ...")
418
563
  } catch (error) {
419
- console.error(errorMessage(error))
420
- process.exitCode = 1
564
+ printCliError(error)
421
565
  }
422
566
  }
423
567
 
@@ -446,8 +590,7 @@ async function traceControl() {
446
590
  }
447
591
  throw new Error("Usage: ocskill trace <show|append> ...")
448
592
  } catch (error) {
449
- console.error(errorMessage(error))
450
- process.exitCode = 1
593
+ printCliError(error)
451
594
  }
452
595
  }
453
596
 
@@ -464,8 +607,7 @@ async function inspectVerificationPlan() {
464
607
  async function inspectTaskGraph() {
465
608
  const file = args[1]
466
609
  if (!file) {
467
- console.error("Usage: ocskill task-graph <plan.json>")
468
- process.exitCode = 2
610
+ printCliError(Object.assign(new Error("Usage: ocskill task-graph <plan.json>"), { code: "UES_USAGE", exitCode: 2 }))
469
611
  return
470
612
  }
471
613
  try {
@@ -474,8 +616,7 @@ async function inspectTaskGraph() {
474
616
  printJson(analysis)
475
617
  if (!analysis.valid) process.exitCode = 1
476
618
  } catch (error) {
477
- console.error(errorMessage(error))
478
- process.exitCode = 1
619
+ printCliError(error)
479
620
  }
480
621
  }
481
622
 
@@ -483,8 +624,7 @@ async function inspectContextPack() {
483
624
  const slug = args[1]
484
625
  const taskID = args[2]
485
626
  if (!slug || !taskID) {
486
- console.error("Usage: ocskill context-pack <slug> <task-id> [dir]")
487
- process.exitCode = 2
627
+ printCliError(Object.assign(new Error("Usage: ocskill context-pack <slug> <task-id> [dir]"), { code: "UES_USAGE", exitCode: 2 }))
488
628
  return
489
629
  }
490
630
  printJson(await contextPack(positionalArg(args, 3) || process.cwd(), slug, taskID))
@@ -493,9 +633,56 @@ async function inspectContextPack() {
493
633
  async function workControl() {
494
634
  const action = args[1]
495
635
  const slug = args[2]
496
- if (!action || !slug) {
497
- console.error("Usage: ocskill work <init|plan|status|resume|gate-receipt|approve-plan|start|attach-session|heartbeat|recover|recover-task|events|verify-command|complete|fail|decision|block|unblock|verify-integration|finalize> <slug> ...")
498
- process.exitCode = 2
636
+ if (!action) {
637
+ const error = new Error("Usage: ocskill work <action> ...")
638
+ error.code = "UES_USAGE"
639
+ error.exitCode = 2
640
+ printCliError(error)
641
+ return
642
+ }
643
+
644
+ if (action === "status" && pathLikeWorkRoot(slug)) {
645
+ const root = path.resolve(slug || process.cwd())
646
+ const workspaces = listWorkspaces(root)
647
+ const active = workspaces.filter((item) => item.status !== "completed")
648
+ const candidate = active.length === 1
649
+ ? active[0]
650
+ : active.length === 0 && workspaces.length === 1
651
+ ? workspaces[0]
652
+ : null
653
+ if (candidate) {
654
+ const status = await workStatus(root, candidate.slug)
655
+ printJson({
656
+ ...status,
657
+ autoResolved: true,
658
+ autoResolvedSlug: candidate.slug,
659
+ requestedRoot: root,
660
+ })
661
+ } else {
662
+ printJson({
663
+ schemaVersion: 1,
664
+ root,
665
+ autoResolved: false,
666
+ workspaces,
667
+ hint: workspaces.length > 1 ? "Pass an explicit work slug because multiple workspaces exist." : null,
668
+ })
669
+ }
670
+ return
671
+ }
672
+
673
+ if (!slug) {
674
+ const error = usageError("Usage: ocskill work " + action + " <slug> ...")
675
+ printCliError(error)
676
+ return
677
+ }
678
+
679
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(String(slug))) {
680
+ printCliError(usageError(
681
+ "Invalid work slug '" + String(slug) + "'. Use lowercase letters, numbers and dashes only.",
682
+ ), {
683
+ usage: "Usage: ocskill work " + action + " <slug> ...",
684
+ hint: "Example slug: recovery-2-foundation",
685
+ })
499
686
  return
500
687
  }
501
688
 
@@ -552,6 +739,44 @@ async function workControl() {
552
739
  }
553
740
  return
554
741
  }
742
+ if (action === "agent-receipt") {
743
+ const taskID = args[3]
744
+ const root = positionalArg(args, 4) || process.cwd()
745
+ const runId = optionValue(args, "--run-id")
746
+ const verdict = String(optionValue(args, "--verdict") || "").toUpperCase()
747
+ const evidence = String(optionValue(args, "--evidence") || "").trim()
748
+ const verifier = String(optionValue(args, "--verifier") || "ues-verifier")
749
+ const sessionID = optionValue(args, "--session-id")
750
+ if (!taskID || !runId || !["PASS", "FAIL"].includes(verdict) || !evidence) {
751
+ const error = new Error("Usage: ocskill work agent-receipt <slug> <task-id> [dir] --run-id <id> --verdict PASS|FAIL --evidence <text> [--verifier <role>] [--session-id <id>]")
752
+ error.code = "UES_USAGE"
753
+ error.exitCode = 2
754
+ throw error
755
+ }
756
+ const fingerprint = workspaceFingerprint(root)
757
+ const timestamp = new Date().toISOString()
758
+ const receipt = createVerificationReceipt({
759
+ task: taskID,
760
+ runId,
761
+ command: "ues-agent-verifier",
762
+ args: [verifier, ...(sessionID ? [sessionID] : [])],
763
+ cwd: path.resolve(root),
764
+ exitCode: verdict === "PASS" ? 0 : 1,
765
+ startedAt: timestamp,
766
+ finishedAt: timestamp,
767
+ durationMs: 0,
768
+ stdout: evidence,
769
+ stderr: verdict === "PASS" ? "" : evidence,
770
+ workspaceBefore: fingerprint,
771
+ workspaceAfter: fingerprint,
772
+ })
773
+ receipt.kind = "agent-verifier"
774
+ receipt.verifier = verifier
775
+ receipt.sessionID = sessionID || null
776
+ receipt.evidence = evidence
777
+ printJson(await recordVerificationReceipt(root, slug, taskID, receipt))
778
+ return
779
+ }
555
780
  if (action === "approve-plan") {
556
781
  const root = positionalArg(args, 3) || process.cwd()
557
782
  const receiptFile = optionValue(args, "--receipt-file")
@@ -740,19 +965,20 @@ async function workControl() {
740
965
  return
741
966
  }
742
967
 
743
- throw new Error("Unknown work action: " + action)
968
+ throw usageError("Unknown work action: " + action)
744
969
  } catch (error) {
745
- console.error(errorMessage(error))
746
- if (error?.validation) printJson(error.validation)
747
- process.exitCode = 1
970
+ if (error?.validation && !jsonOutput) printJson(error.validation)
971
+ printCliError(error, {
972
+ command: "work " + String(action || ""),
973
+ recoverable: error?.code === "UES_USAGE",
974
+ })
748
975
  }
749
976
  }
750
977
 
751
978
  async function modelPolicy() {
752
979
  const role = args[1]
753
980
  if (!role) {
754
- console.error("Usage: ocskill model-policy <role> [--attempt N]")
755
- process.exitCode = 2
981
+ printCliError(Object.assign(new Error("Usage: ocskill model-policy <role> [--attempt N]"), { code: "UES_USAGE", exitCode: 2 }))
756
982
  return
757
983
  }
758
984
  const attempt = optionInt(args, "--attempt", 1)
@@ -786,8 +1012,7 @@ async function modelsControl() {
786
1012
  const tier = args[2]
787
1013
  const model = args[3]
788
1014
  if (!["light", "standard", "heavy"].includes(tier) || !validateModelID(model)) {
789
- console.error("Usage: ocskill models set <light|standard|heavy> <provider/model[#variant]>")
790
- process.exitCode = 2
1015
+ printCliError(Object.assign(new Error("Usage: ocskill models set <light|standard|heavy> <provider/model[#variant]>"), { code: "UES_USAGE", exitCode: 2 }))
791
1016
  return
792
1017
  }
793
1018
  policy = await writeModelPolicy(getConfigDir(), {
@@ -802,8 +1027,7 @@ async function modelsControl() {
802
1027
  const role = args[2]
803
1028
  const tier = args[3]
804
1029
  if (!role || !["light", "standard", "heavy"].includes(tier)) {
805
- console.error("Usage: ocskill models role <role> <light|standard|heavy>")
806
- process.exitCode = 2
1030
+ printCliError(Object.assign(new Error("Usage: ocskill models role <role> <light|standard|heavy>"), { code: "UES_USAGE", exitCode: 2 }))
807
1031
  return
808
1032
  }
809
1033
  policy = await writeModelPolicy(getConfigDir(), {
@@ -817,8 +1041,7 @@ async function modelsControl() {
817
1041
  if (action === "observe") {
818
1042
  const model = args[2]
819
1043
  if (!validateModelID(model)) {
820
- console.error("Usage: ocskill models observe <provider/model> --task-class <class> --passed on|off")
821
- process.exitCode = 2
1044
+ printCliError(Object.assign(new Error("Usage: ocskill models observe <provider/model> --task-class <class> --passed on|off"), { code: "UES_USAGE", exitCode: 2 }))
822
1045
  return
823
1046
  }
824
1047
  const taskClass = optionValue(args, "--task-class") || "general"
@@ -838,8 +1061,7 @@ async function modelsControl() {
838
1061
  if (action === "capability") {
839
1062
  const model = args[2]
840
1063
  if (!validateModelID(model)) {
841
- console.error("Usage: ocskill models capability <provider/model> [capability flags]")
842
- process.exitCode = 2
1064
+ printCliError(Object.assign(new Error("Usage: ocskill models capability <provider/model> [capability flags]"), { code: "UES_USAGE", exitCode: 2 }))
843
1065
  return
844
1066
  }
845
1067
  const current = policy.capabilities?.[model] || {}
@@ -877,8 +1099,7 @@ async function modelsControl() {
877
1099
  return
878
1100
  }
879
1101
 
880
- console.error("Usage: ocskill models <status|on|off|set|role> ...")
881
- process.exitCode = 2
1102
+ printCliError(Object.assign(new Error("Usage: ocskill models <status|on|off|set|role> ..."), { code: "UES_USAGE", exitCode: 2 }))
882
1103
  }
883
1104
 
884
1105
  async function routerControl() {
@@ -887,13 +1108,14 @@ async function routerControl() {
887
1108
  const maxValue = maxIndex >= 0 ? Number.parseInt(optionValue(args, "--max") ?? "", 10) : null
888
1109
 
889
1110
  if (!["status", "on", "off"].includes(action)) {
890
- console.error("Usage: ocskill router [status|on|off] [--max 1..6]")
891
- process.exitCode = 2
1111
+ printCliError(Object.assign(new Error("Usage: ocskill router [status|on|off] [--max 1..6]"), { code: "UES_USAGE", exitCode: 2 }))
892
1112
  return
893
1113
  }
894
1114
  if (maxIndex >= 0 && (!Number.isInteger(maxValue) || maxValue < 1 || maxValue > 6)) {
895
- console.error("--max must be an integer from 1 through 6")
896
- process.exitCode = 2
1115
+ printCliError(Object.assign(new Error("--max must be an integer from 1 through 6"), {
1116
+ code: "UES_USAGE",
1117
+ exitCode: 2,
1118
+ }))
897
1119
  return
898
1120
  }
899
1121
 
@@ -925,8 +1147,7 @@ async function routerControl() {
925
1147
  async function taskPolicyControl() {
926
1148
  const text = args.slice(1).join(" ").trim()
927
1149
  if (!text) {
928
- console.error("Usage: ocskill task-policy <text>")
929
- process.exitCode = 2
1150
+ printCliError(Object.assign(new Error("Usage: ocskill task-policy <text>"), { code: "UES_USAGE", exitCode: 2 }))
930
1151
  return
931
1152
  }
932
1153
  printJson(classifyEngineeringTask(text))
@@ -968,7 +1189,11 @@ async function sandboxControl() {
968
1189
  const taskID = args[3]
969
1190
  const root = positionalArg(args, 4) || process.cwd()
970
1191
  if (!slug || !taskID) throw new Error("Usage: ocskill sandbox create <slug> <task-id> [dir]")
971
- printJson(await createTaskSandbox(root, slug, taskID))
1192
+ printJson(await createTaskSandbox(root, slug, taskID, {
1193
+ inheritDirtyRoot: args.includes("--inherit-dirty-root"),
1194
+ allowDirtyRoot: args.includes("--allow-dirty-root"),
1195
+ startPoint: optionValue(args, "--start-point") || undefined,
1196
+ }))
972
1197
  return
973
1198
  }
974
1199
  if (action === "integrate") {
@@ -978,6 +1203,13 @@ async function sandboxControl() {
978
1203
  printJson(await integrateTaskSandbox(root, dir, { keep: args.includes("--keep") }))
979
1204
  return
980
1205
  }
1206
+ if (action === "rollback") {
1207
+ const dir = args[2]
1208
+ const root = positionalArg(args, 3) || process.cwd()
1209
+ if (!dir) throw new Error("Usage: ocskill sandbox rollback <worktree-path> [dir] [--keep]")
1210
+ printJson(await rollbackTaskSandbox(root, dir, { keep: args.includes("--keep") }))
1211
+ return
1212
+ }
981
1213
  if (action === "remove") {
982
1214
  const dir = args[2]
983
1215
  const root = positionalArg(args, 3) || process.cwd()
@@ -988,10 +1220,9 @@ async function sandboxControl() {
988
1220
  }))
989
1221
  return
990
1222
  }
991
- throw new Error("Usage: ocskill sandbox <capability|exec|list|create|integrate|remove> ...")
1223
+ throw new Error("Usage: ocskill sandbox <capability|exec|list|create|integrate|rollback|remove> ...")
992
1224
  } catch (error) {
993
- console.error(errorMessage(error))
994
- process.exitCode = 1
1225
+ printCliError(error)
995
1226
  }
996
1227
  }
997
1228
 
@@ -1028,8 +1259,7 @@ async function learningControl() {
1028
1259
  }
1029
1260
  throw new Error("Usage: ocskill learn <status|analyze|accept|promote> ...")
1030
1261
  } catch (error) {
1031
- console.error(errorMessage(error))
1032
- process.exitCode = 1
1262
+ printCliError(error)
1033
1263
  }
1034
1264
  }
1035
1265
 
@@ -1043,8 +1273,7 @@ async function hermesControl() {
1043
1273
  const slug = args[2]
1044
1274
  const root = positionalArg(args, 3) || process.cwd()
1045
1275
  if (!slug) {
1046
- console.error("Usage: ocskill hermes <workflow|exec-workflow> <slug> [dir] [--max-concurrent N]")
1047
- process.exitCode = 2
1276
+ printCliError(Object.assign(new Error("Usage: ocskill hermes <workflow|exec-workflow> <slug> [dir] [--max-concurrent N]"), { code: "UES_USAGE", exitCode: 2 }))
1048
1277
  return
1049
1278
  }
1050
1279
  const planFile = path.join(path.resolve(root), ".ues-work", slug, "PLAN.json")
@@ -1078,8 +1307,7 @@ async function hermesControl() {
1078
1307
  const taskID = args[3]
1079
1308
  const root = positionalArg(args, 4) || process.cwd()
1080
1309
  if (!slug || !taskID) {
1081
- console.error("Usage: ocskill hermes <prompt|exec> <slug> <task-id> [dir]")
1082
- process.exitCode = 2
1310
+ printCliError(Object.assign(new Error("Usage: ocskill hermes <prompt|exec> <slug> <task-id> [dir]"), { code: "UES_USAGE", exitCode: 2 }))
1083
1311
  return
1084
1312
  }
1085
1313
  const prompt = buildHermesDelegationPrompt(await contextPack(root, slug, taskID))
@@ -1103,8 +1331,7 @@ async function hermesControl() {
1103
1331
  if ((result.status ?? 1) !== 0) process.exitCode = result.status ?? 1
1104
1332
  return
1105
1333
  }
1106
- console.error("Usage: ocskill hermes <status|prompt|exec|workflow|exec-workflow> ...")
1107
- process.exitCode = 2
1334
+ printCliError(Object.assign(new Error("Usage: ocskill hermes <status|prompt|exec|workflow|exec-workflow> ..."), { code: "UES_USAGE", exitCode: 2 }))
1108
1335
  }
1109
1336
 
1110
1337
  async function evidenceStoreControl() {
@@ -1146,16 +1373,14 @@ async function evidenceStoreControl() {
1146
1373
  }
1147
1374
  throw new Error("Usage: ocskill store <status|put|get|gc> ...")
1148
1375
  } catch (error) {
1149
- console.error(errorMessage(error))
1150
- process.exitCode = 1
1376
+ printCliError(error)
1151
1377
  }
1152
1378
  }
1153
1379
 
1154
1380
  async function capabilityControl() {
1155
1381
  const text = args.slice(1).join(" ").trim()
1156
1382
  if (!text) {
1157
- console.error("Usage: ocskill capabilities <task text>")
1158
- process.exitCode = 2
1383
+ printCliError(Object.assign(new Error("Usage: ocskill capabilities <task text>"), { code: "UES_USAGE", exitCode: 2 }))
1159
1384
  return
1160
1385
  }
1161
1386
  printJson(inferTaskCapabilities(text))
@@ -1204,8 +1429,7 @@ async function visualControl() {
1204
1429
  }
1205
1430
  throw new Error("Usage: ocskill visual <spec|geometry|compare|crop|viewports> ...")
1206
1431
  } catch (error) {
1207
- console.error(errorMessage(error))
1208
- process.exitCode = 1
1432
+ printCliError(error)
1209
1433
  }
1210
1434
  }
1211
1435
 
@@ -1243,8 +1467,7 @@ async function browserControl() {
1243
1467
  }
1244
1468
  throw new Error("Usage: ocskill browser <capability|plan|inspect> ...")
1245
1469
  } catch (error) {
1246
- console.error(errorMessage(error))
1247
- process.exitCode = 1
1470
+ printCliError(error)
1248
1471
  }
1249
1472
  }
1250
1473
 
@@ -1274,16 +1497,83 @@ async function uiControl() {
1274
1497
  }
1275
1498
  throw new Error("Usage: ocskill ui <tokens|layout> ...")
1276
1499
  } catch (error) {
1277
- console.error(errorMessage(error))
1278
- process.exitCode = 1
1500
+ printCliError(error)
1501
+ }
1502
+ }
1503
+
1504
+ async function diffControl() {
1505
+ const root = path.resolve(positionalArg(args, 1) || process.cwd())
1506
+ const base = optionValue(args, "--base")
1507
+ const outputFile = optionValue(args, "--out")
1508
+ const commandArgs = ["diff", "--binary", "--no-ext-diff", ...(base ? [base] : []), "--"]
1509
+ const result = runCapture("git", commandArgs, { cwd: root, maxBuffer: 16 * 1024 * 1024 })
1510
+ if ((result.status ?? 1) !== 0) {
1511
+ const error = new Error((result.stderr || result.stdout || "git diff failed").trim())
1512
+ error.code = "UES_GIT_DIFF"
1513
+ throw error
1514
+ }
1515
+ if (outputFile) {
1516
+ const resolved = path.resolve(outputFile)
1517
+ writeFileSync(resolved, result.stdout || "", "utf8")
1518
+ printJson({ schemaVersion: 1, root, base: base || null, file: resolved, encoding: "utf8", bytes: Buffer.byteLength(result.stdout || "") })
1519
+ return
1520
+ }
1521
+ if (jsonOutput) {
1522
+ printJson({ schemaVersion: 1, root, base: base || null, encoding: "utf8", diff: result.stdout || "" })
1523
+ return
1279
1524
  }
1525
+ process.stdout.write(result.stdout || "")
1526
+ }
1527
+
1528
+ async function textReadControl() {
1529
+ const file = positionalArg(args, 1)
1530
+ if (!file) {
1531
+ const error = new Error("Usage: ocskill text-read <file> [--start N] [--max N] [--json]")
1532
+ error.code = "UES_USAGE"
1533
+ error.exitCode = 2
1534
+ throw error
1535
+ }
1536
+ const text = readTextFile(file)
1537
+ const parsedStart = optionInt(args, "--start", 0)
1538
+ const parsedMax = optionInt(args, "--max", 12000)
1539
+ const start = Number.isFinite(parsedStart) ? Math.max(0, parsedStart) : 0
1540
+ const maxChars = Number.isFinite(parsedMax) ? Math.max(256, Math.min(parsedMax, 100000)) : 12000
1541
+ const slice = text.slice(start, start + maxChars)
1542
+ const truncated = start + slice.length < text.length
1543
+ if (jsonOutput) {
1544
+ printJson({
1545
+ schemaVersion: 1,
1546
+ file: path.resolve(file),
1547
+ chars: text.length,
1548
+ start,
1549
+ returnedChars: slice.length,
1550
+ truncated,
1551
+ text: slice,
1552
+ })
1553
+ return
1554
+ }
1555
+ process.stdout.write(slice)
1556
+ if (truncated) process.stdout.write("\n...[UES text-read truncated]...\n")
1557
+ }
1558
+
1559
+ async function normalizeTextControl() {
1560
+ const file = args[1]
1561
+ if (!file) {
1562
+ const error = new Error("Usage: ocskill normalize-text <file> [--out <utf8-file>]")
1563
+ error.code = "UES_USAGE"
1564
+ error.exitCode = 2
1565
+ throw error
1566
+ }
1567
+ const text = readTextFile(file)
1568
+ const output = path.resolve(optionValue(args, "--out") || file)
1569
+ writeFileSync(output, text, "utf8")
1570
+ printJson({ schemaVersion: 1, input: path.resolve(file), output, encoding: "utf8", bytes: Buffer.byteLength(text) })
1280
1571
  }
1281
1572
 
1282
1573
  async function workflowPlanControl() {
1283
1574
  const file = args[1]
1284
1575
  if (!file) {
1285
- console.error("Usage: ocskill workflow-plan <PLAN.json> [--max-concurrent N]")
1286
- process.exitCode = 2
1576
+ printCliError(Object.assign(new Error("Usage: ocskill workflow-plan <PLAN.json> [--max-concurrent N]"), { code: "UES_USAGE", exitCode: 2 }))
1287
1577
  return
1288
1578
  }
1289
1579
  try {
@@ -1297,23 +1587,20 @@ async function workflowPlanControl() {
1297
1587
  minVisionAgentCost: optionInt(args, "--min-vision-agent-cost", 4),
1298
1588
  }))
1299
1589
  } catch (error) {
1300
- console.error(errorMessage(error))
1301
- process.exitCode = 1
1590
+ printCliError(error)
1302
1591
  }
1303
1592
  }
1304
1593
 
1305
1594
  async function skillsControl() {
1306
1595
  const action = args[1] || "lint"
1307
1596
  if (action !== "lint") {
1308
- console.error("Usage: ocskill skills lint [dir]")
1309
- process.exitCode = 2
1597
+ printCliError(Object.assign(new Error("Usage: ocskill skills lint [dir]"), { code: "UES_USAGE", exitCode: 2 }))
1310
1598
  return
1311
1599
  }
1312
1600
  try {
1313
1601
  printJson(await lintSkillCatalog(positionalArg(args, 2) || packageRoot))
1314
1602
  } catch (error) {
1315
- console.error(errorMessage(error))
1316
- process.exitCode = 1
1603
+ printCliError(error)
1317
1604
  }
1318
1605
  }
1319
1606
 
@@ -1352,8 +1639,7 @@ async function update() {
1352
1639
  compared = compareVersions(latestVersion, currentVersion)
1353
1640
  } catch (error) {
1354
1641
  console.error("[ocskill] npm returned a version that could not be compared safely; refusing update.")
1355
- console.error(errorMessage(error))
1356
- process.exitCode = 1
1642
+ printCliError(error)
1357
1643
  return
1358
1644
  }
1359
1645
 
@@ -1414,7 +1700,13 @@ async function remove() {
1414
1700
  if (code !== 0) process.exitCode = code
1415
1701
  }
1416
1702
 
1417
- switch (command) {
1703
+ async function main() {
1704
+ if (helpRequested) {
1705
+ printCommandHelp(command, args[1])
1706
+ return
1707
+ }
1708
+
1709
+ switch (command) {
1418
1710
  case "install":
1419
1711
  case "sync":
1420
1712
  await install()
@@ -1504,6 +1796,15 @@ switch (command) {
1504
1796
  case "workflow-plan":
1505
1797
  await workflowPlanControl()
1506
1798
  break
1799
+ case "diff":
1800
+ await diffControl().catch((error) => printCliError(error))
1801
+ break
1802
+ case "text-read":
1803
+ await textReadControl().catch((error) => printCliError(error))
1804
+ break
1805
+ case "normalize-text":
1806
+ await normalizeTextControl().catch((error) => printCliError(error))
1807
+ break
1507
1808
  case "ui":
1508
1809
  await uiControl()
1509
1810
  break
@@ -1542,8 +1843,17 @@ switch (command) {
1542
1843
  case "-h":
1543
1844
  printHelp()
1544
1845
  break
1545
- default:
1546
- console.error(`Unknown command: ${command}`)
1547
- printHelp()
1548
- process.exitCode = 1
1846
+ default: {
1847
+ const error = new Error("Unknown command: " + command)
1848
+ error.code = "UES_USAGE"
1849
+ error.exitCode = 2
1850
+ throw error
1851
+ }
1852
+ }
1853
+ }
1854
+
1855
+ try {
1856
+ await main()
1857
+ } catch (error) {
1858
+ printCliError(error)
1549
1859
  }