@kontextmind/kxm 0.7.96 → 0.7.98

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.
Files changed (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/workflows/default.yaml +2 -0
  3. package/CHANGELOG.md +61 -2
  4. package/docs/concepts/architecture.md +1 -1
  5. package/docs/concepts/data-and-storage.md +1 -1
  6. package/docs/concepts/trust-model.md +3 -2
  7. package/docs/contracts/routing.md +1 -1
  8. package/docs/contributing/test-matrix.md +6 -5
  9. package/docs/glossary.md +1 -1
  10. package/docs/guides/peer-messaging.md +2 -2
  11. package/docs/guides/pi-workers.md +1 -1
  12. package/docs/guides/webhook-workflows.md +53 -18
  13. package/docs/operations/backup-and-restore.md +43 -24
  14. package/docs/operations/deploy.md +2 -2
  15. package/docs/operations/troubleshooting.md +3 -2
  16. package/docs/reference/cli-reference.md +65 -30
  17. package/docs/reference/config-reference.md +24 -13
  18. package/docs/reference/configuration.md +2 -2
  19. package/docs/reference/harness-routing.md +3 -3
  20. package/docs/reference/http-api.md +7 -7
  21. package/docs/reference/tools.md +1 -1
  22. package/docs/reference/workflow-definitions.md +2 -2
  23. package/docs/start/first-workflow.md +4 -4
  24. package/docs/start/quickstart-claude-code.md +2 -2
  25. package/docs/start/quickstart-pi.md +1 -1
  26. package/examples/workflow-signal.ts +4 -5
  27. package/package.json +1 -1
  28. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  29. package/plugins/kxm/dist/claude-hook.js +11 -1
  30. package/plugins/kxm/dist/cli.js +552 -228
  31. package/plugins/kxm/dist/client.js +3 -1
  32. package/plugins/kxm/dist/core.js +16 -3
  33. package/plugins/kxm/dist/extension.js +45 -13
  34. package/plugins/kxm/dist/mcp-server.js +20 -4
  35. package/plugins/kxm/dist/runtime-supervisor.js +232 -51
  36. package/plugins/kxm/dist/runtime.js +432 -95
  37. package/plugins/kxm/dist/server.js +122 -20
  38. package/plugins/kxm/package.json +1 -1
  39. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +4 -3
  40. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +3 -2
  41. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +3 -0
  42. package/plugins/kxm/skills/kxm-runs/SKILL.md +8 -7
  43. package/plugins/kxm/src/cli/project.ts +22 -13
  44. package/plugins/kxm/src/cli/system.ts +22 -1
  45. package/plugins/kxm/src/cli/workflows.ts +12 -7
  46. package/plugins/kxm/src/cli.ts +30 -8
  47. package/plugins/kxm/src/client.ts +4 -0
  48. package/plugins/kxm/src/commands.ts +23 -1
  49. package/plugins/kxm/src/database.ts +210 -36
  50. package/plugins/kxm/src/engine.ts +117 -2
  51. package/plugins/kxm/src/extension.ts +20 -14
  52. package/plugins/kxm/src/github-watch.ts +8 -5
  53. package/plugins/kxm/src/harness.ts +29 -0
  54. package/plugins/kxm/src/hub-env.ts +19 -1
  55. package/plugins/kxm/src/hub.ts +105 -21
  56. package/plugins/kxm/src/improve-sources.ts +2 -7
  57. package/plugins/kxm/src/init-guide-setup.ts +43 -28
  58. package/plugins/kxm/src/mcp-server.ts +9 -2
  59. package/plugins/kxm/src/oneshot-producer.ts +16 -8
  60. package/plugins/kxm/src/prices.ts +33 -2
  61. package/plugins/kxm/src/routing.ts +13 -7
  62. package/plugins/kxm/src/runtime-store.ts +23 -0
  63. package/plugins/kxm/src/studio-layout.ts +5 -4
  64. package/plugins/kxm/src/template.ts +31 -0
  65. package/plugins/kxm/src/workflow.ts +70 -1
  66. package/plugins/kxm/src/worktree-witness.ts +71 -0
  67. package/schemas/backup-manifest.schema.json +33 -0
  68. package/scripts/smoke-multi-pi.mjs +5 -1
@@ -14779,7 +14779,7 @@ var require_dist = __commonJS({
14779
14779
  });
14780
14780
 
14781
14781
  // plugins/kxm/src/hub.ts
14782
- import { createHash as createHash5, createHmac } from "node:crypto";
14782
+ import { createHash as createHash5, createHmac as createHmac2 } from "node:crypto";
14783
14783
  import { existsSync as existsSync6 } from "node:fs";
14784
14784
  import { createServer } from "node:http";
14785
14785
  import { isIP } from "node:net";
@@ -15271,6 +15271,10 @@ var READ_ONLY_ONESHOT_ARGS = Object.freeze({
15271
15271
  function oneShotReadOnlyArgs(harness) {
15272
15272
  return Object.hasOwn(READ_ONLY_ONESHOT_ARGS, harness) ? READ_ONLY_ONESHOT_ARGS[harness] : void 0;
15273
15273
  }
15274
+ var WRITER_ONESHOT_ARGS = Object.freeze({
15275
+ pi: Object.freeze(["-a", "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-session"]),
15276
+ grok: Object.freeze(["--always-approve", "--no-subagents", "--disable-web-search"])
15277
+ });
15274
15278
  var BUILTIN_HARNESSES = Object.freeze([
15275
15279
  {
15276
15280
  id: "pi",
@@ -15650,7 +15654,7 @@ function workflowScopeExtras(operation, assignedCoordinatorName) {
15650
15654
  import { createHash as createHash3, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
15651
15655
 
15652
15656
  // plugins/kxm/src/workflow.ts
15653
- import { createHash as createHash2 } from "node:crypto";
15657
+ import { createHash as createHash2, createHmac } from "node:crypto";
15654
15658
 
15655
15659
  // plugins/kxm/src/relevance.ts
15656
15660
  var RELEVANCE_STOPWORDS = Object.freeze(/* @__PURE__ */ new Set([
@@ -16572,6 +16576,30 @@ function canonicalWorkflowDefinitionJson(definition) {
16572
16576
  function workflowDefinitionHash(definition) {
16573
16577
  return createHash2("sha256").update(canonicalWorkflowDefinitionJson(definition), "utf8").digest("hex");
16574
16578
  }
16579
+ var WORKFLOW_WEBHOOK_SIGNATURE_VERSION = "kxm-webhook-v1";
16580
+ var WORKFLOW_WEBHOOK_MAX_SKEW_SECONDS = 300;
16581
+ function workflowWebhookSignedMaterial(scope, timestamp, deliveryId, body) {
16582
+ const fields = [
16583
+ WORKFLOW_WEBHOOK_SIGNATURE_VERSION,
16584
+ scope.runId === void 0 ? "start" : "signal",
16585
+ timestamp,
16586
+ deliveryId,
16587
+ scope.definitionId,
16588
+ scope.runId ?? "",
16589
+ scope.signalKey ?? ""
16590
+ ];
16591
+ if (fields.some((field) => /[\r\n]/.test(field))) {
16592
+ throw new ProtocolError(400, "webhook signature fields must not contain line breaks", "webhook_signature_field_invalid");
16593
+ }
16594
+ return Buffer.concat([
16595
+ Buffer.from(`${fields.join("\n")}
16596
+ `, "utf8"),
16597
+ typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
16598
+ ]);
16599
+ }
16600
+ function workflowWebhookSignature(secret, scope, timestamp, deliveryId, body) {
16601
+ return `sha256=${createHmac("sha256", secret).update(workflowWebhookSignedMaterial(scope, timestamp, deliveryId, body)).digest("hex")}`;
16602
+ }
16575
16603
  function resolveOutcomeRule(stage, outcomeKey) {
16576
16604
  const raw = stage.on?.[outcomeKey];
16577
16605
  return raw === void 0 ? void 0 : normalizeOutcomeValue(raw, `stage ${stage.id} on.${outcomeKey}`);
@@ -16895,6 +16923,13 @@ var HubHttpError = class extends Error {
16895
16923
  };
16896
16924
 
16897
16925
  // plugins/kxm/src/commands.ts
16926
+ function forwardedHops(handling) {
16927
+ if (!handling?.length) return void 0;
16928
+ return {
16929
+ hops: Math.max(...handling.map((message) => message.hops)) + 1,
16930
+ maxHops: Math.min(...handling.map((message) => message.maxHops))
16931
+ };
16932
+ }
16898
16933
  function requiredString(value, name) {
16899
16934
  if (typeof value !== "string" || value.trim() === "") throw new Error(`${name} is required`);
16900
16935
  return value.trim();
@@ -17024,12 +17059,13 @@ var AGENT_COMMANDS = [
17024
17059
  required: ["target", "content"],
17025
17060
  additionalProperties: false
17026
17061
  },
17027
- async execute(client, args) {
17062
+ async execute(client, args, context) {
17028
17063
  const delivery = optionalString2(args.delivery);
17029
17064
  const correlationId = optionalString2(args.correlationId);
17030
17065
  const idempotencyKey = optionalString2(args.idempotencyKey);
17031
17066
  const workflowContext = optionalWorkflowContext(args.workflowContext);
17032
17067
  const message = await client.send({
17068
+ ...forwardedHops(context?.handling),
17033
17069
  target: requiredString(args.target, "target"),
17034
17070
  content: requiredString(args.content, "content"),
17035
17071
  ...delivery ? { delivery } : {},
@@ -17107,6 +17143,7 @@ var AGENT_COMMANDS = [
17107
17143
  const targets = Array.isArray(args.targets) ? args.targets.map((t) => requiredString(t, "target")) : [];
17108
17144
  return {
17109
17145
  responses: await client.fanout({
17146
+ ...forwardedHops(context?.handling),
17110
17147
  targets,
17111
17148
  content: requiredString(args.content, "content"),
17112
17149
  ...optionalString2(args.correlationId) ? { correlationId: optionalString2(args.correlationId) } : {},
@@ -19385,6 +19422,9 @@ var DatabaseSync = class {
19385
19422
  }
19386
19423
  };
19387
19424
 
19425
+ // plugins/kxm/src/bindings.ts
19426
+ var MAX_BINDING_RECORD_BYTES = 256 * 1024;
19427
+
19388
19428
  // plugins/kxm/src/database.ts
19389
19429
  function databaseError(code, file, message) {
19390
19430
  const issue2 = { phase: "semantic", code, file, message };
@@ -21089,20 +21129,59 @@ data: ${JSON.stringify({ type: "ops", project, topic, at: nowIso() })}
21089
21129
  const candidate = payload.webhookEvent ?? payload.event;
21090
21130
  return typeof candidate === "string" && candidate.trim() ? candidate.trim() : void 0;
21091
21131
  }
21092
- function verifyWebhookSignature(request, body, secret) {
21093
- const signature = request.headers["x-hub-signature-256"] ?? request.headers["x-hub-signature"];
21094
- if (typeof signature !== "string") {
21095
- throw new ProtocolError(401, "webhook signature is required", "webhook_signature_missing");
21096
- }
21132
+ function requireSha256Signature(signature) {
21097
21133
  const separator = signature.indexOf("=");
21098
21134
  const algorithm = separator > 0 ? signature.slice(0, separator).toLowerCase() : "";
21099
21135
  if (algorithm !== "sha256") {
21100
21136
  throw new ProtocolError(401, "webhook signature must use sha256", "webhook_signature_unsupported");
21101
21137
  }
21102
- const expected = `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
21138
+ }
21139
+ function verifyProviderWebhookSignature(request, body, definition) {
21140
+ if (definition.source === "generic") {
21141
+ throw new ProtocolError(
21142
+ 401,
21143
+ "generic workflow webhooks require x-kxm-signature over the timestamp, delivery ID, definition, and body",
21144
+ "webhook_signature_missing"
21145
+ );
21146
+ }
21147
+ const signature = request.headers["x-hub-signature-256"] ?? request.headers["x-hub-signature"];
21148
+ if (typeof signature !== "string") {
21149
+ throw new ProtocolError(401, "webhook signature is required", "webhook_signature_missing");
21150
+ }
21151
+ requireSha256Signature(signature);
21152
+ const expected = `sha256=${createHmac2("sha256", definition.secret).update(body).digest("hex")}`;
21103
21153
  if (!safeTokenEqual(signature, expected)) {
21104
21154
  throw new ProtocolError(401, "webhook signature is invalid", "webhook_signature_invalid");
21105
21155
  }
21156
+ const deliveryHeader = definition.source === "jira" ? request.headers["x-atlassian-webhook-identifier"] : request.headers["x-github-delivery"];
21157
+ return requireString(deliveryHeader, "webhook delivery identifier", { max: 128 });
21158
+ }
21159
+ function verifyKxmWebhookSignature(request, body, secret, scope) {
21160
+ const signature = request.headers["x-kxm-signature"];
21161
+ if (typeof signature !== "string") {
21162
+ throw new ProtocolError(
21163
+ 401,
21164
+ "KXM webhooks require x-kxm-signature over the timestamp, delivery ID, route, and body",
21165
+ "webhook_signature_missing"
21166
+ );
21167
+ }
21168
+ requireSha256Signature(signature);
21169
+ const timestamp = request.headers["x-kxm-timestamp"];
21170
+ if (typeof timestamp !== "string" || !/^[0-9]{1,12}$/.test(timestamp)) {
21171
+ throw new ProtocolError(401, "x-kxm-timestamp must be Unix seconds", "webhook_timestamp_invalid");
21172
+ }
21173
+ const deliveryId = requireString(request.headers["x-kxm-delivery-id"], "webhook delivery identifier", { max: 128 });
21174
+ if (!safeTokenEqual(signature, workflowWebhookSignature(secret, scope, timestamp, deliveryId, body))) {
21175
+ throw new ProtocolError(401, "webhook signature is invalid", "webhook_signature_invalid");
21176
+ }
21177
+ if (Math.abs(Date.now() / 1e3 - Number(timestamp)) > WORKFLOW_WEBHOOK_MAX_SKEW_SECONDS) {
21178
+ throw new ProtocolError(
21179
+ 401,
21180
+ `webhook timestamp is outside the ${WORKFLOW_WEBHOOK_MAX_SKEW_SECONDS}-second window; re-sign at send time`,
21181
+ "webhook_timestamp_expired"
21182
+ );
21183
+ }
21184
+ return deliveryId;
21106
21185
  }
21107
21186
  function workflowPrompt(definition, runId, payload) {
21108
21187
  const rendered = renderWorkflowPrompt(definition.promptTemplate, payload);
@@ -21125,7 +21204,7 @@ data: ${JSON.stringify({ type: "ops", project, topic, at: nowIso() })}
21125
21204
  stageList,
21126
21205
  "",
21127
21206
  `At every stage, record material knowledge with kxm_workflow_record in one of these categories: ${JOURNAL_CATEGORIES.join(", ")}. Pass the stageId the entry belongs to; the hub binds the attempt and, when you omit area, uses the stage's declared area.`,
21128
- "Keep repository-local configuration in .kxm/config, logs in .kxm/logs, and durable workflow artifacts in .kxm/assets; never commit runtime logs, state, or secrets.",
21207
+ "Keep reviewed configuration as YAML directly under .kxm/ (such as .kxm/project.yaml and .kxm/workflows/), logs in .kxm/logs, and durable workflow artifacts in .kxm/assets. Never create .kxm/config, which KXM refuses, and never commit runtime logs, .kxm/state, or secrets.",
21129
21208
  "Complete each stage with kxm_workflow_checkpoint. Supply evidence as an object whose keys exactly match the stage's required evidence keys. Unrelated keys never satisfy a requirement. A warning or failure must be corrected and checkpointed again until it passes or the attempt limit is reached.",
21130
21209
  "For a peer-evidence requirement, send or fan out with workflowContext containing this run ID, the exact stage ID, requirement key, and current 1-based attempt. At checkpoint, cite only the returned message IDs under evidenceRefs; the hub derives producer and reply provenance.",
21131
21210
  "When an external system must finish asynchronously, call kxm_workflow_wait with a stable signal key and any already-verified keyed evidence. That evidence is accumulated with the signed callback before the stage can pass; then settle the turn.",
@@ -21463,14 +21542,17 @@ data: ${JSON.stringify({ type: "ops", project, topic, at: nowIso() })}
21463
21542
  const definition = webhookWorkflows2.get(definitionId);
21464
21543
  if (!definition) throw new ProtocolError(404, "webhook workflow not found", "webhook_not_found");
21465
21544
  const rawBody = await readBody(request);
21466
- verifyWebhookSignature(request, rawBody, definition.signalSecret ?? definition.secret);
21545
+ const deliveryId = verifyKxmWebhookSignature(
21546
+ request,
21547
+ rawBody,
21548
+ definition.signalSecret ?? definition.secret,
21549
+ { definitionId: definition.id, runId, signalKey }
21550
+ );
21467
21551
  const body = parseJsonBody(rawBody);
21468
21552
  const run = workflowRuns.get(runId);
21469
21553
  if (!run || run.definitionId !== definition.id) {
21470
21554
  throw new ProtocolError(404, "workflow run not found", "workflow_not_found");
21471
21555
  }
21472
- const deliveryHeader = request.headers["x-atlassian-webhook-identifier"] ?? request.headers["x-github-delivery"] ?? request.headers["x-kxm-delivery-id"];
21473
- const deliveryId = requireString(deliveryHeader, "webhook delivery identifier", { max: 128 });
21474
21556
  const payloadHash = createHash5("sha256").update(rawBody).digest("hex");
21475
21557
  const existingReceipt = run.signalReceipts?.find((receipt2) => receipt2.deliveryId === deliveryId);
21476
21558
  if (existingReceipt) {
@@ -21616,7 +21698,8 @@ data: ${JSON.stringify({ type: "ops", project, topic, at: nowIso() })}
21616
21698
  const definition = webhookWorkflows2.get(definitionId);
21617
21699
  if (!definition) throw new ProtocolError(404, "webhook workflow not found", "webhook_not_found");
21618
21700
  const rawBody = await readBody(request);
21619
- verifyWebhookSignature(request, rawBody, definition.secret);
21701
+ const bodyOnlySignature = request.headers["x-kxm-signature"] === void 0;
21702
+ const deliveryId = bodyOnlySignature ? verifyProviderWebhookSignature(request, rawBody, definition) : verifyKxmWebhookSignature(request, rawBody, definition.secret, { definitionId: definition.id });
21620
21703
  const payload = parseJsonBody(rawBody);
21621
21704
  const event = webhookEvent(request, payload);
21622
21705
  if (definition.event && event !== definition.event) {
@@ -21627,15 +21710,30 @@ data: ${JSON.stringify({ type: "ops", project, topic, at: nowIso() })}
21627
21710
  response.writeHead(204, { "cache-control": "no-store" }).end();
21628
21711
  return;
21629
21712
  }
21630
- const deliveryHeader = request.headers["x-atlassian-webhook-identifier"] ?? request.headers["x-github-delivery"] ?? request.headers["x-kxm-delivery-id"];
21631
- const deliveryId = requireString(deliveryHeader, "webhook delivery identifier", { max: 128 });
21713
+ const payloadHash = createHash5("sha256").update(rawBody).digest("hex");
21632
21714
  const existing = [...workflowRuns.values()].find(
21633
21715
  (run2) => run2.definitionId === definition.id && run2.deliveryId === deliveryId
21634
21716
  );
21635
21717
  if (existing) {
21636
- json(response, 200, { run: existing, duplicate: true });
21718
+ if (existing.payloadHash !== payloadHash) {
21719
+ throw new ProtocolError(
21720
+ 409,
21721
+ "webhook delivery identifier was already used for a different body",
21722
+ "webhook_delivery_conflict"
21723
+ );
21724
+ }
21725
+ json(response, 200, { duplicate: true, runId: existing.id, status: existing.status });
21637
21726
  return;
21638
21727
  }
21728
+ if (bodyOnlySignature && [...workflowRuns.values()].some(
21729
+ (run2) => run2.definitionId === definition.id && run2.payloadHash === payloadHash
21730
+ )) {
21731
+ throw new ProtocolError(
21732
+ 409,
21733
+ "this signed body already started a run under another delivery identifier",
21734
+ "webhook_payload_replayed"
21735
+ );
21736
+ }
21639
21737
  const target = findKnownTarget(definition.project, definition.target);
21640
21738
  const createdAt = nowIso();
21641
21739
  const runId = newId("run");
@@ -21685,7 +21783,7 @@ data: ${JSON.stringify({ type: "ops", project, topic, at: nowIso() })}
21685
21783
  definitionId: definition.id,
21686
21784
  source: definition.source,
21687
21785
  deliveryId,
21688
- payloadHash: createHash5("sha256").update(rawBody).digest("hex"),
21786
+ payloadHash,
21689
21787
  definitionHash: workflowDefinitionHash(definition),
21690
21788
  ...event ? { event } : {},
21691
21789
  project: definition.project,
@@ -21716,7 +21814,7 @@ data: ${JSON.stringify({ type: "ops", project, topic, at: nowIso() })}
21716
21814
  project: definition.project,
21717
21815
  target: target.id
21718
21816
  });
21719
- json(response, 202, { run, duplicate: false });
21817
+ json(response, 202, { run, runId: run.id, duplicate: false });
21720
21818
  return;
21721
21819
  }
21722
21820
  if (method === "GET" && url.pathname === "/metrics") {
@@ -22656,7 +22754,11 @@ data: ${JSON.stringify({ agent: publicAgent(current, staleAfterMs) })}
22656
22754
  const hops = parseBoundedInteger(body.hops, "hops", 0, 0, 100);
22657
22755
  const maxHops = parseBoundedInteger(body.maxHops, "maxHops", DEFAULT_MAX_HOPS, 1, 20);
22658
22756
  if (hops >= maxHops) {
22659
- throw new ProtocolError(400, `hop limit reached (${hops}/${maxHops})`, "hop_limit_reached");
22757
+ throw new ProtocolError(
22758
+ 400,
22759
+ `hop limit reached (${hops}/${maxHops}): this request would extend a chain of forwarded requests past its limit; answer the inbound request directly`,
22760
+ "hop_limit_reached"
22761
+ );
22660
22762
  }
22661
22763
  const ttlMs = parseBoundedInteger(
22662
22764
  body.ttlMs,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.96",
3
+ "version": "0.7.98",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -15,7 +15,7 @@ invent restart or status subcommands.
15
15
  |---|---|---|
16
16
  | `kxm hub view` | Check `/health` and `/ready`; exits 1 when the hub is down | `--json` |
17
17
  | `kxm tenant status` | Hub metadata and Runtime run state as one labeled view; reads only and never starts the supervisor | `--json` |
18
- | `kxm backup` | Verified SQLite backup of the hub stores with a hashed manifest | `--out <dir>`, `--json` |
18
+ | `kxm backup` | Verified SQLite backup of the hub store and the Runtime stores with a hashed manifest; exits 1 when the backup is incomplete | `--out <dir>`, `--json` |
19
19
 
20
20
  ```bash
21
21
  kxm hub view --json
@@ -30,8 +30,9 @@ runs have separate ID spaces, so a comparison with no shared ID is
30
30
  `unverified`, never agreement.
31
31
 
32
32
  The durable hub store defaults to `.kxm/state/kxm.db` (`KXM_DATA_PATH`). Do not
33
- hand-edit it. `kxm backup` does not include the Runtime supervisor's stores
34
- under the user state root.
33
+ hand-edit it. `kxm backup` also copies the Runtime registry and every
34
+ project's run event store and prompt sidecar under the user state root, so a
35
+ `kxm restore` rolls back every project on the machine, not only this one.
35
36
 
36
37
  ## Operator steps
37
38
 
@@ -122,8 +122,9 @@ or store the admin or project token in the conversation. The user enters
122
122
  in a workflow step, then run `kxm init` again. Only when
123
123
  `.kxm/project.yaml` does not exist yet, ask the user to follow the README's
124
124
  move-aside workaround; never do that in a committed project.
125
- - The template `default` workflow declares `limits.maxAgentTimeMs`, so
126
- `kxm runs drive` on it fails with `run_handoff_required`. Use `first`.
125
+ - The template `default` workflow can be driven, but even a simulated drive
126
+ runs its `npm test` gate, and a live drive spends Claude and Grok and lets
127
+ Grok edit the checkout. Use `first` for the first run.
127
128
  - `kxm run` starts the Runtime supervisor. Stop it with `kxm runtime stop`.
128
129
  - Only run workflow IDs that `kxm workflow definitions` lists; any other ID
129
130
  fails with `run_workflow_unknown`.
@@ -73,11 +73,14 @@ Never apply a candidate diff yourself or treat `readyForReview` as approval.
73
73
  |---|---|---|
74
74
  | `kxm routing report` | Compare verified completion, cost, and rework per behavioral configuration | `-f/--file`, `-l/--equivalent-list-cost`, `--list-prices`, `--prices <path>` |
75
75
  | `kxm routing benchmark` | Placeholder side-by-side comparison | `--task`, `--arms`, `--runs` |
76
+ | `kxm prices acknowledge` | Stamp the existing `.kxm/prices.yaml` list as today's estimate without fetching vendor rates | `--json` |
76
77
 
77
78
  `kxm routing report` reads the same sources as `kxm improve`, and
78
79
  `routing report --json` includes the `sources`. `kxm routing benchmark` prints
79
80
  fixed placeholder figures in this build; never cite them as measured cost or
80
81
  quality. Use `kxm routing report` for recorded spend.
82
+ Estimates stay unknown until the catalog carries today's stamp, and a routing
83
+ total is null when any attempt has no cost.
81
84
 
82
85
  `--equivalent-list-cost` loads the price catalog (`.kxm/prices.yaml`, or
83
86
  `--prices <path>`) without a freshness check, so an old catalog quotes old
@@ -7,8 +7,9 @@ description: Create, drive, and inspect local KXM runs. kxm runs drive with --si
7
7
 
8
8
  `kxm run` creates a run of a project workflow in the local Runtime and starts
9
9
  the Runtime supervisor. A created run stays `created` until
10
- `kxm runs drive` executes it. Do not invent get, create, or logs verbs under
11
- `runs`.
10
+ `kxm runs drive` executes the pinned plan. Live drive is the default and spends
11
+ an admitted model; `--simulated` is the model-free producer. Do not invent get,
12
+ create, or logs verbs under `runs`.
12
13
 
13
14
  ## Commands
14
15
 
@@ -58,11 +59,11 @@ the receipt's settlement is terminal `completed`.
58
59
  - `run_workflow_unknown`: the workflow ID is not a project workflow. Run only
59
60
  IDs that `kxm workflow definitions` lists.
60
61
  - `run_handoff_required`: the Runtime does not execute a field the workflow
61
- uses, so drive fails with `runtime request failed with HTTP 409` and the run
62
- stays `preparing`. The template `default` workflow declares
63
- `limits.maxAgentTimeMs`, which is one such field. Cancel the run with
64
- `kxm runs cancel <runId>` and drive a workflow without the field, such as
65
- `first`.
62
+ uses (such as `limits.maxAgentTimeMs`, which a fresh `kxm init` no longer
63
+ writes), so drive fails with `runtime request failed with HTTP 409` and the
64
+ run stays `preparing`. Cancel the run with `kxm runs cancel <runId>` and drive
65
+ a workflow without the field, such as `first`.
66
66
 
67
67
  Listing or status alone is not proof that steps ran; a verified receipt is.
68
+ A read-only step that settles `passed` did not author a checkout change.
68
69
  `kxm workflow list` shows hub webhook runs, not these runs.
@@ -185,36 +185,41 @@ export async function cmdBackup(runtime: Runtime, options: { out?: string | unde
185
185
  try {
186
186
  const backupOptions = {
187
187
  projectRoot: runtime.cwd,
188
+ env: runtime.env,
188
189
  ...(options.out ? { outDir: resolve(runtime.cwd, options.out) } : {}),
189
190
  };
190
191
  if (runtime.dryRun) {
191
192
  const plan = planBackup(backupOptions);
192
193
  printPlan(
193
194
  runtime,
194
- { command: "backup", outDir: plan.outDir, stores: plan.stores },
195
+ { command: "backup", outDir: plan.outDir, stores: plan.stores, files: plan.files },
195
196
  [
196
- ...plan.stores.map((store) => ({ action: "write" as const, target: join(plan.outDir, store.backupFile) })),
197
+ ...[...plan.stores, ...plan.files].map((entry) => ({ action: "write" as const, target: join(plan.outDir, entry.backupFile) })),
197
198
  { action: "write", target: join(plan.outDir, "manifest.json") },
198
199
  ],
199
- `back up ${plan.stores.length} store(s) to ${plan.outDir} (sources are not opened, so their WAL is not checkpointed)`,
200
+ `back up ${plan.stores.length} store(s) and ${plan.files.length} file(s) to ${plan.outDir} (sources are not opened, so their WAL is not checkpointed)`,
200
201
  );
201
202
  return 0;
202
203
  }
203
204
  const { manifest, outDir } = createBackup(backupOptions);
205
+ const complete = manifest.complete === true;
204
206
  const payload = {
205
- ok: true,
207
+ ok: complete,
206
208
  command: "backup",
207
209
  backupId: manifest.backupId,
208
210
  outDir,
209
211
  manifest,
210
212
  };
211
213
  const summary = [
212
- `Created SQLite backup with ${manifest.stores.length} store(s):`,
214
+ complete
215
+ ? `Created SQLite backup with ${manifest.stores.length} store(s):`
216
+ : `Backup is incomplete (${manifest.omitted?.length ?? 0} omitted); not ok:`,
213
217
  ...manifest.stores.map((s) => ` - ${s.storeId}: ${s.sourcePath} -> ${s.backupFile} (schema v${s.schemaVersion}, ${s.bytes} bytes, sha256 ${s.sha256.slice(0, 12)}...)`),
218
+ ...(manifest.omitted ?? []).map((id) => ` - omitted ${id}`),
214
219
  `Manifest: ${join(outDir, "manifest.json")}`,
215
220
  ].join("\n");
216
221
  print(runtime.io, runtime.json, payload, summary);
217
- return 0;
222
+ return complete ? 0 : 1;
218
223
  } catch (error) {
219
224
  if (error instanceof KxmConfigError) {
220
225
  print(runtime.io, runtime.json, { ok: false, command: "backup", error: "backup_failed", issues: error.issues }, `backup failed: ${error.message}`);
@@ -236,14 +241,18 @@ export async function cmdRestore(runtime: Runtime, manifestArg: string): Promise
236
241
  backupId: plan.backupId,
237
242
  manifestPath: plan.manifestPath,
238
243
  stores: plan.stores.map(({ storeId, targetPath, schemaVersion }) => ({ storeId, targetPath, schemaVersion })),
244
+ files: plan.files.map(({ id, targetPath }) => ({ id, targetPath })),
239
245
  },
240
- plan.stores.flatMap((store) => [
241
- { action: "write" as const, target: store.targetPath },
242
- ...[`${store.targetPath}-wal`, `${store.targetPath}-shm`]
243
- .filter((file) => existsSync(file))
244
- .map((file) => ({ action: "delete" as const, target: file })),
245
- ]),
246
- `restore ${plan.stores.length} store(s) from ${plan.manifestPath}; digests verified against the manifest`,
246
+ [
247
+ ...plan.stores.flatMap((store) => [
248
+ { action: "write" as const, target: store.targetPath },
249
+ ...[`${store.targetPath}-wal`, `${store.targetPath}-shm`]
250
+ .filter((file) => existsSync(file))
251
+ .map((file) => ({ action: "delete" as const, target: file })),
252
+ ]),
253
+ ...plan.files.map((file) => ({ action: "write" as const, target: file.targetPath })),
254
+ ],
255
+ `restore ${plan.stores.length} store(s) and ${plan.files.length} file(s) from ${plan.manifestPath}; digests verified against the manifest`,
247
256
  );
248
257
  return 0;
249
258
  }
@@ -20,7 +20,7 @@ import {
20
20
  type RoutingRecord,
21
21
  type RoutingRecordV2,
22
22
  } from "../routing.ts";
23
- import { loadPriceCatalog, type PriceCatalog } from "../prices.ts";
23
+ import { acknowledgePriceCatalog, loadPriceCatalog, type PriceCatalog } from "../prices.ts";
24
24
  import {
25
25
  buildImprovementReport,
26
26
  formatImprovementReport,
@@ -59,6 +59,7 @@ import {
59
59
  import {
60
60
  GUIDE_WORKFLOWS,
61
61
  parseGuideSelection,
62
+ mergeGuideRouteAdmission,
62
63
  planGuideSetup,
63
64
  renderGuideSetupFiles,
64
65
  writeGuideSetupFiles,
@@ -867,8 +868,10 @@ export async function maybeOfferGuideSetup(runtime: Runtime): Promise<void> {
867
868
  const plan = planGuideSetup({ inventory, selected });
868
869
  const files = renderGuideSetupFiles(runtime.cwd, plan);
869
870
  const report = writeGuideSetupFiles(files);
871
+ const admitted = mergeGuideRouteAdmission(runtime.cwd, plan);
870
872
  for (const file of report.written) runtime.io.stdout(`wrote ${file}\n`);
871
873
  for (const file of report.existed) runtime.io.stdout(`kept existing ${file} (not overwritten)\n`);
874
+ for (const selector of admitted) runtime.io.stdout(`admitted route ${selector}\n`);
872
875
  for (const skip of plan.skipped) {
873
876
  runtime.io.stdout(`skipped ${skip.workflow}/${skip.role}: ${skip.reason}\n`);
874
877
  }
@@ -877,6 +880,24 @@ export async function maybeOfferGuideSetup(runtime: Runtime): Promise<void> {
877
880
  }
878
881
  }
879
882
 
883
+ export async function cmdPricesAcknowledge(runtime: Runtime): Promise<number> {
884
+ try {
885
+ const catalog = acknowledgePriceCatalog(runtime.cwd);
886
+ print(runtime.io, runtime.json, {
887
+ ok: true,
888
+ command: "prices acknowledge",
889
+ date: catalog.date,
890
+ sha256: catalog.sha256,
891
+ note: "stamped the existing list as today's estimate; vendor rates were not fetched",
892
+ }, `price catalog stamped ${catalog.date} (list estimate only; vendor rates were not fetched)`);
893
+ return 0;
894
+ } catch (error) {
895
+ const message = error instanceof Error ? error.message : String(error);
896
+ print(runtime.io, runtime.json, { ok: false, command: "prices acknowledge", error: "prices_acknowledge_failed", message }, `prices acknowledge failed: ${message}`);
897
+ return 1;
898
+ }
899
+ }
900
+
880
901
  export async function cmdRoutingReport(
881
902
  runtime: Runtime,
882
903
  options: { file?: string | undefined; equivalentListCost?: boolean | undefined; listPrices?: boolean | undefined; prices?: string | undefined },
@@ -1,4 +1,4 @@
1
- import { createHmac, randomUUID } from "node:crypto";
1
+ import { randomUUID } from "node:crypto";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { basename, join, resolve } from "node:path";
4
4
  import { openReadOnlyDatabase } from "../sqlite.ts";
@@ -16,12 +16,14 @@ import {
16
16
  import {
17
17
  canonicalWorkflowEvidenceKey,
18
18
  parseWorkflowDefinitions,
19
+ workflowWebhookHeaders,
19
20
  type WorkflowEvidenceInput,
20
21
  type WorkflowJournalEntry,
21
22
  type WorkflowRun,
22
23
  } from "../workflow.ts";
23
24
  import { discoverKxmProjectRoot } from "../project-config.ts";
24
25
  import { ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
26
+ import { projectRuntimeOwnsRun } from "../runtime-store.ts";
25
27
  import type { WorkerOutcome } from "../envelope.ts";
26
28
  import {
27
29
  print,
@@ -67,19 +69,22 @@ export async function postWorkflowStart(input: {
67
69
  ? { ...input.payload, event: input.event }
68
70
  : input.payload;
69
71
  const body = JSON.stringify(payload);
70
- const signature = `sha256=${createHmac("sha256", input.secret).update(body).digest("hex")}`;
71
72
  const response = await input.fetchImpl(`${input.serverUrl.replace(/\/$/, "")}/v1/webhooks/${encodeURIComponent(input.definitionId)}`, {
72
73
  method: "POST",
73
74
  headers: {
74
75
  "content-type": "application/json",
75
- "x-hub-signature-256": signature,
76
- "x-kxm-delivery-id": input.deliveryId,
76
+ ...workflowWebhookHeaders({
77
+ secret: input.secret,
78
+ scope: { definitionId: input.definitionId },
79
+ deliveryId: input.deliveryId,
80
+ body,
81
+ }),
77
82
  ...(input.event ? { "x-github-event": input.event } : {}),
78
83
  },
79
84
  body,
80
85
  });
81
86
  const responseText = (await response.text()).slice(0, 8_000);
82
- let parsed: { run?: { id?: string }; duplicate?: boolean } = {};
87
+ let parsed: { runId?: string; duplicate?: boolean } = {};
83
88
  try {
84
89
  parsed = JSON.parse(responseText);
85
90
  } catch {
@@ -88,7 +93,7 @@ export async function postWorkflowStart(input: {
88
93
  if (!response.ok) throw new Error(`workflow_start_http_${response.status}`);
89
94
  return {
90
95
  status: response.status,
91
- ...(parsed.run?.id ? { runId: parsed.run.id } : {}),
96
+ ...(parsed.runId ? { runId: parsed.runId } : {}),
92
97
  duplicate: parsed.duplicate === true,
93
98
  };
94
99
  }
@@ -536,7 +541,7 @@ export async function cmdSignal(runtime: Runtime, runId: string, signalKey: stri
536
541
  }
537
542
  const worker = gateOf(runtime, "signal");
538
543
  const projectRoot = discoverKxmProjectRoot(runtime.cwd);
539
- if (projectRoot && /^run_[a-f0-9]{32}$/i.test(runId)) {
544
+ if (projectRoot && projectRuntimeOwnsRun(projectRoot, runId, runtime.env)) {
540
545
  if (runtime.dryRun) {
541
546
  printWorker(runtime, worker, { ok: true, command: "signal", dryRun: true, runId, signalKey, status, summary, evidence }, "would post signal to KXM run");
542
547
  return 0;