@kontextmind/kxm 0.7.177 → 0.7.179

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.177",
14
+ "version": "0.7.179",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/CHANGELOG.md CHANGED
@@ -6,6 +6,27 @@ All notable user-facing changes are documented here. The project follows [Semant
6
6
 
7
7
  ### Added
8
8
 
9
+ - **KXM configuration now has one owner-validated terminal editor.**
10
+ `kxm config edit --scope project|user` and OMP/Pi `/kxm config` publish the
11
+ same `kxm.config.v1` surface. Accepted writes are saved and re-read; rejected
12
+ values leave file bytes unchanged; clear removes only the selected scope's
13
+ override. Non-TTY and dry-run calls render one plain frame without writing.
14
+ The editor excludes endpoints, key references, credentials, arrays, dynamic
15
+ maps, roster, routes, gates, roles, and workflow definitions. Pi/OMP package
16
+ discovery now loads the tracked extension bundle, so the private TUI
17
+ workspace is bundled rather than becoming an unresolved runtime dependency.
18
+ - **OMP can opt into a focused KXM status band with live subscription limits.**
19
+ `kxm plugin install --omp --status-band` installs the shared extension and
20
+ selects an OMP custom layout with native active-subscription usage, metered
21
+ cost, context, model, mode, branch, pull request, and subagent signals. The KXM hook line
22
+ omits idle, zero-count, clean-ship, cost, and normal local-hub noise while
23
+ retaining degraded hub, active work, inbox, waiting, dirty/ahead, and update
24
+ signals. The work widget retains known metered cost and suppresses unknown
25
+ cost. The flag is explicit because it changes OMP user configuration; restart
26
+ OMP after applying it.
27
+ - Plugin installation now treats an interrupted harness process with no exit
28
+ status as a failure for Pi, OMP, and Claude Code instead of reporting success.
29
+
9
30
  - **Hub project identity and its access key live in `.kxm/config.yaml`.**
10
31
  `hub.local` and `hub.cloud` hold the URL, the project id to present, and a
11
32
  key reference (`key.op` is an `op://` reference, `key.env` is an environment
@@ -2,7 +2,8 @@
2
2
 
3
3
  Read from `plans/kxm-roadmap/state.json` and the pages linked below.
4
4
 
5
- Verified date: null. A review pass has not set `architecture.verified`.
5
+ Verified date: 2026-10-11. The roadmap review checked every page listed here
6
+ against the current checkout.
6
7
 
7
8
  | Page | What it covers |
8
9
  | --- | --- |
@@ -1,15 +1,17 @@
1
1
  # Surfaces
2
2
 
3
- Read from `plugins/kxm/src/cli.ts`, `scripts/kxm-hub.mjs`, `plugins/kxm/src/hub.ts`,
4
- `plugins/kxm/src/runtime-supervisor.ts`, `plugins/kxm/src/runtime-paths.ts`,
5
- `plugins/kxm/src/mcp-server.ts`, `plugins/kxm/src/oneshot-producer.ts`,
6
- `plugins/kxm/src/studio-layout.ts`, `plugins/kxm/src/cli/types.ts`
7
- (`workspaceDirs`), `plugins/kxm/src/local-snapshot.ts`, and
8
- `scripts/kxm-worker.mjs`.
3
+ Read from `plugins/kxm/src/cli.ts`, `plugins/kxm/src/config-editor.ts`,
4
+ `plugins/kxm/src/extension.ts`, `scripts/kxm-hub.mjs`,
5
+ `plugins/kxm/src/hub.ts`, `plugins/kxm/src/runtime-supervisor.ts`,
6
+ `plugins/kxm/src/runtime-paths.ts`, `plugins/kxm/src/mcp-server.ts`,
7
+ `plugins/kxm/src/oneshot-producer.ts`, `plugins/kxm/src/studio-layout.ts`,
8
+ `plugins/kxm/src/cli/types.ts` (`workspaceDirs`),
9
+ `plugins/kxm/src/local-snapshot.ts`, and `scripts/kxm-worker.mjs`.
9
10
 
10
11
  | Surface | Started by | Listener or path | Store it owns | Defining file |
11
12
  | --- | --- | --- | --- | --- |
12
13
  | `kxm` CLI | `node scripts/kxm.mjs` or the installed bin | no listener | none | `plugins/kxm/src/cli.ts` |
14
+ | Configuration editor | `kxm config edit --scope project\|user`; OMP/Pi `/kxm config` uses project scope | `.kxm/config.yaml` or the user `config.yaml` | the selected config file; only bounded non-secret scalar fields | `plugins/kxm/src/config-editor.ts` |
13
15
  | Hub | `kxm hub start` via `scripts/kxm-hub.mjs` | `127.0.0.1:7331` | `.kxm/state/kxm.db` under the workspace state dir | `plugins/kxm/src/hub.ts` |
14
16
  | Hub credentials | written when the hub starts | `hub-env.json` under the user state root | that JSON file, not SQLite | `plugins/kxm/src/hub-env.ts` |
15
17
  | Hub cloud binding | `kxm hub bind <url> --cloud --token-env/--token-command` | no listener; writes the binding record and never stores the token | binding record; token stays in the named env or command | `plugins/kxm/src/cli/hub.ts` |
@@ -2,7 +2,7 @@
2
2
  title: "Decisions"
3
3
  description: "Decisions of medium significance or higher made by automated KXM work in this repository. One entry per decision: the options evaluated with their weights, and why the selected option won. Recorded the turn the decision is taken."
4
4
  audience: "agents and maintainers"
5
- updated: "2026-10-02"
5
+ updated: "2026-10-11"
6
6
  ---
7
7
 
8
8
  # Decisions
@@ -172,3 +172,29 @@ Evidence: Evidence: merge-base 77beda4 predates the CLI restructure; merge-tree
172
172
  - Selected: stash-apply onto a fresh lane off `origin/main`, take upstream
173
173
  for claude-bridge, `dist`, and `package.json`, and merge the rest.
174
174
  Evidence: Evidence: `git diff HEAD origin/main` on the stale branch showed upstream's claude-bridge fix and curl downloader already landed; the lane port keeps both those and the npm/`--all` behavior.
175
+
176
+ ## 2026-10-11 — OMP subscription status integration (extension)
177
+
178
+ - Context: KXM's extension already owns a hook status line while OMP can
179
+ render OAuth subscription quota through its native cached `usage`
180
+ segment. Adding quota to KXM's line would duplicate OMP data; changing
181
+ the OMP layout during every plugin install would unexpectedly overwrite
182
+ an operator's chosen status line.
183
+ - Options:
184
+ - Run `omp usage` from the KXM extension — weight 0.1. Direct, but adds
185
+ process work to refreshes, duplicates OMP's cache and rendering, and
186
+ expands the credential-bearing boundary.
187
+ - Configure the native OMP segment on every install — weight 0.3.
188
+ Discoverable, but silently mutates unrelated user configuration.
189
+ - Add explicit `kxm plugin install --omp --status-band` opt-in and use
190
+ OMP's native `usage` segment — weight 0.9. No duplicate quota
191
+ implementation; configuration mutation is deliberate and verifiable.
192
+ - Selected: explicit opt-in. The installer snapshots the three global
193
+ status-line entries, writes the focused layout, verifies effective
194
+ values in the current working directory, restores prior values, and
195
+ resets entries that were previously absent or `null`.
196
+ - Solid recommendation: keep subscription quota in OMP's native `usage`
197
+ segment and KXM workflow state in the separate hook status line.
198
+ - Interim recommendation: use the opt-in installer until OMP exposes a
199
+ first-class transactional status-preset API.
200
+ Evidence: `plugins/kxm/src/cli/plugins.ts`, `plugins/kxm/src/session-work.ts`, and `test/core/cli.test.ts`.
@@ -170,8 +170,10 @@ see the [plugin README](../../plugins/kxm/README.md).
170
170
  ### Pi package
171
171
 
172
172
  The root `package.json` carries the `pi-package` keyword and a `pi` block:
173
- `pi.extensions` lists `./plugins/kxm/src/extension.ts` and `pi.skills` lists
174
- `./plugins/kxm/skills`. Pi loads the TypeScript source itself.
173
+ `pi.extensions` lists the tracked `./plugins/kxm/dist/extension.js` bundle and
174
+ `pi.skills` lists `./plugins/kxm/skills`. Development can still load
175
+ `plugins/kxm/src/extension.ts` explicitly; published installs use the bundle so
176
+ private workspace packages are never unresolved runtime dependencies.
175
177
 
176
178
  ### Agent Skills
177
179
 
@@ -135,6 +135,7 @@ The context suites in detail:
135
135
  |---|---|
136
136
  | `init`, `validate`, `export` and `watch` in isolated workspaces | `cli.test.ts`, `github-watch.test.ts` |
137
137
  | Every mutating command under `--dry-run` leaves the workspace, state root and hub untouched | `cli-experience.test.ts` ("every mutating command under --dry-run leaves the workspace, state root, and hub untouched") |
138
+ | Owner-validated config editor: persisted writes, exact-byte refusals, scope inheritance, scalar-only publication, CLI frames, and OMP/Pi lifecycle | `config-editor.test.ts` |
138
139
  | Hub auto-start reuses a healthy or claimed hub, persists one admin key, and fails closed on bad claims | `hub-autostart.test.ts` |
139
140
  | Hub-local session brief from SQLite without message bodies; `hub bind` reports on, off or unknown | `session-work.test.ts`, `cli.test.ts` |
140
141
  | Session manifests, fail-closed rosters, worker and result envelopes, hub-owned envelope fields | `session.test.ts`, `cli.test.ts`, `envelope.test.ts`, `envelope-contract.test.ts` |
@@ -8,10 +8,10 @@ The kit is the workspace package
8
8
  [`packages/core/tui`](../../packages/core/tui) (`@kontextmind/tui`), also
9
9
  published on the product as `@kontextmind/kxm/tui`. It is one declarative
10
10
  surface model, one renderer, one input decoder, one contribution registry, and
11
- thin host adapters. Today `kxm dash` uses only its ANSI theme; the panel,
12
- registry and adapters are exported for integrators, and no shipped `kxm` command
13
- draws a panel yet. See [Packages and workspaces](packages.md) for the layout
14
- convention it follows.
11
+ thin host adapters. `kxm config edit --scope project|user` and OMP/Pi
12
+ `/kxm config` ship the first production panel on this kit. `kxm dash` uses its
13
+ ANSI theme but keeps its existing hub and SSE loop. See
14
+ [Packages and workspaces](packages.md) for the layout convention the kit follows.
15
15
 
16
16
  ## Package layout
17
17
 
@@ -99,6 +99,24 @@ The reducer is pure
99
99
  effects; the component layer runs them. That is the same contract that keeps
100
100
  `kxm dash` navigation testable, and it is why no key handling needs a terminal.
101
101
 
102
+ ## Shipped configuration editor
103
+
104
+ The `kxm/config` contribution in
105
+ [`config-editor.ts`](../../plugins/kxm/src/config-editor.ts) owns the supported
106
+ `kxm.config.v1` fields. Both `kxm config edit` and `/kxm config` register that
107
+ same contribution; the CLI uses the standalone adapter, while the OMP/Pi
108
+ extension uses the generic Pi custom panel.
109
+
110
+ Project scope shows built-in, user, then project values. User scope deliberately
111
+ shows only built-in and user values, so a project override cannot make a
112
+ successful user edit look unchanged. Clear deletes the selected scope's key;
113
+ the registry then re-reads the inherited value from disk.
114
+
115
+ The descriptor exposes only bounded scalar preferences. It does not expose hub
116
+ URLs, key references, credentials, arrays, dynamic maps, roster, routes, gates,
117
+ roles, or workflow definitions. Unknown section/field pairs and invalid values
118
+ are rejected before the config writer runs.
119
+
102
120
  ## Adding a surface
103
121
 
104
122
  1. Write the descriptor table. Each row is one setting: id, key path, label,
@@ -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), [`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), [`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)
@@ -101,7 +101,7 @@ kxm -V
101
101
  `--dry-run` changes nothing: no file is written, deleted, or moved, no request that changes hub or Runtime state is sent, no process is started, and no remote command runs. A command that cannot say what it would do without doing some of it refuses the flag instead of acting.
102
102
 
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
- - 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`, and `improve report`. `gate github watch --dry-run` still polls GitHub but does not post the signal.
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
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.
107
107
 
@@ -339,6 +339,34 @@ Stop harness extensions from starting a hub (Not run):
339
339
  kxm config set hub.autoStart off --scope user
340
340
  ```
341
341
 
342
+ ### `kxm config edit`
343
+
344
+ ```text
345
+ kxm config edit --scope project|user
346
+ ```
347
+
348
+ Opens the terminal editor for one fixed list of scalar `kxm.config.v1` preferences. The same editor is `/kxm config` inside the OMP/Pi TUI, and that command mounts only when the session mode is `tui`.
349
+
350
+ | Option | Argument | Default | Description |
351
+ |---|---|---|---|
352
+ | `--scope` | `project` or `user` | none | Which file a write or clear changes. Required |
353
+
354
+ - `--scope project` writes `.kxm/config.yaml` and shows built-in, then user, then project. `--scope user` writes `$KXM_USER_CONFIG_DIR/config.yaml` (default `~/.config/kxm/config.yaml`) and shows built-in, then user. The project file does not mask the user view.
355
+ - A write checks the section and field together, then checks the value. A refusal leaves that file's bytes unchanged. Clear deletes only that scope's key.
356
+ - The editor accepts workflow and harness ids that contain `@`. Whitespace and control characters are refused.
357
+ - A stored scalar whose runtime type does not match the field is shown with `stored type does not match this field`. The text `true` is not a boolean.
358
+ - It does not edit hub URLs, key references, credentials, arrays, dynamic maps, roster, routes, gates, roles, or workflow YAML. `kxm config get`, `set`, and `list` are unchanged and still accept keys this editor does not.
359
+ - No TTY, and `--dry-run`, each print one plain frame and write nothing. `--json`, or a scope other than `project` or `user`, exits 2 and writes nothing.
360
+
361
+ ```bash
362
+ kxm config edit --scope project --dry-run
363
+ ```
364
+
365
+ ```text
366
+ KXM configuration
367
+ ...
368
+ ```
369
+
342
370
  ## `kxm completion`
343
371
 
344
372
  ```text
@@ -671,13 +699,14 @@ Prints recent tasks (workflow runs) and plans (journal entries) from the local h
671
699
  - Side effects: on first use it mints a 24-hour operator session token and writes it to `<KXM_USER_CONFIG_DIR>/session.token` (mode 0600), and it caches the brief in `.kxm/state/session-brief.json`. Under `--dry-run` it still probes the hub and prints the brief, but it mints no token, writes neither file, and appends a `dry run: would write <file>` line for each (JSON: `dryRun` and `planned`); with no saved token the output carries no token. `--token --dry-run` plans only the token file.
672
700
  - The default text output ends with the session token, and `--token` prints only the token. Treat the output as a credential.
673
701
  - JSON (`kxm.session-brief.v1`) keys: `generatedAt`, `staleSeconds`, `source`, `hub` (`state`, `evidence`, `online`, `url`, `scope`), `stats`, `tasks`, `plans`, `statusLine`, `widgetLines`, `sessionToken`.
702
+ - The status line omits cost because harness-native bands already own that signal. The work widget includes known metered cost and suppresses unknown cost.
674
703
 
675
704
  ```bash
676
705
  kxm session brief
677
706
  ```
678
707
 
679
708
  ```text
680
- kxm hub:on · idle · dirty
709
+ kxm · dirty
681
710
 
682
711
  No recent tasks or plans.
683
712
 
@@ -689,7 +718,7 @@ kxm session brief --status
689
718
  ```
690
719
 
691
720
  ```text
692
- kxm hub:on · idle
721
+ kxm
693
722
  ```
694
723
 
695
724
  ```bash
@@ -697,7 +726,7 @@ kxm session brief --json
697
726
  ```
698
727
 
699
728
  ```text
700
- {"schema":"kxm.session-brief.v1","generatedAt":"2026-09-23T13:49:55.253Z","staleSeconds":5,"source":"legacy","hub":{"state":"off","evidence":"probed","online":false,"url":"http://127.0.0.1:59998","scope":"loopback"},"stats":{"activeTasks":0,"waitingTasks":0,"planCount":0,"inbox":0,"runTotal":0},"tasks":[],"plans":[],"statusLine":"kxm hub:off · idle",...,"sessionToken":"<token>"}
729
+ {"schema":"kxm.session-brief.v1","generatedAt":"2026-09-23T13:49:55.253Z","staleSeconds":5,"source":"legacy","hub":{"state":"off","evidence":"probed","online":false,"url":"http://127.0.0.1:59998","scope":"loopback"},"stats":{"activeTasks":0,"waitingTasks":0,"planCount":0,"inbox":0,"runTotal":0},"tasks":[],"plans":[],"statusLine":"kxm · hub:off",...,"sessionToken":"<token>"}
701
730
  ```
702
731
 
703
732
  Before the first token exists:
@@ -707,7 +736,7 @@ kxm session brief --status --dry-run
707
736
  ```
708
737
 
709
738
  ```text
710
- kxm hub:on · idle · dirty
739
+ kxm · dirty
711
740
  dry run: would write ~/.config/kxm/session.token
712
741
  dry run: would write /work/proj/.kxm/state/session-brief.json
713
742
  ```
@@ -924,6 +953,34 @@ kxm harness list --json
924
953
  {"schema":"kxm.cli-result.v1","ok":true,"command":"harness list","defaultHarness":"pi","harnesses":[{"id":"pi","label":"Pi","default":true,"mode":"headless","detected":false,"authenticated":false,"dispatch":{"status":"no","supported":false,"reason":"not_detected"},"canUpdate":{"self":true,"extensions":true,"models":true},"issues":[]},...]}
925
954
  ```
926
955
 
956
+ ## `kxm plugin`
957
+
958
+ ### `kxm plugin install`
959
+
960
+ ```text
961
+ kxm plugin install [--all | --pi | --omp | --claude] [--status-band]
962
+ ```
963
+
964
+ Installs the KXM extension into every discovered supported harness, or only the explicitly selected harnesses. `--status-band` is an explicit OMP-only opt-in: after installing the extension, it switches OMP to a focused custom status layout with model, mode, branch, pull request, subagent, active-subscription usage, metered cost, and context signals. It uses OMP's native background usage fetch and does not call `omp usage`. To support safe rollback, KXM parses OMP's global config file and retains only the three existing `statusLine` entries for the duration of the command; it does not use credential values.
965
+
966
+ | Option | Argument | Default | Description |
967
+ |---|---|---|---|
968
+ | `--all` | none | on when no harness flag is present | Install into every discovered supported harness |
969
+ | `--pi` | none | off | Install into Pi |
970
+ | `--omp` | none | off | Install into OMP |
971
+ | `--claude` | none | off | Install into Claude Code |
972
+ | `--status-band` | none | off | Replace OMP's status layout with the focused subscription layout |
973
+
974
+ - Mutates the selected harness installations. `--status-band` asks OMP for its global config directory, snapshots the three global status-line entries from `config.yml` or `config.yaml`, writes through `omp config set`, and verifies the effective values in the current working directory. On failure it restores prior values and resets entries that were previously absent or `null`. Restart OMP afterward.
975
+ - `--dry-run` reports the install and status-band intent without invoking a harness command.
976
+ - `--status-band` fails when OMP is not selected or detected. A failed or locally shadowed OMP configuration is reported as a failed OMP result rather than a successful install; verification covers the directory where the command runs, not every other project's OMP overrides.
977
+
978
+ ```bash
979
+ kxm plugin install --omp --status-band
980
+ ```
981
+
982
+ Not run in this reference: installs a plugin and changes OMP user configuration.
983
+
927
984
  ## `kxm auth`
928
985
 
929
986
  ### `kxm auth token`
@@ -1315,6 +1315,14 @@ every reader found in `plugins/kxm/src`, `scripts/`, and `packages/`; the
1315
1315
  | `telemetry.anonymize` | Boolean | `true` | Not read by any code path yet |
1316
1316
  | `telemetry.userTelemetryDir` | Path | none | Not read by any code path yet |
1317
1317
 
1318
+ The terminal editor exposes a fixed non-secret scalar subset of this table:
1319
+ theme and token budget; default workflow and harness; dashboard settings;
1320
+ default tracker; hub auto-start; improvement policy and thresholds; shadow
1321
+ execution and circuit-breaker scalars; and federated/anonymized telemetry
1322
+ flags. It deliberately excludes arrays, dynamic maps, paths, sync-provider
1323
+ details, hub endpoints and key references, credentials, and trusted roster,
1324
+ route, gate, role, or workflow-definition files.
1325
+
1318
1326
  Under `auto_threshold`, `minRuns`, `minPassRate` and `minCostSavings` are the
1319
1327
  distinct runs, the accepted share, and the mean recorded cost per attempt a
1320
1328
  candidate needs before `kxm improve` reports it ready for review. A group with
@@ -1376,9 +1384,15 @@ Commands:
1376
1384
  - `kxm config list` prints `user`, `defaults`, `dash`, `sync`, and
1377
1385
  `loadedFrom`; `--json` prints every section. An empty `loadedFrom` means no
1378
1386
  file was found.
1379
-
1380
- There is no `kxm config unset`. Delete the key from the file to fall back to
1381
- the next layer; setting `null` stores `null`.
1387
+ - `kxm config edit --scope project|user` opens the owner-validated terminal
1388
+ editor. Project scope shows Base → User → Project; user scope shows Base →
1389
+ User so a project override cannot mask the edit. Clear deletes that scope's
1390
+ leaf and re-reads the inherited value. Non-TTY and `--dry-run` calls print one
1391
+ plain frame without writing. Invalid scope and `--json` are refused.
1392
+
1393
+ There is no standalone `kxm config unset` command. The editor's clear action
1394
+ deletes its supported key from the selected scope; for other keys, delete the
1395
+ key from the file. Setting `null` stores `null` and does not mean inherit.
1382
1396
 
1383
1397
  ### Configuration layers
1384
1398
 
@@ -323,11 +323,11 @@ Proposes a change to one authoritative state key. Despite its name, it only prop
323
323
 
324
324
  Returns `{ "proposalId" }`. Blocked by the `read-only` preset; under an attempt token it needs an explicit grant.
325
325
 
326
- ## Pi extension commands
326
+ ## Pi and OMP extension commands
327
327
 
328
- The Pi extension connects the session to the hub at start, registers the 19 tools, and adds two slash commands. It also keeps a `kxm` status line and `kxm-work` and `kxm-progress` widgets, refreshed after every turn.
328
+ The shared Pi-compatible extension connects the session to the hub at start, registers the 19 tools, and adds two slash commands. It also keeps a concise `kxm` status line and `kxm-work` and `kxm-progress` widgets, refreshed after every turn.
329
329
 
330
- In Pi:
330
+ In Pi or OMP:
331
331
 
332
332
  ```text
333
333
  /kxm
@@ -344,7 +344,9 @@ In Pi:
344
344
  | `/kxm memory` | Shows the project memory brief by running `kxm memory brief` (or `$KXM_BIN`) |
345
345
  | `/kxm help` | Lists the subcommands. Any unknown subcommand shows the same help |
346
346
 
347
- The task picker, `Continue KXM work?`, lists recent tasks and plans and puts the chosen prompt in the editor. It also appears when a Pi TUI session starts, begins a new session or forks, unless `KXM_SESSION_BRIEF=off`.
347
+ The task picker, `Continue KXM work?`, lists recent tasks and plans and puts the chosen prompt in the editor. It also appears when a Pi or OMP TUI session starts, begins a new session or forks, unless `KXM_SESSION_BRIEF=off`.
348
+
349
+ OMP can also show the active OAuth subscription's quota windows in its native status band. Install the extension and opt into KXM's focused layout with `kxm plugin install --omp --status-band`, then restart OMP. The layout keeps model, mode, branch, pull request, subagent, subscription usage, metered cost, and context signals; it does not add OMP's inline `status` segment because OMP already renders the KXM hook status on its own line. If `statusLine.showHookStatus` is disabled, the native usage and cost segments remain but the separate KXM line stays hidden.
348
350
 
349
351
  Inbound requests arrive as a displayed message that starts a model turn: `steer` at the next decision boundary, `followUp` (and `nextTurn`, which an unattended worker cannot wait on) after the current work. The final response is returned as the reply. If the model provider fails, the request stays `delivered` so a supervised worker can recover it. The extension also registers model providers; see [Harness routing](harness-routing.md) and [Nous providers](../guides/nous-providers.md).
350
352
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.177",
3
+ "version": "0.7.179",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "pi": {
72
72
  "extensions": [
73
- "./plugins/kxm/src/extension.ts"
73
+ "./plugins/kxm/dist/extension.js"
74
74
  ],
75
75
  "skills": [
76
76
  "./plugins/kxm/skills"
@@ -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.177",
5
+ "version": "0.7.179",
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",
@@ -886,12 +886,12 @@ import { spawnSync } from "node:child_process";
886
886
  import { randomUUID } from "node:crypto";
887
887
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
888
888
  import { join as join4 } from "node:path";
889
- function hubPrefix(hub) {
889
+ function hubStatusPart(hub) {
890
890
  const suffix = hub?.scope === "remote" ? "/remote" : "";
891
- if (hub?.state === "on" || hub?.state === void 0 && hub?.online === true) return `kxm hub:on${suffix}`;
892
- if (hub?.state === "off" || hub?.state === void 0 && hub?.online === false) return `kxm hub:off${suffix}`;
893
- if (hub?.state === "unknown") return `kxm hub:unknown${suffix}`;
894
- return "kxm";
891
+ if (hub?.state === "off" || hub?.state === void 0 && hub?.online === false) return `hub:off${suffix}`;
892
+ if (hub?.state === "unknown") return `hub:unknown${suffix}`;
893
+ if (hub?.scope === "remote") return "hub:remote";
894
+ return void 0;
895
895
  }
896
896
  function truncate(value, width) {
897
897
  const text = value.replace(/\s+/g, " ").trim();
@@ -969,17 +969,17 @@ function readGitShip(cwd) {
969
969
  }
970
970
  function formatCostLine(input) {
971
971
  if (!input) return void 0;
972
- if (input.sessionCostUsd === void 0 && input.runCostUsd === void 0) return "unknown";
972
+ if (input.sessionCostUsd === void 0 && input.runCostUsd === void 0) return void 0;
973
973
  const sess = input.sessionCostUsd !== void 0 ? `$${input.sessionCostUsd.toFixed(2)} sess` : void 0;
974
974
  const run = input.runCostUsd !== void 0 ? `$${input.runCostUsd.toFixed(2)} run` : void 0;
975
975
  const route = input.harness && input.model ? `${input.harness}/${input.model}` : input.model ?? void 0;
976
976
  const parts = [sess, run, route].filter(Boolean);
977
- return parts.length > 0 ? parts.join(" \xB7 ") : "unknown";
977
+ return parts.length > 0 ? parts.join(" \xB7 ") : void 0;
978
978
  }
979
- function renderStatusLine(statsOrBrief, current, hub, ship, updateLatest, cost) {
979
+ function renderStatusLine(statsOrBrief, current, hub, ship, updateLatest) {
980
980
  if ("schema" in statsOrBrief && statsOrBrief.schema === "kxm.session-brief.v1") {
981
981
  const brief = statsOrBrief;
982
- if (!current && !hub && !ship && !updateLatest && !cost) {
982
+ if (!current && !hub && !ship && !updateLatest) {
983
983
  return brief.statusLine;
984
984
  }
985
985
  return formatSessionStatusLine(
@@ -987,46 +987,39 @@ function renderStatusLine(statsOrBrief, current, hub, ship, updateLatest, cost)
987
987
  current,
988
988
  hub ?? brief.hub,
989
989
  ship,
990
- updateLatest,
991
- cost ?? brief.cost
990
+ updateLatest
992
991
  );
993
992
  }
994
- return formatSessionStatusLine(statsOrBrief, current, hub, ship, updateLatest, cost);
995
- }
996
- function formatSessionStatusLine(stats, current, hub, ship, updateLatest, cost) {
997
- const head = hubPrefix(hub);
998
- if (!current && stats.activeTasks === 0 && stats.planCount === 0 && stats.inbox === 0) {
999
- const idle = hub?.online === void 0 && hub?.state === void 0 ? "kxm idle" : `${head} \xB7 idle`;
1000
- const withShip = ship?.dirty ? `${idle} \xB7 dirty` : ship && ship.ahead > 0 ? `${idle} \xB7 ${ship.ahead} local` : idle;
1001
- const withCost = cost ? `${withShip} \xB7 ${cost}` : withShip;
1002
- const finalLine = updateLatest ? `${withCost} \xB7 upd ${updateLatest}` : withCost;
1003
- return finalLine.length <= 80 ? finalLine : `${finalLine.slice(0, 79)}\u2026`;
1004
- }
1005
- const parts = [];
1006
- if (current?.kind === "task") parts.push(`${head} ${current.detail}`);
1007
- else if (current?.kind === "plan") parts.push(`${head} plan ${truncate(current.detail, 36)}`);
1008
- else parts.push(head);
1009
- parts.push(`${stats.activeTasks} task${stats.activeTasks === 1 ? "" : "s"}`);
993
+ return formatSessionStatusLine(statsOrBrief, current, hub, ship, updateLatest);
994
+ }
995
+ function formatSessionStatusLine(stats, current, hub, ship, updateLatest) {
996
+ const parts = ["kxm"];
997
+ const hubPart = hubStatusPart(hub);
998
+ if (hubPart) parts.push(hubPart);
999
+ if (current?.kind === "task") parts.push(current.detail);
1000
+ else if (current?.kind === "plan") parts.push(`plan ${truncate(current.detail, 36)}`);
1001
+ if (stats.activeTasks > 0) parts.push(`${stats.activeTasks} task${stats.activeTasks === 1 ? "" : "s"}`);
1010
1002
  if (stats.waitingTasks > 0) parts.push(`${stats.waitingTasks} waiting`);
1011
- parts.push(`${stats.planCount} plan${stats.planCount === 1 ? "" : "s"}`);
1003
+ if (stats.planCount > 0) parts.push(`${stats.planCount} plan${stats.planCount === 1 ? "" : "s"}`);
1012
1004
  if (stats.inbox > 0) parts.push(`inbox ${stats.inbox}`);
1013
1005
  if (ship?.dirty) parts.push("dirty");
1014
1006
  else if (ship && ship.ahead > 0) parts.push(`${ship.ahead} local`);
1015
- if (cost) parts.push(cost);
1016
1007
  if (updateLatest) parts.push(`upd ${updateLatest}`);
1017
- const line = parts.join(" \xB7 ");
1018
- return line.length <= 80 ? line : `${line.slice(0, 79)}\u2026`;
1008
+ return truncate(parts.join(" \xB7 "), 80);
1019
1009
  }
1020
1010
  function formatSessionWidget(stats, current, hub, ship, updateLatest, cost) {
1021
- const hubMark = hub?.state === "on" || hub?.online === true ? "hub:on " : hub?.state === "off" || hub?.online === false ? "hub:off " : hub?.state === "unknown" ? "hub:unknown " : "";
1022
- const lines = [
1023
- `KXM ${hubMark}${stats.activeTasks} tasks ${stats.waitingTasks} waiting ${stats.planCount} plans inbox ${stats.inbox}`
1024
- ];
1011
+ const suffix = hub?.scope === "remote" ? "/remote" : "";
1012
+ const hubMark = hub?.state === "on" || hub?.state === void 0 && hub?.online === true ? `hub:on${suffix}` : hub?.state === "off" || hub?.state === void 0 && hub?.online === false ? `hub:off${suffix}` : hub?.state === "unknown" ? `hub:unknown${suffix}` : void 0;
1013
+ const summary = ["KXM", hubMark];
1014
+ if (stats.activeTasks > 0) summary.push(`${stats.activeTasks} task${stats.activeTasks === 1 ? "" : "s"}`);
1015
+ if (stats.waitingTasks > 0) summary.push(`${stats.waitingTasks} waiting`);
1016
+ if (stats.planCount > 0) summary.push(`${stats.planCount} plan${stats.planCount === 1 ? "" : "s"}`);
1017
+ if (stats.inbox > 0) summary.push(`inbox ${stats.inbox}`);
1018
+ const lines = [summary.filter(Boolean).join(" \xB7 ")];
1025
1019
  if (current) lines.push(`now ${current.kind} ${truncate(current.detail, 56)}`);
1026
- else if (stats.latestPlan) lines.push(`plan ${truncate(stats.latestPlan, 60)}`);
1027
- else lines.push("now no selected work");
1028
- lines.push(formatShipLine(ship));
1029
- if (cost) lines.push(`cost ${cost}`);
1020
+ else if (stats.latestPlan) lines.push(`plan ${truncate(stats.latestPlan, 60)}`);
1021
+ if (ship?.dirty || ship && ship.ahead > 0) lines.push(formatShipLine(ship));
1022
+ if (cost && cost !== "unknown") lines.push(`cost ${cost}`);
1030
1023
  if (updateLatest) lines.push(`update ${updateLatest} available \xB7 kxm update --kxm`);
1031
1024
  return lines;
1032
1025
  }
@@ -1046,7 +1039,7 @@ function buildSessionBrief(snapshot, current, hub, ship, updateLatest, cost, ses
1046
1039
  const tasks = uniqueLabels(recentTasks(snapshot.runs).map(taskItem));
1047
1040
  const plans = uniqueLabels(snapshot.plans.slice(0, MAX_SESSION_BRIEF_PLANS).map(planItem));
1048
1041
  const resolvedHub = hub ?? { state: "off", evidence: "unconfigured", online: false };
1049
- const statusLine = formatSessionStatusLine(stats, current, resolvedHub, ship, updateLatest, cost);
1042
+ const statusLine = formatSessionStatusLine(stats, current, resolvedHub, ship, updateLatest);
1050
1043
  const widgetLines = formatSessionWidget(stats, current, resolvedHub, ship, updateLatest, cost);
1051
1044
  return {
1052
1045
  schema: SESSION_BRIEF_SCHEMA,
@@ -1110,7 +1103,7 @@ function estimateSessionCost(stateDir) {
1110
1103
  latestModel = r.effectiveModel ?? r.requestedModel ?? latestModel;
1111
1104
  latestHarness = r.harness ?? latestHarness;
1112
1105
  }
1113
- if (!hasMetered) return "unknown";
1106
+ if (!hasMetered) return void 0;
1114
1107
  const route = latestHarness && latestModel ? `${latestHarness}/${latestModel}` : latestModel;
1115
1108
  return `$${totalCost.toFixed(2)} sess${route ? ` \xB7 ${route}` : ""}`;
1116
1109
  } catch {
@@ -1147,8 +1140,7 @@ function loadSessionBrief(cwd, env = process.env, current, hub, options = {}) {
1147
1140
  current,
1148
1141
  effectiveHub,
1149
1142
  options.ship,
1150
- options.updateLatest,
1151
- effectiveCost
1143
+ options.updateLatest
1152
1144
  );
1153
1145
  const widgetLines = formatSessionWidget(
1154
1146
  cached.stats,
@@ -1213,8 +1205,7 @@ async function loadSessionBriefAsync(cwd, env = process.env, current, hub, optio
1213
1205
  current,
1214
1206
  effectiveHub,
1215
1207
  options.ship,
1216
- void 0,
1217
- effectiveCost
1208
+ void 0
1218
1209
  );
1219
1210
  const widgetLines = formatSessionWidget(
1220
1211
  cached.stats,
@@ -1275,7 +1266,7 @@ function sessionBriefPickerEnabled(input) {
1275
1266
  function parseKxmSlashArgs(args) {
1276
1267
  const raw = String(args ?? "").trim().toLowerCase();
1277
1268
  if (raw === "" || raw === "brief") return "brief";
1278
- if (raw === "status" || raw === "hub" || raw === "memory" || raw === "progress" || raw === "workflow" || raw === "help") return raw;
1269
+ if (raw === "status" || raw === "hub" || raw === "memory" || raw === "progress" || raw === "workflow" || raw === "config" || raw === "help") return raw;
1279
1270
  return "help";
1280
1271
  }
1281
1272
  function kxmSlashCompletions(prefix) {
@@ -1295,7 +1286,7 @@ var init_session_work = __esm({
1295
1286
  MAX_SESSION_BRIEF_PLANS = 5;
1296
1287
  SESSION_BRIEF_SCHEMA = "kxm.session-brief.v1";
1297
1288
  DEFAULT_SESSION_BRIEF_STALE_SECONDS = 5;
1298
- KXM_SLASH_SUBCOMMANDS = ["status", "hub", "memory", "progress", "workflow", "help"];
1289
+ KXM_SLASH_SUBCOMMANDS = ["status", "hub", "memory", "progress", "workflow", "config", "help"];
1299
1290
  }
1300
1291
  });
1301
1292
 
@@ -9182,6 +9173,7 @@ function loadKxmConfig(repoRoot = process.cwd(), options = {}) {
9182
9173
  const userConfigFile = join6(userDir, "config.yaml");
9183
9174
  const repoDir = repoConfigDirectory(repoRoot);
9184
9175
  const repoConfigFile = join6(repoDir, "config.yaml");
9176
+ const includeProject = options.layers !== "user";
9185
9177
  let userRaw = {};
9186
9178
  let userLoadedPath;
9187
9179
  if (existsSync7(userConfigFile)) {
@@ -9195,7 +9187,7 @@ function loadKxmConfig(repoRoot = process.cwd(), options = {}) {
9195
9187
  }
9196
9188
  let repoRaw = {};
9197
9189
  let repoLoadedPath;
9198
- if (existsSync7(repoConfigFile)) {
9190
+ if (includeProject && existsSync7(repoConfigFile)) {
9199
9191
  try {
9200
9192
  const text = readFileSync7(repoConfigFile, "utf8");
9201
9193
  repoRaw = (0, import_yaml2.parse)(text) ?? {};