@kontextmind/kxm 0.7.135 → 0.7.138

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.
@@ -11,7 +11,7 @@
11
11
  "name": "kxm",
12
12
  "source": "./plugins/kxm",
13
13
  "description": "Durable workflows, peer agents, and kxm tui",
14
- "version": "0.7.135",
14
+ "version": "0.7.138",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.135",
3
+ "version": "0.7.138",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "kxm",
4
4
  "displayName": "KXM",
5
- "version": "0.7.135",
5
+ "version": "0.7.138",
6
6
  "description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
7
7
  "author": {
8
8
  "name": "KontextMind",
@@ -24791,6 +24791,10 @@ function listRoleBindings(root) {
24791
24791
  }
24792
24792
  return result;
24793
24793
  }
24794
+ function isRouteAdmitted(root, modelId2) {
24795
+ const policy = loadRoutePolicy(root);
24796
+ return policy.admitted.includes(modelId2) && !policy.disabled.includes(modelId2);
24797
+ }
24794
24798
  function listInventoryModels(root) {
24795
24799
  const path4 = join14(root, ".kxm", "models", "inventory.yaml");
24796
24800
  if (!existsSync14(path4)) return [];
@@ -27513,10 +27517,73 @@ var init_model_inventory = __esm({
27513
27517
  }
27514
27518
  });
27515
27519
 
27520
+ // plugins/kxm/src/vision-gate.ts
27521
+ import { execFileSync } from "node:child_process";
27522
+ import { existsSync as existsSync24, statSync as statSync5 } from "node:fs";
27523
+ function visionGatePrompt(question, imagePath) {
27524
+ const bounded = question.trim().slice(0, QUESTION_MAX_CHARS);
27525
+ const reference = imagePath ? `Look at the image @${imagePath}.` : "Look at the image attached to this prompt.";
27526
+ return [
27527
+ "You are judging a UI screenshot for an automated verification gate.",
27528
+ reference,
27529
+ bounded,
27530
+ 'Reply with exactly one JSON object and nothing else: {"verdict": true} or {"verdict": false}'
27531
+ ].join(" ");
27532
+ }
27533
+ function parseVisionVerdict(raw) {
27534
+ const match = /"verdict"\s*:\s*(true|false)/i.exec(raw);
27535
+ return match ? match[1] === "true" : null;
27536
+ }
27537
+ function defaultTransport(prompt, timeoutMs) {
27538
+ return execFileSync(
27539
+ "pi",
27540
+ ["--provider", "zai-coding-cn", "--model", "glm-5.3-flash", "-p", "--no-session", prompt],
27541
+ { encoding: "utf8", timeout: timeoutMs, windowsHide: true }
27542
+ );
27543
+ }
27544
+ async function runVisionGate(options) {
27545
+ const projectRoot = options.projectRoot ?? process.cwd();
27546
+ const route = options.route ?? VISION_GATE_DEFAULT_ROUTE;
27547
+ const timeoutMs = options.timeoutMs ?? 9e4;
27548
+ const base = { verdict: null, route, raw: "" };
27549
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 5e3 || timeoutMs > 3e5) {
27550
+ return { ...base, divergence: "timeout_out_of_bounds" };
27551
+ }
27552
+ if (!options.imagePath || !existsSync24(options.imagePath) || !statSync5(options.imagePath).isFile()) {
27553
+ return { ...base, divergence: "image unreadable" };
27554
+ }
27555
+ if (!isRouteAdmitted(projectRoot, route)) {
27556
+ return { ...base, divergence: "route_not_admitted" };
27557
+ }
27558
+ const prompt = visionGatePrompt(options.question, options.imagePath);
27559
+ let raw;
27560
+ try {
27561
+ raw = options.transport ? await options.transport(prompt) : defaultTransport(prompt, timeoutMs);
27562
+ } catch (error) {
27563
+ return { ...base, divergence: `transport failed: ${String(error?.message ?? error).slice(0, 200)}` };
27564
+ }
27565
+ const boundedRaw = raw.slice(0, RAW_MAX_CHARS);
27566
+ const verdict = parseVisionVerdict(boundedRaw);
27567
+ if (verdict === null) {
27568
+ return { ...base, raw: boundedRaw, divergence: "verdict unreadable" };
27569
+ }
27570
+ return { ...base, verdict, raw: boundedRaw };
27571
+ }
27572
+ var VISION_GATE_DEFAULT_ROUTE, QUESTION_MAX_CHARS, RAW_MAX_CHARS;
27573
+ var init_vision_gate = __esm({
27574
+ "plugins/kxm/src/vision-gate.ts"() {
27575
+ "use strict";
27576
+ init_routes();
27577
+ VISION_GATE_DEFAULT_ROUTE = "zai-coding-cn/glm-5.3-flash";
27578
+ QUESTION_MAX_CHARS = 500;
27579
+ RAW_MAX_CHARS = 2e3;
27580
+ }
27581
+ });
27582
+
27516
27583
  // plugins/kxm/src/permission.ts
27517
27584
  import { spawnSync as spawnSync5 } from "node:child_process";
27518
27585
  import { createHash as createHash17 } from "node:crypto";
27519
- import { existsSync as existsSync24, mkdtempSync, mkdirSync as mkdirSync20, readFileSync as readFileSync24, rmSync as rmSync8, writeFileSync as writeFileSync19 } from "node:fs";
27586
+ import { existsSync as existsSync25, mkdtempSync, mkdirSync as mkdirSync20, readFileSync as readFileSync24, rmSync as rmSync8, writeFileSync as writeFileSync19 } from "node:fs";
27520
27587
  import { tmpdir } from "node:os";
27521
27588
  import { dirname as dirname14, join as join28, resolve as resolve19, sep as sep4 } from "node:path";
27522
27589
  function canonical(value) {
@@ -28097,7 +28164,7 @@ function loadKxmProjectAtRevision(root, revision, options = {}) {
28097
28164
  }
28098
28165
  const boundPath = options.repositoryBindings?.[member.repositoryId];
28099
28166
  const memberWorktree = boundPath ? resolve19(boundPath) : member.pathHint ? resolve19(root, ...member.pathHint.split("/")) : void 0;
28100
- if (!memberWorktree || !existsSync24(memberWorktree)) {
28167
+ if (!memberWorktree || !existsSync25(memberWorktree)) {
28101
28168
  if (member.required === false) continue;
28102
28169
  throw new KxmConfigError([{
28103
28170
  phase: "discovery",
@@ -28186,7 +28253,7 @@ function portableMemberPathHint(pathHint) {
28186
28253
  return pathHint !== "." && kxmPortablePath(pathHint);
28187
28254
  }
28188
28255
  function loadBaseProjectDeclarations(projectFile) {
28189
- if (!existsSync24(projectFile)) return [];
28256
+ if (!existsSync25(projectFile)) return [];
28190
28257
  const value = parseRestrictedYaml2(readFileSync24(projectFile, "utf8"), ".kxm/project.yaml");
28191
28258
  const repositories = Array.isArray(value?.repositories) ? value.repositories : [];
28192
28259
  const members = [];
@@ -28263,7 +28330,7 @@ import { randomUUID as randomUUID11 } from "node:crypto";
28263
28330
  import {
28264
28331
  chmodSync as chmodSync5,
28265
28332
  closeSync as closeSync3,
28266
- existsSync as existsSync25,
28333
+ existsSync as existsSync26,
28267
28334
  fsyncSync as fsyncSync2,
28268
28335
  linkSync,
28269
28336
  lstatSync as lstatSync7,
@@ -28372,7 +28439,7 @@ function readRegularBounded(file, label) {
28372
28439
  }
28373
28440
  function readOptionalManaged(projectRoot, path4) {
28374
28441
  const file = join29(projectRoot, ...path4.split("/"));
28375
- if (!existsSync25(file)) return void 0;
28442
+ if (!existsSync26(file)) return void 0;
28376
28443
  let parent = dirname15(file);
28377
28444
  const boundary = resolve20(projectRoot);
28378
28445
  while (parent !== boundary) {
@@ -28385,7 +28452,7 @@ function readOptionalManaged(projectRoot, path4) {
28385
28452
  function readProjectAndProvenance(projectRoot, schemasDir) {
28386
28453
  const projectFile = join29(projectRoot, ".kxm", "project.yaml");
28387
28454
  const provenanceFile = join29(projectRoot, ...KXM_TEMPLATE_PROVENANCE_PATH.split("/"));
28388
- if (!existsSync25(projectFile) || !existsSync25(provenanceFile)) return void 0;
28455
+ if (!existsSync26(projectFile) || !existsSync26(provenanceFile)) return void 0;
28389
28456
  const projectBytes = readOptionalManaged(projectRoot, ".kxm/project.yaml");
28390
28457
  const provenanceBytes = readOptionalManaged(projectRoot, KXM_TEMPLATE_PROVENANCE_PATH);
28391
28458
  if (!projectBytes || !provenanceBytes) return void 0;
@@ -28566,11 +28633,11 @@ function updatePhase(projectRoot, operation, phase) {
28566
28633
  }
28567
28634
  function readOperation(projectRoot, schemasDir) {
28568
28635
  const root = transactionRoot(projectRoot);
28569
- if (!existsSync25(root)) return void 0;
28636
+ if (!existsSync26(root)) return void 0;
28570
28637
  const stat = lstatSync7(root);
28571
28638
  if (stat.isSymbolicLink() || !stat.isDirectory()) fail3("init_transaction_invalid", TRANSACTION_NAME, "initialization transaction must be a regular directory", "path");
28572
28639
  const file = operationFile(projectRoot);
28573
- if (!existsSync25(file)) {
28640
+ if (!existsSync26(file)) {
28574
28641
  const entries = readdirSync9(root, { withFileTypes: true });
28575
28642
  const recoverable = entries.every((entry) => entry.isFile() && /^\.operation-op_[a-f0-9]{32}-[a-f0-9-]{36}\.tmp$/.test(entry.name));
28576
28643
  if (recoverable) return void 0;
@@ -28598,7 +28665,7 @@ function readOperation(projectRoot, schemasDir) {
28598
28665
  return operation;
28599
28666
  }
28600
28667
  function hasKxmInitTransaction(projectRoot) {
28601
- return existsSync25(transactionRoot(projectRoot));
28668
+ return existsSync26(transactionRoot(projectRoot));
28602
28669
  }
28603
28670
  function readTarget(projectRoot, path4) {
28604
28671
  return readOptionalManaged(join29(transactionRoot(projectRoot), "targets"), path4);
@@ -28675,10 +28742,10 @@ function sourceConfigFiles(projectRoot) {
28675
28742
  ".kxm/repo/repo.yaml",
28676
28743
  ".kxm/repo/env.yaml"
28677
28744
  ];
28678
- const files = fixed.filter((path4) => existsSync25(join29(projectRoot, ...path4.split("/"))));
28745
+ const files = fixed.filter((path4) => existsSync26(join29(projectRoot, ...path4.split("/"))));
28679
28746
  for (const directory of ["agents", "models", "workflows"]) {
28680
28747
  const absolute = join29(projectRoot, ".kxm", directory);
28681
- if (!existsSync25(absolute)) continue;
28748
+ if (!existsSync26(absolute)) continue;
28682
28749
  const stat = lstatSync7(absolute);
28683
28750
  if (stat.isSymbolicLink() || !stat.isDirectory()) fail3("repair_directory_invalid", `.kxm/${directory}`, "configuration directory must be regular", "path");
28684
28751
  for (const entry of readdirSync9(absolute, { withFileTypes: true })) {
@@ -28700,7 +28767,7 @@ function absoluteMemberBindings(projectRoot, project, explicit = {}) {
28700
28767
  if (repository.role !== "member" || typeof repository.id !== "string" || Object.hasOwn(bindings, repository.id)) continue;
28701
28768
  if (typeof repository.pathHint === "string") {
28702
28769
  const candidate2 = resolve20(projectRoot, ...repository.pathHint.split("/"));
28703
- if (existsSync25(candidate2)) bindings[repository.id] = candidate2;
28770
+ if (existsSync26(candidate2)) bindings[repository.id] = candidate2;
28704
28771
  }
28705
28772
  }
28706
28773
  return bindings;
@@ -28726,7 +28793,7 @@ function prepareShadow(projectRoot, operation, options) {
28726
28793
  if (!bytes) fail3("init_transaction_target_missing", entry.path, "pinned target artifact is missing");
28727
28794
  const destination = join29(shadow, ...entry.path.split("/"));
28728
28795
  mkdirSync21(dirname15(destination), { recursive: true, mode: 448 });
28729
- const mode = existsSync25(destination) ? lstatSync7(destination).mode & 511 : 420;
28796
+ const mode = existsSync26(destination) ? lstatSync7(destination).mode & 511 : 420;
28730
28797
  writeDurable(destination, bytes, mode);
28731
28798
  }
28732
28799
  const project = parseRestrictedYaml2(readFileSync25(join29(shadow, ".kxm", "project.yaml")), ".kxm/project.yaml");
@@ -28767,7 +28834,7 @@ function prepareOperation(projectRoot, operation, rendered, options) {
28767
28834
  const transaction = transactionRoot(projectRoot);
28768
28835
  validateOperationRecord(operation, options.schemasDir);
28769
28836
  validateOperationIntent(projectRoot, operation);
28770
- if (existsSync25(transaction)) fail3("init_transaction_exists", TRANSACTION_NAME, "an initialization transaction already exists and must be resumed");
28837
+ if (existsSync26(transaction)) fail3("init_transaction_exists", TRANSACTION_NAME, "an initialization transaction already exists and must be resumed");
28771
28838
  mkdirSync21(transaction, { mode: 448 });
28772
28839
  syncDirectory2(projectRoot);
28773
28840
  writeOperation(projectRoot, operation);
@@ -28794,14 +28861,14 @@ function ensureDestinationParents(projectRoot, path4) {
28794
28861
  let current = boundary;
28795
28862
  for (const segment of segments) {
28796
28863
  current = join29(current, segment);
28797
- if (!existsSync25(current)) mkdirSync21(current, { mode: 448 });
28864
+ if (!existsSync26(current)) mkdirSync21(current, { mode: 448 });
28798
28865
  const stat = lstatSync7(current);
28799
28866
  if (stat.isSymbolicLink() || !stat.isDirectory()) fail3("repair_destination_parent_invalid", path4, "destination parent must be a regular directory", "path");
28800
28867
  }
28801
28868
  }
28802
28869
  function destinationSha(projectRoot, path4) {
28803
28870
  const file = join29(projectRoot, ...path4.split("/"));
28804
- if (!existsSync25(file)) return void 0;
28871
+ if (!existsSync26(file)) return void 0;
28805
28872
  return kxmContentSha256(readRegularBounded(file, path4));
28806
28873
  }
28807
28874
  function requireConditionalLinkSupport(directory, operationId, path4) {
@@ -28828,13 +28895,13 @@ function atomicInstallTarget(projectRoot, operation, entry) {
28828
28895
  const temporaryPrefix = `.kxm-repair-${operation.operationId}-`;
28829
28896
  const pathKey = kxmContentSha256(entry.path).slice("sha256:".length, "sha256:".length + 16);
28830
28897
  const displaced = join29(directory, `${temporaryPrefix}${pathKey}.preimage`);
28831
- if (existsSync25(displaced)) {
28898
+ if (existsSync26(displaced)) {
28832
28899
  const stat = lstatSync7(displaced);
28833
28900
  if (stat.isSymbolicLink() || !stat.isFile()) fail3("repair_preimage_temporary_invalid", entry.path, "stale displaced preimage is not a regular file", "path");
28834
28901
  if (entry.observedSha256 === null || kxmContentSha256(readFileSync25(displaced)) !== entry.observedSha256) {
28835
28902
  fail3("repair_preimage_temporary_mismatch", entry.path, "stale displaced preimage does not match the pinned operation");
28836
28903
  }
28837
- if (!existsSync25(destination)) {
28904
+ if (!existsSync26(destination)) {
28838
28905
  try {
28839
28906
  linkSync(displaced, destination);
28840
28907
  } catch {
@@ -28860,14 +28927,14 @@ function atomicInstallTarget(projectRoot, operation, entry) {
28860
28927
  const bytes = readTarget(projectRoot, entry.path);
28861
28928
  if (!bytes) fail3("init_transaction_target_missing", entry.path, "pinned target artifact is missing");
28862
28929
  const temporary = join29(directory, `${temporaryPrefix}${randomUUID11()}.tmp`);
28863
- const mode = existsSync25(destination) ? lstatSync7(destination).mode & 511 : 420;
28930
+ const mode = existsSync26(destination) ? lstatSync7(destination).mode & 511 : 420;
28864
28931
  try {
28865
28932
  writeDurableNew(temporary, bytes, mode);
28866
28933
  if (entry.observedSha256 !== null) {
28867
28934
  renameSync7(destination, displaced);
28868
28935
  syncDirectory2(directory);
28869
28936
  if (kxmContentSha256(readFileSync25(displaced)) !== entry.observedSha256) {
28870
- if (!existsSync25(destination)) {
28937
+ if (!existsSync26(destination)) {
28871
28938
  try {
28872
28939
  linkSync(displaced, destination);
28873
28940
  } catch {
@@ -28883,7 +28950,7 @@ function atomicInstallTarget(projectRoot, operation, entry) {
28883
28950
  try {
28884
28951
  linkSync(temporary, destination);
28885
28952
  } catch {
28886
- if (!existsSync25(destination) && existsSync25(displaced)) {
28953
+ if (!existsSync26(destination) && existsSync26(displaced)) {
28887
28954
  try {
28888
28955
  linkSync(displaced, destination);
28889
28956
  } catch {
@@ -28906,7 +28973,7 @@ function atomicInstallTarget(projectRoot, operation, entry) {
28906
28973
  }
28907
28974
  function installedCreateMatches(projectRoot, operation) {
28908
28975
  const configRoot = join29(projectRoot, ".kxm");
28909
- if (!existsSync25(configRoot)) return false;
28976
+ if (!existsSync26(configRoot)) return false;
28910
28977
  const actual = [];
28911
28978
  const visit2 = (directory) => {
28912
28979
  for (const entry of readdirSync9(directory, { withFileTypes: true })) {
@@ -28926,7 +28993,7 @@ function relativeConfigPath(projectRoot, file) {
28926
28993
  return relative5(resolve20(projectRoot), file).replaceAll("\\", "/");
28927
28994
  }
28928
28995
  function syncTreeDirectories(root) {
28929
- if (!existsSync25(root)) return;
28996
+ if (!existsSync26(root)) return;
28930
28997
  const directories = [];
28931
28998
  const visit2 = (directory) => {
28932
28999
  directories.push(directory);
@@ -28956,7 +29023,7 @@ function applyOperation(projectRoot, original, options, resumed) {
28956
29023
  operation = updatePhase(projectRoot, operation, "applying");
28957
29024
  if (operation.kind === "create") {
28958
29025
  const destination = join29(projectRoot, ".kxm");
28959
- if (!existsSync25(destination)) {
29026
+ if (!existsSync26(destination)) {
28960
29027
  rebuildCreateShadow(projectRoot, operation, effectiveOptions);
28961
29028
  renameSync7(join29(shadowRoot(projectRoot), ".kxm"), destination);
28962
29029
  syncDirectory2(projectRoot);
@@ -29119,7 +29186,7 @@ function resumeKxmInitTransaction(projectRoot, options = {}) {
29119
29186
  let operation = readOperation(projectRoot, options.schemasDir);
29120
29187
  if (!operation) {
29121
29188
  const root = transactionRoot(projectRoot);
29122
- if (existsSync25(root)) {
29189
+ if (existsSync26(root)) {
29123
29190
  rmSync9(root, { recursive: true, force: true });
29124
29191
  syncDirectory2(projectRoot);
29125
29192
  }
@@ -29167,7 +29234,7 @@ var init_repair = __esm({
29167
29234
 
29168
29235
  // plugins/kxm/src/init.ts
29169
29236
  import { randomUUID as randomUUID12 } from "node:crypto";
29170
- import { existsSync as existsSync26 } from "node:fs";
29237
+ import { existsSync as existsSync27 } from "node:fs";
29171
29238
  import { basename as basename6, join as join30 } from "node:path";
29172
29239
  function initIssue(code, file, message) {
29173
29240
  return { phase: "discovery", code, file, message };
@@ -29261,7 +29328,7 @@ function validateBindingIdentities(bundle, persisted, explicit) {
29261
29328
  }
29262
29329
  function initializeKxmProjectAtGitRoot(start, gitRoot, options, mutationLock) {
29263
29330
  const storeOptions = bindingStoreOptions(options);
29264
- const persisted = existsSync26(join30(gitRoot, ".kxm", "project.yaml")) ? readKxmLocalBindings(gitRoot, storeOptions) : void 0;
29331
+ const persisted = existsSync27(join30(gitRoot, ".kxm", "project.yaml")) ? readKxmLocalBindings(gitRoot, storeOptions) : void 0;
29265
29332
  const repositoryBindings = Object.fromEntries([
29266
29333
  ...Object.entries(persisted?.repositories ?? {}),
29267
29334
  ...Object.entries(options.repositoryBindings ?? {})
@@ -29449,7 +29516,7 @@ function initializeKxmProjectAtGitRoot(start, gitRoot, options, mutationLock) {
29449
29516
  const files = [...rendered.files.keys()];
29450
29517
  if (options.dryRun) return { action: "planned", plan, projectRoot: gitRoot, files };
29451
29518
  if (!mutationLock) throw new Error("project mutation lock is required to create configuration");
29452
- if (existsSync26(join30(gitRoot, ".kxm"))) {
29519
+ if (existsSync27(join30(gitRoot, ".kxm"))) {
29453
29520
  throw new KxmConfigError([initIssue("workspace_changed", ".kxm", "workspace changed after planning; existing .kxm state was not overwritten")]);
29454
29521
  }
29455
29522
  const created = prepareAndApplyKxmCreate(gitRoot, rendered, transactionOptions);
@@ -29687,6 +29754,7 @@ __export(project_exports, {
29687
29754
  cmdRouteCount: () => cmdRouteCount,
29688
29755
  cmdRouteList: () => cmdRouteList,
29689
29756
  cmdTenantStatus: () => cmdTenantStatus,
29757
+ cmdVisionAssert: () => cmdVisionAssert,
29690
29758
  explicitRepositoryBindings: () => explicitRepositoryBindings,
29691
29759
  initPlanPayload: () => initPlanPayload,
29692
29760
  kxmDriveCliSeams: () => kxmDriveCliSeams,
@@ -29694,7 +29762,7 @@ __export(project_exports, {
29694
29762
  runEngineNotice: () => runEngineNotice,
29695
29763
  selectInventoryModel: () => selectInventoryModel
29696
29764
  });
29697
- import { existsSync as existsSync27 } from "node:fs";
29765
+ import { existsSync as existsSync28 } from "node:fs";
29698
29766
  import { dirname as dirname16, join as join31, resolve as resolve21 } from "node:path";
29699
29767
  import { createInterface as createInterface2 } from "node:readline";
29700
29768
  function initPlanPayload(plan) {
@@ -29921,7 +29989,7 @@ async function cmdRestore(runtime, manifestArg, options = {}) {
29921
29989
  [
29922
29990
  ...plan.stores.flatMap((store) => [
29923
29991
  { action: "write", target: store.targetPath },
29924
- ...[`${store.targetPath}-wal`, `${store.targetPath}-shm`].filter((file) => existsSync27(file)).map((file) => ({ action: "delete", target: file }))
29992
+ ...[`${store.targetPath}-wal`, `${store.targetPath}-shm`].filter((file) => existsSync28(file)).map((file) => ({ action: "delete", target: file }))
29925
29993
  ]),
29926
29994
  ...plan.files.map((file) => ({ action: "write", target: file.targetPath }))
29927
29995
  ],
@@ -30465,6 +30533,46 @@ async function cmdRouteCount(runtime) {
30465
30533
  print(runtime.io, runtime.json, { ok: true, command: "routes count", admitted: policy.admitted.length, disabled: policy.disabled.length, summary }, summary);
30466
30534
  return 0;
30467
30535
  }
30536
+ async function cmdVisionAssert(runtime, options) {
30537
+ const image = options.image?.trim();
30538
+ const question = options.question?.trim();
30539
+ if (!image || !question) {
30540
+ print(runtime.io, runtime.json, { ok: false, command: "vision assert", error: "image_and_question_required" }, "both --image and --question are required");
30541
+ return 2;
30542
+ }
30543
+ let expect;
30544
+ if (options.expect !== void 0) {
30545
+ if (options.expect !== "true" && options.expect !== "false") {
30546
+ print(runtime.io, runtime.json, { ok: false, command: "vision assert", error: "expect_invalid" }, "--expect must be true or false");
30547
+ return 2;
30548
+ }
30549
+ expect = options.expect === "true";
30550
+ }
30551
+ let timeoutMs;
30552
+ if (options.timeoutMs !== void 0) {
30553
+ const parsed = Number(options.timeoutMs);
30554
+ if (!Number.isInteger(parsed)) {
30555
+ print(runtime.io, runtime.json, { ok: false, command: "vision assert", error: "timeout_invalid" }, "--timeout-ms must be an integer");
30556
+ return 2;
30557
+ }
30558
+ timeoutMs = parsed;
30559
+ }
30560
+ const result = await runVisionGate({
30561
+ projectRoot: runtime.dirs.workdir,
30562
+ imagePath: resolve21(runtime.cwd, image),
30563
+ question,
30564
+ ...options.route !== void 0 ? { route: options.route } : {},
30565
+ ...timeoutMs !== void 0 ? { timeoutMs } : {}
30566
+ });
30567
+ const pass = result.verdict !== null && (expect === void 0 || result.verdict === expect);
30568
+ print(
30569
+ runtime.io,
30570
+ runtime.json,
30571
+ { ok: pass, command: "vision assert", ...expect !== void 0 ? { expect } : {}, ...result },
30572
+ result.verdict === null ? `vision gate fail-closed: ${result.divergence}` : `verdict ${result.verdict}${expect !== void 0 ? ` (expected ${expect})` : ""}`
30573
+ );
30574
+ return pass ? 0 : 1;
30575
+ }
30468
30576
  async function cmdModelInventoryRefresh(runtime) {
30469
30577
  if (runtime.dryRun) {
30470
30578
  print(runtime.io, runtime.json, { ok: true, command: "models inventory refresh", dryRun: true }, "would refresh .kxm/models/inventory.yaml");
@@ -30619,6 +30727,7 @@ var init_project = __esm({
30619
30727
  init_hub_autostart();
30620
30728
  init_model_inventory();
30621
30729
  init_routes();
30730
+ init_vision_gate();
30622
30731
  init_project_config();
30623
30732
  init_init();
30624
30733
  init_permission();
@@ -37033,7 +37142,7 @@ async function cmdRetrospectiveExport(runtime, runId, options) {
37033
37142
  // plugins/kxm/src/cli/tasks.ts
37034
37143
  var import_yaml15 = __toESM(require_dist(), 1);
37035
37144
  init_harness();
37036
- import { existsSync as existsSync28, readFileSync as readFileSync27, readdirSync as readdirSync10 } from "node:fs";
37145
+ import { existsSync as existsSync29, readFileSync as readFileSync27, readdirSync as readdirSync10 } from "node:fs";
37037
37146
  import { basename as basename7, join as join32, resolve as resolve22 } from "node:path";
37038
37147
 
37039
37148
  // plugins/kxm/src/suggest.ts
@@ -38093,7 +38202,7 @@ async function cmdSuggest(runtime, promptParts, probeHarnesses2 = probeHarnesses
38093
38202
  }
38094
38203
  const inventory = runtime.dryRun ? void 0 : await probeHarnesses2({ env: runtime.env });
38095
38204
  const suggestion = suggestWorkflowAndRoles(prompt, { availableHarnesses: inventory?.harnesses });
38096
- if (suggestion.execution.supported && [".yaml", ".yml"].some((extension) => existsSync28(join32(runtime.cwd, ".kxm", "workflows", `${suggestion.workflowId}${extension}`)))) {
38205
+ if (suggestion.execution.supported && [".yaml", ".yml"].some((extension) => existsSync29(join32(runtime.cwd, ".kxm", "workflows", `${suggestion.workflowId}${extension}`)))) {
38097
38206
  suggestion.roles = [];
38098
38207
  suggestion.execution = {
38099
38208
  supported: false,
@@ -38313,7 +38422,7 @@ async function cmdStudioLayout(runtime, workflowPath) {
38313
38422
  }
38314
38423
  let yamlContent;
38315
38424
  let workflowId = "default";
38316
- if (existsSync28(filePath)) {
38425
+ if (existsSync29(filePath)) {
38317
38426
  yamlContent = readFileSync27(filePath, "utf8");
38318
38427
  } else {
38319
38428
  yamlContent = `schema: kxm.workflow.v1
@@ -38389,12 +38498,12 @@ async function cmdStudioServe(runtime, options) {
38389
38498
  try {
38390
38499
  const wfDir = join32(runtime.cwd, ".kxm", "workflows");
38391
38500
  const defaultPath = join32(wfDir, "default.yaml");
38392
- let targetPath = existsSync28(defaultPath) ? defaultPath : void 0;
38393
- if (!targetPath && existsSync28(wfDir)) {
38501
+ let targetPath = existsSync29(defaultPath) ? defaultPath : void 0;
38502
+ if (!targetPath && existsSync29(wfDir)) {
38394
38503
  const yml = readdirSync10(wfDir).find((f2) => f2.endsWith(".yaml") || f2.endsWith(".yml"));
38395
38504
  if (yml) targetPath = join32(wfDir, yml);
38396
38505
  }
38397
- if (targetPath && existsSync28(targetPath)) {
38506
+ if (targetPath && existsSync29(targetPath)) {
38398
38507
  const raw = readFileSync27(targetPath, "utf8");
38399
38508
  const parsed = (0, import_yaml15.parse)(raw);
38400
38509
  const wfId = basename7(targetPath).replace(/\.(yaml|yml)$/, "");
@@ -39017,7 +39126,7 @@ async function cmdPluginInstall(runtime, options) {
39017
39126
  init_project_config();
39018
39127
  init_types();
39019
39128
  import { spawn as spawn4 } from "node:child_process";
39020
- import { existsSync as existsSync29 } from "node:fs";
39129
+ import { existsSync as existsSync30 } from "node:fs";
39021
39130
  import { join as join35 } from "node:path";
39022
39131
  var GENERATOR = "plans/kxm-roadmap/update-dashboard.mjs";
39023
39132
  var SERVER = "ops/docs-site/serve.py";
@@ -39036,7 +39145,7 @@ function requireProject(runtime, command) {
39036
39145
  }
39037
39146
  function requireFile(runtime, command, root, relative8, error) {
39038
39147
  const file = join35(root, relative8);
39039
- if (existsSync29(file)) return file;
39148
+ if (existsSync30(file)) return file;
39040
39149
  print(
39041
39150
  runtime.io,
39042
39151
  runtime.json,
@@ -39090,7 +39199,7 @@ init_runtime_store();
39090
39199
  init_runtime_supervisor();
39091
39200
  init_types();
39092
39201
  import { spawnSync as spawnSync7 } from "node:child_process";
39093
- import { chmodSync as chmodSync6, existsSync as existsSync30, mkdirSync as mkdirSync24, readFileSync as readFileSync29, writeFileSync as writeFileSync22 } from "node:fs";
39202
+ import { chmodSync as chmodSync6, existsSync as existsSync31, mkdirSync as mkdirSync24, readFileSync as readFileSync29, writeFileSync as writeFileSync22 } from "node:fs";
39094
39203
  import { basename as basename8, dirname as dirname17, join as join36, resolve as resolve24 } from "node:path";
39095
39204
  var LANE_SCHEMA = "kxm.lanes.v1";
39096
39205
  var SHA_RE = /^[a-f0-9]{40,64}$/;
@@ -39146,7 +39255,7 @@ function isLaneFile(value) {
39146
39255
  }
39147
39256
  function loadLanes(runtime, command) {
39148
39257
  const file = lanesFile(runtime);
39149
- if (!existsSync30(file)) return { schema: LANE_SCHEMA, lanes: {} };
39258
+ if (!existsSync31(file)) return { schema: LANE_SCHEMA, lanes: {} };
39150
39259
  try {
39151
39260
  const parsed = JSON.parse(readFileSync29(file, "utf8"));
39152
39261
  if (!isLaneFile(parsed)) return refuse(runtime, command, "lanes_unreadable", `lane record ${file} is malformed`);
@@ -39205,7 +39314,7 @@ function withLaneCwd(runtime, unit, command = "lane") {
39205
39314
  const loaded = loadLanes(runtime, command);
39206
39315
  if (typeof loaded === "number") return loaded;
39207
39316
  const lane = loaded.lanes[unit];
39208
- if (!lane || !existsSync30(lane.path)) {
39317
+ if (!lane || !existsSync31(lane.path)) {
39209
39318
  return refuse(runtime, command, "lane_missing", `lane ${unit} is not recorded or its worktree is absent`, { unit });
39210
39319
  }
39211
39320
  return {
@@ -39276,7 +39385,7 @@ function aheadBehind(path4, baseSha) {
39276
39385
  return { behind: Number(match[1]), ahead: Number(match[2]) };
39277
39386
  }
39278
39387
  function laneView(unit, record) {
39279
- const exists = existsSync30(record.path);
39388
+ const exists = existsSync31(record.path);
39280
39389
  const counts = exists ? aheadBehind(record.path, record.baseSha) : { ahead: null, behind: null };
39281
39390
  return {
39282
39391
  unit,
@@ -39353,7 +39462,7 @@ async function laneStoreRuns(runtime, lanePath) {
39353
39462
  }
39354
39463
  }
39355
39464
  const eventsPath = kxmProjectRunEventsPath(lanePath, runtime.env);
39356
- if (!existsSync30(eventsPath)) return void 0;
39465
+ if (!existsSync31(eventsPath)) return void 0;
39357
39466
  const store = new KxmRunEventStore(eventsPath);
39358
39467
  try {
39359
39468
  return store.runsForProject(projectId(), LANE_RUN_LIST_LIMIT).map((run) => ({ runId: run.runId, status: run.status }));
@@ -39422,7 +39531,7 @@ async function cmdLaneCreate(runtime, unit, options = {}) {
39422
39531
  const loaded = loadLanes(runtime, command);
39423
39532
  if (typeof loaded === "number") return loaded;
39424
39533
  const path4 = laneWorktreePath(root, unit);
39425
- if (loaded.lanes[unit] || existsSync30(path4) || branchExists(root, unit)) {
39534
+ if (loaded.lanes[unit] || existsSync31(path4) || branchExists(root, unit)) {
39426
39535
  return refuse(runtime, command, "lane_exists", `lane ${unit} already exists`, { unit });
39427
39536
  }
39428
39537
  const baseSha = resolveBase(root, base);
@@ -39447,7 +39556,7 @@ async function cmdLaneCreate(runtime, unit, options = {}) {
39447
39556
  return refuse(runtime, command, "lane_git_failed", `git worktree add failed: ${added.stderr.trim().slice(0, 300)}`, { unit });
39448
39557
  }
39449
39558
  const recordedPath = discoverKxmProjectRoot(path4) ?? path4;
39450
- if (!existsSync30(join36(recordedPath, ".kxm", "project.yaml"))) {
39559
+ if (!existsSync31(join36(recordedPath, ".kxm", "project.yaml"))) {
39451
39560
  git2(root, ["worktree", "remove", "--force", path4]);
39452
39561
  return refuse(runtime, command, "lane_not_project", `lane ${recordedPath} has no .kxm/project.yaml; the worktree was removed`, { unit, path: recordedPath });
39453
39562
  }
@@ -39513,7 +39622,7 @@ async function unregisterLaneRoot(runtime, projectRoot, force) {
39513
39622
  return void 0;
39514
39623
  }
39515
39624
  const paths = kxmRuntimePaths({ env: runtime.env });
39516
- if (!existsSync30(paths.registryDb)) return void 0;
39625
+ if (!existsSync31(paths.registryDb)) return void 0;
39517
39626
  const registry = new KxmRuntimeRegistry(paths.registryDb);
39518
39627
  let outcome;
39519
39628
  try {
@@ -39543,7 +39652,7 @@ async function cmdLaneDrop(runtime, unit, options = {}) {
39543
39652
  if (!record) return refuse(runtime, command, "lane_missing", `lane ${unit} is not recorded`, { unit });
39544
39653
  const force = options.force === true;
39545
39654
  if (!force) {
39546
- if (existsSync30(record.path)) {
39655
+ if (existsSync31(record.path)) {
39547
39656
  const dirty = porcelainCount(record.path);
39548
39657
  if (dirty === null) {
39549
39658
  return refuse(runtime, command, "lane_git_failed", `lane ${unit} worktree status could not be read`, { unit });
@@ -39569,7 +39678,7 @@ async function cmdLaneDrop(runtime, unit, options = {}) {
39569
39678
  if (unregistered !== void 0) {
39570
39679
  return refuse(runtime, command, "lane_unregister_failed", `lane ${unit} could not be unregistered: ${unregistered.slice(0, 300)}`, { unit });
39571
39680
  }
39572
- if (existsSync30(record.path)) {
39681
+ if (existsSync31(record.path)) {
39573
39682
  const removed = git2(root, ["worktree", "remove", ...force ? ["--force"] : [], record.path]);
39574
39683
  if (removed.status !== 0) {
39575
39684
  return refuse(runtime, command, "lane_git_failed", `git worktree remove failed: ${removed.stderr.trim().slice(0, 300)}`, { unit });
@@ -39680,7 +39789,7 @@ function cmdLand(runtime, options = {}) {
39680
39789
  init_project_config();
39681
39790
  init_types();
39682
39791
  import { spawnSync as spawnSync8 } from "node:child_process";
39683
- import { existsSync as existsSync31 } from "node:fs";
39792
+ import { existsSync as existsSync32 } from "node:fs";
39684
39793
  import { join as join38 } from "node:path";
39685
39794
  var kxmAssignCliSeams = {};
39686
39795
  var RUNNER_REL = "scripts/assignment-run.mjs";
@@ -39753,7 +39862,7 @@ async function cmdAssign(runtime, subcommand, args) {
39753
39862
  const command = `assign ${subcommand}`;
39754
39863
  const root = discoverKxmProjectRoot(runtime.cwd);
39755
39864
  if (!root) return refuse3(runtime, command, "project_required", `${command} requires a KXM project (run kxm init first)`);
39756
- if (!existsSync31(join38(root, "scripts", "assignment-run.mjs"))) {
39865
+ if (!existsSync32(join38(root, "scripts", "assignment-run.mjs"))) {
39757
39866
  return refuse3(runtime, command, "assign_runner_missing", `${command} requires scripts/assignment-run.mjs in the project root`);
39758
39867
  }
39759
39868
  const argv = ["node", RUNNER_REL, ...runnerArgv(subcommand, args)];
@@ -39784,7 +39893,7 @@ async function cmdAssign(runtime, subcommand, args) {
39784
39893
  // plugins/kxm/src/cli/hub.ts
39785
39894
  init_redact();
39786
39895
  import { randomUUID as randomUUID14 } from "node:crypto";
39787
- import { existsSync as existsSync37, mkdirSync as mkdirSync28, readFileSync as readFileSync35, readdirSync as readdirSync12, rmSync as rmSync11, writeFileSync as writeFileSync25 } from "node:fs";
39896
+ import { existsSync as existsSync38, mkdirSync as mkdirSync28, readFileSync as readFileSync35, readdirSync as readdirSync12, rmSync as rmSync11, writeFileSync as writeFileSync25 } from "node:fs";
39788
39897
  import { join as join48, resolve as resolve27 } from "node:path";
39789
39898
  init_hub_env();
39790
39899
  init_envelope();
@@ -48370,7 +48479,7 @@ var SHARED_EFFECT_KINDS = Object.freeze([
48370
48479
  // plugins/kxm/src/local-snapshot.ts
48371
48480
  init_sqlite();
48372
48481
  init_telemetry();
48373
- import { existsSync as existsSync32, readdirSync as readdirSync11, readFileSync as readFileSync30 } from "node:fs";
48482
+ import { existsSync as existsSync33, readdirSync as readdirSync11, readFileSync as readFileSync30 } from "node:fs";
48374
48483
  import { homedir as homedir6 } from "node:os";
48375
48484
  import { isAbsolute as isAbsolute10, join as join42, resolve as resolve25 } from "node:path";
48376
48485
  var DEFAULT_BUSY_TIMEOUT_MS = 5e3;
@@ -48503,15 +48612,15 @@ function resolveKxmSnapshotPaths(cwd, env = process.env) {
48503
48612
  return { dataPath, stateDir };
48504
48613
  }
48505
48614
  function resolveKxmStateRoot(stateDir, options) {
48506
- if (options?.kxmStateRoot && existsSync32(options.kxmStateRoot)) {
48615
+ if (options?.kxmStateRoot && existsSync33(options.kxmStateRoot)) {
48507
48616
  return resolve25(options.kxmStateRoot);
48508
48617
  }
48509
- if (existsSync32(join42(stateDir, "runtime", "registry.db")) || existsSync32(join42(stateDir, "runtime", "projects"))) {
48618
+ if (existsSync33(join42(stateDir, "runtime", "registry.db")) || existsSync33(join42(stateDir, "runtime", "projects"))) {
48510
48619
  return stateDir;
48511
48620
  }
48512
48621
  const env = options?.env ?? process.env;
48513
48622
  const explicit = env.KXM_STATE_HOME?.trim() || env.KXM_USER_STATE_DIR?.trim() || env.KXM_STATE_ROOT?.trim();
48514
- if (explicit && isAbsolute10(explicit) && existsSync32(resolve25(explicit))) {
48623
+ if (explicit && isAbsolute10(explicit) && existsSync33(resolve25(explicit))) {
48515
48624
  return resolve25(explicit);
48516
48625
  }
48517
48626
  let base;
@@ -48526,7 +48635,7 @@ function resolveKxmStateRoot(stateDir, options) {
48526
48635
  base = xdgState && isAbsolute10(xdgState) ? xdgState : join42(homedir6(), ".local", "state");
48527
48636
  base = resolve25(base, "kxm");
48528
48637
  }
48529
- if (existsSync32(base)) return base;
48638
+ if (existsSync33(base)) return base;
48530
48639
  return void 0;
48531
48640
  }
48532
48641
  function readRuntimeRuns(eventDb, projectId) {
@@ -48559,7 +48668,7 @@ function loadLocalMeshSnapshot(dataPath, stateDir, options) {
48559
48668
  let legacyRuns = [];
48560
48669
  let legacyRunTotal = 0;
48561
48670
  let plans = [];
48562
- if (existsSync32(dataPath)) {
48671
+ if (existsSync33(dataPath)) {
48563
48672
  hasLegacy = true;
48564
48673
  const database = openReadOnlyDatabase(dataPath);
48565
48674
  try {
@@ -48594,7 +48703,7 @@ function loadLocalMeshSnapshot(dataPath, stateDir, options) {
48594
48703
  const registryDbPath = join42(runtimeDir, "registry.db");
48595
48704
  const projectsDir = join42(runtimeDir, "projects");
48596
48705
  const projectKeys = /* @__PURE__ */ new Set();
48597
- if (existsSync32(registryDbPath)) {
48706
+ if (existsSync33(registryDbPath)) {
48598
48707
  hasKxm = true;
48599
48708
  try {
48600
48709
  const regDb = openReadOnlyDatabase(registryDbPath);
@@ -48610,7 +48719,7 @@ function loadLocalMeshSnapshot(dataPath, stateDir, options) {
48610
48719
  } catch {
48611
48720
  }
48612
48721
  }
48613
- if (!scope && existsSync32(projectsDir)) {
48722
+ if (!scope && existsSync33(projectsDir)) {
48614
48723
  try {
48615
48724
  for (const entry of readdirSync11(projectsDir, { withFileTypes: true })) {
48616
48725
  if (entry.isDirectory()) {
@@ -48623,7 +48732,7 @@ function loadLocalMeshSnapshot(dataPath, stateDir, options) {
48623
48732
  for (const key of projectKeys) {
48624
48733
  if (scope && !RUNTIME_PROJECT_KEY.test(key)) continue;
48625
48734
  const eventDbPath = join42(projectsDir, key, "run-events.db");
48626
- if (existsSync32(eventDbPath)) {
48735
+ if (existsSync33(eventDbPath)) {
48627
48736
  hasKxm = true;
48628
48737
  try {
48629
48738
  const eventDb = openReadOnlyDatabase(eventDbPath);
@@ -48641,7 +48750,7 @@ function loadLocalMeshSnapshot(dataPath, stateDir, options) {
48641
48750
  }
48642
48751
  }
48643
48752
  const pids = [];
48644
- if (existsSync32(stateDir)) {
48753
+ if (existsSync33(stateDir)) {
48645
48754
  for (const file of readdirSync11(stateDir).filter((name) => name.endsWith(".pid"))) {
48646
48755
  try {
48647
48756
  const record = JSON.parse(readFileSync30(join42(stateDir, file), "utf8"));
@@ -48684,7 +48793,7 @@ function loadLocalMeshSnapshot(dataPath, stateDir, options) {
48684
48793
  source = "legacy";
48685
48794
  }
48686
48795
  const telemetryFile = join42(stateDir, "telemetry.jsonl");
48687
- const spend = existsSync32(telemetryFile) ? readRoutingRecords(telemetryFile) : [];
48796
+ const spend = existsSync33(telemetryFile) ? readRoutingRecords(telemetryFile) : [];
48688
48797
  return {
48689
48798
  source,
48690
48799
  agents: agents.sort((a, b2) => Number(b2.online) - Number(a.online) || a.name.localeCompare(b2.name)),
@@ -49587,7 +49696,7 @@ async function runMeshTui(input) {
49587
49696
  // plugins/kxm/src/session-work.ts
49588
49697
  import { spawnSync as spawnSync10 } from "node:child_process";
49589
49698
  import { randomUUID as randomUUID13 } from "node:crypto";
49590
- import { existsSync as existsSync33, mkdirSync as mkdirSync26, readFileSync as readFileSync31, renameSync as renameSync8, writeFileSync as writeFileSync23 } from "node:fs";
49699
+ import { existsSync as existsSync34, mkdirSync as mkdirSync26, readFileSync as readFileSync31, renameSync as renameSync8, writeFileSync as writeFileSync23 } from "node:fs";
49591
49700
  import { join as join44 } from "node:path";
49592
49701
  init_hub_binding();
49593
49702
  init_telemetry();
@@ -49749,7 +49858,7 @@ function buildSessionBrief(snapshot, current, hub, ship, updateLatest, cost, ses
49749
49858
  function readCachedSessionBrief(stateDir) {
49750
49859
  const file = join44(stateDir, "session-brief.json");
49751
49860
  try {
49752
- if (!existsSync33(file)) return void 0;
49861
+ if (!existsSync34(file)) return void 0;
49753
49862
  const raw = readFileSync31(file, "utf8");
49754
49863
  const parsed = JSON.parse(raw);
49755
49864
  if (parsed && parsed.schema === "kxm.session-brief.v1" && typeof parsed.generatedAt === "string") {
@@ -49776,7 +49885,7 @@ function writeCachedSessionBrief(stateDir, brief) {
49776
49885
  function estimateSessionCost(stateDir) {
49777
49886
  try {
49778
49887
  const telemetryFile = join44(stateDir, "telemetry.jsonl");
49779
- if (!existsSync33(telemetryFile)) return void 0;
49888
+ if (!existsSync34(telemetryFile)) return void 0;
49780
49889
  const records = readRoutingRecords(telemetryFile);
49781
49890
  if (records.length === 0) return void 0;
49782
49891
  let totalCost = 0;
@@ -49948,7 +50057,7 @@ init_hub_binding();
49948
50057
 
49949
50058
  // plugins/kxm/src/kxm-install-kind.ts
49950
50059
  init_bindings();
49951
- import { existsSync as existsSync34, readFileSync as readFileSync32 } from "node:fs";
50060
+ import { existsSync as existsSync35, readFileSync as readFileSync32 } from "node:fs";
49952
50061
  import { dirname as dirname20, join as join45 } from "node:path";
49953
50062
  function packageIsKxm(root) {
49954
50063
  try {
@@ -50006,7 +50115,7 @@ function classifyInstallRoot(probe) {
50006
50115
  const claudeHome = probe.env.CLAUDE_CONFIG_DIR?.trim() || join45(probe.homeDir, ".claude");
50007
50116
  const pluginsRoot = join45(claudeHome, "plugins");
50008
50117
  if (isUnder(probe.moduleDir, pluginsRoot, probe.platform)) return report("claude-marketplace", probe.moduleDir);
50009
- const hasGit = existsSync34(join45(probe.repoRoot, ".git"));
50118
+ const hasGit = existsSync35(join45(probe.repoRoot, ".git"));
50010
50119
  if (hasGit && packageIsKxm(probe.repoRoot)) return report("source", probe.repoRoot);
50011
50120
  if (isNpmPackageLayout(probe.repoRoot, probe.platform) && packageIsKxm(probe.repoRoot) && !hasGit) {
50012
50121
  return report("npm-package", probe.repoRoot);
@@ -50030,12 +50139,12 @@ init_bindings();
50030
50139
 
50031
50140
  // plugins/kxm/src/kxm-update-config.ts
50032
50141
  var import_yaml16 = __toESM(require_dist(), 1);
50033
- import { existsSync as existsSync35, readFileSync as readFileSync33 } from "node:fs";
50142
+ import { existsSync as existsSync36, readFileSync as readFileSync33 } from "node:fs";
50034
50143
  import { join as join46 } from "node:path";
50035
50144
  init_bindings();
50036
50145
  function loadKxmUpdateConfig(env = process.env) {
50037
50146
  const path4 = join46(kxmUserStateRoot({ env }), "update.yaml");
50038
- if (!existsSync35(path4)) return { schema: KXM_UPDATE_SCHEMA, auto: false, source: "github" };
50147
+ if (!existsSync36(path4)) return { schema: KXM_UPDATE_SCHEMA, auto: false, source: "github" };
50039
50148
  let parsed;
50040
50149
  try {
50041
50150
  parsed = (0, import_yaml16.parse)(readFileSync33(path4, "utf8"));
@@ -50066,7 +50175,7 @@ function loadKxmUpdateConfig(env = process.env) {
50066
50175
  // plugins/kxm/src/session.ts
50067
50176
  init_protocol();
50068
50177
  init_envelope();
50069
- import { existsSync as existsSync36, mkdirSync as mkdirSync27, readFileSync as readFileSync34, writeFileSync as writeFileSync24 } from "node:fs";
50178
+ import { existsSync as existsSync37, mkdirSync as mkdirSync27, readFileSync as readFileSync34, writeFileSync as writeFileSync24 } from "node:fs";
50070
50179
  import { join as join47 } from "node:path";
50071
50180
  var SESSION_SCHEMA = "kxm.session.v1";
50072
50181
  var SessionConfigError = class extends Error {
@@ -50146,7 +50255,7 @@ function loadRosterMap(configDir) {
50146
50255
  return byName;
50147
50256
  }
50148
50257
  function loadRoster(path4, key) {
50149
- if (!existsSync36(path4)) return [];
50258
+ if (!existsSync37(path4)) return [];
50150
50259
  let parsed;
50151
50260
  try {
50152
50261
  parsed = JSON.parse(readFileSync34(path4, "utf8"));
@@ -50245,7 +50354,7 @@ function moduleInstallRoot() {
50245
50354
  }
50246
50355
  function warnIgnoredProjectUpdateYaml(runtime) {
50247
50356
  const projectFile = join48(runtime.dirs.workspace, "update.yaml");
50248
- if (!existsSync37(projectFile)) return;
50357
+ if (!existsSync38(projectFile)) return;
50249
50358
  const userFile = join48(kxmUserStateRoot({ env: runtime.env }), "update.yaml");
50250
50359
  runtime.io.stderr(`kxm: ignoring .kxm/update.yaml in ${runtime.dirs.workdir}; update settings are read only from ${userFile}
50251
50360
  `);
@@ -50527,7 +50636,7 @@ async function cmdWorker(runtime, options) {
50527
50636
  return await (runtime.io.spawnWorker ?? ((launchEnv) => spawnScript("kxm-worker.mjs", launchEnv)))(extraEnv);
50528
50637
  }
50529
50638
  async function cmdStop(runtime, waitMsFlag) {
50530
- const pids = existsSync37(runtime.dirs.state) ? readdirSync12(runtime.dirs.state).filter((name) => name.endsWith(".pid")) : [];
50639
+ const pids = existsSync38(runtime.dirs.state) ? readdirSync12(runtime.dirs.state).filter((name) => name.endsWith(".pid")) : [];
50531
50640
  if (runtime.dryRun) {
50532
50641
  print(runtime.io, runtime.json, { ok: true, command: "stop", dryRun: true, pidFiles: pids }, "would signal pid files");
50533
50642
  return 0;
@@ -50619,7 +50728,7 @@ async function cmdStop(runtime, waitMsFlag) {
50619
50728
  }
50620
50729
  async function cmdSessionStatus(runtime) {
50621
50730
  const stateDir = runtime.dirs.state;
50622
- const names3 = existsSync37(stateDir) ? readdirSync12(stateDir) : [];
50731
+ const names3 = existsSync38(stateDir) ? readdirSync12(stateDir) : [];
50623
50732
  const claims = [];
50624
50733
  for (const file of names3.filter((name) => name.endsWith(".pid"))) {
50625
50734
  try {
@@ -50664,7 +50773,7 @@ async function cmdSessionStatus(runtime) {
50664
50773
  async function cmdAuthToken(runtime, options = {}) {
50665
50774
  const tokenFile = sessionTokenPath(runtime.env.KXM_USER_CONFIG_DIR);
50666
50775
  if (options.clear && runtime.dryRun) {
50667
- const present = existsSync37(tokenFile);
50776
+ const present = existsSync38(tokenFile);
50668
50777
  printPlan(runtime, { command: "auth token", cleared: present }, present ? [{ action: "delete", target: tokenFile }] : [], present ? "clear the session token from disk" : "no session token file to clear");
50669
50778
  return 0;
50670
50779
  }
@@ -50863,7 +50972,7 @@ init_routing();
50863
50972
  init_prices();
50864
50973
  import { spawnSync as spawnSync12 } from "node:child_process";
50865
50974
  import { createHash as createHash19 } from "node:crypto";
50866
- import { existsSync as existsSync44, mkdtempSync as mkdtempSync3, readFileSync as readFileSync40, rmSync as rmSync13 } from "node:fs";
50975
+ import { existsSync as existsSync45, mkdtempSync as mkdtempSync3, readFileSync as readFileSync40, rmSync as rmSync13 } from "node:fs";
50867
50976
  import { tmpdir as tmpdir2 } from "node:os";
50868
50977
  import { basename as basename11, extname as extname3, join as join54, resolve as resolve32 } from "node:path";
50869
50978
  import { createInterface as createInterface3 } from "node:readline";
@@ -50873,7 +50982,7 @@ init_routing();
50873
50982
  init_protocol();
50874
50983
  init_relevance();
50875
50984
  import { createHash as createHash18 } from "node:crypto";
50876
- import { existsSync as existsSync38, mkdirSync as mkdirSync29, writeFileSync as writeFileSync26 } from "node:fs";
50985
+ import { existsSync as existsSync39, mkdirSync as mkdirSync29, writeFileSync as writeFileSync26 } from "node:fs";
50877
50986
  import { join as join49, relative as relative7, resolve as resolve28 } from "node:path";
50878
50987
  var CANDIDATE_SCHEMA = "kxm.candidate.v1";
50879
50988
  var IMPROVEMENT_REPORT_SCHEMA = "kxm.improvement-report.v2";
@@ -51166,7 +51275,7 @@ function buildImprovementReport(records, options = {}) {
51166
51275
  createdAt: nowIso()
51167
51276
  };
51168
51277
  if (!options.dryRun) {
51169
- if (!existsSync38(candidatesDir)) {
51278
+ if (!existsSync39(candidatesDir)) {
51170
51279
  mkdirSync29(candidatesDir, { recursive: true });
51171
51280
  }
51172
51281
  writeFileSync26(diffFilePath, diff, "utf8");
@@ -51283,7 +51392,7 @@ init_project_config();
51283
51392
  init_runtime_store();
51284
51393
  init_routing();
51285
51394
  init_telemetry();
51286
- import { existsSync as existsSync39 } from "node:fs";
51395
+ import { existsSync as existsSync40 } from "node:fs";
51287
51396
  import { resolve as resolve29 } from "node:path";
51288
51397
  var ENGINE_EVENTS_SQL = "SELECT run_id, sequence, event_type, payload FROM events WHERE event_type IN ('routing.attempt.recorded','step.entered','run.status_changed') ORDER BY run_id, sequence";
51289
51398
  var SIMULATED_HARNESS = "driver-simulated";
@@ -51306,7 +51415,7 @@ function readEngineRoutingRecords(path4) {
51306
51415
  undecided: 0,
51307
51416
  duplicatesDropped: 0
51308
51417
  };
51309
- if (!existsSync39(path4)) return { records: [], source };
51418
+ if (!existsSync40(path4)) return { records: [], source };
51310
51419
  source.exists = true;
51311
51420
  let rows;
51312
51421
  let database;
@@ -51382,7 +51491,7 @@ function readEngineRoutingRecords(path4) {
51382
51491
  };
51383
51492
  }
51384
51493
  function readJsonlSource(kind, path4) {
51385
- const exists = existsSync39(path4);
51494
+ const exists = existsSync40(path4);
51386
51495
  const records = exists ? readRoutingRecords(path4).map((entry) => entry.routing) : [];
51387
51496
  return { records, source: { kind, path: path4, exists, records: records.length, duplicatesDropped: 0 } };
51388
51497
  }
@@ -51427,7 +51536,7 @@ function loadRoutingSources(options) {
51427
51536
  // plugins/kxm/src/modes.ts
51428
51537
  var import_yaml17 = __toESM(require_dist(), 1);
51429
51538
  init_prices();
51430
- import { existsSync as existsSync40, readFileSync as readFileSync37 } from "node:fs";
51539
+ import { existsSync as existsSync41, readFileSync as readFileSync37 } from "node:fs";
51431
51540
  import { join as join50 } from "node:path";
51432
51541
  var DEFAULT_MODES_CONFIG = Object.freeze({
51433
51542
  schema: "kxm.modes.v1",
@@ -51492,7 +51601,7 @@ function loadModesConfig(projectRoot) {
51492
51601
  return DEFAULT_MODES_CONFIG;
51493
51602
  }
51494
51603
  const modesPath = join50(projectRoot, ".kxm", "modes.yaml");
51495
- if (!existsSync40(modesPath)) {
51604
+ if (!existsSync41(modesPath)) {
51496
51605
  return DEFAULT_MODES_CONFIG;
51497
51606
  }
51498
51607
  try {
@@ -51584,7 +51693,7 @@ function calculatePromptFootprint(resolved, projectRoot = process.cwd(), catalog
51584
51693
  for (const relPath of resolved.contextFiles) {
51585
51694
  const fullPath = join50(projectRoot, relPath);
51586
51695
  let chars = 0;
51587
- if (existsSync40(fullPath)) {
51696
+ if (existsSync41(fullPath)) {
51588
51697
  try {
51589
51698
  chars = readFileSync37(fullPath, "utf8").length;
51590
51699
  } catch {
@@ -51719,7 +51828,7 @@ ${divider}
51719
51828
  // plugins/kxm/src/ssh-remote.ts
51720
51829
  init_safety_integrity();
51721
51830
  import { spawnSync as spawnSync11 } from "node:child_process";
51722
- import { existsSync as existsSync41, mkdirSync as mkdirSync30, readFileSync as readFileSync38, readdirSync as readdirSync13, rmSync as rmSync12, statSync as statSync6 } from "node:fs";
51831
+ import { existsSync as existsSync42, mkdirSync as mkdirSync30, readFileSync as readFileSync38, readdirSync as readdirSync13, rmSync as rmSync12, statSync as statSync7 } from "node:fs";
51723
51832
  import { homedir as homedir8 } from "node:os";
51724
51833
  import { join as join51, resolve as resolve30 } from "node:path";
51725
51834
  var MAX_SSH_OUTPUT_BYTES = 50 * 1024;
@@ -51748,7 +51857,7 @@ function truncateSshOutput(raw) {
51748
51857
  }
51749
51858
  function parseSshConfig(configPath) {
51750
51859
  const targetPath = configPath ?? join51(homedir8(), ".ssh", "config");
51751
- if (!existsSync41(targetPath)) {
51860
+ if (!existsSync42(targetPath)) {
51752
51861
  return [];
51753
51862
  }
51754
51863
  try {
@@ -51830,7 +51939,7 @@ function resolveSshHostG(host, execFn = spawnSync11) {
51830
51939
  }
51831
51940
  function ensureSocketDir(socketDir = DEFAULT_SOCKET_DIR) {
51832
51941
  const resolved = resolve30(socketDir);
51833
- if (!existsSync41(resolved)) {
51942
+ if (!existsSync42(resolved)) {
51834
51943
  mkdirSync30(resolved, { recursive: true, mode: 448 });
51835
51944
  }
51836
51945
  return resolved;
@@ -52325,7 +52434,7 @@ complete -c kxm -n "__fish_seen_subcommand_from goal" -a "create list get"
52325
52434
  }
52326
52435
 
52327
52436
  // plugins/kxm/src/completion-install.ts
52328
- import { existsSync as existsSync42, mkdirSync as mkdirSync31, readFileSync as readFileSync39, writeFileSync as writeFileSync27 } from "node:fs";
52437
+ import { existsSync as existsSync43, mkdirSync as mkdirSync31, readFileSync as readFileSync39, writeFileSync as writeFileSync27 } from "node:fs";
52329
52438
  import { homedir as homedir9 } from "node:os";
52330
52439
  import { basename as basename10, delimiter, dirname as dirname23, isAbsolute as isAbsolute11, join as join52, resolve as resolve31 } from "node:path";
52331
52440
  var COMPLETION_MARKER = "# kxm completion";
@@ -52354,7 +52463,7 @@ function completionScriptPath(shell, options = {}) {
52354
52463
  function bashRcCandidate(options) {
52355
52464
  const home = effectiveHome(options);
52356
52465
  const candidates = [join52(home, ".bashrc"), join52(home, ".bash_profile")];
52357
- const existing = candidates.find((candidate) => existsSync42(candidate));
52466
+ const existing = candidates.find((candidate) => existsSync43(candidate));
52358
52467
  return existing ?? candidates[0];
52359
52468
  }
52360
52469
  function zshRcCandidate(options) {
@@ -52399,13 +52508,13 @@ function installShellCompletion(shellInput, options = {}) {
52399
52508
  const scriptPath = completionScriptPath(shell, options);
52400
52509
  const { rcFile } = completionRcTarget(shell, options);
52401
52510
  const script = generateShellCompletion(shell);
52402
- const existingScript = existsSync42(scriptPath);
52511
+ const existingScript = existsSync43(scriptPath);
52403
52512
  const existingScriptMatches = existingScript && readFileSync39(scriptPath, "utf8") === script;
52404
52513
  let rcModified = false;
52405
52514
  let alreadyInstalled = false;
52406
52515
  if (shell === "fish") {
52407
52516
  const fishTarget = fishCompletionTarget(shell, options);
52408
- const fishInstalled = existsSync42(fishTarget) && readFileSync39(fishTarget, "utf8") === script;
52517
+ const fishInstalled = existsSync43(fishTarget) && readFileSync39(fishTarget, "utf8") === script;
52409
52518
  alreadyInstalled = fishInstalled;
52410
52519
  if (!options.dryRun && (!fishInstalled || options.overwrite)) {
52411
52520
  mkdirSync31(resolve31(fishTarget, ".."), { recursive: true });
@@ -52422,7 +52531,7 @@ function installShellCompletion(shellInput, options = {}) {
52422
52531
  }
52423
52532
  if (rcFile) {
52424
52533
  const line = sourceLine(shell, scriptPath);
52425
- const rcContent = existsSync42(rcFile) ? readFileSync39(rcFile, "utf8") : "";
52534
+ const rcContent = existsSync43(rcFile) ? readFileSync39(rcFile, "utf8") : "";
52426
52535
  alreadyInstalled = rcContent.includes(line) || rcContent.includes(`${COMPLETION_MARKER}`);
52427
52536
  }
52428
52537
  if (!options.dryRun) {
@@ -52438,7 +52547,7 @@ ${marker}
52438
52547
  ${line}
52439
52548
  `;
52440
52549
  mkdirSync31(resolve31(rcFile, ".."), { recursive: true });
52441
- writeFileSync27(rcFile, (existsSync42(rcFile) ? readFileSync39(rcFile, "utf8") : "") + stanza, { encoding: "utf8" });
52550
+ writeFileSync27(rcFile, (existsSync43(rcFile) ? readFileSync39(rcFile, "utf8") : "") + stanza, { encoding: "utf8" });
52442
52551
  rcModified = true;
52443
52552
  }
52444
52553
  }
@@ -52455,13 +52564,13 @@ function kxmBinDir(env = process.env) {
52455
52564
  const argv1 = env.KXM_ENTRY ?? process.argv[1];
52456
52565
  if (argv1 && isAbsolute11(argv1)) {
52457
52566
  const dir = dirname23(argv1);
52458
- if (existsSync42(join52(dir, process.platform === "win32" ? "kxm.cmd" : "kxm"))) return dir;
52567
+ if (existsSync43(join52(dir, process.platform === "win32" ? "kxm.cmd" : "kxm"))) return dir;
52459
52568
  }
52460
52569
  const pathEnv = env.PATH ?? "";
52461
52570
  for (const part of pathEnv.split(delimiter)) {
52462
52571
  if (!part) continue;
52463
52572
  const candidate = resolve31(part, process.platform === "win32" ? "kxm.cmd" : "kxm");
52464
- if (existsSync42(candidate)) return resolve31(part);
52573
+ if (existsSync43(candidate)) return resolve31(part);
52465
52574
  }
52466
52575
  return void 0;
52467
52576
  }
@@ -52479,7 +52588,7 @@ function installPathEntry(shellInput, options = {}) {
52479
52588
  }
52480
52589
  const { rcFile } = completionRcTarget(shell, options);
52481
52590
  const line = pathLine(binDir);
52482
- const rcContent = existsSync42(rcFile ?? "") ? readFileSync39(rcFile, "utf8") : "";
52591
+ const rcContent = existsSync43(rcFile ?? "") ? readFileSync39(rcFile, "utf8") : "";
52483
52592
  const alreadyInstalled = rcContent.includes(binDir) || (options.env?.PATH ?? process.env.PATH ?? "").split(delimiter).includes(binDir);
52484
52593
  if (!options.dryRun && rcFile && !alreadyInstalled) {
52485
52594
  mkdirSync31(dirname23(rcFile), { recursive: true });
@@ -52494,7 +52603,7 @@ ${line}
52494
52603
  // plugins/kxm/src/init-guide-setup.ts
52495
52604
  var import_yaml18 = __toESM(require_dist(), 1);
52496
52605
  init_routes();
52497
- import { existsSync as existsSync43, mkdirSync as mkdirSync32, writeFileSync as writeFileSync28 } from "node:fs";
52606
+ import { existsSync as existsSync44, mkdirSync as mkdirSync32, writeFileSync as writeFileSync28 } from "node:fs";
52498
52607
  import { dirname as dirname24, join as join53 } from "node:path";
52499
52608
  var ADMITTED_GUIDE_BINDINGS = Object.freeze({
52500
52609
  "anthropic/claude-fable-5.1": { harness: "claude", provider: "anthropic", model: "fable" },
@@ -52998,7 +53107,7 @@ function writeGuideSetupFiles(files) {
52998
53107
  const written = [];
52999
53108
  const existed = [];
53000
53109
  for (const file of files) {
53001
- if (existsSync43(file.path)) {
53110
+ if (existsSync44(file.path)) {
53002
53111
  existed.push(file.path);
53003
53112
  continue;
53004
53113
  }
@@ -53078,7 +53187,7 @@ function applyKxmPackageUpdate(runtime, notice) {
53078
53187
  for (const step of planned) {
53079
53188
  if (step.kind === "verify") {
53080
53189
  if (!verifyReleaseAssetDigest(step.path, step.sha256)) {
53081
- const actual = existsSync44(step.path) ? createHash19("sha256").update(readFileSync40(step.path)).digest("hex") : "missing";
53190
+ const actual = existsSync45(step.path) ? createHash19("sha256").update(readFileSync40(step.path)).digest("hex") : "missing";
53082
53191
  return {
53083
53192
  ok: false,
53084
53193
  error: "release_digest_mismatch",
@@ -53356,7 +53465,7 @@ async function cmdValidate(runtime, fileFlag) {
53356
53465
  }
53357
53466
  const selectedFile = explicitFile || configuredFile;
53358
53467
  const file = selectedFile ? resolve32(runtime.cwd, selectedFile) : void 0;
53359
- if (file && !existsSync44(file)) {
53468
+ if (file && !existsSync45(file)) {
53360
53469
  printWorker(runtime, worker, { ok: false, command: "validate", error: "file_not_found", file }, `workflow file not found: ${file}`);
53361
53470
  return 1;
53362
53471
  }
@@ -53609,7 +53718,7 @@ async function maybeOfferCompletionInstall(runtime) {
53609
53718
  if (!pathNeeded) {
53610
53719
  const scriptPath = completionScriptPath(shell, { env: runtime.env, configDir: runtime.env.KXM_USER_CONFIG_DIR });
53611
53720
  const { rcFile } = completionRcTarget(shell, { env: runtime.env });
53612
- if (rcFile && existsSync44(rcFile)) {
53721
+ if (rcFile && existsSync45(rcFile)) {
53613
53722
  try {
53614
53723
  if (readFileSync40(rcFile, "utf8").includes(scriptPath)) return;
53615
53724
  } catch {
@@ -54319,6 +54428,11 @@ function createProgram(ctx, result, argv) {
54319
54428
  result.code = await cmdRouteChange(runtimeFrom(ctx, this), status === "admit" ? "admitted" : "disabled", options.model);
54320
54429
  });
54321
54430
  }
54431
+ const visionCmd = addGlobalOptions(program2.command("vision").description("Judge captured UI screenshots through the admitted vision gate"));
54432
+ visionCmd.helpCommand("help", "Show vision help");
54433
+ addGlobalOptions(visionCmd.command("assert").description("Assert a UI state on a screenshot (deterministic vision gate)")).requiredOption("--image <path>", "Path to the captured screenshot").requiredOption("--question <text>", "Yes/no assertion to verify on the screenshot").option("--route <id>", `Vision route (default ${"zai-coding-cn/glm-5.3-flash"})`).option("--timeout-ms <n>", "Transport timeout in milliseconds (5000-300000)").option("--expect <bool>", "Exit 0 only when the verdict matches true|false").action(async function visionAssertAction(options) {
54434
+ result.code = await cmdVisionAssert(runtimeFrom(ctx, this), options ?? {});
54435
+ });
54322
54436
  const harnessCmd = addGlobalOptions(program2.command("harness").description("Detect coding-agent harnesses and authentication"));
54323
54437
  harnessCmd.helpCommand("help", "Show harness help");
54324
54438
  addGlobalOptions(harnessCmd.command("list").description("Show installed harnesses, auth, and native updaters")).action(bind(cmdHarnessList));
@@ -17313,7 +17313,7 @@ function sessionTokenFixHint(policy) {
17313
17313
  }
17314
17314
 
17315
17315
  // plugins/kxm/src/mcp-server.ts
17316
- var VERSION = "0.7.135";
17316
+ var VERSION = "0.7.138";
17317
17317
  var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
17318
17318
  var inbox = /* @__PURE__ */ new Map();
17319
17319
  var notifiedInbox = /* @__PURE__ */ new Set();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.135",
3
+ "version": "0.7.138",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -94,3 +94,17 @@ const browser = await connectBrowserOverCdp(
94
94
  - Save test traces to `.kxm/artifacts/browser/trace-<runId>.zip`.
95
95
  - Sanitize recorded traces and screenshots: mask password fields, authorization headers, and personal data.
96
96
  - Keep permanent regression tests under `test/e2e/`. Scratch reproduction scripts stay out of that directory.
97
+
98
+ ## Vision Gate (screenshot assertion)
99
+
100
+ After capturing a screenshot, a deterministic vision gate can assert UI state through an admitted vision route — bounded prompt, strict `{"verdict": true|false}` verdict, fail-closed (no free-form chat):
101
+
102
+ ```bash
103
+ kxm vision assert --image .kxm/artifacts/browser/shot.png \
104
+ --question "Does the page show the error banner?" \
105
+ --expect false
106
+ ```
107
+
108
+ - Exit 0 only when the verdict resolves and matches `--expect`; fail-closed divergences (`route_not_admitted`, `image unreadable`, `verdict unreadable`) exit 1.
109
+ - The route must be admitted in `.kxm/routes.yaml` (default: `zai-coding-cn/glm-5.3-flash`, the 12/12-accuracy verified vision route).
110
+ - The gate is a supplement to deterministic Playwright assertions, never a replacement.
@@ -7,6 +7,7 @@ import { applyRestorePlan, createBackup, databaseError, planBackup, planRestore,
7
7
  import { readLiveHubClaim } from "../hub-autostart.ts";
8
8
  import { refreshModelInventory } from "../model-inventory.ts";
9
9
  import { listInventoryModels, listRoleBindings, loadRoutePolicy, rolesForInventoryModel, setRouteState, updateRouteState } from "../routes.ts";
10
+ import { runVisionGate } from "../vision-gate.ts";
10
11
  import {
11
12
  KxmConfigError,
12
13
  discoverKxmProjectRoot,
@@ -921,6 +922,58 @@ export async function cmdRouteCount(runtime: Runtime): Promise<number> {
921
922
  return 0;
922
923
  }
923
924
 
925
+ export interface VisionAssertOptions {
926
+ image?: string | undefined;
927
+ question?: string | undefined;
928
+ route?: string | undefined;
929
+ timeoutMs?: string | undefined;
930
+ expect?: string | undefined;
931
+ }
932
+
933
+ /** Judge a captured UI screenshot through the admitted vision gate. Exit 0
934
+ * when the verdict resolves and matches --expect (or when no expectation was
935
+ * given and the verdict resolved); fail-closed divergences exit 1. */
936
+ export async function cmdVisionAssert(runtime: Runtime, options: VisionAssertOptions): Promise<number> {
937
+ const image = options.image?.trim();
938
+ const question = options.question?.trim();
939
+ if (!image || !question) {
940
+ print(runtime.io, runtime.json, { ok: false, command: "vision assert", error: "image_and_question_required" }, "both --image and --question are required");
941
+ return 2;
942
+ }
943
+ let expect: boolean | undefined;
944
+ if (options.expect !== undefined) {
945
+ if (options.expect !== "true" && options.expect !== "false") {
946
+ print(runtime.io, runtime.json, { ok: false, command: "vision assert", error: "expect_invalid" }, "--expect must be true or false");
947
+ return 2;
948
+ }
949
+ expect = options.expect === "true";
950
+ }
951
+ let timeoutMs: number | undefined;
952
+ if (options.timeoutMs !== undefined) {
953
+ const parsed = Number(options.timeoutMs);
954
+ if (!Number.isInteger(parsed)) {
955
+ print(runtime.io, runtime.json, { ok: false, command: "vision assert", error: "timeout_invalid" }, "--timeout-ms must be an integer");
956
+ return 2;
957
+ }
958
+ timeoutMs = parsed;
959
+ }
960
+ const result = await runVisionGate({
961
+ projectRoot: runtime.dirs.workdir,
962
+ imagePath: resolve(runtime.cwd, image),
963
+ question,
964
+ ...(options.route !== undefined ? { route: options.route } : {}),
965
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
966
+ });
967
+ const pass = result.verdict !== null && (expect === undefined || result.verdict === expect);
968
+ print(
969
+ runtime.io,
970
+ runtime.json,
971
+ { ok: pass, command: "vision assert", ...(expect !== undefined ? { expect } : {}), ...result },
972
+ result.verdict === null ? `vision gate fail-closed: ${result.divergence}` : `verdict ${result.verdict}${expect !== undefined ? ` (expected ${expect})` : ""}`,
973
+ );
974
+ return pass ? 0 : 1;
975
+ }
976
+
924
977
  export async function cmdModelInventoryRefresh(runtime: Runtime): Promise<number> {
925
978
  if (runtime.dryRun) {
926
979
  print(runtime.io, runtime.json, { ok: true, command: "models inventory refresh", dryRun: true }, "would refresh .kxm/models/inventory.yaml");
@@ -114,6 +114,7 @@ cmdBackup,
114
114
  cmdRouteChange,
115
115
  cmdModelsScreen,
116
116
  cmdRouteList,
117
+ cmdVisionAssert,
117
118
  cmdRouteCount,
118
119
  cmdModelInventoryRefresh,
119
120
  cmdKxmRuntime,
@@ -675,6 +676,16 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
675
676
  addGlobalOptions(routesCmd.command(status).description(`${status === "admit" ? "Admit" : "Disable"} a model route from the inventory`)).option("--model <id>", "Exact model id; omit to choose interactively").action(async function routeChangeAction(this: Command, options: { model?: string }) { result.code = await cmdRouteChange(runtimeFrom(ctx, this), status === "admit" ? "admitted" : "disabled", options.model); });
676
677
  }
677
678
 
679
+ const visionCmd = addGlobalOptions(program.command("vision").description("Judge captured UI screenshots through the admitted vision gate"));
680
+ visionCmd.helpCommand("help", "Show vision help");
681
+ addGlobalOptions(visionCmd.command("assert").description("Assert a UI state on a screenshot (deterministic vision gate)"))
682
+ .requiredOption("--image <path>", "Path to the captured screenshot")
683
+ .requiredOption("--question <text>", "Yes/no assertion to verify on the screenshot")
684
+ .option("--route <id>", `Vision route (default ${"zai-coding-cn/glm-5.3-flash"})`)
685
+ .option("--timeout-ms <n>", "Transport timeout in milliseconds (5000-300000)")
686
+ .option("--expect <bool>", "Exit 0 only when the verdict matches true|false")
687
+ .action(async function visionAssertAction(this: Command, options: { image?: string; question?: string; route?: string; timeoutMs?: string; expect?: string }) { result.code = await cmdVisionAssert(runtimeFrom(ctx, this), options ?? {}); });
688
+
678
689
  const harnessCmd = addGlobalOptions(program.command("harness").description("Detect coding-agent harnesses and authentication"));
679
690
  harnessCmd.helpCommand("help", "Show harness help");
680
691
  addGlobalOptions(harnessCmd.command("list").description("Show installed harnesses, auth, and native updaters")).action(bind(cmdHarnessList));
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
11
11
  import type { HubEvent, MessageRecord } from "./protocol.ts";
12
12
  import { sessionTokenFixHint } from "./session-token-hint.ts";
13
13
 
14
- const VERSION = "0.7.135";
14
+ const VERSION = "0.7.138";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();