@kontextmind/kxm 0.7.54 → 0.7.57

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.54",
14
+ "version": "0.7.57",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/CHANGELOG.md CHANGED
@@ -35,6 +35,53 @@ All notable user-facing changes are documented here. The project follows [Semant
35
35
 
36
36
  ### Changed
37
37
 
38
+ - **`kxm tenant status`: one composed read for the portal, with the authorities labelled.**
39
+ The portal needs hub metadata (roster, message queue, the hub's run projection) *and* the
40
+ Runtime's authoritative run state, and the failure mode S2 exists to prevent is rendering
41
+ one as the other. The new command reads both and labels every value with its source
42
+ (`hub-projection` vs `runtime-authoritative`); an unreadable upstream becomes
43
+ `unavailable` with a stable reason instead of being filled from the surviving source;
44
+ projection/authoritative disagreements are listed as `discrepancies` rather than averaged;
45
+ and `degraded` marks a partial read so `ok: true` never means "everything was seen". The
46
+ hub read resolves the **admin** credential only — a project token would 401 on the snapshot route —
47
+ and a malformed persisted record degrades that one source as `hub_credential_unreadable` rather than
48
+ aborting the Runtime read with it. Reads run concurrently with a 5 s hub deadline, a 200 with an
49
+ unusable body is `hub_response_invalid` rather than a healthy empty snapshot, and Runtime rows are
50
+ event-log folded (the listing endpoint now folds, so a cached row can no longer be presented as
51
+ state). A run whose fold refuses is labelled `runtime-cached` — the cache, not state — and is
52
+ excluded from the cross-check, which reports `unverifiedFoldRuns` (or `runtime_fold_failed`)
53
+ instead of letting a corrupt event log print "agree"; `kxm runs list` names the same failure on
54
+ the row instead of hiding it. The cross-check is honest about id spaces: hub runs and Runtime
55
+ runs mint ids independently, so zero shared ids reports `unverified` (`run_identity_link_absent`)
56
+ — never agreement — and only cleanly folded shared ids are compared. The Runtime read **attaches** to a live supervisor
57
+ (`attachKxmSupervisor`, new) and never starts one — a portal poller must not conjure a daemon, and
58
+ "nothing is running" is an answer to render, not a condition to repair. Exits non-zero only when
59
+ neither source could be read.
60
+
61
+ - **A Pi producer reply can no longer mint its own success.** `determineOutcome` scanned the
62
+ reply for any declared outcome *word* and, failing that, returned `passed`. So
63
+ `"the gate did not pass, so I would not call this passed"` settled the step as passed — the
64
+ word was all it took — and an empty or prose-only reply was passed by default. Only a declared
65
+ result counts now: a reply that is one JSON object, or prose carrying an explicit
66
+ `{"outcome": "…"}` block, and only when the step declares that outcome. Anything else is
67
+ `failed`; an undeclared value still lands in `outcome_unknown` and terminates as `failed`, so a
68
+ step without a `failed` transition cannot pass on a bad reply either. This matches the rule the
69
+ one-shot producer already enforced, and `docs/contracts/lifecycles.md` now states it where
70
+ `result_recorded` is defined. Three usage-capture fixtures that had been replying in prose now
71
+ declare their result, which is what they were always supposed to do; the new test in
72
+ `test/core/pi-producer.test.ts` covers prose, empty, out-of-vocabulary, and both accepted
73
+ structured shapes. Review of that first cut found two more ways to mint success, both now
74
+ closed: a result block was matched **anywhere** in the reply, so
75
+ `Example: {"outcome": "passed"}. Actual result: {"outcome": "failed"}` returned `passed`;
76
+ a declaration is now a standalone JSON object — the whole reply, or one object on its own
77
+ line, with the **last** such object winning so an illustration cannot outrank the answer, and
78
+ the whole reply settling `failed` when anything after that line still looks like an outcome
79
+ key, because at that point the producer cannot tell which declaration was meant.
80
+ And cancellation fell through to `allowedOutcomes[0]` when a step declared neither
81
+ `cancelled` nor `failed`, so aborting a `passed`-only step reported `passed`; a cancel now
82
+ reports `cancelled` unconditionally and the engine terminates it `failed` when the step
83
+ does not declare that outcome.
84
+
38
85
  - **`kxm migrate` is gone, and so is the state that only it could unlock.** Deleting the
39
86
  migration lanes left a converter with nothing to convert into: `plugins/kxm/src/migrate.ts`
40
87
  (1,848 lines), its 1,598-line suite, the `migrate plan|apply|verify` commands, the
@@ -273,6 +273,7 @@ The current hub command groups are `agent`, `session`, `workflow`, `gate`, `hub`
273
273
  | Command | Purpose |
274
274
  |---|---|
275
275
  | `kxm init` | Atomically create a provenance-tracked minimal KXM project, validate it without rewriting, resume a pinned interrupted create/repair, apply conflict-free non-authority template updates, or join an existing clone with repeatable `--repository <id=absolute-path>` member bindings stored outside Git. `--dry-run` performs no writes. Provenance-free/ambiguous repair and permission-expanding changes remain planning-only. These configuration slices do **not** activate a KXM Runtime. `kxm init` is project-only; bind a running hub with `kxm hub bind <url>` |
276
+ | `kxm tenant status` | One composed read for machine clients (the portal backend): hub metadata (agent roster, message queue, plans, the hub own workflow runs with stages/progress) and the Runtime **authoritative** run state (event-log folded, per-run `homeRuntimeId`), every value labelled with its source. Reads both sources concurrently with a hub deadline (5 s default); an unreadable upstream becomes `unavailable` with a stable reason (`hub_unreachable`, `hub_timeout`, `hub_unauthorized`, `hub_response_invalid`, `hub_credential_unreadable`, `runtime_supervisor_not_running`) instead of being filled from the other source. The cross-check is honest about id spaces: hub and Runtime runs mint ids independently, so no shared id reports `unverified` (`run_identity_link_absent`) — never agreement; shared ids are compared and disagreements listed; a Runtime row whose event-log fold failed is labelled `runtime-cached` and excluded from the comparison (`unverifiedFoldRuns`), never counted as agreement. `degraded` marks a partial read. Attaches to a live supervisor and never starts one; resolves the **admin** credential only (a project token cannot read the snapshot). Requires a KXM project |
276
277
  | `kxm trust diff [--base <rev>]` | Print the structured `kxm.permission-diff.v1` report between a base Git revision (default `HEAD`, materialized into a temporary shadow with a sanitized environment) and the working tree: every authority-bearing field change classified as expansion, narrowing, or neutral with per-field hashes. Performs no project writes |
277
278
  | `kxm trust check [--base <rev>]` | Exit non-zero when any expansion exists, so an authority-bearing change cannot merge without a reviewed Git change. Formatting/description-only changes never require review |
278
279
  | `kxm run <workflow> [prompt]` | Auto-start the KXM Runtime supervisor if needed, then create an immutable run offline: pins `homeRuntimeId` plus config/executor/tool policy revisions and stores only the prompt hash. `--dry-run` prints the plan without creating anything |
@@ -105,6 +105,22 @@ created → accepted → dispatched → executing → result_recorded → termin
105
105
  | `blocked_uncertain` | A dependent effect cannot be reconciled safely |
106
106
  | `terminal` | The logical assignment outcome is final and immutable: passed, failed, or cancelled |
107
107
 
108
+ A producer reply becomes a terminal outcome **only** through a declared result: the reply is
109
+ one JSON object, or it carries a `{"outcome": "…"}` object **on a line of its own**. With more
110
+ than one such line the last one is the answer; if anything after it still looks like an outcome
111
+ key — an inline `Actual result: {"outcome": "failed"}`, a pretty-printed object, a second
112
+ mention — the reply is **ambiguous and settles `failed`**, because guessing which declaration
113
+ was meant is the behaviour this rule removes. Ordinary trailing prose (a sign-off, a token
114
+ count) does not disturb a declared result.
115
+
116
+ Naming an outcome *word* anywhere in a reply is not a result — `"the gate did not pass, so I
117
+ would not call this passed"` must not advance a step — and an empty or unstructured reply is
118
+ never treated as success. A declared outcome outside the step's declared set is not accepted
119
+ either:
120
+ the assignment is recorded `outcome_unknown` and terminates as `failed`, which is also what
121
+ happens when a step declares no `failed` transition. Producers do not guess on the model's
122
+ behalf, and no fallback path mints `passed`.
123
+
108
124
  `assignmentId` remains stable. A retry moves the nonterminal assignment through
109
125
  `retry_pending` to `accepted`, creates a new `attemptId`, and never rewrites or
110
126
  exits the previous attempt's terminal state. The effective
@@ -13,6 +13,8 @@ npm run verify
13
13
 
14
14
  | Feature | Automated evidence |
15
15
  |---|---|
16
+ | Portal tenant read composes two labelled authorities: hub runs carry `hub-projection`, Runtime runs carry `runtime-authoritative`; a down hub leaves runtime state readable and vice versa with stable reasons; projection/authoritative disagreements surface as `discrepancies`; binding scope is labelled loopback/remote | `test/core/studio-layout.test.ts` |
17
+ | Structured-result settlement only: outcome words in prose, empty replies, and outcomes outside the step's declared set all fail closed; a declared JSON object and a JSON result block inside prose both settle, and the engine records `outcome_unknown` and terminates `failed` | `test/core/pi-producer.test.ts`, `test/core/engine.test.ts` |
16
18
  | Health, readiness, metrics, request IDs, security headers | `test/core/hub-api.test.ts` |
17
19
  | Shared and per-project authentication, project isolation | `test/core/hub-api.test.ts` |
18
20
  | Registration, discovery, presence, stale detection, identity resumption | `test/core/hub-api.test.ts`, `test/core/hub.test.ts` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.54",
3
+ "version": "0.7.57",
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.54",
5
+ "version": "0.7.57",
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",
@@ -19678,6 +19678,11 @@ function resolveClientHubAuthToken(env, project) {
19678
19678
  const record = readHubEnvRecord(env);
19679
19679
  return record?.projectTokens?.[project]?.trim() || record?.authToken?.trim() || void 0;
19680
19680
  }
19681
+ function resolveClientAdminAuthToken(env = process.env) {
19682
+ const envToken = env.KXM_AUTH_TOKEN?.trim();
19683
+ if (envToken) return envToken;
19684
+ return readHubEnvRecord(env)?.authToken?.trim() || void 0;
19685
+ }
19681
19686
 
19682
19687
  // plugins/kxm/src/project-name.ts
19683
19688
  import { readFileSync as readFileSync3 } from "node:fs";
@@ -25725,6 +25730,15 @@ async function probeSupervisor(port, expectedRuntimeId, token, timeoutMs = 750)
25725
25730
  function hashKxmTokenProof(token, nonce) {
25726
25731
  return createHmac("sha256", token).update(`kxm-runtime-token-proof\0${nonce}`, "utf8").digest("hex");
25727
25732
  }
25733
+ async function attachKxmSupervisor(options = {}) {
25734
+ const paths = kxmRuntimePaths(options.stateRoot !== void 0 ? { stateRoot: options.stateRoot } : { ...options.env ? { env: options.env } : {} });
25735
+ const status = kxmSupervisorStatus(paths);
25736
+ if (!status.running || !status.port || !status.runtimeId) return void 0;
25737
+ const token = readKxmSupervisorToken(paths);
25738
+ if (!token) return void 0;
25739
+ if (!await probeSupervisor(status.port, status.runtimeId, token)) return void 0;
25740
+ return { runtimeId: status.runtimeId, port: status.port, token, started: false };
25741
+ }
25728
25742
  async function ensureKxmSupervisor(options = {}) {
25729
25743
  const paths = kxmRuntimePaths(options.stateRoot !== void 0 ? { stateRoot: options.stateRoot } : { ...options.env ? { env: options.env } : {} });
25730
25744
  const status = kxmSupervisorStatus(paths);
@@ -31761,6 +31775,176 @@ function initializeKxmProject(start = process.cwd(), options = {}) {
31761
31775
  return withKxmLocalBindingLock(gitRoot, storeOptions, (lock) => initializeKxmProjectAtGitRoot(start, gitRoot, options, lock));
31762
31776
  }
31763
31777
 
31778
+ // plugins/kxm/src/tenant-status.ts
31779
+ var TENANT_STATUS_SCHEMA = "kxm.tenant-status.v1";
31780
+ var DEFAULT_HUB_TIMEOUT_MS = 5e3;
31781
+ function hubSourceReason(status) {
31782
+ if (status === 401 || status === 403) return "hub_unauthorized";
31783
+ if (status === 404) return "hub_not_found";
31784
+ return `hub_http_${status}`;
31785
+ }
31786
+ function asString(value) {
31787
+ return typeof value === "string" && value.length > 0 ? value : void 0;
31788
+ }
31789
+ function isObject(value) {
31790
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31791
+ }
31792
+ async function assembleTenantStatus(input) {
31793
+ const now = input.now ?? (() => /* @__PURE__ */ new Date());
31794
+ const base = input.hubUrl.replace(/\/$/, "");
31795
+ const at = () => now().toISOString();
31796
+ const readHub = async () => {
31797
+ const attemptedAt = at();
31798
+ let token;
31799
+ try {
31800
+ token = input.resolveAdminToken();
31801
+ } catch (error) {
31802
+ void error;
31803
+ return { state: "unavailable", observedAt: attemptedAt, reason: "hub_credential_unreadable" };
31804
+ }
31805
+ try {
31806
+ const response = await input.fetchImpl(`${base}/v1/ops/snapshot?project=${encodeURIComponent(input.project)}`, {
31807
+ headers: token ? { authorization: `Bearer ${token}` } : {},
31808
+ signal: AbortSignal.timeout(input.hubTimeoutMs ?? DEFAULT_HUB_TIMEOUT_MS)
31809
+ });
31810
+ if (!response.ok) {
31811
+ return { state: "unavailable", observedAt: attemptedAt, reason: hubSourceReason(response.status) };
31812
+ }
31813
+ let body;
31814
+ try {
31815
+ body = await response.json();
31816
+ } catch (error) {
31817
+ const name = error instanceof Error ? error.name : void 0;
31818
+ if (name === "TimeoutError" || name === "AbortError") {
31819
+ return { state: "unavailable", observedAt: attemptedAt, reason: "hub_timeout" };
31820
+ }
31821
+ return { state: "unavailable", observedAt: attemptedAt, reason: "hub_response_invalid" };
31822
+ }
31823
+ if (!isObject(body) || typeof body.project !== "string" || typeof body.fetchedAt !== "string" || !Array.isArray(body.agents) || !Array.isArray(body.runs) || !Array.isArray(body.plans ?? []) || typeof body.openMessageTotal !== "number" || typeof body.runTotal !== "number") {
31824
+ return { state: "unavailable", observedAt: attemptedAt, reason: "hub_response_invalid" };
31825
+ }
31826
+ if (body.project !== input.project) {
31827
+ return { state: "unavailable", observedAt: attemptedAt, reason: "hub_project_mismatch" };
31828
+ }
31829
+ const runRows = body.runs.filter(isObject);
31830
+ const value = {
31831
+ project: body.project,
31832
+ fetchedAt: body.fetchedAt,
31833
+ agents: body.agents.filter(isObject).map((agent) => ({
31834
+ id: asString(agent.id) ?? "",
31835
+ name: asString(agent.name) ?? "",
31836
+ online: agent.online === true
31837
+ })),
31838
+ openMessageTotal: body.openMessageTotal,
31839
+ runTotal: body.runTotal,
31840
+ runs: runRows.map((run) => {
31841
+ const stages = Array.isArray(run.stages) ? run.stages.filter(isObject).map((stage) => ({
31842
+ id: asString(stage.id) ?? "",
31843
+ status: asString(stage.status) ?? "unknown",
31844
+ ...typeof stage.attempts === "number" ? { attempts: stage.attempts } : {}
31845
+ })) : void 0;
31846
+ const done = stages?.filter((stage) => stage.status === "passed" || stage.status === "failed" || stage.status === "warning").length;
31847
+ return {
31848
+ id: asString(run.id) ?? "",
31849
+ status: asString(run.status) ?? "unknown",
31850
+ definitionId: asString(run.definitionId) ?? "",
31851
+ ...asString(run.currentStage) ? { currentStage: asString(run.currentStage) } : {},
31852
+ ...asString(run.targetAgentName) ? { targetAgentName: asString(run.targetAgentName) } : {},
31853
+ ...asString(run.updatedAt) ? { updatedAt: asString(run.updatedAt) } : {},
31854
+ ...stages !== void 0 ? { progress: { done: done ?? 0, total: stages.length }, stages } : {},
31855
+ source: "hub-projection"
31856
+ };
31857
+ }),
31858
+ plans: (Array.isArray(body.plans) ? body.plans : []).filter(isObject).map((plan) => ({
31859
+ id: asString(plan.id) ?? "",
31860
+ runId: asString(plan.runId) ?? "",
31861
+ summary: asString(plan.summary) ?? "",
31862
+ createdAt: asString(plan.createdAt) ?? "",
31863
+ ...asString(plan.stageId) ? { stageId: asString(plan.stageId) } : {},
31864
+ ...asString(plan.severity) ? { severity: asString(plan.severity) } : {}
31865
+ }))
31866
+ };
31867
+ return { state: "ok", observedAt: attemptedAt, value };
31868
+ } catch (error) {
31869
+ const name = error instanceof Error ? error.name : void 0;
31870
+ if (name === "TimeoutError" || name === "AbortError") {
31871
+ return { state: "unavailable", observedAt: attemptedAt, reason: "hub_timeout" };
31872
+ }
31873
+ return { state: "unavailable", observedAt: attemptedAt, reason: "hub_unreachable" };
31874
+ }
31875
+ };
31876
+ const readRuntime = async () => {
31877
+ const attemptedAt = at();
31878
+ try {
31879
+ const runs = await input.runtime.listRuns();
31880
+ return { state: "ok", observedAt: attemptedAt, value: { runs } };
31881
+ } catch (error) {
31882
+ const code = error instanceof Error && "issues" in error && Array.isArray(error.issues) && typeof error.issues[0]?.code === "string" ? error.issues[0].code : void 0;
31883
+ return { state: "unavailable", observedAt: attemptedAt, reason: code ?? "runtime_unavailable" };
31884
+ }
31885
+ };
31886
+ const [hub, runtime] = await Promise.all([readHub(), readRuntime()]);
31887
+ let runComparison;
31888
+ if (!hub.value || !runtime.value) {
31889
+ runComparison = {
31890
+ state: "unavailable",
31891
+ reason: !hub.value ? `hub_${hub.reason ?? "unavailable"}` : `runtime_${runtime.reason ?? "unavailable"}`
31892
+ };
31893
+ } else {
31894
+ const authoritative = new Map(
31895
+ runtime.value.runs.filter((run) => run.projectionError === void 0).map((run) => [run.runId, run.status])
31896
+ );
31897
+ const hubIds = new Set(hub.value.runs.map((run) => run.id));
31898
+ const foldFailed = runtime.value.runs.filter((run) => run.projectionError !== void 0 && hubIds.has(run.runId));
31899
+ const discrepancies = [];
31900
+ let matched = 0;
31901
+ for (const projected of hub.value.runs) {
31902
+ const runtimeStatus = authoritative.get(projected.id);
31903
+ if (runtimeStatus === void 0) continue;
31904
+ matched += 1;
31905
+ if (runtimeStatus !== projected.status) {
31906
+ discrepancies.push({ runId: projected.id, hubStatus: projected.status, runtimeStatus });
31907
+ }
31908
+ }
31909
+ if (matched > 0) {
31910
+ runComparison = {
31911
+ state: "compared",
31912
+ matched,
31913
+ ...foldFailed.length > 0 ? { unverifiedFoldRuns: foldFailed.length } : {},
31914
+ ...discrepancies.length > 0 ? { discrepancies } : {}
31915
+ };
31916
+ } else {
31917
+ runComparison = foldFailed.length > 0 ? { state: "unverified", reason: "runtime_fold_failed", matched: 0, unverifiedFoldRuns: foldFailed.length } : { state: "unverified", reason: "run_identity_link_absent", matched: 0 };
31918
+ }
31919
+ }
31920
+ return {
31921
+ schema: TENANT_STATUS_SCHEMA,
31922
+ project: input.project,
31923
+ generatedAt: at(),
31924
+ hubUrl: input.hubUrl,
31925
+ bindingScope: hubBindingScope(input.hubUrl),
31926
+ hub,
31927
+ runtime,
31928
+ runComparison,
31929
+ degraded: hub.state !== "ok" || runtime.state !== "ok"
31930
+ };
31931
+ }
31932
+ function formatTenantStatus(payload) {
31933
+ const hubLine = payload.hub.state === "ok" && payload.hub.value ? `hub ${payload.hub.value.agents.filter((agent) => agent.online).length}/${payload.hub.value.agents.length} agents online, ${payload.hub.value.runs.length} hub runs, ${payload.hub.value.openMessageTotal} open messages` : `hub unavailable (${payload.hub.reason ?? "unknown"})`;
31934
+ const runtimeLine = payload.runtime.state === "ok" && payload.runtime.value ? (() => {
31935
+ const cached = payload.runtime.value.runs.filter((run) => run.projectionError !== void 0).length;
31936
+ const authoritative = payload.runtime.value.runs.length - cached;
31937
+ return `runtime ${authoritative} runs (authoritative, on this box)${cached > 0 ? `, ${cached} cached (fold failed, not state)` : ""}`;
31938
+ })() : `runtime unavailable (${payload.runtime.reason ?? "unknown"})`;
31939
+ const comparisonLine = payload.runComparison.state === "compared" ? payload.runComparison.discrepancies && payload.runComparison.discrepancies.length > 0 ? `cross-check: ${payload.runComparison.discrepancies.length} of ${payload.runComparison.matched} matched run(s) disagree${payload.runComparison.unverifiedFoldRuns ? ` (${payload.runComparison.unverifiedFoldRuns} unverified: fold failed)` : ""}` : `cross-check: ${payload.runComparison.matched} matched run(s) agree${payload.runComparison.unverifiedFoldRuns ? ` (${payload.runComparison.unverifiedFoldRuns} unverified: fold failed)` : ""}` : payload.runComparison.state === "unverified" ? payload.runComparison.reason === "runtime_fold_failed" ? `cross-check: unavailable \u2014 ${payload.runComparison.unverifiedFoldRuns ?? 0} shared run(s) could not be verified (runtime fold failed)` : "cross-check: unavailable \u2014 no run id appears in both sources (independent id spaces)" : `cross-check: unavailable (${payload.runComparison.reason ?? "unknown"})`;
31940
+ return [
31941
+ `tenant ${payload.project} @ ${payload.hubUrl} (${payload.bindingScope})`,
31942
+ hubLine,
31943
+ runtimeLine,
31944
+ comparisonLine
31945
+ ].join("\n");
31946
+ }
31947
+
31764
31948
  // plugins/kxm/src/cli/project.ts
31765
31949
  var kxmDriveCliSeams = {};
31766
31950
  function initPlanPayload(plan) {
@@ -32236,7 +32420,7 @@ async function cmdKxmRunList(runtime) {
32236
32420
  runtime.io,
32237
32421
  runtime.json,
32238
32422
  { ok: true, command: "runs list", runs },
32239
- runs.length === 0 ? "no runs" : runs.map((run) => `${run.runId} ${run.status} ${run.workflowId} ${run.createdAt}`).join("\n")
32423
+ runs.length === 0 ? "no runs" : runs.map((run) => `${run.runId} ${run.status} ${run.workflowId} ${run.createdAt}${run.projectionError ? ` [state unverified: ${run.projectionError}]` : ""}`).join("\n")
32240
32424
  );
32241
32425
  return 0;
32242
32426
  } catch (error) {
@@ -32248,6 +32432,67 @@ async function cmdKxmRunList(runtime) {
32248
32432
  return 1;
32249
32433
  }
32250
32434
  }
32435
+ async function cmdTenantStatus(runtime) {
32436
+ let projectRoot;
32437
+ let projectId;
32438
+ try {
32439
+ projectRoot = discoverKxmProjectRoot(runtime.cwd) ?? void 0;
32440
+ if (!projectRoot) {
32441
+ print(runtime.io, runtime.json, { ok: false, command: "tenant status", error: "project_required" }, "kxm tenant status requires a KXM project (run kxm init first)");
32442
+ return 1;
32443
+ }
32444
+ const bundle = loadKxmProject(projectRoot, {});
32445
+ projectId = String(bundle.project.value.id);
32446
+ const root = projectRoot;
32447
+ const payload = await assembleTenantStatus({
32448
+ project: projectId,
32449
+ hubUrl: runtime.serverUrl,
32450
+ // Admin-scoped route: resolve the admin credential only (a project token would 401),
32451
+ // and resolve it inside the hub source so a malformed persisted record degrades that
32452
+ // one source instead of aborting the Runtime read with it.
32453
+ resolveAdminToken: () => resolveClientAdminAuthToken(runtime.env),
32454
+ fetchImpl: runtime.fetchImpl,
32455
+ runtime: {
32456
+ listRuns: async () => {
32457
+ const handle = await attachKxmSupervisor({ env: runtime.env });
32458
+ if (!handle) {
32459
+ throw runtimeError("runtime_supervisor_not_running", "runtime", "no live runtime supervisor on this box");
32460
+ }
32461
+ const result = await kxmRuntimeRequest(handle, "GET", `/v1/projects/${encodeURIComponent(projectId)}/runs?projectRoot=${encodeURIComponent(root)}`);
32462
+ const runs = result.runs ?? [];
32463
+ return runs.map((run) => ({
32464
+ runId: String(run.runId ?? ""),
32465
+ status: String(run.status ?? "unknown"),
32466
+ homeRuntimeId: String(run.homeRuntimeId ?? ""),
32467
+ ...typeof run.workflowId === "string" ? { workflowId: run.workflowId } : {},
32468
+ ...typeof run.createdAt === "string" ? { createdAt: run.createdAt } : {},
32469
+ ...typeof run.updatedAt === "string" ? { updatedAt: run.updatedAt } : {},
32470
+ ...typeof run.projectionError === "string" ? { projectionError: run.projectionError } : {},
32471
+ source: typeof run.projectionError === "string" ? "runtime-cached" : "runtime-authoritative"
32472
+ }));
32473
+ }
32474
+ }
32475
+ });
32476
+ if (payload.hub.state !== "ok" && payload.runtime.state !== "ok") {
32477
+ print(
32478
+ runtime.io,
32479
+ runtime.json,
32480
+ { ok: false, command: "tenant status", error: "tenant_status_no_source", payload },
32481
+ `neither source could be read: hub ${payload.hub.reason ?? "unknown"}, runtime ${payload.runtime.reason ?? "unknown"}`
32482
+ );
32483
+ return 1;
32484
+ }
32485
+ print(runtime.io, runtime.json, { ok: true, command: "tenant status", ...payload }, formatTenantStatus(payload));
32486
+ return 0;
32487
+ } catch (error) {
32488
+ if (error instanceof KxmConfigError) {
32489
+ print(runtime.io, runtime.json, { ok: false, command: "tenant status", error: "tenant_status_failed", issues: error.issues }, `tenant status failed: ${error.message}`);
32490
+ return 1;
32491
+ }
32492
+ print(runtime.io, runtime.json, { ok: false, command: "tenant status", error: "tenant_status_io_failed" }, "tenant status failed because a local operation did not complete");
32493
+ return 1;
32494
+ }
32495
+ }
32251
32496
  async function cmdHarnessList(runtime) {
32252
32497
  const inventory = await probeHarnessesAsync({ env: runtime.env });
32253
32498
  print(runtime.io, runtime.json, { ok: true, command: "harness list", ...inventory }, formatHarnessInventory(inventory));
@@ -45628,6 +45873,7 @@ var TOP_LEVEL_COMMANDS = [
45628
45873
  "restore",
45629
45874
  "run",
45630
45875
  "runs",
45876
+ "tenant",
45631
45877
  "harness",
45632
45878
  "update",
45633
45879
  "runtime",
@@ -45656,6 +45902,7 @@ var TOP_LEVEL_COMMANDS = [
45656
45902
  ];
45657
45903
  var SUBCOMMANDS = {
45658
45904
  runs: ["status", "cancel", "list"],
45905
+ tenant: ["status"],
45659
45906
  harness: ["list"],
45660
45907
  runtime: ["start", "status", "stop"],
45661
45908
  trust: ["diff", "check"],
@@ -47372,6 +47619,11 @@ function createProgram(ctx, result) {
47372
47619
  addGlobalOptions(runCmd.command("list").description("List recent runs for the current project")).action(async function runListAction() {
47373
47620
  result.code = await cmdKxmRunList(runtimeFrom(ctx, this));
47374
47621
  });
47622
+ const tenantCmd = addGlobalOptions(program2.command("tenant").description("Composed tenant reads for machine clients (portal)"));
47623
+ tenantCmd.helpCommand("help", "Show tenant help");
47624
+ addGlobalOptions(tenantCmd.command("status").description("Read hub metadata and authoritative Runtime run state as one labeled view")).action(async function tenantStatusAction() {
47625
+ result.code = await cmdTenantStatus(runtimeFrom(ctx, this));
47626
+ });
47375
47627
  const modelsCmd = addGlobalOptions(program2.command("models").description("Manage model catalogs, roles, and route state"));
47376
47628
  modelsCmd.action(async function modelsScreenAction() {
47377
47629
  result.code = await cmdModelsScreen(runtimeFrom(ctx, this));
@@ -17121,7 +17121,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
17121
17121
  }
17122
17122
 
17123
17123
  // plugins/kxm/src/mcp-server.ts
17124
- var VERSION = "0.7.54";
17124
+ var VERSION = "0.7.57";
17125
17125
  var inbox = /* @__PURE__ */ new Map();
17126
17126
  var notifiedInbox = /* @__PURE__ */ new Set();
17127
17127
  var meshClient;
@@ -26837,6 +26837,15 @@ async function probeSupervisor(port, expectedRuntimeId, token, timeoutMs = 750)
26837
26837
  function hashKxmTokenProof(token, nonce) {
26838
26838
  return createHmac("sha256", token).update(`kxm-runtime-token-proof\0${nonce}`, "utf8").digest("hex");
26839
26839
  }
26840
+ async function attachKxmSupervisor(options = {}) {
26841
+ const paths = kxmRuntimePaths(options.stateRoot !== void 0 ? { stateRoot: options.stateRoot } : { ...options.env ? { env: options.env } : {} });
26842
+ const status = kxmSupervisorStatus(paths);
26843
+ if (!status.running || !status.port || !status.runtimeId) return void 0;
26844
+ const token = readKxmSupervisorToken(paths);
26845
+ if (!token) return void 0;
26846
+ if (!await probeSupervisor(status.port, status.runtimeId, token)) return void 0;
26847
+ return { runtimeId: status.runtimeId, port: status.port, token, started: false };
26848
+ }
26840
26849
  async function ensureKxmSupervisor(options = {}) {
26841
26850
  const paths = kxmRuntimePaths(options.stateRoot !== void 0 ? { stateRoot: options.stateRoot } : { ...options.env ? { env: options.env } : {} });
26842
26851
  const status = kxmSupervisorStatus(paths);
@@ -27246,7 +27255,16 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now) {
27246
27255
  sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: `project ${requestedProjectId} is not the bound project ${context.projectId}` });
27247
27256
  return;
27248
27257
  }
27249
- const runs = context.eventStore.runsForProject(requestedProjectId, 50);
27258
+ const runs = context.eventStore.runsForProject(requestedProjectId, 50).map((stored) => {
27259
+ try {
27260
+ return projectKxmRunReadOnly(context, stored.runId);
27261
+ } catch (error) {
27262
+ return {
27263
+ ...stored,
27264
+ projectionError: error instanceof KxmConfigError ? error.issues[0]?.code ?? "runtime_projection_failed" : "runtime_projection_failed"
27265
+ };
27266
+ }
27267
+ });
27250
27268
  sendJson(response, 200, { ok: true, runs });
27251
27269
  return;
27252
27270
  }
@@ -27379,6 +27397,7 @@ async function kxmRuntimeRequest(handle, method, path, body) {
27379
27397
  export {
27380
27398
  DEFAULT_RUNTIME_STOP_GRACE_MS,
27381
27399
  MAX_RUNTIME_STOP_GRACE_MS,
27400
+ attachKxmSupervisor,
27382
27401
  ensureKxmSupervisor,
27383
27402
  hashKxmSupervisorToken,
27384
27403
  hashKxmTokenProof,
@@ -27349,6 +27349,15 @@ async function probeSupervisor(port, expectedRuntimeId, token, timeoutMs = 750)
27349
27349
  function hashKxmTokenProof(token, nonce) {
27350
27350
  return createHmac("sha256", token).update(`kxm-runtime-token-proof\0${nonce}`, "utf8").digest("hex");
27351
27351
  }
27352
+ async function attachKxmSupervisor(options = {}) {
27353
+ const paths = kxmRuntimePaths(options.stateRoot !== void 0 ? { stateRoot: options.stateRoot } : { ...options.env ? { env: options.env } : {} });
27354
+ const status = kxmSupervisorStatus(paths);
27355
+ if (!status.running || !status.port || !status.runtimeId) return void 0;
27356
+ const token = readKxmSupervisorToken(paths);
27357
+ if (!token) return void 0;
27358
+ if (!await probeSupervisor(status.port, status.runtimeId, token)) return void 0;
27359
+ return { runtimeId: status.runtimeId, port: status.port, token, started: false };
27360
+ }
27352
27361
  async function ensureKxmSupervisor(options = {}) {
27353
27362
  const paths = kxmRuntimePaths(options.stateRoot !== void 0 ? { stateRoot: options.stateRoot } : { ...options.env ? { env: options.env } : {} });
27354
27363
  const status = kxmSupervisorStatus(paths);
@@ -27758,7 +27767,16 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now) {
27758
27767
  sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: `project ${requestedProjectId} is not the bound project ${context.projectId}` });
27759
27768
  return;
27760
27769
  }
27761
- const runs = context.eventStore.runsForProject(requestedProjectId, 50);
27770
+ const runs = context.eventStore.runsForProject(requestedProjectId, 50).map((stored) => {
27771
+ try {
27772
+ return projectKxmRunReadOnly(context, stored.runId);
27773
+ } catch (error) {
27774
+ return {
27775
+ ...stored,
27776
+ projectionError: error instanceof KxmConfigError ? error.issues[0]?.code ?? "runtime_projection_failed" : "runtime_projection_failed"
27777
+ };
27778
+ }
27779
+ });
27762
27780
  sendJson(response, 200, { ok: true, runs });
27763
27781
  return;
27764
27782
  }
@@ -27913,20 +27931,37 @@ function extractText(content) {
27913
27931
  }
27914
27932
  return "";
27915
27933
  }
27916
- function determineOutcome2(text, allowedOutcomes) {
27917
- const normalized = text.trim();
27918
- const jsonMatch = /"outcome"\s*:\s*"([^"]+)"/.exec(normalized);
27919
- if (jsonMatch && allowedOutcomes.includes(jsonMatch[1])) {
27920
- return jsonMatch[1];
27921
- }
27922
- for (const outcome of allowedOutcomes) {
27923
- const regex = new RegExp(`\\b${outcome}\\b`, "i");
27924
- if (regex.test(normalized)) {
27925
- return outcome;
27926
- }
27934
+ function asJsonObject2(text) {
27935
+ try {
27936
+ const parsed = JSON.parse(text);
27937
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
27938
+ } catch {
27939
+ return void 0;
27927
27940
  }
27928
- if (allowedOutcomes.includes("passed")) return "passed";
27929
- return allowedOutcomes[0] ?? "completed";
27941
+ }
27942
+ function outcomeField(value) {
27943
+ const outcome = value.outcome;
27944
+ return typeof outcome === "string" ? outcome : void 0;
27945
+ }
27946
+ function declaredOutcomeOf(text) {
27947
+ const trimmed = text.trim();
27948
+ if (!trimmed) return void 0;
27949
+ const whole = asJsonObject2(trimmed);
27950
+ if (whole) return outcomeField(whole);
27951
+ const lines = trimmed.split(/\r?\n/);
27952
+ let declaration;
27953
+ for (let index = 0; index < lines.length; index += 1) {
27954
+ const outcome = outcomeField(asJsonObject2(lines[index].trim()) ?? {});
27955
+ if (outcome !== void 0) declaration = { index, outcome };
27956
+ }
27957
+ if (!declaration) return void 0;
27958
+ const tail = lines.slice(declaration.index + 1).join("\n");
27959
+ if (/"outcome"\s*:/.test(tail)) return void 0;
27960
+ return declaration.outcome;
27961
+ }
27962
+ function determineOutcome2(text, allowedOutcomes) {
27963
+ const declared = declaredOutcomeOf(text);
27964
+ return declared !== void 0 && allowedOutcomes.includes(declared) ? declared : "failed";
27930
27965
  }
27931
27966
  var PiSession = class {
27932
27967
  key;
@@ -28056,7 +28091,7 @@ var PiSession = class {
28056
28091
  const isAborted = active.aborted || active.signal?.aborted;
28057
28092
  let outcome;
28058
28093
  if (isAborted) {
28059
- outcome = active.allowedOutcomes.includes("cancelled") ? "cancelled" : active.allowedOutcomes.includes("failed") ? "failed" : active.allowedOutcomes[0];
28094
+ outcome = "cancelled";
28060
28095
  } else {
28061
28096
  outcome = determineOutcome2(active.text, active.allowedOutcomes);
28062
28097
  }
@@ -28108,8 +28143,7 @@ var PiSession = class {
28108
28143
  throw new Error("pi_session_busy");
28109
28144
  }
28110
28145
  if (signal?.aborted) {
28111
- const outcome = allowedOutcomes.includes("cancelled") ? "cancelled" : allowedOutcomes.includes("failed") ? "failed" : allowedOutcomes[0];
28112
- return { outcome, text: "aborted", usage: {} };
28146
+ return { outcome: "cancelled", text: "aborted", usage: {} };
28113
28147
  }
28114
28148
  this.status = "busy";
28115
28149
  return new Promise((resolve8, reject) => {
@@ -30211,6 +30245,7 @@ export {
30211
30245
  acceptKxmRun,
30212
30246
  assertClosedGateObservation,
30213
30247
  assertKxmConfigError,
30248
+ attachKxmSupervisor,
30214
30249
  backupDatabaseFile,
30215
30250
  buildImprovementReport,
30216
30251
  buildSshArgs,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.54",
3
+ "version": "0.7.57",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -6,6 +6,7 @@ const TOP_LEVEL_COMMANDS = [
6
6
  "restore",
7
7
  "run",
8
8
  "runs",
9
+ "tenant",
9
10
  "harness",
10
11
  "update",
11
12
  "runtime",
@@ -35,6 +36,7 @@ const TOP_LEVEL_COMMANDS = [
35
36
 
36
37
  const SUBCOMMANDS: Record<string, string[]> = {
37
38
  runs: ["status", "cancel", "list"],
39
+ tenant: ["status"],
38
40
  harness: ["list"],
39
41
  runtime: ["start", "status", "stop"],
40
42
  trust: ["diff", "check"],