@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +14 -1
- package/docs/operations/troubleshooting.md +5 -0
- package/docs/reference/cli-reference.md +35 -3
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +32 -12
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +2 -2
- package/plugins/kxm/src/cli/hub.ts +7 -1
- package/plugins/kxm/src/init.ts +2 -2
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/vision-gate.ts +29 -8
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
|
|
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
|
@@ -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.
|
|
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",
|
package/plugins/kxm/dist/cli.js
CHANGED
|
@@ -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
|
|
28907
|
-
|
|
28908
|
-
"
|
|
28909
|
-
|
|
28910
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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.
|
|
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();
|
package/plugins/kxm/package.json
CHANGED
|
@@ -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
|
|
109
|
-
- The route must be admitted in `.kxm/routes.yaml
|
|
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
|
|
package/plugins/kxm/src/init.ts
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 {
|
|
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
|
}
|