@algosuite/vo-mcp 0.2.0-beta.71 → 0.2.0-beta.72

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.
@@ -314,21 +314,21 @@ function backupConfigOnce(configPath) {
314
314
  copyFileSync(configPath, backupPath);
315
315
  return backupPath;
316
316
  }
317
- function writeFileAtomic(path23, content) {
318
- sweepStaleTempFiles(path23);
319
- const temp = `${path23}.vo-mcp-tmp-${process.pid}-${Date.now()}`;
317
+ function writeFileAtomic(path24, content) {
318
+ sweepStaleTempFiles(path24);
319
+ const temp = `${path24}.vo-mcp-tmp-${process.pid}-${Date.now()}`;
320
320
  try {
321
321
  writeFileSync2(temp, content, { encoding: "utf8", mode: 384 });
322
- if (existsSync3(path23)) {
322
+ if (existsSync3(path24)) {
323
323
  try {
324
- chmodSync2(temp, statSync(path23).mode & 511);
324
+ chmodSync2(temp, statSync(path24).mode & 511);
325
325
  } catch {
326
326
  }
327
327
  }
328
328
  let lastErr = null;
329
329
  for (let attempt = 0; attempt < RENAME_RETRIES; attempt += 1) {
330
330
  try {
331
- renameSync(temp, path23);
331
+ renameSync(temp, path24);
332
332
  return;
333
333
  } catch (err) {
334
334
  lastErr = err;
@@ -337,7 +337,7 @@ function writeFileAtomic(path23, content) {
337
337
  sleepSync(RENAME_RETRY_MS);
338
338
  }
339
339
  }
340
- writeFileSync2(path23, content, "utf8");
340
+ writeFileSync2(path24, content, "utf8");
341
341
  try {
342
342
  unlinkSync2(temp);
343
343
  } catch {
@@ -503,8 +503,8 @@ function tablePath(line) {
503
503
  function tableSections(lines) {
504
504
  const starts = [];
505
505
  for (let index = 0; index < lines.length; index += 1) {
506
- const path23 = tablePath(lines[index] ?? "");
507
- if (path23) starts.push({ path: path23, start: index });
506
+ const path24 = tablePath(lines[index] ?? "");
507
+ if (path24) starts.push({ path: path24, start: index });
508
508
  }
509
509
  return starts.map((section, index) => ({
510
510
  ...section,
@@ -696,11 +696,11 @@ function resolveLinuxConfigHome(home, env2) {
696
696
  const configured = env2["XDG_CONFIG_HOME"]?.trim();
697
697
  return configured && isAbsolute2(configured) ? configured : join5(home, ".config");
698
698
  }
699
- function launcherIsCurrent(path23, desiredContent, label, log2) {
700
- if (!existsSync6(path23)) return false;
701
- if (readFileSync5(path23, "utf8") === desiredContent) return true;
702
- const backupPath = `${path23}.backup-${Date.now()}`;
703
- copyFileSync2(path23, backupPath);
699
+ function launcherIsCurrent(path24, desiredContent, label, log2) {
700
+ if (!existsSync6(path24)) return false;
701
+ if (readFileSync5(path24, "utf8") === desiredContent) return true;
702
+ const backupPath = `${path24}.backup-${Date.now()}`;
703
+ copyFileSync2(path24, backupPath);
704
704
  log2(` Backed up existing ${label} to: ${backupPath}`);
705
705
  return false;
706
706
  }
@@ -916,17 +916,17 @@ function resolveDesktopConfigPath(home, plat, appData) {
916
916
  }
917
917
  return join6(home, ".config", "Claude", "claude_desktop_config.json");
918
918
  }
919
- function readClaudeConfig(path23) {
920
- if (!existsSync7(path23)) return { kind: "absent", config: {}, mtimeMs: null };
919
+ function readClaudeConfig(path24) {
920
+ if (!existsSync7(path24)) return { kind: "absent", config: {}, mtimeMs: null };
921
921
  for (let attempt = 0; attempt < 3; attempt += 1) {
922
- const before = statSync3(path23).mtimeMs;
922
+ const before = statSync3(path24).mtimeMs;
923
923
  let raw;
924
924
  try {
925
- raw = readFileSync6(path23, "utf8");
925
+ raw = readFileSync6(path24, "utf8");
926
926
  } catch {
927
927
  return { kind: "invalid", config: {}, mtimeMs: before };
928
928
  }
929
- if (!existsSync7(path23) || statSync3(path23).mtimeMs !== before) continue;
929
+ if (!existsSync7(path24) || statSync3(path24).mtimeMs !== before) continue;
930
930
  const text = raw.replace(/^\uFEFF/u, "");
931
931
  if (!text.trim()) return { kind: "empty", config: {}, mtimeMs: before };
932
932
  try {
@@ -938,9 +938,9 @@ function readClaudeConfig(path23) {
938
938
  }
939
939
  return { kind: "invalid", config: {}, mtimeMs: null };
940
940
  }
941
- function writeClaudeConfig(path23, config) {
942
- mkdirSync6(dirname4(path23), { recursive: true });
943
- writeFileAtomic(path23, `${JSON.stringify(config, null, 2)}
941
+ function writeClaudeConfig(path24, config) {
942
+ mkdirSync6(dirname4(path24), { recursive: true });
943
+ writeFileAtomic(path24, `${JSON.stringify(config, null, 2)}
944
944
  `);
945
945
  }
946
946
  function carriedEntryKeys(entry) {
@@ -3879,13 +3879,13 @@ async function getTaskKnowledgeContextRequest(req, taskId, { query, knowledgeReq
3879
3879
  const canonicalQuery = canonicalizeKnowledgeContextQuery(query);
3880
3880
  if (canonicalQuery.trim()) body.query = canonicalQuery;
3881
3881
  if (typeof knowledgeRequestId === "string" && knowledgeRequestId) body.knowledge_request_id = knowledgeRequestId;
3882
- const path23 = `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`;
3882
+ const path24 = `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`;
3883
3883
  const timeoutMs = Math.max(Number(taskRequestTimeoutMs) || 0, MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS);
3884
3884
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
3885
3885
  let res;
3886
3886
  let cause;
3887
3887
  try {
3888
- res = await req("POST", path23, body, { timeoutMs });
3888
+ res = await req("POST", path24, body, { timeoutMs });
3889
3889
  } catch (err) {
3890
3890
  cause = err;
3891
3891
  }
@@ -3953,10 +3953,10 @@ async function getPreparedJobRequest(req, taskId, options = {}, invalidateToken
3953
3953
  if (typeof taskId !== "string" || taskId.length === 0) {
3954
3954
  return { ok: false, reason: "missing_task_id", status: 0, ...envMeta };
3955
3955
  }
3956
- const path23 = `/api/v1/code-task/${encodeURIComponent(taskId)}/prepared-job?${query}`;
3956
+ const path24 = `/api/v1/code-task/${encodeURIComponent(taskId)}/prepared-job?${query}`;
3957
3957
  let res;
3958
3958
  try {
3959
- res = await req("GET", path23, void 0, { timeoutMs });
3959
+ res = await req("GET", path24, void 0, { timeoutMs });
3960
3960
  } catch (err) {
3961
3961
  return { ok: false, reason: `transport: ${err?.message || String(err)}`, status: 0, ...envMeta };
3962
3962
  }
@@ -4056,11 +4056,11 @@ function createControlPlaneClient({
4056
4056
  if (!resolvedBaseUrl) throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
4057
4057
  const root = resolvedBaseUrl.replace(/\/+$/, "");
4058
4058
  const claimOccurrences = /* @__PURE__ */ new Map();
4059
- async function req(method, path23, body, { timeoutMs } = {}) {
4059
+ async function req(method, path24, body, { timeoutMs } = {}) {
4060
4060
  const bearer = await resolveBearer(env2);
4061
4061
  const controller = timeoutMs ? new AbortController() : null;
4062
4062
  let timeoutId;
4063
- const request = Promise.resolve(fetchImpl(`${root}${path23}`, {
4063
+ const request = Promise.resolve(fetchImpl(`${root}${path24}`, {
4064
4064
  method,
4065
4065
  headers: {
4066
4066
  "content-type": "application/json",
@@ -4073,7 +4073,7 @@ function createControlPlaneClient({
4073
4073
  const timeout = new Promise((_, reject) => {
4074
4074
  timeoutId = setTimeout(() => {
4075
4075
  controller.abort();
4076
- reject(new Error(`control-plane ${path23} timed out after ${timeoutMs}ms`));
4076
+ reject(new Error(`control-plane ${path24} timed out after ${timeoutMs}ms`));
4077
4077
  }, timeoutMs);
4078
4078
  });
4079
4079
  try {
@@ -4082,7 +4082,7 @@ function createControlPlaneClient({
4082
4082
  clearTimeout(timeoutId);
4083
4083
  }
4084
4084
  }
4085
- const taskReq = (method, path23, body, options = {}) => req(method, path23, body, { timeoutMs: taskRequestTimeoutMs, ...options });
4085
+ const taskReq = (method, path24, body, options = {}) => req(method, path24, body, { timeoutMs: taskRequestTimeoutMs, ...options });
4086
4086
  const claimGate = makeClaimGateNotice({ log: (m) => console.warn(`[code-runner ${(/* @__PURE__ */ new Date()).toISOString()}] ${m}`) });
4087
4087
  return {
4088
4088
  getClaimGate: () => claimGate.current(),
@@ -4253,8 +4253,8 @@ function createControlPlaneClient({
4253
4253
  return listAllPrOpenedTasks(taskReq);
4254
4254
  },
4255
4255
  async downloadTaskAttachment(taskId, attachmentId) {
4256
- const path23 = `/api/v1/code-task/${encodeURIComponent(taskId)}/attachment/${encodeURIComponent(attachmentId)}`;
4257
- const res = await taskReq("GET", path23);
4256
+ const path24 = `/api/v1/code-task/${encodeURIComponent(taskId)}/attachment/${encodeURIComponent(attachmentId)}`;
4257
+ const res = await taskReq("GET", path24);
4258
4258
  if (res.status === 401) cachedFirebaseToken = null;
4259
4259
  if (!res.ok) throw new Error(`attachment download failed: HTTP ${res.status}`);
4260
4260
  return Buffer.from(await res.arrayBuffer());
@@ -4907,7 +4907,7 @@ function normalizeClaudePermissionMode(value) {
4907
4907
  }
4908
4908
  return normalized;
4909
4909
  }
4910
- function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, model, effort, maxBudgetUsd, researchHarness = false, toolPolicy = "default", env: env2 = process.env } = {}) {
4910
+ function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, model, effort, maxBudgetUsd, researchHarness = false, toolPolicy = "default", structuredOutputSchema, env: env2 = process.env } = {}) {
4911
4911
  const effectivePermissionMode = normalizeClaudePermissionMode(permissionMode);
4912
4912
  if (!["default", "skill_readonly", "frozen_inputs_only"].includes(toolPolicy)) {
4913
4913
  throw new Error(`unsupported Claude tool policy "${toolPolicy}"`);
@@ -4945,6 +4945,12 @@ function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, m
4945
4945
  "none"
4946
4946
  );
4947
4947
  }
4948
+ if (structuredOutputSchema !== void 0) {
4949
+ if (!restrictedSkill || !structuredOutputSchema || typeof structuredOutputSchema !== "object" || Array.isArray(structuredOutputSchema)) {
4950
+ throw new Error("structured output schema is allowed only for a restricted skill");
4951
+ }
4952
+ args.push("--json-schema", JSON.stringify(structuredOutputSchema));
4953
+ }
4948
4954
  if (frozenInputsOnly) {
4949
4955
  args.push("--strict-mcp-config", "--safe-mode");
4950
4956
  }
@@ -5370,13 +5376,15 @@ function cappedRunLastMessage(evt, lastProgress) {
5370
5376
  return salvage;
5371
5377
  }
5372
5378
  function buildResultEvent(evt) {
5373
- const isError = Boolean(evt.is_error) || evt.subtype === "error_max_turns" || evt.subtype === "error_during_execution";
5379
+ const isError = Boolean(evt.is_error) || evt.subtype === "error_max_budget_usd" || evt.subtype === "error_max_turns" || evt.subtype === "error_max_structured_output_retries" || evt.subtype === "error_during_execution";
5374
5380
  return {
5375
5381
  kind: "result",
5376
5382
  isError,
5377
5383
  costUsd: typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : null,
5378
5384
  summary: typeof evt.result === "string" && evt.result.length > 0 ? evt.result : evt.subtype || (isError ? "error" : "completed"),
5385
+ terminalSubtype: typeof evt.subtype === "string" ? evt.subtype : null,
5379
5386
  numTurns: typeof evt.num_turns === "number" ? evt.num_turns : null,
5387
+ structuredOutput: Object.hasOwn(evt, "structured_output") ? evt.structured_output : null,
5380
5388
  tokenUsage: extractTokenUsage(evt),
5381
5389
  modelUsage: extractModelUsage(evt)
5382
5390
  };
@@ -5520,6 +5528,148 @@ var init_cli_version_floor = __esm({
5520
5528
  }
5521
5529
  });
5522
5530
 
5531
+ // ../../scripts/virtual-office/code-runner/claude-skill-capability.mjs
5532
+ import { spawnSync as spawnSync6 } from "node:child_process";
5533
+ import { accessSync, constants, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
5534
+ import path14 from "node:path";
5535
+ function hasOption(help, option) {
5536
+ const literal = option.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
5537
+ return new RegExp(`(^|\\s)${literal}(?=\\s|,|=|<|$)`, "mu").test(help);
5538
+ }
5539
+ function assessClaudeSkillCapability({ versionOutput, helpOutput }) {
5540
+ const version = parseCliVersion(versionOutput);
5541
+ if (!version || !VALIDATED_CLAUDE_SKILL_VERSIONS.includes(version)) {
5542
+ return { compatible: false, version, reason: "claude version is not in the validated restricted-skill manifest" };
5543
+ }
5544
+ const help = String(helpOutput ?? "");
5545
+ const missing = REQUIRED_CLAUDE_SKILL_HELP.filter((option) => !hasOption(help, option));
5546
+ if (missing.length > 0) {
5547
+ return { compatible: false, version, reason: `claude help is missing required options: ${missing.join(", ")}` };
5548
+ }
5549
+ if (!/--permission-prompts[\s\S]{0,300}(?:"none"|\bnone\b)/mu.test(help) || !/--output-format[\s\S]{0,300}\bstream-json\b/mu.test(help)) {
5550
+ return { compatible: false, version, reason: "claude help does not prove required none/stream-json values" };
5551
+ }
5552
+ return { compatible: true, version, reason: "validated restricted-skill CLI contract" };
5553
+ }
5554
+ function runProbe(bin, args, env2, timeoutMs = PROBE_TIMEOUT_MS) {
5555
+ if (process.platform === "win32") {
5556
+ try {
5557
+ const launch = buildWindowsClaudeLaunch({ bin, args, env: env2 });
5558
+ return spawnSync6(launch.bin, launch.args, {
5559
+ ...launch.spawnOptions,
5560
+ env: env2,
5561
+ encoding: "utf8",
5562
+ timeout: timeoutMs
5563
+ });
5564
+ } catch (error) {
5565
+ return { status: null, stdout: "", stderr: "", error };
5566
+ }
5567
+ }
5568
+ return spawnSync6(bin, args, { env: env2, encoding: "utf8", timeout: timeoutMs, windowsHide: true });
5569
+ }
5570
+ function probeText(probe) {
5571
+ return `${String(probe?.stdout ?? "")}
5572
+ ${String(probe?.stderr ?? "")}`.trim();
5573
+ }
5574
+ function resolveClaudeBinaryIdentity(bin = "claude", env2 = process.env) {
5575
+ let resolvedBin = String(bin);
5576
+ try {
5577
+ if (process.platform === "win32") {
5578
+ resolvedBin = buildWindowsClaudeLaunch({ bin: resolvedBin, args: [], env: env2 }).bin;
5579
+ } else if (!path14.isAbsolute(resolvedBin)) {
5580
+ const found = String(env2?.PATH ?? "").split(path14.delimiter).find((dir) => {
5581
+ try {
5582
+ accessSync(path14.join(dir, resolvedBin), constants.X_OK);
5583
+ return true;
5584
+ } catch {
5585
+ return false;
5586
+ }
5587
+ });
5588
+ if (found) resolvedBin = path14.join(found, resolvedBin);
5589
+ }
5590
+ const canonical = realpathSync2(resolvedBin);
5591
+ const stat3 = statSync4(canonical);
5592
+ return { resolvedBin: canonical, fingerprint: `${canonical}\0${stat3.size}\0${stat3.mtimeMs}` };
5593
+ } catch {
5594
+ const pathValue2 = String(env2?.PATH ?? env2?.Path ?? "");
5595
+ return { resolvedBin, fingerprint: `${resolvedBin}\0${pathValue2}` };
5596
+ }
5597
+ }
5598
+ function probeClaudeSkillCapability({
5599
+ bin = "claude",
5600
+ env: env2 = process.env,
5601
+ versionOutput,
5602
+ spawnProbe = runProbe,
5603
+ now = () => Date.now(),
5604
+ cacheTtlMs = CACHE_TTL_MS,
5605
+ timeoutMs = PROBE_TIMEOUT_MS,
5606
+ freshIdentity = false,
5607
+ resolveIdentity = resolveClaudeBinaryIdentity
5608
+ } = {}) {
5609
+ const identity = resolveIdentity(bin, env2);
5610
+ const key = identity.fingerprint;
5611
+ const existing = cache.get(key);
5612
+ if (!freshIdentity && versionOutput === void 0 && existing && now() - existing.at < cacheTtlMs) {
5613
+ return existing.value;
5614
+ }
5615
+ const versionProbe = freshIdentity || versionOutput === void 0 ? spawnProbe(identity.resolvedBin, ["--version"], env2, timeoutMs) : null;
5616
+ if (versionProbe?.error || versionProbe && versionProbe.status !== 0) {
5617
+ return {
5618
+ compatible: false,
5619
+ version: null,
5620
+ resolvedBin: identity.resolvedBin,
5621
+ reason: "claude version capability probe failed"
5622
+ };
5623
+ }
5624
+ const effectiveVersionOutput = versionProbe ? probeText(versionProbe) : versionOutput;
5625
+ const suppliedVersion = parseCliVersion(effectiveVersionOutput);
5626
+ if (existing && now() - existing.at < cacheTtlMs && suppliedVersion === existing.value.version) {
5627
+ return existing.value;
5628
+ }
5629
+ const helpProbe = spawnProbe(identity.resolvedBin, ["--help"], env2, timeoutMs);
5630
+ if (helpProbe?.error || helpProbe?.status !== 0) {
5631
+ return {
5632
+ compatible: false,
5633
+ version: suppliedVersion,
5634
+ resolvedBin: identity.resolvedBin,
5635
+ reason: "claude help capability probe failed"
5636
+ };
5637
+ }
5638
+ const assessed = assessClaudeSkillCapability({
5639
+ versionOutput: effectiveVersionOutput,
5640
+ helpOutput: probeText(helpProbe)
5641
+ });
5642
+ const value = { ...assessed, resolvedBin: identity.resolvedBin };
5643
+ cache.set(key, { at: now(), value });
5644
+ return value;
5645
+ }
5646
+ var VALIDATED_CLAUDE_SKILL_VERSIONS, REQUIRED_CLAUDE_SKILL_HELP, PROBE_TIMEOUT_MS, CACHE_TTL_MS, cache;
5647
+ var init_claude_skill_capability = __esm({
5648
+ "../../scripts/virtual-office/code-runner/claude-skill-capability.mjs"() {
5649
+ "use strict";
5650
+ init_cli_version_floor();
5651
+ init_windows_claude_launch();
5652
+ VALIDATED_CLAUDE_SKILL_VERSIONS = Object.freeze(["2.1.263"]);
5653
+ REQUIRED_CLAUDE_SKILL_HELP = Object.freeze([
5654
+ "--allowedTools",
5655
+ "--disable-slash-commands",
5656
+ "--json-schema",
5657
+ "--max-budget-usd",
5658
+ "--no-chrome",
5659
+ "--no-session-persistence",
5660
+ "--output-format",
5661
+ "--permission-mode",
5662
+ "--permission-prompts",
5663
+ "--safe-mode",
5664
+ "--strict-mcp-config",
5665
+ "--tools"
5666
+ ]);
5667
+ PROBE_TIMEOUT_MS = 2e3;
5668
+ CACHE_TTL_MS = 5 * 60 * 1e3;
5669
+ cache = /* @__PURE__ */ new Map();
5670
+ }
5671
+ });
5672
+
5523
5673
  // ../../scripts/virtual-office/code-runner/claude-auth-check.mjs
5524
5674
  function errorCode(error) {
5525
5675
  return String(error?.code || "").toUpperCase();
@@ -5546,9 +5696,12 @@ async function checkClaudeAuth({
5546
5696
  spawnVersion = spawnClaudeSync,
5547
5697
  probeLogin = probeClaudeLoginState,
5548
5698
  getStoredKey = getAnthropicKey,
5549
- env: env2 = process.env
5699
+ probeSkillCapability = probeClaudeSkillCapability,
5700
+ env: env2 = process.env,
5701
+ now = () => Date.now()
5550
5702
  } = {}) {
5551
5703
  try {
5704
+ const startedAt = now();
5552
5705
  let probe = spawnVersion(["--version"], {
5553
5706
  timeout: FIRST_VERSION_TIMEOUT_MS,
5554
5707
  encoding: "utf8",
@@ -5582,23 +5735,40 @@ async function checkClaudeAuth({
5582
5735
  }
5583
5736
  const floorGate = applyCliVersionFloor({ versionOutput: probe.stdout, env: env2 });
5584
5737
  if (floorGate.refused) {
5585
- return { installed: true, authenticated: false, message: floorGate.message };
5738
+ return {
5739
+ installed: true,
5740
+ authenticated: false,
5741
+ version: floorGate.check.version ?? void 0,
5742
+ skillCapable: false,
5743
+ message: floorGate.message
5744
+ };
5586
5745
  }
5587
5746
  const loggedIn = retriedAfterTimeout ? null : probeLogin();
5588
5747
  if (loggedIn === false) {
5589
5748
  return {
5590
5749
  installed: true,
5591
5750
  authenticated: false,
5751
+ version: floorGate.check.version ?? void 0,
5752
+ skillCapable: false,
5592
5753
  message: "claude CLI is installed but NOT logged in \u2014 its login is SEPARATE from the Claude Desktop app and the Claude Code IDE extension. Run: claude auth login (Claude subscription), then restart the runner."
5593
5754
  };
5594
5755
  }
5756
+ const authTier = resolveClaudeAuthTier({ env: env2, loggedIn, getStoredKey });
5757
+ const remainingMs = AUTH_PROBE_BUDGET_MS - (now() - startedAt);
5758
+ const skillCapability = loggedIn === true && remainingMs >= MIN_SKILL_PROBE_MS ? probeSkillCapability({
5759
+ versionOutput: probe.stdout,
5760
+ env: env2,
5761
+ timeoutMs: Math.min(2e3, remainingMs)
5762
+ }) : { compatible: false };
5595
5763
  return {
5596
5764
  installed: true,
5597
5765
  authenticated: true,
5766
+ version: floorGate.check.version ?? void 0,
5767
+ skillCapable: skillCapability.compatible === true,
5598
5768
  // Dispatch-time billing signal, carried on the same probe that already
5599
5769
  // paid for the login read. Never sent for a non-authenticated result:
5600
5770
  // there is no tier without a working credential.
5601
- authTier: resolveClaudeAuthTier({ env: env2, loggedIn, getStoredKey }),
5771
+ authTier,
5602
5772
  message: loggedIn === true ? "claude CLI installed and logged in (claude auth status)" : "claude binary found (login state unknown \u2014 auth check is best-effort)"
5603
5773
  };
5604
5774
  } catch (error) {
@@ -5609,7 +5779,7 @@ async function checkClaudeAuth({
5609
5779
  };
5610
5780
  }
5611
5781
  }
5612
- var FIRST_VERSION_TIMEOUT_MS, RETRY_VERSION_TIMEOUT_MS;
5782
+ var FIRST_VERSION_TIMEOUT_MS, RETRY_VERSION_TIMEOUT_MS, AUTH_PROBE_BUDGET_MS, MIN_SKILL_PROBE_MS;
5613
5783
  var init_claude_auth_check = __esm({
5614
5784
  "../../scripts/virtual-office/code-runner/claude-auth-check.mjs"() {
5615
5785
  "use strict";
@@ -5617,8 +5787,11 @@ var init_claude_auth_check = __esm({
5617
5787
  init_agent_auth_tier();
5618
5788
  init_cli_version_floor();
5619
5789
  init_windows_claude_launch();
5790
+ init_claude_skill_capability();
5620
5791
  FIRST_VERSION_TIMEOUT_MS = 4500;
5621
5792
  RETRY_VERSION_TIMEOUT_MS = 2e3;
5793
+ AUTH_PROBE_BUDGET_MS = 9500;
5794
+ MIN_SKILL_PROBE_MS = 250;
5622
5795
  }
5623
5796
  });
5624
5797
 
@@ -5639,6 +5812,7 @@ function runAgentTask({
5639
5812
  maxBudgetUsd = null,
5640
5813
  researchHarness = false,
5641
5814
  toolPolicy = "default",
5815
+ structuredOutputSchema,
5642
5816
  env: env2 = process.env,
5643
5817
  onProgress = () => {
5644
5818
  },
@@ -5656,7 +5830,7 @@ function runAgentTask({
5656
5830
  sandbox = null
5657
5831
  }) {
5658
5832
  return new Promise((resolve3) => {
5659
- const args = runner.buildArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy, prompt });
5833
+ const args = runner.buildArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy, structuredOutputSchema, prompt });
5660
5834
  const spawnEnv = typeof runner.applyAuthEnv === "function" ? runner.applyAuthEnv(env2) : env2;
5661
5835
  const costBasis = typeof runner.costBasis === "function" ? runner.costBasis(spawnEnv) : "unknown";
5662
5836
  if (costBasis === "vendor_billed" && runner.enforcesBudgetCap !== true && env2.VO_CODE_RUNNER_ALLOW_UNCAPPED_VENDOR_BILLED !== "1") {
@@ -5701,7 +5875,7 @@ function runAgentTask({
5701
5875
  } catch {
5702
5876
  }
5703
5877
  let buffer = "";
5704
- let result = { ok: false, costUsd: null, costBasis, summary: "", lastAgentMessage: null, numTurns: null, tokenUsage: null, modelUsage: null, executionStarted: false, killed: false };
5878
+ let result = { ok: false, costUsd: null, costBasis, summary: "", lastAgentMessage: null, structuredOutput: null, terminalSubtype: null, numTurns: null, tokenUsage: null, modelUsage: null, executionStarted: false, killed: false };
5705
5879
  child.once("spawn", () => {
5706
5880
  result = { ...result, executionStarted: true };
5707
5881
  Promise.resolve(onSpawn()).catch(() => {
@@ -5739,6 +5913,8 @@ function runAgentTask({
5739
5913
  // A budget/turn-capped run's honest last message, kept OUT of summary (see
5740
5914
  // claude-result-event.cappedRunLastMessage) and surfaced in the PR body.
5741
5915
  lastAgentMessage: cappedRunLastMessage(evt, lastProgress),
5916
+ structuredOutput: Object.hasOwn(evt, "structuredOutput") ? evt.structuredOutput : result.structuredOutput,
5917
+ terminalSubtype: Object.hasOwn(evt, "terminalSubtype") ? evt.terminalSubtype : result.terminalSubtype,
5742
5918
  numTurns: evt.numTurns,
5743
5919
  // MUST be listed explicitly. This assignment spreads the PREVIOUS
5744
5920
  // result and then names each field it carries forward, so anything
@@ -5820,7 +5996,9 @@ function runAgentTask({
5820
5996
  hardKill();
5821
5997
  return;
5822
5998
  }
5823
- if (decision.delayMs != null) wallTimer = setTimeout(armDeadline, decision.delayMs);
5999
+ if (decision.delayMs !== null && decision.delayMs !== void 0) {
6000
+ wallTimer = setTimeout(armDeadline, decision.delayMs);
6001
+ }
5824
6002
  };
5825
6003
  armDeadline();
5826
6004
  child.stdout.on("data", (chunk) => {
@@ -5881,6 +6059,7 @@ var init_claude_runner = __esm({
5881
6059
  init_claude_stream_event();
5882
6060
  init_claude_result_event();
5883
6061
  init_claude_auth_check();
6062
+ init_claude_skill_capability();
5884
6063
  ClaudeRunner = class {
5885
6064
  get enforcesBudgetCap() {
5886
6065
  return true;
@@ -5888,8 +6067,8 @@ var init_claude_runner = __esm({
5888
6067
  get binary() {
5889
6068
  return "claude";
5890
6069
  }
5891
- buildArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy } = {}) {
5892
- return buildClaudeArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy });
6070
+ buildArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy, structuredOutputSchema } = {}) {
6071
+ return buildClaudeArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy, structuredOutputSchema });
5893
6072
  }
5894
6073
  parseEvent(line) {
5895
6074
  return parseStreamEvent(line);
@@ -5913,6 +6092,9 @@ var init_claude_runner = __esm({
5913
6092
  async checkAuth() {
5914
6093
  return checkClaudeAuth();
5915
6094
  }
6095
+ checkSkillCapability({ bin = this.binary, env: env2 = process.env } = {}) {
6096
+ return probeClaudeSkillCapability({ bin, env: env2, freshIdentity: true });
6097
+ }
5916
6098
  };
5917
6099
  claudeRunner = new ClaudeRunner();
5918
6100
  }
@@ -6096,7 +6278,7 @@ var init_error_message = __esm({
6096
6278
  });
6097
6279
 
6098
6280
  // ../../scripts/virtual-office/code-runner/codex-runner.mjs
6099
- import { spawnSync as spawnSync6 } from "node:child_process";
6281
+ import { spawnSync as spawnSync7 } from "node:child_process";
6100
6282
  import { existsSync as existsSync10 } from "node:fs";
6101
6283
  import { win32 as win322 } from "node:path";
6102
6284
  function isTruthyFlag2(value) {
@@ -6211,7 +6393,7 @@ var init_codex_runner = __esm({
6211
6393
  CODEX_PREFER_LOGIN_ENV = "VO_RUNNER_CODEX_PREFER_LOGIN";
6212
6394
  LEGACY_PREFER_LOGIN_ENV = "VO_RUNNER_PREFER_LOGIN";
6213
6395
  CodexRunner = class {
6214
- constructor({ spawn: spawn5 = spawnSync6, resolveBinary = resolveCodexBinary, env: env2 = process.env } = {}) {
6396
+ constructor({ spawn: spawn5 = spawnSync7, resolveBinary = resolveCodexBinary, env: env2 = process.env } = {}) {
6215
6397
  this.spawn = spawn5;
6216
6398
  this.resolveBinary = resolveBinary;
6217
6399
  this.env = env2;
@@ -6324,7 +6506,7 @@ ${login.stderr || ""}`.trim();
6324
6506
  });
6325
6507
 
6326
6508
  // ../../scripts/virtual-office/code-runner/cursor-runner.mjs
6327
- import { spawnSync as spawnSync7 } from "node:child_process";
6509
+ import { spawnSync as spawnSync8 } from "node:child_process";
6328
6510
  function buildCursorArgs({ model, prompt } = {}) {
6329
6511
  const args = ["-p", "--output-format", "stream-json", "--force"];
6330
6512
  if (model) {
@@ -6436,7 +6618,7 @@ var init_cursor_runner = __esm({
6436
6618
  /** Best-effort: is `cursor-agent` on PATH? Never throws. */
6437
6619
  async checkAuth() {
6438
6620
  try {
6439
- const { status, error } = spawnSync7("cursor-agent", ["--version"], {
6621
+ const { status, error } = spawnSync8("cursor-agent", ["--version"], {
6440
6622
  shell: false,
6441
6623
  windowsHide: true,
6442
6624
  timeout: 3e3,
@@ -7155,9 +7337,9 @@ var init_rate_limit_detector_core = __esm({
7155
7337
 
7156
7338
  // ../../scripts/virtual-office/code-runner/rate-limit-resume-state.mjs
7157
7339
  import fsp10 from "node:fs/promises";
7158
- import path14 from "node:path";
7340
+ import path15 from "node:path";
7159
7341
  async function atomicWrite(file, content) {
7160
- await fsp10.mkdir(path14.dirname(file), { recursive: true });
7342
+ await fsp10.mkdir(path15.dirname(file), { recursive: true });
7161
7343
  const temp = `${file}.tmp-${process.pid}-${Date.now()}`;
7162
7344
  const handle = await fsp10.open(temp, "wx");
7163
7345
  try {
@@ -7218,7 +7400,7 @@ function writeResumeAttempts(file, store) {
7218
7400
  }
7219
7401
  async function acquireLock(lockFile, { now = Date.now, sleep: sleep3 = delay } = {}) {
7220
7402
  const deadline = now() + LOCK_WAIT_MS;
7221
- await fsp10.mkdir(path14.dirname(lockFile), { recursive: true });
7403
+ await fsp10.mkdir(path15.dirname(lockFile), { recursive: true });
7222
7404
  for (; ; ) {
7223
7405
  let handle;
7224
7406
  try {
@@ -7522,7 +7704,7 @@ var init_auto_merge = __esm({
7522
7704
  });
7523
7705
 
7524
7706
  // ../../scripts/virtual-office/code-runner/pr-overlap-gate.mjs
7525
- import { spawnSync as spawnSync8 } from "node:child_process";
7707
+ import { spawnSync as spawnSync9 } from "node:child_process";
7526
7708
  import { existsSync as existsSync11 } from "node:fs";
7527
7709
  import { dirname as dirname6, join as join9 } from "node:path";
7528
7710
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -7688,14 +7870,14 @@ function parsePorcelainZ(out) {
7688
7870
  for (let i = 0; i < tokens.length; i += 1) {
7689
7871
  const token2 = tokens[i];
7690
7872
  if (!token2) continue;
7691
- const path23 = token2.slice(3);
7692
- if (path23) files.push(path23);
7873
+ const path24 = token2.slice(3);
7874
+ if (path24) files.push(path24);
7693
7875
  if (token2[0] === "R" || token2[0] === "C") i += 1;
7694
7876
  }
7695
7877
  return files;
7696
7878
  }
7697
- function isAgentScratch(path23) {
7698
- const normalized = String(path23 || "");
7879
+ function isAgentScratch(path24) {
7880
+ const normalized = String(path24 || "");
7699
7881
  return SCRATCH_PATTERNS.some((pattern) => pattern.test(normalized));
7700
7882
  }
7701
7883
  var SCRATCH_PATTERNS;
@@ -7713,7 +7895,7 @@ var init_publish_file_state = __esm({
7713
7895
  });
7714
7896
 
7715
7897
  // ../../scripts/virtual-office/code-runner/publish.mjs
7716
- import { spawnSync as spawnSync9 } from "node:child_process";
7898
+ import { spawnSync as spawnSync10 } from "node:child_process";
7717
7899
  function isMaxTurnsResult(summary) {
7718
7900
  return /(^|[^a-z])error[-_ ]?max[-_ ]?turns([^a-z]|$)|max[-_ ]?turns/i.test(String(summary || ""));
7719
7901
  }
@@ -7952,7 +8134,7 @@ var init_executor = __esm({
7952
8134
 
7953
8135
  // ../../scripts/virtual-office/code-runner/test-gen-gate.mjs
7954
8136
  import fs7 from "node:fs";
7955
- import path15 from "node:path";
8137
+ import path16 from "node:path";
7956
8138
  async function postFailed(client, id, message, result) {
7957
8139
  try {
7958
8140
  await client.postProgress(id, {
@@ -7988,7 +8170,7 @@ async function gateTestGenTaskOrFail({ client, id, task, files, worktreeDir, env
7988
8170
  }
7989
8171
  let testSource = "";
7990
8172
  try {
7991
- testSource = fs7.readFileSync(path15.join(worktreeDir, testFile), "utf8");
8173
+ testSource = fs7.readFileSync(path16.join(worktreeDir, testFile), "utf8");
7992
8174
  } catch (err) {
7993
8175
  await postFailed(client, id, `could not read generated test ${testFile}: ${err.message}`, "gate_test_unreadable");
7994
8176
  return true;
@@ -8038,7 +8220,7 @@ var init_test_gen_gate = __esm({
8038
8220
  // ../../scripts/virtual-office/code-runner/completion-gate.mjs
8039
8221
  import { execFile } from "node:child_process";
8040
8222
  import fs8 from "node:fs";
8041
- import path16 from "node:path";
8223
+ import path17 from "node:path";
8042
8224
  function resolveCompletionGate(task) {
8043
8225
  const raw = task?.completion_gate;
8044
8226
  if (raw === void 0 || raw === null) return null;
@@ -8076,14 +8258,14 @@ function workspaceFingerprint(worktreeDir, execFileImpl = execFile) {
8076
8258
  }
8077
8259
  function readState(worktreeDir) {
8078
8260
  try {
8079
- return JSON.parse(fs8.readFileSync(path16.join(worktreeDir, COMPLETION_GATE_STATE_FILE), "utf8"));
8261
+ return JSON.parse(fs8.readFileSync(path17.join(worktreeDir, COMPLETION_GATE_STATE_FILE), "utf8"));
8080
8262
  } catch {
8081
8263
  return null;
8082
8264
  }
8083
8265
  }
8084
8266
  function writeState(worktreeDir, state) {
8085
8267
  try {
8086
- fs8.writeFileSync(path16.join(worktreeDir, COMPLETION_GATE_STATE_FILE), `${JSON.stringify(state)}
8268
+ fs8.writeFileSync(path17.join(worktreeDir, COMPLETION_GATE_STATE_FILE), `${JSON.stringify(state)}
8087
8269
  `, "utf8");
8088
8270
  } catch {
8089
8271
  }
@@ -9221,7 +9403,7 @@ var init_headless_execution_contract = __esm({
9221
9403
  });
9222
9404
 
9223
9405
  // ../../scripts/virtual-office/code-runner/skill-catalog.mjs
9224
- import { readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
9406
+ import { readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync5 } from "node:fs";
9225
9407
  import { dirname as dirname7, join as join10 } from "node:path";
9226
9408
  import { fileURLToPath as fileURLToPath4 } from "node:url";
9227
9409
  function parseFrontmatterNameDescription(raw) {
@@ -9243,12 +9425,12 @@ function parseFrontmatterNameDescription(raw) {
9243
9425
  }
9244
9426
  function isRepoCheckout(dir) {
9245
9427
  try {
9246
- if (!statSync4(join10(dir, ".claude", "skills")).isDirectory()) return false;
9428
+ if (!statSync5(join10(dir, ".claude", "skills")).isDirectory()) return false;
9247
9429
  } catch {
9248
9430
  return false;
9249
9431
  }
9250
9432
  try {
9251
- statSync4(join10(dir, ".git"));
9433
+ statSync5(join10(dir, ".git"));
9252
9434
  return true;
9253
9435
  } catch {
9254
9436
  return false;
@@ -9274,7 +9456,7 @@ function loadSkillCatalog({ repoRoot: repoRoot2 = resolveDefaultRepoRoot() } = {
9274
9456
  for (const entry of readdirSync3(skillsDir)) {
9275
9457
  const dir = join10(skillsDir, entry);
9276
9458
  try {
9277
- if (!statSync4(dir).isDirectory()) continue;
9459
+ if (!statSync5(dir).isDirectory()) continue;
9278
9460
  const parsed = parseFrontmatterNameDescription(
9279
9461
  readFileSync8(join10(dir, "SKILL.md"), "utf8")
9280
9462
  );
@@ -9804,7 +9986,7 @@ var init_task_prompt = __esm({
9804
9986
  import { createHash as createHash5, randomUUID as randomUUID4 } from "node:crypto";
9805
9987
  import { chmod, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
9806
9988
  import os2 from "node:os";
9807
- import path17 from "node:path";
9989
+ import path18 from "node:path";
9808
9990
  function safeTaskToken(taskId) {
9809
9991
  return String(taskId || "task").replace(/[^0-9A-Za-z_-]/gu, "_").slice(0, 48) || "task";
9810
9992
  }
@@ -9817,9 +9999,9 @@ function hasGeneratedPrefix(name) {
9817
9999
  return name.startsWith(DIRECTORY_PREFIX) || name.startsWith(LEGACY_DIRECTORY_PREFIX);
9818
10000
  }
9819
10001
  function assertGeneratedDirectory(directory, containmentRoot) {
9820
- const resolvedDirectory = path17.resolve(directory);
9821
- const resolvedRoot = path17.resolve(containmentRoot);
9822
- if (path17.dirname(resolvedDirectory) !== resolvedRoot || !hasGeneratedPrefix(path17.basename(resolvedDirectory))) {
10002
+ const resolvedDirectory = path18.resolve(directory);
10003
+ const resolvedRoot = path18.resolve(containmentRoot);
10004
+ if (path18.dirname(resolvedDirectory) !== resolvedRoot || !hasGeneratedPrefix(path18.basename(resolvedDirectory))) {
9823
10005
  throw new Error("refusing to clean an unverified task-attachment directory");
9824
10006
  }
9825
10007
  return resolvedDirectory;
@@ -9828,7 +10010,7 @@ async function resolveContainmentRoot(worktreeDir) {
9828
10010
  if (typeof worktreeDir !== "string" || !worktreeDir.trim()) {
9829
10011
  throw new Error("refusing to materialize task attachments outside an agent-readable worktree: no worktreeDir given");
9830
10012
  }
9831
- const root = path17.resolve(worktreeDir);
10013
+ const root = path18.resolve(worktreeDir);
9832
10014
  const stats = await stat(root).catch(() => null);
9833
10015
  if (!stats?.isDirectory()) {
9834
10016
  throw new Error(`refusing to materialize task attachments: agent worktree root is not a directory (${root})`);
@@ -9837,15 +10019,15 @@ async function resolveContainmentRoot(worktreeDir) {
9837
10019
  }
9838
10020
  async function createAttachmentDirectory(taskId, containmentRoot) {
9839
10021
  const root = await resolveContainmentRoot(containmentRoot);
9840
- const directory = await mkdtemp(path17.join(root, `${DIRECTORY_PREFIX}${safeTaskToken(taskId)}-`));
10022
+ const directory = await mkdtemp(path18.join(root, `${DIRECTORY_PREFIX}${safeTaskToken(taskId)}-`));
9841
10023
  const [realRoot, realDirectory] = await Promise.all([realpath(root), realpath(directory)]);
9842
- if (path17.dirname(realDirectory) !== realRoot) {
10024
+ if (path18.dirname(realDirectory) !== realRoot) {
9843
10025
  await rm(directory, { recursive: true, force: true }).catch(() => void 0);
9844
10026
  throw new Error("task-attachment directory escaped the agent worktree root");
9845
10027
  }
9846
- await writeFile(path17.join(directory, GITIGNORE_FILE), GITIGNORE_BODY, { encoding: "utf8", mode: 384 });
9847
- const marker = JSON.stringify({ owner: MARKER_OWNER, token: randomUUID4(), directory: path17.basename(directory), created_at: (/* @__PURE__ */ new Date()).toISOString() });
9848
- await writeFile(path17.join(directory, MARKER_FILE), marker, { encoding: "utf8", mode: 384 });
10028
+ await writeFile(path18.join(directory, GITIGNORE_FILE), GITIGNORE_BODY, { encoding: "utf8", mode: 384 });
10029
+ const marker = JSON.stringify({ owner: MARKER_OWNER, token: randomUUID4(), directory: path18.basename(directory), created_at: (/* @__PURE__ */ new Date()).toISOString() });
10030
+ await writeFile(path18.join(directory, MARKER_FILE), marker, { encoding: "utf8", mode: 384 });
9849
10031
  return { directory, marker, root, cleaned: false };
9850
10032
  }
9851
10033
  async function cleanupGeneratedDirectory(state) {
@@ -9859,7 +10041,7 @@ async function cleanupGeneratedDirectory(state) {
9859
10041
  state.cleaned = true;
9860
10042
  return;
9861
10043
  }
9862
- const marker = await readFile(path17.join(directory, MARKER_FILE), "utf8").catch(() => "");
10044
+ const marker = await readFile(path18.join(directory, MARKER_FILE), "utf8").catch(() => "");
9863
10045
  if (marker !== state.marker) throw new Error("refusing to clean a task-attachment directory without its exact marker");
9864
10046
  await rm(directory, { recursive: true, force: true });
9865
10047
  state.cleaned = true;
@@ -9878,7 +10060,7 @@ async function sweepStaleTaskAttachmentDirectories({
9878
10060
  now = Date.now(),
9879
10061
  maxAgeMs = DEFAULT_STALE_AGE_MS
9880
10062
  } = {}) {
9881
- const root = path17.resolve(tempRoot);
10063
+ const root = path18.resolve(tempRoot);
9882
10064
  if (!Number.isFinite(maxAgeMs) || maxAgeMs <= 0) throw new Error("stale attachment age must be positive");
9883
10065
  const entries = await readdir(root, { withFileTypes: true }).catch((error) => {
9884
10066
  if (error?.code === "ENOENT") return [];
@@ -9887,8 +10069,8 @@ async function sweepStaleTaskAttachmentDirectories({
9887
10069
  let removed = 0;
9888
10070
  for (const entry of entries) {
9889
10071
  if (!entry.isDirectory() || !hasGeneratedPrefix(entry.name)) continue;
9890
- const directory = assertGeneratedDirectory(path17.join(root, entry.name), root);
9891
- const markerRaw = await readFile(path17.join(directory, MARKER_FILE), "utf8").catch(() => "");
10072
+ const directory = assertGeneratedDirectory(path18.join(root, entry.name), root);
10073
+ const markerRaw = await readFile(path18.join(directory, MARKER_FILE), "utf8").catch(() => "");
9892
10074
  const marker = parseOwnedMarker(markerRaw, entry.name);
9893
10075
  if (!marker) continue;
9894
10076
  const directoryStat = await stat(directory);
@@ -9945,8 +10127,8 @@ async function materializeTaskAttachments(client, task, { worktreeDir } = {}) {
9945
10127
  const sha2562 = createHash5("sha256").update(content).digest("hex");
9946
10128
  if (sha2562 !== ref.sha256) throw new Error(`attachment ${ref.attachment_id} sha256 mismatch`);
9947
10129
  const name = sanitizeTaskAttachmentName(ref.name, index);
9948
- const filePath = path17.resolve(state.directory, name);
9949
- if (path17.dirname(filePath) !== state.directory) throw new Error(`attachment ${ref.attachment_id} resolved outside its task directory`);
10130
+ const filePath = path18.resolve(state.directory, name);
10131
+ if (path18.dirname(filePath) !== state.directory) throw new Error(`attachment ${ref.attachment_id} resolved outside its task directory`);
9950
10132
  await writeFile(filePath, content, { flag: "wx", mode: 384 });
9951
10133
  await chmod(filePath, 384);
9952
10134
  files.push({ attachmentId: ref.attachment_id, name, mime: ref.mime, sizeBytes: ref.size_bytes, sha256: sha2562, path: filePath });
@@ -10031,9 +10213,9 @@ async function readSpool(spoolDir = SPOOL_DIR) {
10031
10213
  }
10032
10214
  return out;
10033
10215
  }
10034
- async function readCloudMap(path23) {
10216
+ async function readCloudMap(path24) {
10035
10217
  try {
10036
- return JSON.parse(await readFile2(path23, "utf8"));
10218
+ return JSON.parse(await readFile2(path24, "utf8"));
10037
10219
  } catch {
10038
10220
  return {};
10039
10221
  }
@@ -10334,9 +10516,9 @@ function backoffMs(streak, baseMs) {
10334
10516
  if (streak <= 0) return 0;
10335
10517
  return Math.min(baseMs * 2 ** Math.min(streak - 1, 20), MAX_BACKOFF_MS);
10336
10518
  }
10337
- async function loadState(path23) {
10519
+ async function loadState(path24) {
10338
10520
  try {
10339
- const parsed = JSON.parse(await readFile3(path23, "utf8"));
10521
+ const parsed = JSON.parse(await readFile3(path24, "utf8"));
10340
10522
  if (parsed && typeof parsed === "object" && Number.isInteger(parsed.byte_offset) && parsed.byte_offset >= 0) {
10341
10523
  return { ...parsed, byte_offset: parsed.byte_offset };
10342
10524
  }
@@ -10344,15 +10526,15 @@ async function loadState(path23) {
10344
10526
  }
10345
10527
  return { byte_offset: 0, last_event_id: null, forwarded_total: 0, rejected_total: 0, rejected_event_ids: [] };
10346
10528
  }
10347
- async function saveState(path23, state) {
10348
- await mkdir2(dirname9(path23), { recursive: true });
10349
- await writeFile3(path23, JSON.stringify(state, null, 2), "utf8");
10529
+ async function saveState(path24, state) {
10530
+ await mkdir2(dirname9(path24), { recursive: true });
10531
+ await writeFile3(path24, JSON.stringify(state, null, 2), "utf8");
10350
10532
  }
10351
- async function readNewBytes(path23, offset, max) {
10352
- const st = await stat2(path23);
10533
+ async function readNewBytes(path24, offset, max) {
10534
+ const st = await stat2(path24);
10353
10535
  if (st.size <= offset) return { buf: Buffer.alloc(0), size: st.size };
10354
10536
  const length = Math.min(st.size - offset, max);
10355
- const fh = await open(path23, "r");
10537
+ const fh = await open(path24, "r");
10356
10538
  try {
10357
10539
  const buf = Buffer.alloc(length);
10358
10540
  const { bytesRead } = await fh.read(buf, 0, length, offset);
@@ -10603,6 +10785,10 @@ var init_telemetry_forwarder = __esm({
10603
10785
  });
10604
10786
 
10605
10787
  // ../../scripts/virtual-office/code-runner/loop-ticks.mjs
10788
+ function supportedTaskKindsFor(availableAgents) {
10789
+ const claude = Array.isArray(availableAgents) ? availableAgents.find((row) => row?.agent === "claude") : null;
10790
+ return claude?.installed === true && claude.authenticated === true && claude.skill_capable === true ? RUNNER_SUPPORTED_TASK_KINDS : RUNNER_SUPPORTED_TASK_KINDS.filter((kind) => kind !== "skill");
10791
+ }
10606
10792
  function makeLoopTicks({
10607
10793
  client,
10608
10794
  cfg,
@@ -10739,7 +10925,7 @@ function makeLoopTicks({
10739
10925
  uptimeSec: Math.floor(process.uptime()),
10740
10926
  activeTasks: getActive(),
10741
10927
  maxConcurrency: cfg.maxConcurrency,
10742
- supportedTaskKinds: RUNNER_SUPPORTED_TASK_KINDS,
10928
+ supportedTaskKinds: supportedTaskKindsFor(availableAgents),
10743
10929
  ...capacityFields,
10744
10930
  ...localModelFields,
10745
10931
  ...preparedJobFields
@@ -10899,7 +11085,7 @@ function resolveAgentClaimContext(provider, defaultAgent) {
10899
11085
  async function collectAgentAvailability({
10900
11086
  agents = listAgents(),
10901
11087
  runnerFor,
10902
- probeTimeoutMs = PROBE_TIMEOUT_MS
11088
+ probeTimeoutMs = PROBE_TIMEOUT_MS2
10903
11089
  } = {}) {
10904
11090
  const probes = agents.map(async (agent) => {
10905
11091
  const degraded = { agent, installed: false, authenticated: false };
@@ -10917,6 +11103,7 @@ async function collectAgentAvailability({
10917
11103
  installed,
10918
11104
  authenticated,
10919
11105
  ...typeof r?.version === "string" && r.version ? { version: r.version } : {},
11106
+ ...typeof r?.skillCapable === "boolean" ? { skill_capable: r.skillCapable } : {},
10920
11107
  // Omitted when unknown, which is what an older daemon's silence already
10921
11108
  // means — the control-plane schema resolves BOTH to 'unknown'. Never
10922
11109
  // invent a tier to fill the gap.
@@ -10977,7 +11164,7 @@ function makeAgentAvailabilityProvider({
10977
11164
  }
10978
11165
  };
10979
11166
  }
10980
- var DEFAULT_TTL_MS, PROBE_TIMEOUT_MS;
11167
+ var DEFAULT_TTL_MS, PROBE_TIMEOUT_MS2;
10981
11168
  var init_agent_availability = __esm({
10982
11169
  "../../scripts/virtual-office/code-runner/agent-availability.mjs"() {
10983
11170
  "use strict";
@@ -10985,7 +11172,7 @@ var init_agent_availability = __esm({
10985
11172
  init_agent_auth_probe_process();
10986
11173
  init_agent_auth_tier();
10987
11174
  DEFAULT_TTL_MS = 5 * 60 * 1e3;
10988
- PROBE_TIMEOUT_MS = 1e4;
11175
+ PROBE_TIMEOUT_MS2 = 1e4;
10989
11176
  }
10990
11177
  });
10991
11178
 
@@ -11447,10 +11634,10 @@ function formatShadowLogLine(record) {
11447
11634
  const loud = record.unexplained_fields?.length ? "!! " : "";
11448
11635
  return `${loud}[prepared-job-shadow] ${parts.join(" ")}`;
11449
11636
  }
11450
- function appendShadowRecord(record, { path: path23 = PREPARED_JOB_SHADOW_SINK, append = appendFileSync, mkdir: mkdir5 = mkdirSync8 } = {}) {
11637
+ function appendShadowRecord(record, { path: path24 = PREPARED_JOB_SHADOW_SINK, append = appendFileSync, mkdir: mkdir5 = mkdirSync8 } = {}) {
11451
11638
  try {
11452
- mkdir5(dirname10(path23), { recursive: true });
11453
- append(path23, `${JSON.stringify(record)}
11639
+ mkdir5(dirname10(path24), { recursive: true });
11640
+ append(path24, `${JSON.stringify(record)}
11454
11641
  `, "utf8");
11455
11642
  return true;
11456
11643
  } catch {
@@ -11770,7 +11957,7 @@ var init_shared = __esm({
11770
11957
  // ../../scripts/virtual-office/code-runner/account-usage/claude.mjs
11771
11958
  import fs10 from "node:fs";
11772
11959
  import os3 from "node:os";
11773
- import path18 from "node:path";
11960
+ import path19 from "node:path";
11774
11961
  function fileCaptureTime(filePath, explicit, statFn) {
11775
11962
  if (typeof explicit === "string" && explicit) return explicit;
11776
11963
  try {
@@ -11784,7 +11971,7 @@ function usageBaseUrl(env2 = process.env) {
11784
11971
  return String(raw).replace(/\/+$/, "");
11785
11972
  }
11786
11973
  function readOAuthToken({ homeDir = os3.homedir(), read = readJson2, now = Date.now() } = {}) {
11787
- const creds = read(path18.join(homeDir, ".claude", ".credentials.json"));
11974
+ const creds = read(path19.join(homeDir, ".claude", ".credentials.json"));
11788
11975
  const oauth = creds && typeof creds === "object" ? creds.claudeAiOauth : null;
11789
11976
  if (!oauth || typeof oauth !== "object") return null;
11790
11977
  const token2 = typeof oauth.accessToken === "string" ? oauth.accessToken.trim() : "";
@@ -11794,7 +11981,7 @@ function readOAuthToken({ homeDir = os3.homedir(), read = readJson2, now = Date.
11794
11981
  return token2;
11795
11982
  }
11796
11983
  function readAccountId({ homeDir = os3.homedir(), read = readJson2 } = {}) {
11797
- const cfg = read(path18.join(homeDir, ".claude.json"));
11984
+ const cfg = read(path19.join(homeDir, ".claude.json"));
11798
11985
  const account = cfg && typeof cfg === "object" ? cfg.oauthAccount : null;
11799
11986
  return account && typeof account.accountUuid === "string" ? account.accountUuid : null;
11800
11987
  }
@@ -11904,7 +12091,7 @@ function readClaudeFileUsage({
11904
12091
  if (age === null || age > MAX_FILE_AGE_MS) return null;
11905
12092
  return row;
11906
12093
  };
11907
- const statusPath = path18.join(homeDir, ".claude", "claude-usage.json");
12094
+ const statusPath = path19.join(homeDir, ".claude", "claude-usage.json");
11908
12095
  const status = read(statusPath);
11909
12096
  if (status && (status.seven_day || status.five_hour)) {
11910
12097
  const row = fresh(makeUsageRow({
@@ -11919,7 +12106,7 @@ function readClaudeFileUsage({
11919
12106
  }));
11920
12107
  if (row) return row;
11921
12108
  }
11922
- const weeklyPath = path18.join(homeDir, ".claude", "claude-weekly-usage.json");
12109
+ const weeklyPath = path19.join(homeDir, ".claude", "claude-weekly-usage.json");
11923
12110
  const weekly = read(weeklyPath);
11924
12111
  if (weekly) {
11925
12112
  const row = fresh(makeUsageRow({
@@ -12733,11 +12920,11 @@ var init_watcher_adoption = __esm({
12733
12920
  // ../../scripts/virtual-office/code-runner/watcher-github-token.mjs
12734
12921
  function makeWatcherTokenProvider(client, { required = true, allowAmbient = false, now = () => Date.now(), log: log2 = () => {
12735
12922
  } } = {}) {
12736
- const cache = /* @__PURE__ */ new Map();
12923
+ const cache2 = /* @__PURE__ */ new Map();
12737
12924
  let ciUnreadableLoggedAt = null;
12738
12925
  return async (repo) => {
12739
12926
  const key = String(repo).toLowerCase();
12740
- const prior = cache.get(key);
12927
+ const prior = cache2.get(key);
12741
12928
  if (prior && now() - prior.at < 45 * 60 * 1e3) return prior.token;
12742
12929
  const result = await client.getInstallationToken({ required, readOnly: true, repo });
12743
12930
  if (!result?.token) {
@@ -12748,7 +12935,7 @@ function makeWatcherTokenProvider(client, { required = true, allowAmbient = fals
12748
12935
  ciUnreadableLoggedAt = now();
12749
12936
  log2(`watch: the plane minted a read token for ${repo} WITHOUT CI read (ci_readable=false \u2014 the GitHub App installation has not accepted checks:read/statuses:read); PR CI stays unreadable until the operator accepts the App permission update`);
12750
12937
  }
12751
- cache.set(key, { token: result.token, at: now() });
12938
+ cache2.set(key, { token: result.token, at: now() });
12752
12939
  return result.token;
12753
12940
  };
12754
12941
  }
@@ -12914,7 +13101,7 @@ function noteCiViaRest(log2) {
12914
13101
  log2("watch: CI status read via REST check-runs/status (gh's GraphQL rollup needs actions:read for checkSuite.workflowRun, which the read scope does not carry)");
12915
13102
  }
12916
13103
  async function readCommitCiViaRest(repo, sha, { run, env: env2 }) {
12917
- const api = async (path23) => JSON.parse(await run("gh", ["api", path23], { timeout: 3e4, env: env2 }) || "{}");
13104
+ const api = async (path24) => JSON.parse(await run("gh", ["api", path24], { timeout: 3e4, env: env2 }) || "{}");
12918
13105
  const rollup = [];
12919
13106
  let total = null;
12920
13107
  for (let page = 1; page <= REST_MAX_PAGES && (total === null || rollup.length < total); page += 1) {
@@ -13578,9 +13765,9 @@ function buildControlHandler({ getStatus, requestStop, allowedOrigin }) {
13578
13765
  res.end();
13579
13766
  return;
13580
13767
  }
13581
- const path23 = String(req.url || "").split("?")[0];
13768
+ const path24 = String(req.url || "").split("?")[0];
13582
13769
  res.setHeader("content-type", "application/json");
13583
- if (req.method === "GET" && path23 === "/status") {
13770
+ if (req.method === "GET" && path24 === "/status") {
13584
13771
  let status;
13585
13772
  try {
13586
13773
  status = getStatus();
@@ -13591,7 +13778,7 @@ function buildControlHandler({ getStatus, requestStop, allowedOrigin }) {
13591
13778
  res.end(JSON.stringify({ ok: true, ...status }));
13592
13779
  return;
13593
13780
  }
13594
- if (req.method === "POST" && path23 === "/stop") {
13781
+ if (req.method === "POST" && path24 === "/stop") {
13595
13782
  if (!isControlOriginAllowed(req.headers.origin, allowedOrigin) || !req.headers["x-vo-control"]) {
13596
13783
  res.statusCode = 403;
13597
13784
  res.end(JSON.stringify({ ok: false, error: "forbidden" }));
@@ -13785,22 +13972,22 @@ var init_effort_mode_config = __esm({
13785
13972
  import { randomUUID as randomUUID7 } from "node:crypto";
13786
13973
  import fs11 from "node:fs";
13787
13974
  import os4 from "node:os";
13788
- import path19 from "node:path";
13975
+ import path20 from "node:path";
13789
13976
  import { fileURLToPath as fileURLToPath6 } from "node:url";
13790
13977
  function userCacheRoot() {
13791
13978
  try {
13792
13979
  const home = os4.homedir();
13793
- if (home) return path19.join(home, ".claude");
13980
+ if (home) return path20.join(home, ".claude");
13794
13981
  } catch {
13795
13982
  }
13796
- return path19.join(os4.tmpdir(), `vo-model-registry-${randomUUID7()}`);
13983
+ return path20.join(os4.tmpdir(), `vo-model-registry-${randomUUID7()}`);
13797
13984
  }
13798
13985
  function resolveCacheBaseDir(env2 = process.env, moduleDir = __dirname) {
13799
13986
  if (env2.VO_MODEL_REGISTRY_CACHE_DIR) return env2.VO_MODEL_REGISTRY_CACHE_DIR;
13800
13987
  if (env2.VO_RUNNER_RUNTIME_ROOT) return env2.VO_RUNNER_RUNTIME_ROOT;
13801
- const segments = moduleDir.split(path19.sep);
13988
+ const segments = moduleDir.split(path20.sep);
13802
13989
  const isRepoCheckout2 = segments.at(-1) === "virtual-office" && segments.at(-2) === "scripts";
13803
- return isRepoCheckout2 ? path19.resolve(moduleDir, "..", "..") : userCacheRoot();
13990
+ return isRepoCheckout2 ? path20.resolve(moduleDir, "..", "..") : userCacheRoot();
13804
13991
  }
13805
13992
  function uniqueModels(models = []) {
13806
13993
  return [...new Set(models.map((model) => String(model || "").trim()).filter(Boolean))];
@@ -13923,7 +14110,7 @@ function readCache(cacheFile = DEFAULT_CACHE_FILE, nowMs = Date.now(), ttlMs = D
13923
14110
  }
13924
14111
  }
13925
14112
  function writeCache(cacheFile = DEFAULT_CACHE_FILE, payload) {
13926
- fs11.mkdirSync(path19.dirname(cacheFile), { recursive: true });
14113
+ fs11.mkdirSync(path20.dirname(cacheFile), { recursive: true });
13927
14114
  fs11.writeFileSync(cacheFile, JSON.stringify(payload, null, 2));
13928
14115
  }
13929
14116
  async function fetchRegistryCatalog({
@@ -13981,13 +14168,13 @@ var __dirname, DEFAULT_CACHE_DIR, DEFAULT_CACHE_FILE, DEFAULT_TTL_MS3, ANTHROPIC
13981
14168
  var init_model_registry = __esm({
13982
14169
  "../../scripts/virtual-office/model-registry.mjs"() {
13983
14170
  "use strict";
13984
- __dirname = path19.dirname(fileURLToPath6(import.meta.url));
13985
- DEFAULT_CACHE_DIR = path19.join(
14171
+ __dirname = path20.dirname(fileURLToPath6(import.meta.url));
14172
+ DEFAULT_CACHE_DIR = path20.join(
13986
14173
  resolveCacheBaseDir(),
13987
14174
  ".virtual-office-cache",
13988
14175
  "model-registry"
13989
14176
  );
13990
- DEFAULT_CACHE_FILE = path19.join(DEFAULT_CACHE_DIR, "catalog.json");
14177
+ DEFAULT_CACHE_FILE = path20.join(DEFAULT_CACHE_DIR, "catalog.json");
13991
14178
  DEFAULT_TTL_MS3 = 60 * 60 * 1e3;
13992
14179
  ANTHROPIC_API_VERSION = "2023-06-01";
13993
14180
  FAMILY_DEFINITIONS = {
@@ -14626,18 +14813,18 @@ function operatorTierToRung(tier, difficulty, thresholds) {
14626
14813
  if (tier === "best" && difficulty >= thresholds.rungBounds.R5) return "R5";
14627
14814
  return base;
14628
14815
  }
14629
- function readCodexModelsCache({ path: path23 = DEFAULT_CODEX_MODELS_CACHE, read = readFileSync9 } = {}) {
14816
+ function readCodexModelsCache({ path: path24 = DEFAULT_CODEX_MODELS_CACHE, read = readFileSync9 } = {}) {
14630
14817
  try {
14631
- const parsed = JSON.parse(read(path23, "utf8"));
14818
+ const parsed = JSON.parse(read(path24, "utf8"));
14632
14819
  return Array.isArray(parsed?.models) ? parsed : null;
14633
14820
  } catch {
14634
14821
  return null;
14635
14822
  }
14636
14823
  }
14637
- function clampCodexEffort(effort, cache) {
14824
+ function clampCodexEffort(effort, cache2) {
14638
14825
  if (!effort) return { effort: null, degraded: false };
14639
14826
  const supported = /* @__PURE__ */ new Set();
14640
- for (const model of cache?.models || []) {
14827
+ for (const model of cache2?.models || []) {
14641
14828
  if (model?.visibility === "hide") continue;
14642
14829
  for (const lvl of model?.supported_reasoning_levels || []) {
14643
14830
  if (lvl?.effort) supported.add(lvl.effort);
@@ -14885,15 +15072,15 @@ function formatDecisionReason(decision, maxLen = 480) {
14885
15072
  const s = `[${decision.routerVersion}] ${decision.taskClass} d=${decision.difficulty} c=${decision.confidence} \u2192 ${decision.rung}/${decision.tier}${decision.effort ? ` effort=${decision.effort}` : ""} turns=${decision.maxTurns} $${decision.maxBudgetUsd}${decision.flags.length ? ` [${decision.flags.join(",")}]` : ""} :: ${decision.reasons.join("; ")}`;
14886
15073
  return s.length > maxLen ? `${s.slice(0, maxLen - 1)}\u2026` : s;
14887
15074
  }
14888
- function appendDecisionFallback(decision, { path: path23 = DECISION_FALLBACK_PATH, append = appendFileSync2, mkdir: mkdir5 = mkdirSync9, task, thresholds, roleCostInputs } = {}) {
15075
+ function appendDecisionFallback(decision, { path: path24 = DECISION_FALLBACK_PATH, append = appendFileSync2, mkdir: mkdir5 = mkdirSync9, task, thresholds, roleCostInputs } = {}) {
14889
15076
  try {
14890
- mkdir5(dirname12(path23), { recursive: true });
14891
- append(path23, `${JSON.stringify(decision)}
15077
+ mkdir5(dirname12(path24), { recursive: true });
15078
+ append(path24, `${JSON.stringify(decision)}
14892
15079
  `, "utf8");
14893
15080
  if (isRouterDecision(decision)) {
14894
15081
  try {
14895
15082
  const records = buildShadowRecords({ decision, task, thresholds: thresholds || loadThresholds(), roleCostInputs });
14896
- for (const record of records) append(path23, `${JSON.stringify(record)}
15083
+ for (const record of records) append(path24, `${JSON.stringify(record)}
14897
15084
  `, "utf8");
14898
15085
  } catch {
14899
15086
  }
@@ -16215,6 +16402,53 @@ var init_terminal_delivery = __esm({
16215
16402
  }
16216
16403
  });
16217
16404
 
16405
+ // ../../scripts/virtual-office/code-runner/skill-result-json-schema.mjs
16406
+ function buildSkillResultJsonSchema({ expectedSkill, producedByAgent }) {
16407
+ return {
16408
+ type: "object",
16409
+ additionalProperties: false,
16410
+ properties: {
16411
+ schema_version: { const: 1 },
16412
+ skill: { const: expectedSkill },
16413
+ outcome: { enum: ["findings", "no_findings", "refused"] },
16414
+ findings: {
16415
+ type: "array",
16416
+ maxItems: MAX_FINDINGS,
16417
+ items: {
16418
+ type: "object",
16419
+ additionalProperties: false,
16420
+ properties: {
16421
+ claim: { type: "string", minLength: 1, maxLength: 500 },
16422
+ evidence: { type: "string", minLength: 1, maxLength: 2e3 },
16423
+ source: { type: "string", maxLength: 500 },
16424
+ confidence: { enum: ["high", "medium", "low"] }
16425
+ },
16426
+ required: ["claim", "evidence", "confidence"]
16427
+ }
16428
+ },
16429
+ findings_truncated: { type: "boolean" },
16430
+ summary: { type: "string", minLength: 1, maxLength: 2e3 },
16431
+ produced_by_agent: { const: producedByAgent }
16432
+ },
16433
+ required: [
16434
+ "schema_version",
16435
+ "skill",
16436
+ "outcome",
16437
+ "findings",
16438
+ "findings_truncated",
16439
+ "summary",
16440
+ "produced_by_agent"
16441
+ ]
16442
+ };
16443
+ }
16444
+ var MAX_FINDINGS;
16445
+ var init_skill_result_json_schema = __esm({
16446
+ "../../scripts/virtual-office/code-runner/skill-result-json-schema.mjs"() {
16447
+ "use strict";
16448
+ MAX_FINDINGS = 50;
16449
+ }
16450
+ });
16451
+
16218
16452
  // ../../scripts/virtual-office/code-runner/skill-task-runner.mjs
16219
16453
  import { createHash as createHash9 } from "node:crypto";
16220
16454
  import { mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
@@ -16275,13 +16509,7 @@ function skillResultPayloadSha256(result) {
16275
16509
  };
16276
16510
  return createHash9("sha256").update(JSON.stringify(payload), "utf8").digest("hex");
16277
16511
  }
16278
- function parseSkillResult(text, { expectedSkill, producedByAgent }) {
16279
- let value;
16280
- try {
16281
- value = JSON.parse(String(text ?? "").trim());
16282
- } catch {
16283
- throw new Error("skill agent did not return one strict JSON result");
16284
- }
16512
+ function parseSkillResultValue(value, { expectedSkill, producedByAgent }) {
16285
16513
  if (!exactKeys(value, ["schema_version", "skill", "outcome", "findings", "findings_truncated", "summary", "produced_by_agent"])) {
16286
16514
  throw new Error("skill result shape is invalid");
16287
16515
  }
@@ -16290,7 +16518,7 @@ function parseSkillResult(text, { expectedSkill, producedByAgent }) {
16290
16518
  if (value.produced_by_agent !== producedByAgent) throw new Error("skill result agent attribution is invalid");
16291
16519
  if (typeof value.findings_truncated !== "boolean") throw new Error("skill result truncation flag is invalid");
16292
16520
  boundedString(value.summary, 1, 2e3, "skill result summary");
16293
- if (!Array.isArray(value.findings) || value.findings.length > MAX_FINDINGS) throw new Error("skill result findings are invalid");
16521
+ if (!Array.isArray(value.findings) || value.findings.length > MAX_FINDINGS2) throw new Error("skill result findings are invalid");
16294
16522
  if (value.outcome === "findings" ? value.findings.length === 0 : value.findings.length > 0) {
16295
16523
  throw new Error("skill result findings do not match its outcome");
16296
16524
  }
@@ -16307,6 +16535,27 @@ function parseSkillResult(text, { expectedSkill, producedByAgent }) {
16307
16535
  }
16308
16536
  return value;
16309
16537
  }
16538
+ function assertSkillRunWithinDispatch(run, { maxTurns, maxBudgetUsd } = {}) {
16539
+ if (run?.terminalSubtype !== "success") {
16540
+ throw new Error(`skill agent terminal subtype was not success (${run?.terminalSubtype || "unknown"})`);
16541
+ }
16542
+ if (Number.isInteger(maxTurns) && maxTurns > 0) {
16543
+ if (!Number.isInteger(run?.numTurns) || run.numTurns < 0) {
16544
+ throw new Error("skill agent did not report valid turn usage for the active ceiling");
16545
+ }
16546
+ if (run.numTurns > maxTurns) {
16547
+ throw new Error(`skill agent exceeded the turn ceiling (${run.numTurns} > ${maxTurns})`);
16548
+ }
16549
+ }
16550
+ if (typeof maxBudgetUsd === "number" && Number.isFinite(maxBudgetUsd) && maxBudgetUsd > 0) {
16551
+ if (typeof run?.costUsd !== "number" || !Number.isFinite(run.costUsd) || run.costUsd < 0) {
16552
+ throw new Error("skill agent did not report valid cost usage for the active ceiling");
16553
+ }
16554
+ if (run.costUsd > maxBudgetUsd) {
16555
+ throw new Error(`skill agent exceeded the cost ceiling (${run.costUsd} > ${maxBudgetUsd})`);
16556
+ }
16557
+ }
16558
+ }
16310
16559
  function composeSkillTaskPrompt({ skillBody, invocation, taskPrompt, producedByAgent }) {
16311
16560
  const body = boundedString(skillBody, 1, MAX_SKILL_BODY_CHARS, "skill body");
16312
16561
  const request = boundedString(taskPrompt, 1, 810201, "skill task request");
@@ -16341,8 +16590,9 @@ async function processSkillTask(client, task, cfg, {
16341
16590
  swarmAdmission = null,
16342
16591
  runTask = runAgentTask,
16343
16592
  resolveDispatch = resolveEffortDispatch,
16593
+ checkSkillCapability = ({ runner, bin, env: capabilityEnv }) => typeof runner.checkSkillCapability === "function" ? runner.checkSkillCapability({ bin, env: capabilityEnv }) : { compatible: false, reason: "resolved runner has no restricted-skill capability probe" },
16344
16594
  createScratch = () => mkdtemp2(join19(tmpdir(), "algohq-skill-task-")),
16345
- removeScratch = (path23) => rm2(path23, { recursive: true, force: true })
16595
+ removeScratch = (path24) => rm2(path24, { recursive: true, force: true })
16346
16596
  } = {}) {
16347
16597
  const id = task.code_task_id;
16348
16598
  let run = null;
@@ -16389,12 +16639,25 @@ async function processSkillTask(client, task, cfg, {
16389
16639
  if (selected.agent !== "claude") {
16390
16640
  throw new Error("skill execution requires the policy-restricted Claude runner");
16391
16641
  }
16642
+ const capability = await checkSkillCapability({
16643
+ runner: selected.runner,
16644
+ bin: selected.runnerBin,
16645
+ env: env2
16646
+ });
16647
+ if (!capability?.compatible) {
16648
+ throw new Error(`resolved Claude CLI is incompatible with restricted skill execution: ${capability?.reason || "unknown capability"}`);
16649
+ }
16650
+ const executionBin = capability.resolvedBin || selected.runnerBin;
16392
16651
  const basePrompt = composeSkillTaskPrompt({
16393
16652
  skillBody: skill.body,
16394
16653
  invocation,
16395
16654
  taskPrompt: task.prompt,
16396
16655
  producedByAgent: selected.agent
16397
16656
  });
16657
+ const structuredOutputSchema = buildSkillResultJsonSchema({
16658
+ expectedSkill: invocation.skill,
16659
+ producedByAgent: selected.agent
16660
+ });
16398
16661
  const dispatch = await resolveDispatch({ client, task, agent: selected.agent, env: env2, basePrompt });
16399
16662
  assertRunnerGovernors({ agent: selected.agent, task });
16400
16663
  scratch = await createScratch();
@@ -16412,10 +16675,11 @@ async function processSkillTask(client, task, cfg, {
16412
16675
  });
16413
16676
  run = await runTask({
16414
16677
  runner: selected.runner,
16415
- bin: selected.runnerBin,
16678
+ bin: executionBin,
16416
16679
  prompt: dispatch.prompt,
16417
16680
  cwd: scratch,
16418
16681
  toolPolicy: frozenInputsOnly ? "frozen_inputs_only" : "skill_readonly",
16682
+ structuredOutputSchema,
16419
16683
  permissionMode: dispatch.permissionMode,
16420
16684
  maxTurns: selected.agent === "claude" ? dispatch.maxTurns : void 0,
16421
16685
  model: dispatch.model,
@@ -16466,7 +16730,11 @@ async function processSkillTask(client, task, cfg, {
16466
16730
  } });
16467
16731
  return;
16468
16732
  }
16469
- const parsedResult = parseSkillResult(resultText(run), {
16733
+ assertSkillRunWithinDispatch(run, dispatch);
16734
+ if (!run.structuredOutput || typeof run.structuredOutput !== "object" || Array.isArray(run.structuredOutput)) {
16735
+ throw new Error("skill agent did not return provider-validated structured output");
16736
+ }
16737
+ const parsedResult = parseSkillResultValue(run.structuredOutput, {
16470
16738
  expectedSkill: invocation.skill,
16471
16739
  producedByAgent: selected.agent
16472
16740
  });
@@ -16509,7 +16777,7 @@ async function processSkillTask(client, task, cfg, {
16509
16777
  } });
16510
16778
  }
16511
16779
  }
16512
- var SKILL_SUCCESS_STATUS, SKILL_NAME_RE, INPUT_KEY_RE, MAX_SKILL_BODY_CHARS, MAX_INPUTS, MAX_FINDINGS, SHA256_RE;
16780
+ var SKILL_SUCCESS_STATUS, SKILL_NAME_RE, INPUT_KEY_RE, MAX_SKILL_BODY_CHARS, MAX_INPUTS, MAX_FINDINGS2, SHA256_RE;
16513
16781
  var init_skill_task_runner = __esm({
16514
16782
  "../../scripts/virtual-office/code-runner/skill-task-runner.mjs"() {
16515
16783
  "use strict";
@@ -16523,12 +16791,13 @@ var init_skill_task_runner = __esm({
16523
16791
  init_killed_run_outcome();
16524
16792
  init_terminal_delivery();
16525
16793
  init_cancelled_run_report();
16794
+ init_skill_result_json_schema();
16526
16795
  SKILL_SUCCESS_STATUS = "no_changes_needed";
16527
16796
  SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}(?::[a-z0-9][a-z0-9-]{0,62})?$/u;
16528
16797
  INPUT_KEY_RE = /^[a-z][a-z0-9_]{0,39}$/u;
16529
16798
  MAX_SKILL_BODY_CHARS = 256e3;
16530
16799
  MAX_INPUTS = 12;
16531
- MAX_FINDINGS = 50;
16800
+ MAX_FINDINGS2 = 50;
16532
16801
  SHA256_RE = /^[a-f0-9]{64}$/u;
16533
16802
  }
16534
16803
  });
@@ -16536,7 +16805,7 @@ var init_skill_task_runner = __esm({
16536
16805
  // ../../scripts/virtual-office/code-runner/isolation-audit.mjs
16537
16806
  import fs12 from "node:fs";
16538
16807
  import fsp11 from "node:fs/promises";
16539
- import path20 from "node:path";
16808
+ import path21 from "node:path";
16540
16809
  async function defaultRun3(command, args, cwd, options = {}) {
16541
16810
  return runProcess2(command, args, { cwd, timeout: 6e4, ...options });
16542
16811
  }
@@ -16549,7 +16818,7 @@ async function canonicalRootForWorktree(worktreeDir, run) {
16549
16818
  "--path-format=absolute",
16550
16819
  "--git-common-dir"
16551
16820
  ])).trim();
16552
- const root = path20.dirname(commonDir);
16821
+ const root = path21.dirname(commonDir);
16553
16822
  return samePath3(root, worktreeDir) ? null : root;
16554
16823
  }
16555
16824
  async function snapshot(root, run) {
@@ -16591,21 +16860,21 @@ async function changedPaths(root, run) {
16591
16860
  }
16592
16861
  async function quarantineCanonicalWrites({ baseline, worktreeDir, taskId, run, now }) {
16593
16862
  const paths = await changedPaths(baseline.root, run);
16594
- const quarantineDir = path20.join(
16595
- path20.dirname(worktreeDir),
16863
+ const quarantineDir = path21.join(
16864
+ path21.dirname(worktreeDir),
16596
16865
  ".canonical-recovery",
16597
16866
  `${String(taskId || "unknown").replace(/[^a-z0-9-]/gi, "-")}-${now().toISOString().replace(/[:.]/g, "-")}`
16598
16867
  );
16599
16868
  await fsp11.mkdir(quarantineDir, { recursive: true });
16600
16869
  const patch = await git2(run, baseline.root, ["diff", "--binary", "HEAD"], { raw: true });
16601
- await fsp11.writeFile(path20.join(quarantineDir, "tracked.patch"), patch, "utf8");
16870
+ await fsp11.writeFile(path21.join(quarantineDir, "tracked.patch"), patch, "utf8");
16602
16871
  for (const relative of paths.untracked) {
16603
- const source = path20.join(baseline.root, relative);
16604
- const target = path20.join(quarantineDir, "untracked", relative);
16605
- await fsp11.mkdir(path20.dirname(target), { recursive: true });
16872
+ const source = path21.join(baseline.root, relative);
16873
+ const target = path21.join(quarantineDir, "untracked", relative);
16874
+ await fsp11.mkdir(path21.dirname(target), { recursive: true });
16606
16875
  await fsp11.copyFile(source, target);
16607
16876
  }
16608
- await fsp11.writeFile(path20.join(quarantineDir, "manifest.json"), `${JSON.stringify({
16877
+ await fsp11.writeFile(path21.join(quarantineDir, "manifest.json"), `${JSON.stringify({
16609
16878
  taskId,
16610
16879
  canonicalRoot: baseline.root,
16611
16880
  canonicalHead: baseline.head,
@@ -16627,8 +16896,8 @@ async function restoreExactCanonicalPaths(baseline, evidence, run) {
16627
16896
  ]);
16628
16897
  }
16629
16898
  for (const relative of evidence.untracked) {
16630
- const target = path20.resolve(baseline.root, relative);
16631
- const prefix = `${path20.resolve(baseline.root)}${path20.sep}`;
16899
+ const target = path21.resolve(baseline.root, relative);
16900
+ const prefix = `${path21.resolve(baseline.root)}${path21.sep}`;
16632
16901
  if (!target.startsWith(prefix) || !fs12.existsSync(target)) continue;
16633
16902
  await fsp11.rm(target, { force: true });
16634
16903
  }
@@ -16665,7 +16934,7 @@ var init_isolation_audit = __esm({
16665
16934
  init_process_runner2();
16666
16935
  splitZ2 = (value) => String(value || "").split("\0").map((item) => item.trim()).filter(Boolean);
16667
16936
  samePath3 = (left, right) => {
16668
- const [a, b] = [left, right].map((value) => path20.resolve(value));
16937
+ const [a, b] = [left, right].map((value) => path21.resolve(value));
16669
16938
  return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
16670
16939
  };
16671
16940
  }
@@ -17082,7 +17351,7 @@ var init_publication_outcome = __esm({
17082
17351
 
17083
17352
  // ../../scripts/virtual-office/code-runner/committed-scratch-cleanup.mjs
17084
17353
  import fsp12 from "node:fs/promises";
17085
- import path21 from "node:path";
17354
+ import path22 from "node:path";
17086
17355
  function defaultRun4(command, args, cwd, options = {}) {
17087
17356
  return runProcess2(command, args, { cwd, ...options });
17088
17357
  }
@@ -17090,13 +17359,13 @@ async function resolveSafeScratchTarget(worktreeDir, file) {
17090
17359
  if (!isAgentScratch(file)) {
17091
17360
  throw new Error(`refusing to remove non-scratch publication path: ${file}`);
17092
17361
  }
17093
- const root = path21.resolve(worktreeDir);
17094
- const target = path21.resolve(root, file);
17095
- const relative = path21.relative(root, target);
17096
- if (!relative || relative.startsWith(`..${path21.sep}`) || path21.isAbsolute(relative)) {
17362
+ const root = path22.resolve(worktreeDir);
17363
+ const target = path22.resolve(root, file);
17364
+ const relative = path22.relative(root, target);
17365
+ if (!relative || relative.startsWith(`..${path22.sep}`) || path22.isAbsolute(relative)) {
17097
17366
  throw new Error(`refusing to remove publication scratch outside worktree: ${file}`);
17098
17367
  }
17099
- for (let cursor = target; cursor !== root; cursor = path21.dirname(cursor)) {
17368
+ for (let cursor = target; cursor !== root; cursor = path22.dirname(cursor)) {
17100
17369
  try {
17101
17370
  if ((await fsp12.lstat(cursor)).isSymbolicLink()) {
17102
17371
  throw new Error(`refusing to follow symlink while removing publication scratch: ${file}`);
@@ -17218,7 +17487,7 @@ var init_publication_scope = __esm({
17218
17487
  // ../../scripts/virtual-office/code-runner/recovery-ledger.mjs
17219
17488
  import fs13 from "node:fs";
17220
17489
  import fsp13 from "node:fs/promises";
17221
- import path22 from "node:path";
17490
+ import path23 from "node:path";
17222
17491
  function recoveryTaskId(prompt) {
17223
17492
  const match = String(prompt || "").match(/VO_RECOVERY_FROM_CODE_TASK:\s*([0-9a-f-]{36})/i);
17224
17493
  return match ? match[1].toLowerCase() : null;
@@ -17232,10 +17501,10 @@ function cloneLeaf(repo) {
17232
17501
  function recoveryLedgerCandidates(repo, clonesRoot2) {
17233
17502
  const leaf = cloneLeaf(repo);
17234
17503
  if (!leaf || !clonesRoot2) return [];
17235
- const canonical = path22.join(clonesRoot2, leaf);
17504
+ const canonical = path23.join(clonesRoot2, leaf);
17236
17505
  return [
17237
- path22.join(clonesRoot2, ".agent-worktrees", leaf, "recovery-ledger.jsonl"),
17238
- path22.join(canonical, ".agent-worktrees", "recovery-ledger.jsonl")
17506
+ path23.join(clonesRoot2, ".agent-worktrees", leaf, "recovery-ledger.jsonl"),
17507
+ path23.join(canonical, ".agent-worktrees", "recovery-ledger.jsonl")
17239
17508
  ];
17240
17509
  }
17241
17510
  async function readLedger(file, readFile6) {