@kontextmind/kxm 0.7.194 → 0.7.195

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.194",
14
+ "version": "0.7.195",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/CHANGELOG.md CHANGED
@@ -714,6 +714,14 @@ All notable user-facing changes are documented here. The project follows [Semant
714
714
 
715
715
  ### Fixed
716
716
 
717
+ - **`kxm vision assert --route` selects the Pi model, and an unadmitted route names the fix.**
718
+ The flag used to change only the admission check. Pi still ran
719
+ `zai-coding-cn/glm-5.3-flash`. Another admitted id is now `pi --model <id>`
720
+ after the native-vendor brake. The verified default invocation is unchanged.
721
+ `route_not_admitted` includes the route id and `kxm routes admit --model`.
722
+ This checkout does not admit the default; pass
723
+ `--route openrouter/z-ai/glm-5.3-flash`.
724
+
717
725
  - **The weekly model refresh opens its pull request with a GitHub App.**
718
726
  `.github/workflows/refresh-models.yml` uses `actions/create-github-app-token`
719
727
  with repository secrets `KXM_REFRESH_APP_ID` and
@@ -731,7 +739,6 @@ All notable user-facing changes are documented here. The project follows [Semant
731
739
  the same). The refresh does not rewrite a shared anchor that a
732
740
  non-OpenRouter row depends on. `.kxm/prices.yaml` is dated 2026-10-20.
733
741
  The OpenRouter `standard` rates for those two inventory rows match.
734
-
735
742
  - **`kxm improve report` and `kxm routing report` include attempts settled in a registered lane.**
736
743
  Inside a KXM project, a present user-state `registry.db` is opened read-only at schema version 2. One query loads the home row and every lane row for that project id. Event-store paths come from the stored project keys, so a stale checkout path cannot hide a lane. A missing store stays a zero-record source. An absent registry, or a checkout that is not registered, still reads only that checkout's own store. `--file` still reads only the named file. See
737
744
  [kxm improve report](docs/reference/cli-reference.md#kxm-improve-report).
@@ -13,7 +13,7 @@ Output shown under examples was captured from a source checkout, inside a throwa
13
13
  - [Task to command](#task-to-command)
14
14
  - Setup: [`init`](#kxm-init), [`config`](#kxm-config), [`completion`](#kxm-completion), [`trust`](#kxm-trust)
15
15
  - Hub and sessions: [`hub`](#kxm-hub-view), [`session`](#kxm-session), [`dash`](#kxm-dash), [`studio`](#kxm-studio)
16
- - Harnesses, models, and roles: [`harness`](#kxm-harness), [`plugin`](#kxm-plugin), [`auth`](#kxm-auth), [`update`](#kxm-update), [`models`](#kxm-models), [`routes`](#kxm-routes), [`role`](#kxm-role)
16
+ - Harnesses, models, and roles: [`harness`](#kxm-harness), [`plugin`](#kxm-plugin), [`auth`](#kxm-auth), [`update`](#kxm-update), [`models`](#kxm-models), [`routes`](#kxm-routes), [`vision`](#kxm-vision), [`role`](#kxm-role)
17
17
  - Running work: [`lane`](#kxm-lane), [`land`](#kxm-land), [`assign`](#kxm-assign), [`run`](#kxm-run), [`runs`](#kxm-runs), [`docs`](#kxm-docs), [`runtime`](#kxm-runtime), [`agent`](#kxm-agent), [`workflow`](#kxm-workflow), [`gate`](#kxm-gate), [`peer`](#kxm-peer), [`task`](#kxm-task), [`goal`](#kxm-goal), [`suggest`](#kxm-suggest), [`explain`](#kxm-explain)
18
18
  - Context and learning: [`context`](#kxm-context), [`memory`](#kxm-memory), [`skills`](#kxm-skills), [`improve`](#kxm-improve), [`routing`](#kxm-routing), [`prices`](#kxm-prices)
19
19
  - Operations: [`backup`](#kxm-backup), [`restore`](#kxm-restore), [`tenant`](#kxm-tenant), [`ssh`](#kxm-ssh), [`help`](#kxm-help)
@@ -103,7 +103,7 @@ kxm -V
103
103
  - Plan with `planned`: `backup`, `restore`, `config set`, `role add|remove|modify|resume`, `workflow add|remove|modify`, `goal create`, `task create|run|sync`, `memory note|sync`, `skills evaluate|promote|reject`, `context promote`, `context wiki-compile --out`, `auth token`, `session token`, `session brief`, and `ssh run|file|close`. Each prints its normal result plus `dryRun: true` and `planned`, a list of `{action, target}` entries whose `action` is `write`, `delete`, `move`, `request`, or `ssh`. Text mode prints `dry run: <summary>` and one indented `would <action> <target>` line per entry (`session brief` appends `dry run: would write <file>` lines to the brief instead).
104
104
  - Plan in their own shape (described in each section): `init`, `run`, `runs drive|cancel`, `docs build|serve`, `runtime start|stop|sync-retry`, `hub start|stop|bind|unbind`, `session start|stop`, `agent worker`, `dash`, `studio serve`, `models inventory-refresh`, `routes admit|disable`, `update`, `completion install`, `workflow start|signal|export|checkpoint|record|wait`, every `peer` subcommand, `gate degrade|signal`, `skills create`, `improve report`, and `config edit`. `config edit --dry-run` prints one plain frame and writes nothing. `gate github watch --dry-run` still polls GitHub but does not post the signal.
105
105
  - Read-only commands run as usual, without leaving a trace: a local SQLite store is opened without creating `-wal` or `-shm` files, and `runs status|list|receipt` only attach to a running Runtime supervisor. With no supervisor running they exit 2 with `dry_run_unsupported` instead of starting one. `context wiki-compile --dry-run` still asks the hub to compile, which is a read.
106
- - Refused: `kxm models` (the interactive screen) and `kxm prices acknowledge` exit 2 with `dry_run_unsupported`. The CLI keeps one list of the commands that answer `--dry-run` and refuses every other command the same way before it runs, so a command added without dry-run support fails closed.
106
+ - Refused: `kxm models` (the interactive screen), `kxm vision assert`, and `kxm prices acknowledge` exit 2 with `dry_run_unsupported`. The CLI keeps one list of the commands that answer `--dry-run` and refuses every other command the same way before it runs, so a command added without dry-run support fails closed.
107
107
 
108
108
  ```bash
109
109
  kxm config set user.theme light --dry-run
@@ -908,6 +908,38 @@ kxm studio serve --port 5000
908
908
 
909
909
  Not run: starts a long-lived server.
910
910
 
911
+ ## `kxm vision`
912
+
913
+ Judge one captured screenshot through an admitted Pi route. The gate sends a bounded yes/no prompt and accepts only `{"verdict": true}` or `{"verdict": false}`. Any other answer fails closed. It does not replace a Playwright assertion.
914
+
915
+ ### `kxm vision assert`
916
+
917
+ ```text
918
+ kxm vision assert --image <path> --question <text> [--route <id>] [--timeout-ms <n>] [--expect <true|false>]
919
+ ```
920
+
921
+ | Option | Argument | Default | Description |
922
+ |---|---|---|---|
923
+ | `--image` | `<path>` | required | Screenshot file. A missing file fails closed with `image unreadable` |
924
+ | `--question` | `<text>` | required | Yes/no assertion, trimmed to 500 characters inside the prompt |
925
+ | `--route` | `<id>` | `zai-coding-cn/glm-5.3-flash` | Admitted route id. The default keeps the verified Pi command `--provider zai-coding-cn --model glm-5.3-flash`. Any other id is `pi --model <id>` after the native-vendor brake |
926
+ | `--timeout-ms` | `<n>` | `90000` | Integer from 5000 through 300000 |
927
+ | `--expect` | `true` or `false` | none | Exit 0 only when the verdict equals this value |
928
+
929
+ - Global options apply, except `--dry-run`, which exits 2 with `dry_run_unsupported` because the command calls Pi.
930
+ - The route must be listed under `admitted` in `.kxm/routes.yaml`. Otherwise the command exits 1 and the `divergence` names the id and `kxm routes admit --model <id>`.
931
+ - This repository does not admit the default `zai-coding-cn/glm-5.3-flash` (`zai-coding-cn` is not an allowlisted harness). Pass `--route openrouter/z-ai/glm-5.3-flash`, which is admitted here, or admit the route you pass.
932
+ - A native-vendor id such as `xai/grok-4.7` or `anthropic/fable` is refused before Pi starts, even when that id is admitted. The `divergence` is the Pi brake message.
933
+ - Exit 0 when the verdict resolves and, if `--expect` is set, matches it. Exit 1 for a fail-closed divergence. Exit 2 when `--image` or `--question` is missing, `--expect` is not `true` or `false`, or `--timeout-ms` is not an integer.
934
+ - JSON keys: `ok`, `command` (`vision assert`), `verdict` (`true`, `false`, or `null`), `route`, `raw`, `divergence` when the gate fails closed, and `expect` when `--expect` is set.
935
+ - Text mode prints `verdict true` or `verdict false`, or `vision gate fail-closed: <divergence>`.
936
+
937
+ Not run. The command calls Pi.
938
+
939
+ ```bash
940
+ kxm vision assert --image .kxm/artifacts/browser/shot.png --question "Does the page show the error banner?" --route openrouter/z-ai/glm-5.3-flash --expect false
941
+ ```
942
+
911
943
  ## `kxm harness`
912
944
 
913
945
  ### `kxm harness list`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.194",
3
+ "version": "0.7.195",
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.194",
5
+ "version": "0.7.195",
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",
@@ -28903,12 +28903,22 @@ function parseVisionVerdict(raw) {
28903
28903
  const match = /"verdict"\s*:\s*(true|false)/i.exec(raw);
28904
28904
  return match ? match[1] === "true" : null;
28905
28905
  }
28906
- function defaultTransport(prompt, timeoutMs) {
28907
- return execFileSync(
28908
- "pi",
28909
- ["--provider", "zai-coding-cn", "--model", "glm-5.3-flash", "-p", "--no-session", prompt],
28910
- { encoding: "utf8", timeout: timeoutMs, windowsHide: true }
28911
- );
28906
+ function visionPiArgs(route) {
28907
+ if (route === VISION_GATE_DEFAULT_ROUTE) {
28908
+ return { ok: true, args: ["--provider", "zai-coding-cn", "--model", "glm-5.3-flash", "-p", "--no-session"] };
28909
+ }
28910
+ const validation = validateHarnessModelPair("pi", route);
28911
+ if (!validation.valid) {
28912
+ return { ok: false, divergence: validation.message ?? "route_not_pi" };
28913
+ }
28914
+ const trimmed = route.trim();
28915
+ if (!trimmed.includes("/")) {
28916
+ return { ok: false, divergence: `route_not_pi: ${trimmed} is not a provider-qualified Pi model id` };
28917
+ }
28918
+ return { ok: true, args: ["--model", trimmed, "-p", "--no-session"] };
28919
+ }
28920
+ function defaultTransport(args, timeoutMs) {
28921
+ return execFileSync("pi", [...args], { encoding: "utf8", timeout: timeoutMs, windowsHide: true });
28912
28922
  }
28913
28923
  async function runVisionGate(options) {
28914
28924
  const projectRoot = options.projectRoot ?? process.cwd();
@@ -28922,12 +28932,17 @@ async function runVisionGate(options) {
28922
28932
  return { ...base, divergence: "image unreadable" };
28923
28933
  }
28924
28934
  if (!isRouteAdmitted(projectRoot, route)) {
28925
- return { ...base, divergence: "route_not_admitted" };
28935
+ return {
28936
+ ...base,
28937
+ divergence: `route_not_admitted: ${route} is not admitted; run kxm routes admit --model ${route} or pass --route <admitted-id>`
28938
+ };
28926
28939
  }
28940
+ const piArgs = visionPiArgs(route);
28941
+ if (!piArgs.ok) return { ...base, divergence: piArgs.divergence };
28927
28942
  const prompt = visionGatePrompt(options.question, options.imagePath);
28928
28943
  let raw;
28929
28944
  try {
28930
- raw = options.transport ? await options.transport(prompt) : defaultTransport(prompt, timeoutMs);
28945
+ raw = options.transport ? await options.transport(prompt) : defaultTransport([...piArgs.args, prompt], timeoutMs);
28931
28946
  } catch (error) {
28932
28947
  return { ...base, divergence: `transport failed: ${String(error?.message ?? error).slice(0, 200)}` };
28933
28948
  }
@@ -28942,6 +28957,7 @@ var VISION_GATE_DEFAULT_ROUTE, QUESTION_MAX_CHARS, RAW_MAX_CHARS;
28942
28957
  var init_vision_gate = __esm({
28943
28958
  "plugins/kxm/src/vision-gate.ts"() {
28944
28959
  "use strict";
28960
+ init_harness();
28945
28961
  init_routes();
28946
28962
  VISION_GATE_DEFAULT_ROUTE = "zai-coding-cn/glm-5.3-flash";
28947
28963
  QUESTION_MAX_CHARS = 500;
@@ -25462,7 +25462,7 @@ function sessionTokenFixHint(policy) {
25462
25462
  }
25463
25463
 
25464
25464
  // plugins/kxm/src/mcp-server.ts
25465
- var VERSION = "0.7.194";
25465
+ var VERSION = "0.7.195";
25466
25466
  var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
25467
25467
  var inbox = /* @__PURE__ */ new Map();
25468
25468
  var notifiedInbox = /* @__PURE__ */ new Set();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.194",
3
+ "version": "0.7.195",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -105,6 +105,6 @@ kxm vision assert --image .kxm/artifacts/browser/shot.png \
105
105
  --expect false
106
106
  ```
107
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).
108
+ - Exit 0 only when the verdict resolves and matches `--expect`; fail-closed divergences (`route_not_admitted`, `image unreadable`, `verdict unreadable`, or the Pi native-vendor brake) exit 1.
109
+ - The route must be admitted in `.kxm/routes.yaml`. The built-in default is `zai-coding-cn/glm-5.3-flash` (the verified Pi invocation `--provider zai-coding-cn --model glm-5.3-flash`). This checkout does not admit that id. Pass `--route openrouter/z-ai/glm-5.3-flash`, which is admitted here. `--route` selects the Pi model; a native-vendor id is refused before Pi starts.
110
110
  - The gate is a supplement to deterministic Playwright assertions, never a replacement.
@@ -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.194";
14
+ const VERSION = "0.7.195";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { existsSync, statSync } from "node:fs";
3
+ import { validateHarnessModelPair } from "./harness.ts";
3
4
  import { isRouteAdmitted } from "./routes.ts";
4
5
 
5
6
  /** Deterministic vision gate: judge a UI screenshot through an admitted
@@ -52,12 +53,27 @@ export function parseVisionVerdict(raw: string): boolean | null {
52
53
  return match ? match[1] === "true" : null;
53
54
  }
54
55
 
55
- function defaultTransport(prompt: string, timeoutMs: number): string {
56
- return execFileSync(
57
- "pi",
58
- ["--provider", "zai-coding-cn", "--model", "glm-5.3-flash", "-p", "--no-session", prompt],
59
- { encoding: "utf8", timeout: timeoutMs, windowsHide: true },
60
- );
56
+ /** Pi argv for an admitted vision route, without the prompt.
57
+ * The verified default keeps `--provider zai-coding-cn --model glm-5.3-flash`.
58
+ * Every other id is `pi --model <id>` after the native-vendor brake, so
59
+ * `--route` selects the model instead of only the admission check. */
60
+ export function visionPiArgs(route: string): { ok: true; args: readonly string[] } | { ok: false; divergence: string } {
61
+ if (route === VISION_GATE_DEFAULT_ROUTE) {
62
+ return { ok: true, args: ["--provider", "zai-coding-cn", "--model", "glm-5.3-flash", "-p", "--no-session"] };
63
+ }
64
+ const validation = validateHarnessModelPair("pi", route);
65
+ if (!validation.valid) {
66
+ return { ok: false, divergence: validation.message ?? "route_not_pi" };
67
+ }
68
+ const trimmed = route.trim();
69
+ if (!trimmed.includes("/")) {
70
+ return { ok: false, divergence: `route_not_pi: ${trimmed} is not a provider-qualified Pi model id` };
71
+ }
72
+ return { ok: true, args: ["--model", trimmed, "-p", "--no-session"] };
73
+ }
74
+
75
+ function defaultTransport(args: readonly string[], timeoutMs: number): string {
76
+ return execFileSync("pi", [...args], { encoding: "utf8", timeout: timeoutMs, windowsHide: true });
61
77
  }
62
78
 
63
79
  export async function runVisionGate(options: VisionGateOptions): Promise<VisionGateResult> {
@@ -73,15 +89,20 @@ export async function runVisionGate(options: VisionGateOptions): Promise<VisionG
73
89
  return { ...base, divergence: "image unreadable" };
74
90
  }
75
91
  if (!isRouteAdmitted(projectRoot, route)) {
76
- return { ...base, divergence: "route_not_admitted" };
92
+ return {
93
+ ...base,
94
+ divergence: `route_not_admitted: ${route} is not admitted; run kxm routes admit --model ${route} or pass --route <admitted-id>`,
95
+ };
77
96
  }
97
+ const piArgs = visionPiArgs(route);
98
+ if (!piArgs.ok) return { ...base, divergence: piArgs.divergence };
78
99
  const prompt = visionGatePrompt(options.question, options.imagePath);
79
100
 
80
101
  let raw: string;
81
102
  try {
82
103
  raw = options.transport
83
104
  ? await options.transport(prompt)
84
- : defaultTransport(prompt, timeoutMs);
105
+ : defaultTransport([...piArgs.args, prompt], timeoutMs);
85
106
  } catch (error) {
86
107
  return { ...base, divergence: `transport failed: ${String((error as Error)?.message ?? error).slice(0, 200)}` };
87
108
  }