@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.kxm/workflows/default.yaml +2 -0
- package/CHANGELOG.md +61 -2
- package/docs/concepts/architecture.md +1 -1
- package/docs/concepts/data-and-storage.md +1 -1
- package/docs/concepts/trust-model.md +3 -2
- package/docs/contracts/routing.md +1 -1
- package/docs/contributing/test-matrix.md +6 -5
- package/docs/glossary.md +1 -1
- package/docs/guides/peer-messaging.md +2 -2
- package/docs/guides/pi-workers.md +1 -1
- package/docs/guides/webhook-workflows.md +53 -18
- package/docs/operations/backup-and-restore.md +43 -24
- package/docs/operations/deploy.md +2 -2
- package/docs/operations/troubleshooting.md +3 -2
- package/docs/reference/cli-reference.md +65 -30
- package/docs/reference/config-reference.md +24 -13
- package/docs/reference/configuration.md +2 -2
- package/docs/reference/harness-routing.md +3 -3
- package/docs/reference/http-api.md +7 -7
- package/docs/reference/tools.md +1 -1
- package/docs/reference/workflow-definitions.md +2 -2
- package/docs/start/first-workflow.md +4 -4
- package/docs/start/quickstart-claude-code.md +2 -2
- package/docs/start/quickstart-pi.md +1 -1
- package/examples/workflow-signal.ts +4 -5
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/claude-hook.js +11 -1
- package/plugins/kxm/dist/cli.js +552 -228
- package/plugins/kxm/dist/client.js +3 -1
- package/plugins/kxm/dist/core.js +16 -3
- package/plugins/kxm/dist/extension.js +45 -13
- package/plugins/kxm/dist/mcp-server.js +20 -4
- package/plugins/kxm/dist/runtime-supervisor.js +232 -51
- package/plugins/kxm/dist/runtime.js +432 -95
- package/plugins/kxm/dist/server.js +122 -20
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +4 -3
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +3 -2
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +3 -0
- package/plugins/kxm/skills/kxm-runs/SKILL.md +8 -7
- package/plugins/kxm/src/cli/project.ts +22 -13
- package/plugins/kxm/src/cli/system.ts +22 -1
- package/plugins/kxm/src/cli/workflows.ts +12 -7
- package/plugins/kxm/src/cli.ts +30 -8
- package/plugins/kxm/src/client.ts +4 -0
- package/plugins/kxm/src/commands.ts +23 -1
- package/plugins/kxm/src/database.ts +210 -36
- package/plugins/kxm/src/engine.ts +117 -2
- package/plugins/kxm/src/extension.ts +20 -14
- package/plugins/kxm/src/github-watch.ts +8 -5
- package/plugins/kxm/src/harness.ts +29 -0
- package/plugins/kxm/src/hub-env.ts +19 -1
- package/plugins/kxm/src/hub.ts +105 -21
- package/plugins/kxm/src/improve-sources.ts +2 -7
- package/plugins/kxm/src/init-guide-setup.ts +43 -28
- package/plugins/kxm/src/mcp-server.ts +9 -2
- package/plugins/kxm/src/oneshot-producer.ts +16 -8
- package/plugins/kxm/src/prices.ts +33 -2
- package/plugins/kxm/src/routing.ts +13 -7
- package/plugins/kxm/src/runtime-store.ts +23 -0
- package/plugins/kxm/src/studio-layout.ts +5 -4
- package/plugins/kxm/src/template.ts +31 -0
- package/plugins/kxm/src/workflow.ts +70 -1
- package/plugins/kxm/src/worktree-witness.ts +71 -0
- package/schemas/backup-manifest.schema.json +33 -0
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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,
|
package/plugins/kxm/package.json
CHANGED
|
@@ -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`
|
|
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
|
|
126
|
-
`
|
|
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
|
|
11
|
-
`
|
|
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
|
|
62
|
-
|
|
63
|
-
`
|
|
64
|
-
|
|
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((
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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 {
|
|
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
|
-
|
|
76
|
-
|
|
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: {
|
|
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.
|
|
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 &&
|
|
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;
|