@kontextmind/kxm 0.7.194 → 0.7.196

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.196",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/CHANGELOG.md CHANGED
@@ -714,6 +714,20 @@ All notable user-facing changes are documented here. The project follows [Semant
714
714
 
715
715
  ### Fixed
716
716
 
717
+ - **A first run says what to do next when Git or the hub is missing.**
718
+ `kxm init` outside a Git worktree still exits 1 with `git_root_required` and
719
+ now says to run `git init`, then `kxm init` again. `kxm hub view` adds
720
+ `hint` when both probes are unreachable or time out: on loopback, start a
721
+ hub with `kxm hub start`; on a remote URL, check the address and
722
+ `kxm hub bind`.
723
+
724
+ - **`kxm vision assert --route` selects the Pi model, and an unadmitted route names the fix.**
725
+ The flag used to change only the admission check. Pi still ran
726
+ `zai-coding-cn/glm-5.3-flash`. Another admitted id is now `pi --model <id>`
727
+ after the native-vendor brake. The verified default invocation is unchanged.
728
+ `route_not_admitted` includes the route id and `kxm routes admit --model`.
729
+ This checkout does not admit the default; pass
730
+ `--route openrouter/z-ai/glm-5.3-flash`.
717
731
  - **The weekly model refresh opens its pull request with a GitHub App.**
718
732
  `.github/workflows/refresh-models.yml` uses `actions/create-github-app-token`
719
733
  with repository secrets `KXM_REFRESH_APP_ID` and
@@ -731,7 +745,6 @@ All notable user-facing changes are documented here. The project follows [Semant
731
745
  the same). The refresh does not rewrite a shared anchor that a
732
746
  non-OpenRouter row depends on. `.kxm/prices.yaml` is dated 2026-10-20.
733
747
  The OpenRouter `standard` rates for those two inventory rows match.
734
-
735
748
  - **`kxm improve report` and `kxm routing report` include attempts settled in a registered lane.**
736
749
  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
750
  [kxm improve report](docs/reference/cli-reference.md#kxm-improve-report).
@@ -19,6 +19,7 @@ Work from the smallest boundary outward:
19
19
  | Symptom | Cause | Fix |
20
20
  |---|---|---|
21
21
  | `kxm: command not found` | Only the Pi package or the Claude Code plugin was installed; neither installs the CLI | `npm install --global --omit=peer @kontextmind/kxm`; see [Install KXM](../start/install.md) |
22
+ | `git_root_required` from `kxm init` | The current directory is not inside a Git worktree | Run `git init` in that directory, then `kxm init` again. If Git is present, run `kxm init` from the worktree root |
22
23
  | `kxm` works in one shell but not in another | npm's global bin directory is not on that shell's `PATH` | Run `kxm completion install` (bash and zsh also get a `PATH` entry), then open a new shell |
23
24
  | Tab completion does nothing | Completion is not installed for this shell | `kxm completion install --shell bash`, `zsh` or `fish`; it is idempotent, and `--dry-run` previews it |
24
25
  | Plugin tools fail on start | Node.js is older than 22.19 on the 22.x line, or older than 24 | Install a supported Node.js on the `PATH` the harness uses |
@@ -42,6 +43,10 @@ If it still reports `not_detected`, confirm that `%AppData%\Roaming\npm` is on t
42
43
 
43
44
  ## Hub and authentication
44
45
 
46
+ ### Nothing is listening
47
+
48
+ `kxm hub view` prints `hub health=false ready=false` and, for a loopback URL, `Nothing is listening on this loopback URL. Start a local hub with kxm hub start.` A remote URL names `kxm hub bind` instead. An HTML login page or a redirect is a different failure: the status line says `auth proxy`, and the fix is the proxy or the hub URL, not `kxm hub start`.
49
+
45
50
  ### The hub refuses to start
46
51
 
47
52
  | Message | Fix |
@@ -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
@@ -511,7 +511,7 @@ Calls `GET /health` and `GET /ready` on the target hub and reports whether the U
511
511
  No command-specific options.
512
512
 
513
513
  - Needs a hub to succeed. Reads only.
514
- - JSON keys: `target` (`url`, `scope`, and `source: "env"` when `KXM_SERVER_URL` overrides a binding), `identity` (`mode`, `modeSource`, `url`, `urlSource`, `project`, `projectSource`, `keySource`), `health`, `ready`. An unreachable hub reports `{"error":"hub_unreachable"}` for both probes. An auth proxy or redirect reports `{"error":"auth_proxy"}`.
514
+ - JSON keys: `target` (`url`, `scope`, and `source: "env"` when `KXM_SERVER_URL` overrides a binding), `identity` (`mode`, `modeSource`, `url`, `urlSource`, `project`, `projectSource`, `keySource`), `health`, `ready`, and `hint` when both probes are unreachable or time out. An unreachable hub reports `{"error":"hub_unreachable"}` for both probes. On a loopback URL the hint is `Nothing is listening on this loopback URL. Start a local hub with kxm hub start.` A remote URL names `kxm hub bind`. An auth proxy or redirect reports `{"error":"auth_proxy"}` and has no hint.
515
515
  - Exit 0 when both probes succeed, 1 otherwise. A cloud mode with no resolvable key exits 2 with `cloud_token_missing`.
516
516
 
517
517
  ```bash
@@ -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.196",
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.196",
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;
@@ -30877,7 +30893,7 @@ function initializeKxmProjectAtGitRoot(start, gitRoot, options, mutationLock) {
30877
30893
  };
30878
30894
  }
30879
30895
  if (plan.projectRoot !== gitRoot) {
30880
- throw new KxmConfigError([initIssue("git_root_required", ".", "kxm init must run inside the authoritative Git worktree")]);
30896
+ throw new KxmConfigError([initIssue("git_root_required", ".", "kxm init must run from the Git worktree root")]);
30881
30897
  }
30882
30898
  const projectId = generatedProjectId(options.projectId);
30883
30899
  const projectName = normalizedProjectName(gitRoot, options.projectName);
@@ -30902,7 +30918,7 @@ function initializeKxmProject(start = process.cwd(), options = {}) {
30902
30918
  assertNoRegisteredGates(options);
30903
30919
  const gitRoot = discoverGitRoot(start);
30904
30920
  if (!gitRoot) {
30905
- throw new KxmConfigError([initIssue("git_root_required", ".", "kxm init must run inside the authoritative Git worktree")]);
30921
+ throw new KxmConfigError([initIssue("git_root_required", ".", "kxm init must run inside a Git worktree. Run git init in this directory, then run kxm init again.")]);
30906
30922
  }
30907
30923
  if (options.projectId !== void 0) generatedProjectId(options.projectId);
30908
30924
  if (options.projectName !== void 0) normalizedProjectName(gitRoot, options.projectName);
@@ -53981,6 +53997,8 @@ async function cmdStatus(runtime) {
53981
53997
  const notHub = !proxyKind && (health.kind === "not_hub" || ready.kind === "not_hub");
53982
53998
  const probeNote = proxyKind === "redirect" ? " \xB7 auth proxy or redirect" : proxyKind === "auth_proxy" ? " \xB7 auth proxy (HTML login page)" : notHub ? " \xB7 not a kxm hub" : "";
53983
53999
  const effectiveScope = connection.scope;
54000
+ const hubDown = (health.kind === "unreachable" || health.kind === "timeout") && (ready.kind === "unreachable" || ready.kind === "timeout");
54001
+ const hint = !hubDown ? void 0 : effectiveScope === "loopback" ? "Nothing is listening on this loopback URL. Start a local hub with kxm hub start." : "Nothing answered this hub URL. Check the address, the tunnel, and kxm hub bind.";
53984
54002
  const overridden = connection.urlSource === "env";
53985
54003
  const identity = {
53986
54004
  mode: connection.mode,
@@ -53997,13 +54015,15 @@ async function cmdStatus(runtime) {
53997
54015
  target: { url: runtime.serverUrl, scope: effectiveScope, ...overridden ? { source: "env" } : {} },
53998
54016
  identity,
53999
54017
  health: health.body,
54000
- ready: ready.body
54018
+ ready: ready.body,
54019
+ ...hint ? { hint } : {}
54001
54020
  };
54002
54021
  print(
54003
54022
  runtime.io,
54004
54023
  runtime.json,
54005
54024
  payload,
54006
- `hub health=${health.ok} ready=${ready.ok} \xB7 ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""} \xB7 mode=${identity.mode} project=${identity.project} (${connection.sourceLabel}) key=${identity.keySource}${probeNote}`
54025
+ `hub health=${health.ok} ready=${ready.ok} \xB7 ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""} \xB7 mode=${identity.mode} project=${identity.project} (${connection.sourceLabel}) key=${identity.keySource}${probeNote}${hint ? `
54026
+ ${hint}` : ""}`
54007
54027
  );
54008
54028
  return payload.ok ? 0 : 1;
54009
54029
  }
@@ -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.196";
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.196",
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.
@@ -202,6 +202,11 @@ export async function cmdStatus(runtime: Runtime): Promise<number> {
202
202
  // A cloud mode is remote when the contacted URL is that endpoint, including a
203
203
  // loopback SSH forward.
204
204
  const effectiveScope = connection.scope;
205
+ const hubDown = (health.kind === "unreachable" || health.kind === "timeout")
206
+ && (ready.kind === "unreachable" || ready.kind === "timeout");
207
+ const hint = !hubDown ? undefined : effectiveScope === "loopback"
208
+ ? "Nothing is listening on this loopback URL. Start a local hub with kxm hub start."
209
+ : "Nothing answered this hub URL. Check the address, the tunnel, and kxm hub bind.";
205
210
  const overridden = connection.urlSource === "env";
206
211
  const identity = {
207
212
  mode: connection.mode,
@@ -219,9 +224,10 @@ export async function cmdStatus(runtime: Runtime): Promise<number> {
219
224
  identity,
220
225
  health: health.body,
221
226
  ready: ready.body,
227
+ ...(hint ? { hint } : {}),
222
228
  };
223
229
  print(runtime.io, runtime.json, payload,
224
- `hub health=${health.ok} ready=${ready.ok} · ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""} · mode=${identity.mode} project=${identity.project} (${connection.sourceLabel}) key=${identity.keySource}${probeNote}`);
230
+ `hub health=${health.ok} ready=${ready.ok} · ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""} · mode=${identity.mode} project=${identity.project} (${connection.sourceLabel}) key=${identity.keySource}${probeNote}${hint ? `\n${hint}` : ""}`);
225
231
  return payload.ok ? 0 : 1;
226
232
  }
227
233
 
@@ -390,7 +390,7 @@ function initializeKxmProjectAtGitRoot(
390
390
  }
391
391
 
392
392
  if (plan.projectRoot !== gitRoot) {
393
- throw new KxmConfigError([initIssue("git_root_required", ".", "kxm init must run inside the authoritative Git worktree")]);
393
+ throw new KxmConfigError([initIssue("git_root_required", ".", "kxm init must run from the Git worktree root")]);
394
394
  }
395
395
  const projectId = generatedProjectId(options.projectId);
396
396
  const projectName = normalizedProjectName(gitRoot, options.projectName);
@@ -416,7 +416,7 @@ export function initializeKxmProject(start = process.cwd(), options: KxmInitOpti
416
416
  assertNoRegisteredGates(options);
417
417
  const gitRoot = discoverGitRoot(start);
418
418
  if (!gitRoot) {
419
- throw new KxmConfigError([initIssue("git_root_required", ".", "kxm init must run inside the authoritative Git worktree")]);
419
+ throw new KxmConfigError([initIssue("git_root_required", ".", "kxm init must run inside a Git worktree. Run git init in this directory, then run kxm init again.")]);
420
420
  }
421
421
  if (options.projectId !== undefined) generatedProjectId(options.projectId);
422
422
  if (options.projectName !== undefined) normalizedProjectName(gitRoot, options.projectName);
@@ -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.196";
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
  }