@kontextmind/kxm 0.7.141 → 0.7.143
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 +58 -14
- package/docs/contracts/lifecycles.md +7 -10
- package/docs/reference/cli-reference.md +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +85 -8
- package/plugins/kxm/dist/runtime.js +85 -8
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/oneshot-producer.ts +92 -8
- package/plugins/kxm/src/worktree-witness.ts +21 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,8 +6,6 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
6
6
|
|
|
7
7
|
### Added
|
|
8
8
|
|
|
9
|
-
- **Playwright uses Obscura by default.** `resolveBrowserCdpEndpoint()` returns `OBSCURA_CDP_URL` or `http://127.0.0.1:${OBSCURA_PORT:-9222}`. `KXM_BROWSER=steel` still returns the Steel session CDP URL. `node scripts/obscura.mjs` downloads pinned Obscura v0.2.3 and serves it with `--allow-private-network`. `npm run e2e` runs the Playwright smoke test in `test/e2e/` over CDP and does not run `playwright install`. Steel remains the path for human takeover, MFA, and the live session viewer. See [ADR-0005](docs/adr/ADR-0005-obscura-default-playwright.md) and [How do I connect Playwright to Obscura?](docs/kb/how-to-connect-playwright-to-obscura.md).
|
|
10
|
-
|
|
11
9
|
- **A live agent step uses a configurable one-shot timeout, and a cancelling run recovers when its child has already exited.**
|
|
12
10
|
The bound is the step `timeoutMs`, or the project `limits.agentStepTimeoutMs`
|
|
13
11
|
when the step omits it (minimum 60 seconds, default one hour). A wider step
|
|
@@ -33,6 +31,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
33
31
|
A required review is reported. The `land` workflow in `.kxm/workflows/land.yaml`
|
|
34
32
|
runs the same gates and may be refused until gate-only workflows are supported.
|
|
35
33
|
See the [CLI reference](docs/reference/cli-reference.md#kxm-land).
|
|
34
|
+
|
|
36
35
|
- **`kxm assign` is the entry to the developer assignment runner.**
|
|
37
36
|
`run`, `witness`, `plan-current`, `attribute`, `observe-cost`, `accept` and
|
|
38
37
|
`change-report` spawn `scripts/assignment-run.mjs` with the same flags as the
|
|
@@ -138,19 +137,10 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
138
137
|
hub check and still lose run history, the prompts that explain it, the project definition,
|
|
139
138
|
and the bindings that make the box reproducible.
|
|
140
139
|
|
|
140
|
+
- **Playwright uses Obscura by default.** `resolveBrowserCdpEndpoint()` returns `OBSCURA_CDP_URL` or `http://127.0.0.1:${OBSCURA_PORT:-9222}`. `KXM_BROWSER=steel` still returns the Steel session CDP URL. `node scripts/obscura.mjs` downloads pinned Obscura v0.2.3 and serves it with `--allow-private-network`. `npm run e2e` runs the Playwright smoke test in `test/e2e/` over CDP and does not run `playwright install`. Steel remains the path for human takeover, MFA, and the live session viewer. See [ADR-0005](docs/adr/ADR-0005-obscura-default-playwright.md) and [How do I connect Playwright to Obscura?](docs/kb/how-to-connect-playwright-to-obscura.md).
|
|
141
|
+
|
|
141
142
|
### Changed
|
|
142
143
|
|
|
143
|
-
- **Steel clients authenticate to Authentik with `Authorization: Basic`.**
|
|
144
|
-
`STEEL_AUTH_BASIC`, or `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`, set that
|
|
145
|
-
header on Steel HTTP requests and on the CDP options from `formatCDPConnect()`.
|
|
146
|
-
`KXM_BROWSER=steel` passes those headers through `connectBrowserOverCdp()`
|
|
147
|
-
into `chromium.connectOverCDP`. Obscura stays the default and sends no Steel
|
|
148
|
-
headers. `STEEL_AUTH_HEADER` overrides the value. The CDP URL omits the credential when
|
|
149
|
-
those variables are set. A 302 to the identity provider fails closed and does
|
|
150
|
-
not follow the login redirect. `STEEL_API_KEY` still sends the legacy
|
|
151
|
-
`x-steel-api-key` header and `apiKey` query parameter for the temporary proxy
|
|
152
|
-
shim, and warns once. See
|
|
153
|
-
[Browser automation](docs/guides/browser-automation.md).
|
|
154
144
|
- **Dispatch reads role and model files, and agents bind a role.**
|
|
155
145
|
`scripts/roster-policy.mjs` builds the developer policy from
|
|
156
146
|
`.kxm/models/*.yaml` and `.kxm/roles/*.yaml` at `refs/remotes/origin/main`.
|
|
@@ -162,6 +152,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
162
152
|
admitted and named by no roster, so it is absent from `routes` and the
|
|
163
153
|
lineups. `gemini-agy` is in the writer lineup. An agent `tools.preset`
|
|
164
154
|
may only narrow its role preset; that rule is recorded and enforced in P3.
|
|
155
|
+
|
|
165
156
|
- **Role and model files are live `kxm.role.v2` and `kxm.model.v2`.**
|
|
166
157
|
`schemas/role.schema.json` and `schemas/model.schema.json` are the files
|
|
167
158
|
`kxm config` validates. Each admitted roster route is a
|
|
@@ -219,6 +210,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
219
210
|
the example project and the unsupported-gate fixture now route gate failures on
|
|
220
211
|
`implementation-failure`. **Check your workflows:** a gate step that routes failures only
|
|
221
212
|
on `failed` no longer loads.
|
|
213
|
+
|
|
222
214
|
- **`kxm run` prints how to drive the run it created, and drive refusals say why.** The
|
|
223
215
|
text output's second line is
|
|
224
216
|
`drive it model-free: kxm runs drive <runId> --simulated --wait (or cancel: kxm runs cancel <runId>)`,
|
|
@@ -230,11 +222,13 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
230
222
|
a workflow that declares `limits.maxAgentTimeMs`, for example, reports `limit_unsupported`
|
|
231
223
|
on that field. Top-level help names the product KXM instead of KontextMind,
|
|
232
224
|
and `kxm init` text output lists each validation issue as `file: code: message`.
|
|
225
|
+
|
|
233
226
|
- **`kxm suggest` recommends only KXM command skills.** Suggested skills come from the
|
|
234
227
|
command skills shipped in `plugins/kxm/skills` (such as `kxm-workflow`, `kxm-runs`,
|
|
235
228
|
`kxm-peer` and `kxm-context-memory`), never from skills that do not ship
|
|
236
229
|
(`troubleshooting`, `modern-web-guidance`) or from the KontextMind knowledge-plane
|
|
237
230
|
skills.
|
|
231
|
+
|
|
238
232
|
- **The Claude plugin's MCP errors name the user's next step, and a session appears to
|
|
239
233
|
peers before its first tool call.** An unreachable hub names the URL and `kxm hub start`
|
|
240
234
|
or `/plugin configure kxm@kxm`; `invalid_auth` names the project token; a
|
|
@@ -246,6 +240,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
246
240
|
server registers right after the MCP handshake instead of at the first tool call, and it
|
|
247
241
|
leaves the hub when stdin closes. The server instructions point Claude at `kxm_context`
|
|
248
242
|
and at telling the user the next step, in under 800 characters.
|
|
243
|
+
|
|
249
244
|
- **The Claude plugin README is rewritten, and its tool table is pinned to the MCP
|
|
250
245
|
server.** It covers requirements (`node` on `PATH`, a hub, and the `kxm` CLI for the
|
|
251
246
|
operator only), installing from Claude Code or the shell, each `userConfig` option and
|
|
@@ -255,6 +250,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
255
250
|
be entered again), and troubleshooting for each user-directed error. A test fails when
|
|
256
251
|
the README's `## MCP tools` rows and the server's `tools/list` disagree in either
|
|
257
252
|
direction. The configuration docs now say that `kxm_await` waits at most 60 seconds.
|
|
253
|
+
|
|
258
254
|
- **The skill suite is rescoped: every command has one owning skill, and `kxm-setup` is
|
|
259
255
|
renamed `kxm-mind-setup` with no alias.** `skill-suite.json` declares all 29 bundled
|
|
260
256
|
skills (13 KXM command skills, 7 browser skills, 9 KontextMind knowledge-plane skills),
|
|
@@ -269,6 +265,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
269
265
|
stopping where the user reviews and commits `.kxm` changes, and `kxm session brief`
|
|
270
266
|
(which saves a 24-hour operator token) appears only under its operator steps. **Rename:**
|
|
271
267
|
anything that names the `kxm-setup` skill must name `kxm-mind-setup`.
|
|
268
|
+
|
|
272
269
|
- **Context packets rank by deterministic task relevance.** `kxm context get`,
|
|
273
270
|
`kxm_context` and Runtime dispatch order eligible items by nine keys: open
|
|
274
271
|
contradictions first, project before `_shared` defaults, items that share a word with
|
|
@@ -280,6 +277,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
280
277
|
stops smaller ones from fitting, and non-current state and proposed skills no longer
|
|
281
278
|
consume budget. `audit.relevance` reports numbers only (`taskTokens`,
|
|
282
279
|
`matchedCandidates`, and a rounded score per selected item).
|
|
280
|
+
|
|
283
281
|
- **`kxm_improvement_report` returns ranked, redacted cross-run signals.** Alongside the
|
|
284
282
|
per-area reports, `GET /v1/improvements` returns `signals`: journal entries from the
|
|
285
283
|
project's runs merged by evidence class, then an error's stage, then a normalized
|
|
@@ -289,14 +287,17 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
289
287
|
is labelled `unknown`, never 0; security signals rank first. The journal and
|
|
290
288
|
retrospective loop covers hub webhook runs only; `kxm run` (Runtime) runs have no
|
|
291
289
|
journal yet.
|
|
290
|
+
|
|
292
291
|
- **Recall ranks exact phrases, then token relevance, then id, and returns a relevance
|
|
293
292
|
per item.** `kxm context recall` and `kxm_recall` previously returned substring matches
|
|
294
293
|
in id order. Items that neither contain the query nor share a word with it are still
|
|
295
294
|
left out, and results still carry metadata only, never summaries.
|
|
295
|
+
|
|
296
296
|
- **The hub logs task and query sizes, not their text.** `context_packet_assembled` now
|
|
297
297
|
records `taskChars`, `taskTokens` and `matchedCandidates`, and `context_recall` records
|
|
298
298
|
`queryChars` and `queryTokens`. The caller still receives its own request in the
|
|
299
299
|
response.
|
|
300
|
+
|
|
300
301
|
- **Engine routing records carry an ask identity and only gate-negative outcomes.** Every
|
|
301
302
|
`routing.attempt.recorded` record carries four engine-reserved `providerMetadata` keys,
|
|
302
303
|
written after the producer's so a producer cannot spoof them: `workflowId`, `askSha256`
|
|
@@ -306,6 +307,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
306
307
|
only as `blocked` (a back edge) or `failed` (a producer error, an undeclared outcome or a
|
|
307
308
|
failing terminal); acceptance is resolved later from the event log. Records written
|
|
308
309
|
before this change are not backfilled.
|
|
310
|
+
|
|
309
311
|
- **`improvement.promotionPolicy` reports review readiness and never authorizes.**
|
|
310
312
|
`kxm improve` now reads `improvement.*` and reports, per candidate, `readyForReview` and a
|
|
311
313
|
reason under the configured policy: `manual_pr` is always ready for an operator PR,
|
|
@@ -317,12 +319,14 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
317
319
|
out-of-range thresholds fall back to 10, 0.95 and 0.5.
|
|
318
320
|
`improvement.telemetryHalfLifeDays` orders report rows through `weightedRecurrence` and
|
|
319
321
|
never decides candidacy.
|
|
322
|
+
|
|
320
323
|
- **`kxm routing report` reads Runtime records by default and counts only event-log
|
|
321
324
|
acceptance as a Runtime pass.** Without `--file` it reads the current project's Runtime
|
|
322
325
|
event store and then `.kxm/logs/telemetry.jsonl` (the same sources as `kxm improve`),
|
|
323
326
|
and `--json` output gains `sources`. A Runtime attempt counts as a pass only when its run
|
|
324
327
|
completed and the step was not re-entered. The ranking code is unchanged, and the rework
|
|
325
328
|
column still reads `transitions`, which Runtime records do not set.
|
|
329
|
+
|
|
326
330
|
- **`kxm improve --target` is removed.** It was accepted and never applied. Passing it
|
|
327
331
|
is now an unknown-option error. `KXM_IMPROVE_TARGET` still labels telemetry when it is
|
|
328
332
|
written; no report reads that label.
|
|
@@ -423,6 +427,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
423
427
|
in both the JSON payload and the prose line, so `--json` consumers are not left with a bare
|
|
424
428
|
code and no way forward; a stored-but-unusable binding otherwise reads later like a
|
|
425
429
|
network fault and gets debugged as one.
|
|
430
|
+
|
|
426
431
|
- **`kxm hub view` and the session brief label the binding `loopback` or `remote`.**
|
|
427
432
|
"Attached across a network" and "attached on this box" looked identical before, and only
|
|
428
433
|
one of them puts a bearer on a wire. `localhost`, `127.0.0.1`, `::1` and `*.localhost` are
|
|
@@ -438,10 +443,23 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
438
443
|
folder names dropped the `kxm-` prefix. `docs/vnext/` is `docs/contracts/`,
|
|
439
444
|
`examples/vnext/` is `examples/project/`. Dated evidence under `plans/` and
|
|
440
445
|
`.kxm/logs/` keeps its original wording.
|
|
446
|
+
|
|
441
447
|
- **Read-only run projection:** `GET /v1/runs/:id` folds the event log without
|
|
442
448
|
persisting a projection write, so a read cannot mutate run state or surface a
|
|
443
449
|
false `run_projection_divergent`.
|
|
444
450
|
|
|
451
|
+
- **Steel clients authenticate to Authentik with `Authorization: Basic`.**
|
|
452
|
+
`STEEL_AUTH_BASIC`, or `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`, set that
|
|
453
|
+
header on Steel HTTP requests and on the CDP options from `formatCDPConnect()`.
|
|
454
|
+
`KXM_BROWSER=steel` passes those headers through `connectBrowserOverCdp()`
|
|
455
|
+
into `chromium.connectOverCDP`. Obscura stays the default and sends no Steel
|
|
456
|
+
headers. `STEEL_AUTH_HEADER` overrides the value. The CDP URL omits the credential when
|
|
457
|
+
those variables are set. A 302 to the identity provider fails closed and does
|
|
458
|
+
not follow the login redirect. `STEEL_API_KEY` still sends the legacy
|
|
459
|
+
`x-steel-api-key` header and `apiKey` query parameter for the temporary proxy
|
|
460
|
+
shim, and warns once. See
|
|
461
|
+
[Browser automation](docs/guides/browser-automation.md).
|
|
462
|
+
|
|
445
463
|
### Removed
|
|
446
464
|
|
|
447
465
|
- **The developer roster file and the transport just recipes.**
|
|
@@ -452,6 +470,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
452
470
|
Transfer policy from `.kxm/roster.yaml` to the role and model files and
|
|
453
471
|
delete the retired roster before running KXM. KXM refuses a leftover
|
|
454
472
|
`.kxm/roster.yaml`.
|
|
473
|
+
|
|
455
474
|
- **`.kxm/template-provenance.yaml` was removed from this project, a repository
|
|
456
475
|
change rather than a product change,** because the installed kxm no longer
|
|
457
476
|
recognizes its recorded revision and a project without the file validates as
|
|
@@ -459,8 +478,10 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
459
478
|
|
|
460
479
|
### Fixed
|
|
461
480
|
|
|
462
|
-
-
|
|
481
|
+
- **A committed checkout counts as authored work, and a one-shot outcome must be a standalone JSON object.** The authoring witness includes `HEAD` with porcelain status and both diffs, so a write that commits its edits is `changed` and can stay `passed`. A `rev-parse` failure keeps that empty head term only when the repository has no commits; any other git failure is `unwitnessed`. A one-shot outcome is accepted when the whole reply is one JSON object, or when that object stands alone on the final line. A closing code fence around the final object is allowed. An object followed by prose, a truncated reply, and an ambiguous tail settle `failed`.
|
|
482
|
+
|
|
463
483
|
- **The test suite no longer passes `--test-timeout`.** Under `node --test` that flag bounds each file, so coverage on CI timed out `test/core/engine.test.ts` at three minutes. The wall clock in `scripts/run-bounded.mjs` still bounds each script.
|
|
484
|
+
|
|
464
485
|
- **A git worktree lane registers under its project.** The Runtime registry
|
|
465
486
|
keeps the home row and adds the lane as its own control root and event
|
|
466
487
|
store, with the same project id and home runtime (`lane_of` on the lane
|
|
@@ -475,11 +496,13 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
475
496
|
unsettled run in that lane's event store unless `--force` is set. Unregister
|
|
476
497
|
answers 409 `runtime_project_busy` or `runtime_project_has_lanes` unless the
|
|
477
498
|
body sets `force`.
|
|
499
|
+
|
|
478
500
|
- **`npm test` exits when the suite finishes.** The script passes
|
|
479
501
|
`--test-force-exit`, so a green run does not sit in the event loop and a
|
|
480
502
|
red run still prints its failures. Two full runs of the suite without the
|
|
481
503
|
flag also exited once the files finished; the handle that kept an earlier
|
|
482
504
|
run alive was not reproduced in this tree.
|
|
505
|
+
|
|
483
506
|
- **`kxm land` names the pull request from the first commit subject and matches the Release run by time.**
|
|
484
507
|
`--title` sets the title; otherwise the subject of the first commit on the
|
|
485
508
|
branch is used, and a missing subject refuses `land_pr_title_missing`. The
|
|
@@ -496,14 +519,17 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
496
519
|
writes nothing. A `--file` in the shape written through 0.7.92 is refused, and
|
|
497
520
|
`--overwrite` repairs a file left in that shape. See
|
|
498
521
|
`docs/reference/cli-reference.md#kxm-workflow-add`.
|
|
522
|
+
|
|
499
523
|
- **`kxm workflow add --pick <global-id>` copies the global definition.** In local scope,
|
|
500
524
|
picking a global definition wrote the one-step scaffold under its id and reported
|
|
501
525
|
success. It now writes the global definition's content, with `--description` replacing
|
|
502
526
|
its description, and the loader check refuses one the project cannot load.
|
|
527
|
+
|
|
503
528
|
- **`kxm role add --pick <global-id>` copies the global role.** In local scope, picking a
|
|
504
529
|
global role wrote an empty `Role <id>` with no skills or roster under its ID. It now
|
|
505
530
|
writes the global role's content, with `--description`, `--skills` and `--model`
|
|
506
531
|
replacing those fields the way they do for a built-in template.
|
|
532
|
+
|
|
507
533
|
- **`kxm role add` no longer leaves a project that refuses to load.** A local add now needs
|
|
508
534
|
a KXM project (`project_not_found` otherwise, and no stray `.kxm/` that would make
|
|
509
535
|
`kxm init` refuse), writes under the project root from any subdirectory, and is checked
|
|
@@ -511,6 +537,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
511
537
|
`writer` role whose roster leaves out the `implementer` agent's model, including the
|
|
512
538
|
built-in `writer` template for such a project, is refused with `role_invalid`, exit 2,
|
|
513
539
|
and nothing is written, also under `--dry-run`.
|
|
540
|
+
|
|
514
541
|
- **Live `kxm runs drive` can author on an audited writer profile.** A write-repository
|
|
515
542
|
step on pi (`-a`, with extensions, skills, and the session off) or grok
|
|
516
543
|
(`--always-approve`, with subagents and web search off) runs against the checkout.
|
|
@@ -565,18 +592,23 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
565
592
|
run (`webhook_payload_replayed`). A reused delivery ID with a different body is
|
|
566
593
|
refused with 409 `webhook_delivery_conflict`, and a duplicate start returns only
|
|
567
594
|
`duplicate`, `runId` and `status`. Update any custom sender to the new contract.
|
|
595
|
+
|
|
568
596
|
- **Agents never borrow the hub admin token.** With `KXM_AUTH_TOKEN` unset, the Pi
|
|
569
597
|
extension and the `kxm peer` / `kxm workflow` agent commands used the persisted admin
|
|
570
598
|
token. They now use only this project's saved project token, as the Claude MCP server
|
|
571
599
|
does, and otherwise stop with a message naming the fix (`project_token_missing`, exit 2,
|
|
572
600
|
on the CLI).
|
|
601
|
+
|
|
573
602
|
- **The hop limit bounds agent forwarding chains.** `kxm_send` and `kxm_fanout` from Pi
|
|
574
603
|
or the Claude MCP server send one hop past the inbound request being handled, so a
|
|
575
604
|
chain of agents forwarding to each other stops at `hop_limit_reached`.
|
|
605
|
+
|
|
576
606
|
- **Workflow prompts no longer point agents at `.kxm/config`**, a path KXM refuses.
|
|
607
|
+
|
|
577
608
|
- **`kxm gate signal`, `kxm workflow wait` and `kxm role resume` inside a KXM project reach
|
|
578
609
|
the hub for hub runs.** They go to the local Runtime only for a run its store holds, and
|
|
579
610
|
the lookup leaves no files behind, so `--dry-run` changes nothing.
|
|
611
|
+
|
|
580
612
|
- **`kxm peer inbox` lists the requests waiting for a named CLI agent.** It returned
|
|
581
613
|
`{"messages":[]}` every time. The hub now serves `GET /v1/agents/:id/inbox`
|
|
582
614
|
(agent-authenticated, project-scoped): the caller's queued and delivered requests,
|
|
@@ -584,6 +616,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
584
616
|
`codex`) to list requests peers queued for it while it was offline, then answer them
|
|
585
617
|
with `kxm peer reply`. The Pi extension's `kxm_inbox` tool now refuses instead of
|
|
586
618
|
returning an empty list, because Pi activates each inbound request as a turn itself.
|
|
619
|
+
|
|
587
620
|
- **A restarted Claude Code session keeps the requests it acknowledged but never answered.**
|
|
588
621
|
The MCP server acknowledges a request on arrival, and the hub pushes only unacknowledged
|
|
589
622
|
requests again on reconnect, so a session that restarted under the same agent name (the
|
|
@@ -606,11 +639,13 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
606
639
|
user-directed fixes) followed by the unchanged memory brief. The plugin version stays
|
|
607
640
|
0.7.1, so an existing install gets the hook only after the reinstall described in the
|
|
608
641
|
plugin README.
|
|
642
|
+
|
|
609
643
|
- **The Claude plugin's MCP server never authenticates with the hub admin token.** With a
|
|
610
644
|
blank `auth_token` it fell back to the admin token saved in `hub-env.json` and registered
|
|
611
645
|
the agent in a project nobody had issued it a token for. It now uses `KXM_AUTH_TOKEN` or
|
|
612
646
|
this project's saved project token, and with neither it refuses before contacting the
|
|
613
647
|
hub. The operator CLI and the Runtime supervisor resolve credentials as before.
|
|
648
|
+
|
|
614
649
|
- **`kxm workflow add` writes workflows that load.** The one-step scaffold and the three
|
|
615
650
|
built-in templates used `role:` where an agent step needs `agent:`, the scaffold added a
|
|
616
651
|
top-level `id`, and the templates' gate steps named a `verify-gate` no project defines
|
|
@@ -635,6 +670,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
635
670
|
Workflow-step candidates now propose a `kind: gate` step and a `gates.yaml` entry with a
|
|
636
671
|
placeholder command, and skill candidates are labelled consolidation. Candidates remain
|
|
637
672
|
proposals; nothing is applied.
|
|
673
|
+
|
|
638
674
|
- **Runtime-dispatched agents receive committed, pinned project memory and hash-verified
|
|
639
675
|
promoted skills.** The engine built each agent's context packet with no project items, so
|
|
640
676
|
`.kxm/memory` and promoted skills never reached a `kxm run` agent. Now, when either
|
|
@@ -644,6 +680,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
644
680
|
memory revision. Otherwise the context is withheld with a `dispatch_context_*` gap in the
|
|
645
681
|
packet (never in the prompt) and the step still runs. Promoted skills render under a new
|
|
646
682
|
`### Active Skills` heading. No hub call is made at dispatch.
|
|
683
|
+
|
|
647
684
|
- **Journal entries accept all ten categories and stage provenance.** The shared
|
|
648
685
|
`kxm_workflow_record` tool (MCP, Pi and `kxm workflow record`) offered 5 of the 10
|
|
649
686
|
categories, required an area and dropped `stageId`. It now takes every category and an
|
|
@@ -654,11 +691,13 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
654
691
|
approval and premature settlement) carry the stage and attempt; the checkpoint and
|
|
655
692
|
premature-settlement cases are the ones under test. `kxm workflow record` gained
|
|
656
693
|
`--stage-id` and accepts `record <runId> <category> <summary>` when area is omitted.
|
|
694
|
+
|
|
657
695
|
- **Late journal entries and promotions refresh the exported retrospective.** A terminal
|
|
658
696
|
run's retrospective is re-exported when an entry is recorded or a promotion decided
|
|
659
697
|
afterwards. Retrospectives also count only error entries as recurring error classes and
|
|
660
698
|
propose up to 12 ranked error and lesson signals. A promotion now publishes its update to
|
|
661
699
|
the run's project rather than to the run id.
|
|
700
|
+
|
|
662
701
|
- **Context packets deliver the evidence they select.** Evidence items could be selected
|
|
663
702
|
and budgeted but no packet section carried them; packets now have an `evidence` section,
|
|
664
703
|
and the repro and implementer roles receive evidence, so the error, observation and
|
|
@@ -695,6 +734,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
695
734
|
- **`kxm hub start` no longer generates and persists an admin token when it is
|
|
696
735
|
about to refuse** because another hub already owns the claim. A refused start
|
|
697
736
|
used to leave behind credentials the running hub never issued.
|
|
737
|
+
|
|
698
738
|
- **Runtime intake contract fixes (released in 0.7.46, found in review):**
|
|
699
739
|
a lost intake insert race accepted different content under an already-used
|
|
700
740
|
idempotency key; resume drained only one page, so held intent beyond 500
|
|
@@ -707,6 +747,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
707
747
|
arrival order, so a backdated timestamp cannot jump the queue; persisted records
|
|
708
748
|
are cross-checked against every duplicated column on read; and the intake schema
|
|
709
749
|
no longer admits contradictory states.
|
|
750
|
+
|
|
710
751
|
- **A coordinator rebind could widen a tool ceiling by clearing its allow list.**
|
|
711
752
|
An absent or empty allow list imposes no restriction, so dropping a populated one
|
|
712
753
|
is now refused (`coordinator_rebind_clears_allowlist`). Resume drains held intent
|
|
@@ -716,6 +757,7 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
716
757
|
require a policy rebind after upgrade — and the equivalence test is shared, so a
|
|
717
758
|
row written by 0.7.46 is not a `coordinator_write_lost` conflict just because it
|
|
718
759
|
was found by losing an insert race instead of reading the slot.
|
|
760
|
+
|
|
719
761
|
- **A failed `BEGIN` poisoned the database connection.** The transaction marker was
|
|
720
762
|
claimed before `BEGIN` and the statement sat outside the `try/finally`, so a
|
|
721
763
|
`BEGIN` that gave up on a busy writer left every later transaction failing with a
|
|
@@ -750,6 +792,8 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
750
792
|
object in this process — it is not cross-process, and it does not leak to another
|
|
751
793
|
connection to the same database.
|
|
752
794
|
|
|
795
|
+
- **`kxm vision` is owned by the browser-verify skill.** `plugins/kxm/skill-suite.json` lists `vision` on `kxm-browser-verify`, the skill that already teaches `kxm vision assert`.
|
|
796
|
+
|
|
753
797
|
## 0.7.0 - 2026-09-11
|
|
754
798
|
|
|
755
799
|
### Added
|
|
@@ -105,16 +105,13 @@ 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
|
|
109
|
-
one JSON object, or
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
108
|
+
A producer reply becomes a terminal outcome **only** through a declared result: the whole
|
|
109
|
+
reply is one JSON object, or the object stands alone on the final line. A closing code fence
|
|
110
|
+
around that final object is allowed. If the tail is ambiguous, or prose follows the object,
|
|
111
|
+
the outcome is `failed`.
|
|
112
|
+
|
|
113
|
+
Naming an outcome *word* anywhere in a reply is not a result. `"the gate did not pass, so I
|
|
114
|
+
would not call this passed"` must not advance a step, and an empty or unstructured reply is
|
|
118
115
|
never treated as success. A declared outcome outside the step's declared set is not accepted
|
|
119
116
|
either:
|
|
120
117
|
the assignment is recorded `outcome_unknown` and terminates as `failed`, which is also what
|
|
@@ -1689,7 +1689,7 @@ kxm runs status --dry-run refused: the Runtime supervisor is not running and --d
|
|
|
1689
1689
|
kxm runs drive <runId> [--simulated] [--wait] [--timeout-ms <n>] [--lane <unit>]
|
|
1690
1690
|
```
|
|
1691
1691
|
|
|
1692
|
-
Opens a drive of the run. With `--simulated`, a model-free producer reports every agent step as passed. Without `--simulated` the drive runs in live mode: each agent step invokes its harness through a one-shot producer, and the agent's model must be an admitted route (otherwise `producer_route_not_admitted`; there is no fallback model). A read-only step runs with the harness's read-only flags. A step with `write` access runs with an audited writer profile, which only `pi` and `grok` have; it must be a single assignment in a project whose `limits.maxConcurrentRuns` is 1, and its route must be on the writer roster in `.kxm/roles/writer.yaml`. Otherwise the drive hands the run off with `step_unsupported`. Around each live attempt the Runtime fingerprints the checkout with `git
|
|
1692
|
+
Opens a drive of the run. With `--simulated`, a model-free producer reports every agent step as passed. Without `--simulated` the drive runs in live mode: each agent step invokes its harness through a one-shot producer, and the agent's model must be an admitted route (otherwise `producer_route_not_admitted`; there is no fallback model). A read-only step runs with the harness's read-only flags. A step with `write` access runs with an audited writer profile, which only `pi` and `grok` have; it must be a single assignment in a project whose `limits.maxConcurrentRuns` is 1, and its route must be on the writer roster in `.kxm/roles/writer.yaml`. Otherwise the drive hands the run off with `step_unsupported`. Around each live attempt the Runtime fingerprints the checkout with `git rev-parse HEAD`, `git status`, and `git diff`: a write step settles `passed` only when the checkout changed (routing metadata `authored: true`), and a read-only step that changed it settles `failed` (`authoringWitness: readonly_mutated`).
|
|
1693
1693
|
|
|
1694
1694
|
| Option | Argument | Default | Description |
|
|
1695
1695
|
|---|---|---|---|
|
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.143",
|
|
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",
|
|
@@ -17313,7 +17313,7 @@ function sessionTokenFixHint(policy) {
|
|
|
17313
17313
|
}
|
|
17314
17314
|
|
|
17315
17315
|
// plugins/kxm/src/mcp-server.ts
|
|
17316
|
-
var VERSION = "0.7.
|
|
17316
|
+
var VERSION = "0.7.143";
|
|
17317
17317
|
var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
17318
17318
|
var inbox = /* @__PURE__ */ new Map();
|
|
17319
17319
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
@@ -24508,14 +24508,23 @@ function gitText(cwd, args) {
|
|
|
24508
24508
|
if (result.error || result.status !== 0) return void 0;
|
|
24509
24509
|
return result.stdout ?? "";
|
|
24510
24510
|
}
|
|
24511
|
+
function repositoryHasNoCommits(cwd) {
|
|
24512
|
+
const listed = gitText(cwd, ["rev-list", "--max-count=1", "--all"]);
|
|
24513
|
+
return listed !== void 0 && listed.trim() === "";
|
|
24514
|
+
}
|
|
24511
24515
|
function captureWorktreeWitness(cwd) {
|
|
24516
|
+
const headRun = gitText(cwd, ["rev-parse", "HEAD"]);
|
|
24517
|
+
if (headRun === void 0 && !repositoryHasNoCommits(cwd)) {
|
|
24518
|
+
return { unwitnessed: true, fingerprint: "" };
|
|
24519
|
+
}
|
|
24520
|
+
const head = headRun ?? "";
|
|
24512
24521
|
const porcelain = gitText(cwd, ["status", "--porcelain=v1", "-uall"]);
|
|
24513
24522
|
const diff = gitText(cwd, ["diff", "--no-ext-diff"]);
|
|
24514
24523
|
const staged = gitText(cwd, ["diff", "--cached", "--no-ext-diff"]);
|
|
24515
24524
|
if (porcelain === void 0 || diff === void 0 || staged === void 0) {
|
|
24516
24525
|
return { unwitnessed: true, fingerprint: "" };
|
|
24517
24526
|
}
|
|
24518
|
-
return { unwitnessed: false, fingerprint: `${porcelain}\0${diff}\0${staged}` };
|
|
24527
|
+
return { unwitnessed: false, fingerprint: `${head}\0${porcelain}\0${diff}\0${staged}` };
|
|
24519
24528
|
}
|
|
24520
24529
|
function worktreeChanged(before, after) {
|
|
24521
24530
|
if (before.unwitnessed || after.unwitnessed) return false;
|
|
@@ -29890,16 +29899,84 @@ function recoverKxmRun(context, runId, request) {
|
|
|
29890
29899
|
}
|
|
29891
29900
|
|
|
29892
29901
|
// plugins/kxm/src/oneshot-producer.ts
|
|
29893
|
-
function
|
|
29894
|
-
|
|
29895
|
-
|
|
29896
|
-
|
|
29897
|
-
|
|
29898
|
-
|
|
29902
|
+
function lastBalancedJsonObject(text) {
|
|
29903
|
+
let depth = 0;
|
|
29904
|
+
let inString = false;
|
|
29905
|
+
let escape2 = false;
|
|
29906
|
+
let start = -1;
|
|
29907
|
+
let lastStart = -1;
|
|
29908
|
+
let lastEnd = -1;
|
|
29909
|
+
for (let i = 0; i < text.length; i++) {
|
|
29910
|
+
const ch = text[i];
|
|
29911
|
+
if (inString) {
|
|
29912
|
+
if (escape2) {
|
|
29913
|
+
escape2 = false;
|
|
29914
|
+
continue;
|
|
29915
|
+
}
|
|
29916
|
+
if (ch === "\\") {
|
|
29917
|
+
escape2 = true;
|
|
29918
|
+
continue;
|
|
29919
|
+
}
|
|
29920
|
+
if (ch === '"') inString = false;
|
|
29921
|
+
continue;
|
|
29899
29922
|
}
|
|
29923
|
+
if (ch === '"') {
|
|
29924
|
+
inString = true;
|
|
29925
|
+
continue;
|
|
29926
|
+
}
|
|
29927
|
+
if (ch === "{") {
|
|
29928
|
+
if (depth === 0) start = i;
|
|
29929
|
+
depth++;
|
|
29930
|
+
continue;
|
|
29931
|
+
}
|
|
29932
|
+
if (ch === "}") {
|
|
29933
|
+
if (depth === 0) continue;
|
|
29934
|
+
depth--;
|
|
29935
|
+
if (depth === 0) {
|
|
29936
|
+
lastStart = start;
|
|
29937
|
+
lastEnd = i;
|
|
29938
|
+
}
|
|
29939
|
+
}
|
|
29940
|
+
}
|
|
29941
|
+
if (lastStart < 0) return void 0;
|
|
29942
|
+
return text.slice(lastStart, lastEnd + 1);
|
|
29943
|
+
}
|
|
29944
|
+
function standaloneObjectText(text) {
|
|
29945
|
+
const trimmed = text.trim();
|
|
29946
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return void 0;
|
|
29947
|
+
const slice = lastBalancedJsonObject(trimmed);
|
|
29948
|
+
return slice === trimmed ? slice : void 0;
|
|
29949
|
+
}
|
|
29950
|
+
function outcomeOfObjectText(text) {
|
|
29951
|
+
const slice = standaloneObjectText(text);
|
|
29952
|
+
if (slice === void 0) return void 0;
|
|
29953
|
+
try {
|
|
29954
|
+
const result = JSON.parse(slice);
|
|
29955
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return void 0;
|
|
29956
|
+
const outcome = result.outcome;
|
|
29957
|
+
return typeof outcome === "string" ? outcome : void 0;
|
|
29900
29958
|
} catch {
|
|
29959
|
+
return void 0;
|
|
29901
29960
|
}
|
|
29902
|
-
|
|
29961
|
+
}
|
|
29962
|
+
function isClosingFence(line) {
|
|
29963
|
+
return /^(`{3,}|~{3,})\s*$/.test(line.trim());
|
|
29964
|
+
}
|
|
29965
|
+
function determineOutcome(text, allowedOutcomes) {
|
|
29966
|
+
const trimmed = text.trim();
|
|
29967
|
+
if (!trimmed) return "failed";
|
|
29968
|
+
const whole = outcomeOfObjectText(trimmed);
|
|
29969
|
+
if (whole !== void 0) return allowedOutcomes.includes(whole) ? whole : "failed";
|
|
29970
|
+
const lines = trimmed.split(/\r?\n/);
|
|
29971
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
29972
|
+
if (lines.length > 0 && isClosingFence(lines[lines.length - 1])) {
|
|
29973
|
+
lines.pop();
|
|
29974
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
29975
|
+
}
|
|
29976
|
+
if (lines.length === 0) return "failed";
|
|
29977
|
+
const declared = outcomeOfObjectText(lines[lines.length - 1]);
|
|
29978
|
+
if (declared === void 0 || !allowedOutcomes.includes(declared)) return "failed";
|
|
29979
|
+
return declared;
|
|
29903
29980
|
}
|
|
29904
29981
|
function createKxmOneShotProducer(options = {}) {
|
|
29905
29982
|
const running = /* @__PURE__ */ new Map();
|
|
@@ -25235,14 +25235,23 @@ function gitText(cwd, args) {
|
|
|
25235
25235
|
if (result.error || result.status !== 0) return void 0;
|
|
25236
25236
|
return result.stdout ?? "";
|
|
25237
25237
|
}
|
|
25238
|
+
function repositoryHasNoCommits(cwd) {
|
|
25239
|
+
const listed = gitText(cwd, ["rev-list", "--max-count=1", "--all"]);
|
|
25240
|
+
return listed !== void 0 && listed.trim() === "";
|
|
25241
|
+
}
|
|
25238
25242
|
function captureWorktreeWitness(cwd) {
|
|
25243
|
+
const headRun = gitText(cwd, ["rev-parse", "HEAD"]);
|
|
25244
|
+
if (headRun === void 0 && !repositoryHasNoCommits(cwd)) {
|
|
25245
|
+
return { unwitnessed: true, fingerprint: "" };
|
|
25246
|
+
}
|
|
25247
|
+
const head = headRun ?? "";
|
|
25239
25248
|
const porcelain = gitText(cwd, ["status", "--porcelain=v1", "-uall"]);
|
|
25240
25249
|
const diff = gitText(cwd, ["diff", "--no-ext-diff"]);
|
|
25241
25250
|
const staged = gitText(cwd, ["diff", "--cached", "--no-ext-diff"]);
|
|
25242
25251
|
if (porcelain === void 0 || diff === void 0 || staged === void 0) {
|
|
25243
25252
|
return { unwitnessed: true, fingerprint: "" };
|
|
25244
25253
|
}
|
|
25245
|
-
return { unwitnessed: false, fingerprint: `${porcelain}\0${diff}\0${staged}` };
|
|
25254
|
+
return { unwitnessed: false, fingerprint: `${head}\0${porcelain}\0${diff}\0${staged}` };
|
|
25246
25255
|
}
|
|
25247
25256
|
function worktreeChanged(before, after) {
|
|
25248
25257
|
if (before.unwitnessed || after.unwitnessed) return false;
|
|
@@ -30638,16 +30647,84 @@ function recoverKxmRun(context, runId, request) {
|
|
|
30638
30647
|
}
|
|
30639
30648
|
|
|
30640
30649
|
// plugins/kxm/src/oneshot-producer.ts
|
|
30641
|
-
function
|
|
30642
|
-
|
|
30643
|
-
|
|
30644
|
-
|
|
30645
|
-
|
|
30646
|
-
|
|
30650
|
+
function lastBalancedJsonObject(text) {
|
|
30651
|
+
let depth = 0;
|
|
30652
|
+
let inString = false;
|
|
30653
|
+
let escape2 = false;
|
|
30654
|
+
let start = -1;
|
|
30655
|
+
let lastStart = -1;
|
|
30656
|
+
let lastEnd = -1;
|
|
30657
|
+
for (let i = 0; i < text.length; i++) {
|
|
30658
|
+
const ch = text[i];
|
|
30659
|
+
if (inString) {
|
|
30660
|
+
if (escape2) {
|
|
30661
|
+
escape2 = false;
|
|
30662
|
+
continue;
|
|
30663
|
+
}
|
|
30664
|
+
if (ch === "\\") {
|
|
30665
|
+
escape2 = true;
|
|
30666
|
+
continue;
|
|
30667
|
+
}
|
|
30668
|
+
if (ch === '"') inString = false;
|
|
30669
|
+
continue;
|
|
30670
|
+
}
|
|
30671
|
+
if (ch === '"') {
|
|
30672
|
+
inString = true;
|
|
30673
|
+
continue;
|
|
30674
|
+
}
|
|
30675
|
+
if (ch === "{") {
|
|
30676
|
+
if (depth === 0) start = i;
|
|
30677
|
+
depth++;
|
|
30678
|
+
continue;
|
|
30647
30679
|
}
|
|
30680
|
+
if (ch === "}") {
|
|
30681
|
+
if (depth === 0) continue;
|
|
30682
|
+
depth--;
|
|
30683
|
+
if (depth === 0) {
|
|
30684
|
+
lastStart = start;
|
|
30685
|
+
lastEnd = i;
|
|
30686
|
+
}
|
|
30687
|
+
}
|
|
30688
|
+
}
|
|
30689
|
+
if (lastStart < 0) return void 0;
|
|
30690
|
+
return text.slice(lastStart, lastEnd + 1);
|
|
30691
|
+
}
|
|
30692
|
+
function standaloneObjectText(text) {
|
|
30693
|
+
const trimmed = text.trim();
|
|
30694
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return void 0;
|
|
30695
|
+
const slice = lastBalancedJsonObject(trimmed);
|
|
30696
|
+
return slice === trimmed ? slice : void 0;
|
|
30697
|
+
}
|
|
30698
|
+
function outcomeOfObjectText(text) {
|
|
30699
|
+
const slice = standaloneObjectText(text);
|
|
30700
|
+
if (slice === void 0) return void 0;
|
|
30701
|
+
try {
|
|
30702
|
+
const result = JSON.parse(slice);
|
|
30703
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return void 0;
|
|
30704
|
+
const outcome = result.outcome;
|
|
30705
|
+
return typeof outcome === "string" ? outcome : void 0;
|
|
30648
30706
|
} catch {
|
|
30707
|
+
return void 0;
|
|
30649
30708
|
}
|
|
30650
|
-
|
|
30709
|
+
}
|
|
30710
|
+
function isClosingFence(line) {
|
|
30711
|
+
return /^(`{3,}|~{3,})\s*$/.test(line.trim());
|
|
30712
|
+
}
|
|
30713
|
+
function determineOutcome(text, allowedOutcomes) {
|
|
30714
|
+
const trimmed = text.trim();
|
|
30715
|
+
if (!trimmed) return "failed";
|
|
30716
|
+
const whole = outcomeOfObjectText(trimmed);
|
|
30717
|
+
if (whole !== void 0) return allowedOutcomes.includes(whole) ? whole : "failed";
|
|
30718
|
+
const lines = trimmed.split(/\r?\n/);
|
|
30719
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
30720
|
+
if (lines.length > 0 && isClosingFence(lines[lines.length - 1])) {
|
|
30721
|
+
lines.pop();
|
|
30722
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
30723
|
+
}
|
|
30724
|
+
if (lines.length === 0) return "failed";
|
|
30725
|
+
const declared = outcomeOfObjectText(lines[lines.length - 1]);
|
|
30726
|
+
if (declared === void 0 || !allowedOutcomes.includes(declared)) return "failed";
|
|
30727
|
+
return declared;
|
|
30651
30728
|
}
|
|
30652
30729
|
function createKxmOneShotProducer(options = {}) {
|
|
30653
30730
|
const running = /* @__PURE__ */ new Map();
|
package/plugins/kxm/package.json
CHANGED
|
@@ -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.143";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -51,15 +51,99 @@ export interface KxmOneShotProducer extends KxmProducer {
|
|
|
51
51
|
close(): Promise<void>;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
// The last complete top-level `{...}`. One forward pass tracks strings,
|
|
55
|
+
// escapes, and brace depth. A `{` that lifts depth from 0 to 1 opens a
|
|
56
|
+
// candidate; the matching `}` that returns depth from 1 to 0 closes it.
|
|
57
|
+
// The last such span wins. Callers accept the span only when it is the
|
|
58
|
+
// entire trimmed reply or the entire final line; an inner object is not
|
|
59
|
+
// an outcome.
|
|
60
|
+
function lastBalancedJsonObject(text: string): string | undefined {
|
|
61
|
+
let depth = 0;
|
|
62
|
+
let inString = false;
|
|
63
|
+
let escape = false;
|
|
64
|
+
let start = -1;
|
|
65
|
+
let lastStart = -1;
|
|
66
|
+
let lastEnd = -1;
|
|
67
|
+
for (let i = 0; i < text.length; i++) {
|
|
68
|
+
const ch = text[i];
|
|
69
|
+
if (inString) {
|
|
70
|
+
if (escape) {
|
|
71
|
+
escape = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === "\\") {
|
|
75
|
+
escape = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === '"') inString = false;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ch === '"') {
|
|
82
|
+
inString = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (ch === "{") {
|
|
86
|
+
if (depth === 0) start = i;
|
|
87
|
+
depth++;
|
|
88
|
+
continue;
|
|
60
89
|
}
|
|
61
|
-
|
|
62
|
-
|
|
90
|
+
if (ch === "}") {
|
|
91
|
+
if (depth === 0) continue;
|
|
92
|
+
depth--;
|
|
93
|
+
if (depth === 0) {
|
|
94
|
+
lastStart = start;
|
|
95
|
+
lastEnd = i;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (lastStart < 0) return undefined;
|
|
100
|
+
return text.slice(lastStart, lastEnd + 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A standalone object is one balanced `{...}` covering the entire trimmed text.
|
|
104
|
+
function standaloneObjectText(text: string): string | undefined {
|
|
105
|
+
const trimmed = text.trim();
|
|
106
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return undefined;
|
|
107
|
+
const slice = lastBalancedJsonObject(trimmed);
|
|
108
|
+
return slice === trimmed ? slice : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function outcomeOfObjectText(text: string): string | undefined {
|
|
112
|
+
const slice = standaloneObjectText(text);
|
|
113
|
+
if (slice === undefined) return undefined;
|
|
114
|
+
try {
|
|
115
|
+
const result: unknown = JSON.parse(slice);
|
|
116
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return undefined;
|
|
117
|
+
const outcome = (result as Record<string, unknown>).outcome;
|
|
118
|
+
return typeof outcome === "string" ? outcome : undefined;
|
|
119
|
+
} catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isClosingFence(line: string): boolean {
|
|
125
|
+
return /^(`{3,}|~{3,})\s*$/.test(line.trim());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The whole reply is one JSON object, or the object stands alone on the final
|
|
129
|
+
// line. A closing code fence after that line is allowed. Prose after the
|
|
130
|
+
// object, an ambiguous tail, a truncated reply, and a missing or disallowed
|
|
131
|
+
// outcome settle failed.
|
|
132
|
+
function determineOutcome(text: string, allowedOutcomes: readonly string[]): string {
|
|
133
|
+
const trimmed = text.trim();
|
|
134
|
+
if (!trimmed) return "failed";
|
|
135
|
+
const whole = outcomeOfObjectText(trimmed);
|
|
136
|
+
if (whole !== undefined) return allowedOutcomes.includes(whole) ? whole : "failed";
|
|
137
|
+
const lines = trimmed.split(/\r?\n/);
|
|
138
|
+
while (lines.length > 0 && lines[lines.length - 1]!.trim() === "") lines.pop();
|
|
139
|
+
if (lines.length > 0 && isClosingFence(lines[lines.length - 1]!)) {
|
|
140
|
+
lines.pop();
|
|
141
|
+
while (lines.length > 0 && lines[lines.length - 1]!.trim() === "") lines.pop();
|
|
142
|
+
}
|
|
143
|
+
if (lines.length === 0) return "failed";
|
|
144
|
+
const declared = outcomeOfObjectText(lines[lines.length - 1]!);
|
|
145
|
+
if (declared === undefined || !allowedOutcomes.includes(declared)) return "failed";
|
|
146
|
+
return declared;
|
|
63
147
|
}
|
|
64
148
|
|
|
65
149
|
export function createKxmOneShotProducer(options: KxmOneShotProducerOptions = {}): KxmOneShotProducer {
|
|
@@ -3,8 +3,10 @@ import type { KxmProducerResult } from "./engine.ts";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Fingerprint of a checkout taken around a live producer spawn.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* The fingerprint covers HEAD plus the working tree. Porcelain alone misses a
|
|
7
|
+
* content edit that keeps the same status line, and a commit leaves porcelain
|
|
8
|
+
* and both diffs empty, so the witness is `head`, porcelain, unstaged diff,
|
|
9
|
+
* and staged diff.
|
|
8
10
|
*/
|
|
9
11
|
export interface WorktreeWitness {
|
|
10
12
|
readonly unwitnessed: boolean;
|
|
@@ -21,14 +23,30 @@ function gitText(cwd: string, args: readonly string[]): string | undefined {
|
|
|
21
23
|
return result.stdout ?? "";
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
function repositoryHasNoCommits(cwd: string): boolean {
|
|
27
|
+
// `rev-parse HEAD` prints the same failure for an unborn branch and for a
|
|
28
|
+
// HEAD that names a missing ref. An empty `rev-list --all` is the repository
|
|
29
|
+
// with no commits. Any other failure is not that case.
|
|
30
|
+
const listed = gitText(cwd, ["rev-list", "--max-count=1", "--all"]);
|
|
31
|
+
return listed !== undefined && listed.trim() === "";
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
export function captureWorktreeWitness(cwd: string): WorktreeWitness {
|
|
35
|
+
// No commits yet: keep an empty head term so porcelain and the diffs still
|
|
36
|
+
// witness the tree. A rev-parse failure in a repository that has commits, or
|
|
37
|
+
// any failure to ask git whether commits exist, is unwitnessed.
|
|
38
|
+
const headRun = gitText(cwd, ["rev-parse", "HEAD"]);
|
|
39
|
+
if (headRun === undefined && !repositoryHasNoCommits(cwd)) {
|
|
40
|
+
return { unwitnessed: true, fingerprint: "" };
|
|
41
|
+
}
|
|
42
|
+
const head = headRun ?? "";
|
|
25
43
|
const porcelain = gitText(cwd, ["status", "--porcelain=v1", "-uall"]);
|
|
26
44
|
const diff = gitText(cwd, ["diff", "--no-ext-diff"]);
|
|
27
45
|
const staged = gitText(cwd, ["diff", "--cached", "--no-ext-diff"]);
|
|
28
46
|
if (porcelain === undefined || diff === undefined || staged === undefined) {
|
|
29
47
|
return { unwitnessed: true, fingerprint: "" };
|
|
30
48
|
}
|
|
31
|
-
return { unwitnessed: false, fingerprint: `${porcelain}\0${diff}\0${staged}` };
|
|
49
|
+
return { unwitnessed: false, fingerprint: `${head}\0${porcelain}\0${diff}\0${staged}` };
|
|
32
50
|
}
|
|
33
51
|
|
|
34
52
|
export function worktreeChanged(before: WorktreeWitness, after: WorktreeWitness): boolean {
|