@kontextmind/kxm 0.7.91 → 0.7.93
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 +1 -1
- package/CHANGELOG.md +212 -0
- package/README.md +3 -0
- package/docs/README.md +3 -0
- package/docs/agent-skills.md +123 -60
- package/docs/architecture.md +5 -2
- package/docs/cli-reference.md +3527 -0
- package/docs/config-reference.md +1943 -0
- package/docs/configuration.md +30 -4
- package/docs/continuous-improvement.md +122 -10
- package/docs/contracts/routing.md +95 -11
- package/docs/harness-routing.md +616 -0
- package/docs/kxm-handbook.md +106 -19
- package/docs/templates/README.md +1 -1
- package/docs/test-matrix.md +12 -6
- package/docs/troubleshooting.md +2 -2
- package/examples/project/.kxm/workflows/fix.yaml +1 -1
- package/examples/project/.kxm/workflows/improve.yaml +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +9 -10
- package/plugins/kxm/README.md +238 -56
- package/plugins/kxm/dist/claude-hook.js +10083 -0
- package/plugins/kxm/dist/cli.js +2487 -1848
- package/plugins/kxm/dist/client.js +64 -0
- package/plugins/kxm/dist/core.js +102 -9
- package/plugins/kxm/dist/extension.js +210 -68
- package/plugins/kxm/dist/mcp-server.js +217 -40
- package/plugins/kxm/dist/runtime-supervisor.js +1628 -157
- package/plugins/kxm/dist/runtime.js +1874 -298
- package/plugins/kxm/dist/server.js +416 -82
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/hints.json +1 -1
- package/plugins/kxm/skills/kxm/SKILL.md +48 -24
- package/plugins/kxm/skills/kxm/references/protocol.md +3 -3
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +67 -21
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +9 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +82 -16
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +55 -27
- package/plugins/kxm/skills/kxm-insights/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-mind/SKILL.md +2 -2
- package/plugins/kxm/skills/{kxm-setup → kxm-mind-setup}/SKILL.md +4 -4
- package/plugins/kxm/skills/kxm-peer/SKILL.md +68 -93
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +156 -23
- package/plugins/kxm/skills/kxm-projects/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-query/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +74 -15
- package/plugins/kxm/skills/kxm-runs/SKILL.md +46 -17
- package/plugins/kxm/skills/kxm-session/SKILL.md +64 -36
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +44 -15
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +16 -4
- package/plugins/kxm/skills/kxm-triage/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-work/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +60 -19
- package/plugins/kxm/src/arbiter.ts +67 -22
- package/plugins/kxm/src/autocomplete.ts +1 -1
- package/plugins/kxm/src/claude-hook.ts +192 -0
- package/plugins/kxm/src/cli/project.ts +11 -5
- package/plugins/kxm/src/cli/system.ts +85 -13
- package/plugins/kxm/src/cli/types.ts +4 -1
- package/plugins/kxm/src/cli/workflows.ts +18 -16
- package/plugins/kxm/src/cli.ts +23 -13
- package/plugins/kxm/src/client.ts +15 -4
- package/plugins/kxm/src/commands.ts +19 -9
- package/plugins/kxm/src/config.ts +42 -7
- package/plugins/kxm/src/context-packet.ts +14 -2
- package/plugins/kxm/src/context.ts +16 -5
- package/plugins/kxm/src/dispatch-context.ts +286 -0
- package/plugins/kxm/src/engine-plan.ts +40 -0
- package/plugins/kxm/src/engine.ts +138 -6
- package/plugins/kxm/src/hub-env.ts +17 -1
- package/plugins/kxm/src/hub.ts +92 -29
- package/plugins/kxm/src/improve-sources.ts +228 -0
- package/plugins/kxm/src/improve.ts +325 -140
- package/plugins/kxm/src/local-snapshot.ts +101 -42
- package/plugins/kxm/src/mcp-server.ts +129 -30
- package/plugins/kxm/src/memory.ts +43 -20
- package/plugins/kxm/src/project-config.ts +25 -0
- package/plugins/kxm/src/protocol.ts +11 -0
- package/plugins/kxm/src/relevance.ts +138 -0
- package/plugins/kxm/src/retrospective.ts +16 -10
- package/plugins/kxm/src/runtime-service.ts +8 -1
- package/plugins/kxm/src/runtime-supervisor.ts +16 -2
- package/plugins/kxm/src/session-token-hint.ts +17 -0
- package/plugins/kxm/src/suggest.ts +7 -7
- package/plugins/kxm/src/workflow-manager.ts +80 -78
- package/plugins/kxm/src/workflow.ts +202 -12
- package/scripts/build-runtime.mjs +7 -1
- package/scripts/check-generated.mjs +1 -0
- package/scripts/emit-codex-artifacts.mjs +1 -1
|
@@ -25,12 +25,22 @@ import {
|
|
|
25
25
|
KXM_RUN_PLAN_SCHEMA,
|
|
26
26
|
freezeKxmCompiledPlan,
|
|
27
27
|
hashKxmRunPlanEnvelope,
|
|
28
|
+
kxmAttemptFinalOutcome,
|
|
29
|
+
kxmStepAskSha256,
|
|
28
30
|
loadKxmRunPlanEnvelope,
|
|
29
31
|
parseGateDefinition,
|
|
30
32
|
rehydrateKxmCompiledPlanFromStore,
|
|
31
33
|
type KxmPinnedGates,
|
|
32
34
|
type KxmRunPlanEnvelope,
|
|
33
35
|
} from "./engine-plan.ts";
|
|
36
|
+
import {
|
|
37
|
+
EMPTY_DISPATCH_SOURCES,
|
|
38
|
+
NOT_LOADED_DISPATCH_SOURCES,
|
|
39
|
+
assembleDispatchContext,
|
|
40
|
+
dispatchContextPresent,
|
|
41
|
+
loadDispatchContextSources,
|
|
42
|
+
type DispatchContextSources,
|
|
43
|
+
} from "./dispatch-context.ts";
|
|
34
44
|
import { gateRegistryHash } from "./gate-hash.ts";
|
|
35
45
|
import {
|
|
36
46
|
admitKxmRun,
|
|
@@ -100,6 +110,7 @@ import {
|
|
|
100
110
|
type KxmGateObservationInput,
|
|
101
111
|
} from "./engine-gate-records.ts";
|
|
102
112
|
import {
|
|
113
|
+
MAX_PROVIDER_METADATA_FIELDS,
|
|
103
114
|
ROUTING_RECORD_V2_SCHEMA,
|
|
104
115
|
type RoutingRecordV2,
|
|
105
116
|
parseRoutingRecordV2,
|
|
@@ -916,8 +927,38 @@ export interface KxmGateDispatchSeams {
|
|
|
916
927
|
|
|
917
928
|
export const kxmGateDispatchSeams: KxmGateDispatchSeams = {};
|
|
918
929
|
|
|
930
|
+
/**
|
|
931
|
+
* Load the dispatch context for the step this drive is about to enter. Every
|
|
932
|
+
* git check, hash and file read happens here, before the IMMEDIATE transaction.
|
|
933
|
+
* A project with no authored memory and no promoted skill is detected by a
|
|
934
|
+
* directory probe alone and loads nothing. When the peek cannot see an agent
|
|
935
|
+
* step about to be entered, the sources are marked not loaded, so a birth that
|
|
936
|
+
* races the peek records a gap instead of dispatching unchecked context.
|
|
937
|
+
*/
|
|
938
|
+
function peekDispatchContextSources(context: KxmRuntimeContext, runId: string): DispatchContextSources {
|
|
939
|
+
if (!dispatchContextPresent(context.projectRoot)) return EMPTY_DISPATCH_SOURCES;
|
|
940
|
+
let run: KxmRunRecord;
|
|
941
|
+
try {
|
|
942
|
+
run = requireRun(context, runId);
|
|
943
|
+
const state = foldStoredKxmRun(context, run);
|
|
944
|
+
const plan = rehydrateKxmCompiledPlanFromStore(context.eventStore, run);
|
|
945
|
+
if (state.status !== "running" || state.currentStep) return NOT_LOADED_DISPATCH_SOURCES;
|
|
946
|
+
const next = plan.steps[state.pendingStepId ?? plan.entryStepId];
|
|
947
|
+
if (next?.kind !== "agent" && next?.kind !== "moa") return NOT_LOADED_DISPATCH_SOURCES;
|
|
948
|
+
} catch {
|
|
949
|
+
// prepareDispatch reads the same run and raises the authoritative error.
|
|
950
|
+
return NOT_LOADED_DISPATCH_SOURCES;
|
|
951
|
+
}
|
|
952
|
+
return loadDispatchContextSources({
|
|
953
|
+
projectRoot: context.projectRoot,
|
|
954
|
+
projectId: run.projectId,
|
|
955
|
+
pinnedMemoryRevision: run.memoryRevision,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
|
|
919
959
|
async function stepLocked(context: KxmRuntimeContext, runId: string, producer: KxmProducer, token: string): Promise<KxmRunDriveResult> {
|
|
920
|
-
const
|
|
960
|
+
const dispatchSources = peekDispatchContextSources(context, runId);
|
|
961
|
+
const prepared = context.eventStore.transaction(() => prepareDispatch(context, runId, producer.id, dispatchSources));
|
|
921
962
|
if (prepared.kind === "return") {
|
|
922
963
|
return prepared.handoff ? { state: prepared.state, handoff: prepared.handoff } : { state: prepared.state };
|
|
923
964
|
}
|
|
@@ -1263,6 +1304,8 @@ interface PreparedPanel {
|
|
|
1263
1304
|
maxParallel: number;
|
|
1264
1305
|
first: PreparedDispatch;
|
|
1265
1306
|
state: KxmRunState;
|
|
1307
|
+
/** One snapshot shared by every member born for this step attempt. */
|
|
1308
|
+
dispatchSources: DispatchContextSources;
|
|
1266
1309
|
}
|
|
1267
1310
|
|
|
1268
1311
|
export interface KxmPanelMemberHook {
|
|
@@ -1397,7 +1440,8 @@ function resolveProducerRoute(
|
|
|
1397
1440
|
function prepareDispatch(
|
|
1398
1441
|
context: KxmRuntimeContext,
|
|
1399
1442
|
runId: string,
|
|
1400
|
-
producerId
|
|
1443
|
+
producerId: "driver-simulated" | "pi" | string | undefined,
|
|
1444
|
+
dispatchSources: DispatchContextSources,
|
|
1401
1445
|
): { kind: "panel"; panel: PreparedPanel } | { kind: "return"; state: KxmRunState; handoff?: KxmRunHandoff } | ({ kind: "gate" } & KxmPreparedGateDispatch) {
|
|
1402
1446
|
const run = requireRun(context, runId);
|
|
1403
1447
|
const plan = rehydrateKxmCompiledPlanFromStore(context.eventStore, run);
|
|
@@ -1584,6 +1628,7 @@ function prepareDispatch(
|
|
|
1584
1628
|
enterRunning: true,
|
|
1585
1629
|
producerId,
|
|
1586
1630
|
resolvedRoute,
|
|
1631
|
+
dispatchSources,
|
|
1587
1632
|
});
|
|
1588
1633
|
return {
|
|
1589
1634
|
kind: "panel",
|
|
@@ -1597,6 +1642,7 @@ function prepareDispatch(
|
|
|
1597
1642
|
maxParallel: step.assignments.maxParallel,
|
|
1598
1643
|
first,
|
|
1599
1644
|
state: first.state,
|
|
1645
|
+
dispatchSources,
|
|
1600
1646
|
},
|
|
1601
1647
|
};
|
|
1602
1648
|
}
|
|
@@ -1643,6 +1689,7 @@ function birthMember(
|
|
|
1643
1689
|
enterRunning?: boolean | undefined;
|
|
1644
1690
|
producerId?: ("driver-simulated" | "pi" | string) | undefined;
|
|
1645
1691
|
resolvedRoute?: { provider: string; model: string; selector: string } | undefined;
|
|
1692
|
+
dispatchSources: DispatchContextSources;
|
|
1646
1693
|
},
|
|
1647
1694
|
): PreparedDispatch {
|
|
1648
1695
|
const run = requireRun(context, input.run.runId);
|
|
@@ -1671,6 +1718,15 @@ function birthMember(
|
|
|
1671
1718
|
resolvedRoute = routeResult;
|
|
1672
1719
|
}
|
|
1673
1720
|
}
|
|
1721
|
+
const dispatchContext = assembleDispatchContext(input.dispatchSources, {
|
|
1722
|
+
projectId: run.projectId,
|
|
1723
|
+
runId: run.runId,
|
|
1724
|
+
stepId: input.stepId,
|
|
1725
|
+
agentId,
|
|
1726
|
+
task: [input.step.instructions, promptText]
|
|
1727
|
+
.filter((part): part is string => typeof part === "string" && part.trim().length > 0)
|
|
1728
|
+
.join("\n\n"),
|
|
1729
|
+
});
|
|
1674
1730
|
const assignmentId = newKxmAssignmentId();
|
|
1675
1731
|
const attemptId = newKxmAttemptId();
|
|
1676
1732
|
const minted = mintCapabilitySecret();
|
|
@@ -1738,6 +1794,8 @@ function birthMember(
|
|
|
1738
1794
|
totalSteps: input.plan.order.length,
|
|
1739
1795
|
settledDecisions: [],
|
|
1740
1796
|
},
|
|
1797
|
+
...(dispatchContext.items.length > 0 ? { arbitratedItems: dispatchContext.items } : {}),
|
|
1798
|
+
...(dispatchContext.unresolvedGaps.length > 0 ? { unresolvedGaps: dispatchContext.unresolvedGaps } : {}),
|
|
1741
1799
|
});
|
|
1742
1800
|
const { packet: contextPacket } = pruneContextPacket(rawContextPacket);
|
|
1743
1801
|
const generatedPrompt = formatContextPacketForPrompt(contextPacket);
|
|
@@ -1773,6 +1831,28 @@ function birthMember(
|
|
|
1773
1831
|
controller,
|
|
1774
1832
|
state: next,
|
|
1775
1833
|
};
|
|
1834
|
+
if (input.dispatchSources.present && context.logger) {
|
|
1835
|
+
// Ids and counts only: never the task, the request, or any summary.
|
|
1836
|
+
try {
|
|
1837
|
+
context.logger({
|
|
1838
|
+
event: "dispatch_context_assembled",
|
|
1839
|
+
runId: run.runId,
|
|
1840
|
+
stepId: input.stepId,
|
|
1841
|
+
stepAttempt: input.stepAttempt,
|
|
1842
|
+
agentId,
|
|
1843
|
+
role: dispatchContext.role,
|
|
1844
|
+
deliveredIds: dispatchContext.deliveredIds,
|
|
1845
|
+
renderDeferred: dispatchContext.renderDeferred,
|
|
1846
|
+
provenanceSummary: dispatchContext.audit?.provenanceSummary ?? {},
|
|
1847
|
+
estimatedTokens: dispatchContext.audit?.estimatedTokens ?? 0,
|
|
1848
|
+
budgetTokens: dispatchContext.audit?.budgetTokens ?? null,
|
|
1849
|
+
skippedUnboundScopes: input.dispatchSources.skippedUnboundScopes,
|
|
1850
|
+
unresolvedGaps: dispatchContext.unresolvedGaps,
|
|
1851
|
+
});
|
|
1852
|
+
} catch {
|
|
1853
|
+
// Logging is best effort and never fails a birth.
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1776
1856
|
kxmPanelDispatchSeams.afterBirth?.(member);
|
|
1777
1857
|
return member;
|
|
1778
1858
|
}
|
|
@@ -1941,6 +2021,7 @@ async function drivePanel(
|
|
|
1941
2021
|
stepId: panel.stepId,
|
|
1942
2022
|
stepAttempt: panel.stepAttempt,
|
|
1943
2023
|
producerId: producer.id,
|
|
2024
|
+
dispatchSources: panel.dispatchSources,
|
|
1944
2025
|
});
|
|
1945
2026
|
});
|
|
1946
2027
|
};
|
|
@@ -2076,7 +2157,44 @@ async function drivePanel(
|
|
|
2076
2157
|
}
|
|
2077
2158
|
}
|
|
2078
2159
|
|
|
2079
|
-
|
|
2160
|
+
/** Engine-reserved providerMetadata keys. The engine writes them last, so a
|
|
2161
|
+
* producer can never spoof the ask identity of the attempt it reports on. */
|
|
2162
|
+
const ENGINE_ROUTING_METADATA_KEYS: ReadonlySet<string> = new Set(["workflowId", "askSha256", "objectiveSha256", "stepWrites"]);
|
|
2163
|
+
const MAX_PRODUCER_ROUTING_METADATA_FIELDS = MAX_PROVIDER_METADATA_FIELDS - ENGINE_ROUTING_METADATA_KEYS.size;
|
|
2164
|
+
|
|
2165
|
+
function engineRoutingMetadata(
|
|
2166
|
+
dispatch: PreparedDispatch,
|
|
2167
|
+
producer?: Record<string, string | number | boolean>,
|
|
2168
|
+
): Record<string, string | number | boolean> {
|
|
2169
|
+
const metadata: Record<string, string | number | boolean> = {};
|
|
2170
|
+
// A null producer map reads as absent, as the routing parser always treated it.
|
|
2171
|
+
if (producer !== undefined && producer !== null) {
|
|
2172
|
+
if (typeof producer !== "object" || Array.isArray(producer)) {
|
|
2173
|
+
throw new Error("routing providerMetadata must be an object");
|
|
2174
|
+
}
|
|
2175
|
+
let kept = 0;
|
|
2176
|
+
for (const [key, value] of Object.entries(producer)) {
|
|
2177
|
+
if (kept >= MAX_PRODUCER_ROUTING_METADATA_FIELDS) break;
|
|
2178
|
+
if (ENGINE_ROUTING_METADATA_KEYS.has(key)) continue;
|
|
2179
|
+
metadata[key] = value;
|
|
2180
|
+
kept += 1;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
metadata.workflowId = dispatch.plan.workflowId;
|
|
2184
|
+
metadata.askSha256 = kxmStepAskSha256(dispatch.plan, dispatch.stepId, dispatch.agentId);
|
|
2185
|
+
// Already `sha256:<hex>` from acceptance; the prompt text is never read here.
|
|
2186
|
+
metadata.objectiveSha256 = dispatch.run.promptSha256;
|
|
2187
|
+
metadata.stepWrites = Object.values(dispatch.step.repositories).some((access) => access === "write");
|
|
2188
|
+
return metadata;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function producerRoutingRecord(
|
|
2192
|
+
context: KxmRuntimeContext,
|
|
2193
|
+
dispatch: PreparedDispatch,
|
|
2194
|
+
result: KxmProducerResult,
|
|
2195
|
+
now: string,
|
|
2196
|
+
finalOutcome?: "blocked" | "failed",
|
|
2197
|
+
): RoutingRecordV2 {
|
|
2080
2198
|
const run = requireRun(context, dispatch.run.runId);
|
|
2081
2199
|
if (result.costBasis === undefined || result.costBasis === null) {
|
|
2082
2200
|
throw runtimeError("settle_missing_cost_basis", run.runId, `attempt settlement rejected: missing required costBasis for attempt ${dispatch.attemptId}`);
|
|
@@ -2107,9 +2225,12 @@ function producerRoutingRecord(context: KxmRuntimeContext, dispatch: PreparedDis
|
|
|
2107
2225
|
retries: Math.max(0, dispatch.stepAttempt - 1),
|
|
2108
2226
|
thinking: result.thinking ?? dispatch.request.thinking,
|
|
2109
2227
|
};
|
|
2110
|
-
for (const field of ["
|
|
2228
|
+
for (const field of ["contextTokens", "tokensIn", "tokensOut", "cacheReadTokens", "cacheWriteTokens", "priceRef"] as const) {
|
|
2111
2229
|
if (result[field] !== undefined) record[field] = result[field];
|
|
2112
2230
|
}
|
|
2231
|
+
record.agentRole = result.agentRole ?? dispatch.agentId;
|
|
2232
|
+
record.providerMetadata = engineRoutingMetadata(dispatch, result.providerMetadata);
|
|
2233
|
+
if (finalOutcome !== undefined) record.finalOutcome = finalOutcome;
|
|
2113
2234
|
return parseRoutingRecordV2(record);
|
|
2114
2235
|
}
|
|
2115
2236
|
|
|
@@ -2185,6 +2306,9 @@ function settleMember(
|
|
|
2185
2306
|
costBasis: "unknown",
|
|
2186
2307
|
costUsd: null,
|
|
2187
2308
|
retries: Math.max(0, dispatch.stepAttempt - 1),
|
|
2309
|
+
agentRole: dispatch.agentId,
|
|
2310
|
+
finalOutcome: "failed",
|
|
2311
|
+
providerMetadata: engineRoutingMetadata(dispatch),
|
|
2188
2312
|
};
|
|
2189
2313
|
push("routing.attempt.recorded", { routing: parseRoutingRecordV2(routingRecord) });
|
|
2190
2314
|
|
|
@@ -2200,10 +2324,18 @@ function settleMember(
|
|
|
2200
2324
|
if (!result) {
|
|
2201
2325
|
throw runtimeError("settle_missing_cost_basis", run.runId, `attempt settlement rejected: missing required costBasis for attempt ${dispatch.attemptId}`);
|
|
2202
2326
|
}
|
|
2203
|
-
push("routing.attempt.recorded", { routing: producerRoutingRecord(context, dispatch, result, now) });
|
|
2204
|
-
|
|
2205
2327
|
const outcome = typeof result.outcome === "string" ? result.outcome : undefined;
|
|
2206
2328
|
const known = outcome !== undefined && dispatch.step.outcomes.includes(outcome);
|
|
2329
|
+
push("routing.attempt.recorded", {
|
|
2330
|
+
routing: producerRoutingRecord(
|
|
2331
|
+
context,
|
|
2332
|
+
dispatch,
|
|
2333
|
+
result,
|
|
2334
|
+
now,
|
|
2335
|
+
known ? kxmAttemptFinalOutcome(dispatch.step, { resultClass: "outcome", outcome }) : "failed",
|
|
2336
|
+
),
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2207
2339
|
if (!known) {
|
|
2208
2340
|
push("assignment.result_recorded", {
|
|
2209
2341
|
assignmentId: dispatch.assignmentId,
|
|
@@ -192,7 +192,7 @@ export function resolveHubCredentials(options: ResolveHubCredentialsOptions = {}
|
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
/** Read-only token for one-shot hub clients (CLI,
|
|
195
|
+
/** Read-only token for one-shot operator hub clients (CLI, runtime supervisor, dashboards).
|
|
196
196
|
*
|
|
197
197
|
* Precedence: explicit KXM_AUTH_TOKEN, then the persisted project token for
|
|
198
198
|
* the resolved project, then the persisted admin token. Never generates or
|
|
@@ -235,6 +235,22 @@ export function resolveClientHubAuthToken(env: NodeJS.ProcessEnv, project: strin
|
|
|
235
235
|
return record?.projectTokens?.[project]?.trim() || record?.authToken?.trim() || undefined;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Token an agent session (the Claude MCP server) registers with: explicit `KXM_AUTH_TOKEN`,
|
|
240
|
+
* else the persisted project token for `project`, else nothing. It never returns the
|
|
241
|
+
* persisted admin token. The hub accepts the admin token for any project missing from its
|
|
242
|
+
* project-token map, so an agent falling back to it would join a project nobody issued it a
|
|
243
|
+
* token for. Operator tools keep `resolveClientHubAuthToken`. Throws HubEnvError on a
|
|
244
|
+
* malformed persisted record, same as `resolveClientHubAuthToken`.
|
|
245
|
+
*/
|
|
246
|
+
export function resolveAgentHubAuthToken(env: NodeJS.ProcessEnv, project: string): string | undefined {
|
|
247
|
+
const envToken = env.KXM_AUTH_TOKEN?.trim();
|
|
248
|
+
if (envToken) return envToken;
|
|
249
|
+
const tokens = readHubEnvRecord(env)?.projectTokens;
|
|
250
|
+
if (!tokens || !Object.hasOwn(tokens, project)) return undefined;
|
|
251
|
+
return tokens[project]?.trim() || undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
238
254
|
/**
|
|
239
255
|
* Admin-scoped hub reads (`/v1/ops/snapshot`, …) are rejected with 401 by a project token,
|
|
240
256
|
* so this variant resolves the admin credential only: explicit `KXM_AUTH_TOKEN`, else the
|
package/plugins/kxm/src/hub.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
DEFAULT_RATE_LIMIT_MAX,
|
|
12
12
|
DEFAULT_RATE_LIMIT_WINDOW_MS,
|
|
13
13
|
DEFAULT_STALE_AFTER_MS,
|
|
14
|
+
IMPROVEMENT_AREAS,
|
|
14
15
|
MAX_AGENT_HOST_CHARS,
|
|
15
16
|
MAX_BODY_BYTES,
|
|
16
17
|
MAX_CONTENT_CHARS,
|
|
@@ -45,6 +46,7 @@ import { timingSafeStringCompare } from "./commands.ts";
|
|
|
45
46
|
import { arbitrate, explainContextItem, journalEntryToContextItem, memoryRecordToContextItem, rolePolicy } from "./arbiter.ts";
|
|
46
47
|
import { loadAuthoredMemory } from "./memory.ts";
|
|
47
48
|
import { contextItemAuditMetadata, CONTEXT_AUTHORITIES, CONTEXT_CONFIDENCES, type ContextAuthority, type ContextConfidence, type ContextItem } from "./context.ts";
|
|
49
|
+
import { rankRecall, relevanceTokens } from "./relevance.ts";
|
|
48
50
|
import { NativeStateProvider } from "./state.ts";
|
|
49
51
|
import { SkillLifecycle } from "./skills.ts";
|
|
50
52
|
import { compileKnowledgeWiki, lintKnowledgeWiki, type WikiSourcePool } from "./wiki.ts";
|
|
@@ -56,8 +58,11 @@ import {
|
|
|
56
58
|
checkpointRun,
|
|
57
59
|
improvementReport,
|
|
58
60
|
applyJournalPromotion,
|
|
61
|
+
JOURNAL_CATEGORIES,
|
|
62
|
+
journalAttemptFor,
|
|
59
63
|
journalEvidenceRequired,
|
|
60
64
|
parseJournalCategory,
|
|
65
|
+
rankImprovementSignals,
|
|
61
66
|
renderWorkflowPrompt,
|
|
62
67
|
resumeWorkflowFromSignal,
|
|
63
68
|
valueAtPath,
|
|
@@ -1007,7 +1012,7 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
1007
1012
|
"Execute these stages in order:",
|
|
1008
1013
|
stageList,
|
|
1009
1014
|
"",
|
|
1010
|
-
|
|
1015
|
+
`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.`,
|
|
1011
1016
|
"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.",
|
|
1012
1017
|
"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.",
|
|
1013
1018
|
"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.",
|
|
@@ -1047,7 +1052,7 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
1047
1052
|
`Evidence: ${workflowEvidenceStrings(evidence).join(", ") || "none supplied"}`,
|
|
1048
1053
|
"",
|
|
1049
1054
|
nextInstruction,
|
|
1050
|
-
"Review the run with kxm_workflow_get and keep recording material
|
|
1055
|
+
"Review the run with kxm_workflow_get and keep recording material learning with kxm_workflow_record (any of its ten categories; pass stageId for stage-bound entries).",
|
|
1051
1056
|
"Do not claim the workflow is complete until the checkpoint response reports completed=true.",
|
|
1052
1057
|
].join("\n"), "workflow resume prompt", { max: MAX_CONTENT_CHARS });
|
|
1053
1058
|
const seq = store.nextAgentSequence(run.targetAgentId);
|
|
@@ -1103,6 +1108,7 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
1103
1108
|
evidence: [`wait-created:${waiting.createdAt}`, `wait-expired:${waiting.expiresAt}`],
|
|
1104
1109
|
relatedEntryIds: [],
|
|
1105
1110
|
createdAt: timestamp,
|
|
1111
|
+
...(stage ? { stageId: stage.id, attempt: stage.attempts + 1 } : {}),
|
|
1106
1112
|
};
|
|
1107
1113
|
const definition = webhookWorkflows.get(transition.definitionId);
|
|
1108
1114
|
const ttlMs = parseBoundedInteger(
|
|
@@ -1183,6 +1189,7 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
1183
1189
|
(candidate) => candidate.messageId === message.id && candidate.status === "running",
|
|
1184
1190
|
);
|
|
1185
1191
|
if (run) {
|
|
1192
|
+
const expiredStage = run.stages.find((candidate) => candidate.id === run.currentStage);
|
|
1186
1193
|
run.status = "failed";
|
|
1187
1194
|
delete run.currentStage;
|
|
1188
1195
|
run.updatedAt = nowIso();
|
|
@@ -1199,6 +1206,7 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
1199
1206
|
evidence: [`message:${message.id}`],
|
|
1200
1207
|
relatedEntryIds: [],
|
|
1201
1208
|
createdAt: run.updatedAt,
|
|
1209
|
+
...(expiredStage ? { stageId: expiredStage.id, attempt: expiredStage.attempts + 1 } : {}),
|
|
1202
1210
|
};
|
|
1203
1211
|
store.saveJournalEntry(entry);
|
|
1204
1212
|
counters.journalEntries += 1;
|
|
@@ -1406,6 +1414,8 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
1406
1414
|
evidence: workflowEvidenceStrings(evidence),
|
|
1407
1415
|
relatedEntryIds: [],
|
|
1408
1416
|
createdAt: receivedAt,
|
|
1417
|
+
stageId: stage.id,
|
|
1418
|
+
attempt: stage.attempts,
|
|
1409
1419
|
};
|
|
1410
1420
|
} else if (result.degraded) {
|
|
1411
1421
|
const stage = transition.stages.find((candidate) => candidate.id === result.stageId)!;
|
|
@@ -1423,6 +1433,8 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
1423
1433
|
],
|
|
1424
1434
|
relatedEntryIds: [],
|
|
1425
1435
|
createdAt: receivedAt,
|
|
1436
|
+
stageId: stage.id,
|
|
1437
|
+
attempt: stage.attempts,
|
|
1426
1438
|
};
|
|
1427
1439
|
}
|
|
1428
1440
|
if (transition.status === "running") {
|
|
@@ -1700,6 +1712,8 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
1700
1712
|
],
|
|
1701
1713
|
relatedEntryIds: [],
|
|
1702
1714
|
createdAt: timestamp,
|
|
1715
|
+
stageId,
|
|
1716
|
+
attempt: result.approval.attempt,
|
|
1703
1717
|
};
|
|
1704
1718
|
store.saveWorkflowTransition(transition, undefined, entry);
|
|
1705
1719
|
publishOps(transition.project, "workflows");
|
|
@@ -1781,9 +1795,18 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
1781
1795
|
...(skillLifecycle ? { skillLifecycle } : {}),
|
|
1782
1796
|
});
|
|
1783
1797
|
counters.contextRequests += 1;
|
|
1798
|
+
// Sizes, never the task text (Q-J). The caller still receives its own
|
|
1799
|
+
// audit, request included, in the response below.
|
|
1800
|
+
const assembledRequest = outcome.audit.request;
|
|
1784
1801
|
logger({
|
|
1785
1802
|
event: "context_packet_assembled",
|
|
1786
|
-
|
|
1803
|
+
project: assembledRequest.project,
|
|
1804
|
+
role: assembledRequest.role,
|
|
1805
|
+
...(assembledRequest.workflowRunId !== undefined ? { workflowRunId: assembledRequest.workflowRunId } : {}),
|
|
1806
|
+
...(assembledRequest.stageId !== undefined ? { stageId: assembledRequest.stageId } : {}),
|
|
1807
|
+
taskChars: assembledRequest.task.length,
|
|
1808
|
+
taskTokens: outcome.audit.relevance.taskTokens,
|
|
1809
|
+
matchedCandidates: outcome.audit.relevance.matchedCandidates,
|
|
1787
1810
|
selectedIds: outcome.audit.selectedIds,
|
|
1788
1811
|
provenanceSummary: outcome.audit.provenanceSummary,
|
|
1789
1812
|
estimatedTokens: outcome.audit.estimatedTokens,
|
|
@@ -1799,21 +1822,30 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
1799
1822
|
if (method === "POST" && contextRecallMatch) {
|
|
1800
1823
|
const body = await readJson(request);
|
|
1801
1824
|
const { project: callerProject, caller: callerId } = contextCallerProject(request, body.project);
|
|
1802
|
-
const query = requireString(body.query ?? "", "query", { max: 500, allowEmpty: true })
|
|
1825
|
+
const query = requireString(body.query ?? "", "query", { max: 500, allowEmpty: true });
|
|
1803
1826
|
const kinds = Array.isArray(body.kinds)
|
|
1804
1827
|
? body.kinds.filter((kind: unknown): kind is string => typeof kind === "string")
|
|
1805
1828
|
: undefined;
|
|
1806
1829
|
const limit = parseBoundedInteger(body.limit, "limit", 25, 1, 100);
|
|
1807
1830
|
const { pool } = projectContextPool(callerProject);
|
|
1808
|
-
const
|
|
1831
|
+
const live = pool
|
|
1809
1832
|
.filter((item) => item.status !== "superseded" && item.status !== "rejected")
|
|
1810
|
-
.filter((item) => kinds === undefined || kinds.includes(item.kind))
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
.slice(0, limit);
|
|
1833
|
+
.filter((item) => kinds === undefined || kinds.includes(item.kind));
|
|
1834
|
+
// Exact-phrase hits, then any-token BM25 hits, then id.
|
|
1835
|
+
const recalled = rankRecall(query, live, limit);
|
|
1814
1836
|
counters.contextRequests += 1;
|
|
1815
|
-
logger({
|
|
1816
|
-
|
|
1837
|
+
logger({
|
|
1838
|
+
event: "context_recall",
|
|
1839
|
+
project: callerProject,
|
|
1840
|
+
queryChars: query.length,
|
|
1841
|
+
queryTokens: new Set(relevanceTokens(query)).size,
|
|
1842
|
+
limit,
|
|
1843
|
+
results: recalled.length,
|
|
1844
|
+
});
|
|
1845
|
+
json(response, 200, {
|
|
1846
|
+
items: recalled.map(({ item, relevance }) => ({ ...contextItemAuditMetadata(item), relevance })),
|
|
1847
|
+
unresolvedGaps: recalled.length === 0 ? ["no matching context records"] : [],
|
|
1848
|
+
});
|
|
1817
1849
|
return;
|
|
1818
1850
|
}
|
|
1819
1851
|
|
|
@@ -1982,7 +2014,13 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
1982
2014
|
nowIso(),
|
|
1983
2015
|
);
|
|
1984
2016
|
store.saveJournalEntry(updated);
|
|
1985
|
-
|
|
2017
|
+
// Publish to the run's project (not its id) and refresh a terminal
|
|
2018
|
+
// run's exported retrospective with the new promotion state.
|
|
2019
|
+
const promotedRun = workflowRuns.get(entry.runId);
|
|
2020
|
+
if (promotedRun) {
|
|
2021
|
+
publishOps(promotedRun.project, "workflows");
|
|
2022
|
+
exportTerminalRetrospective(promotedRun);
|
|
2023
|
+
}
|
|
1986
2024
|
logger({
|
|
1987
2025
|
event: "journal_promotion_recorded",
|
|
1988
2026
|
journalEntryId: entry.id,
|
|
@@ -2120,6 +2158,9 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2120
2158
|
: undefined,
|
|
2121
2159
|
);
|
|
2122
2160
|
const checkpointStage = transition.stages.find((candidate) => candidate.id === stageId)!;
|
|
2161
|
+
// The attempt this checkpoint consumed. A self-edge transition re-enters
|
|
2162
|
+
// the stage and resets its counter, so prefer the transition record.
|
|
2163
|
+
const checkpointAttempt = result.transition?.attempt ?? checkpointStage.attempts;
|
|
2123
2164
|
if (result.transition) {
|
|
2124
2165
|
const transitionEntry: WorkflowJournalEntry = {
|
|
2125
2166
|
id: newId("journal"),
|
|
@@ -2132,6 +2173,8 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2132
2173
|
evidence: result.transition.evidenceKeys.map((key) => `requirement:${key}`),
|
|
2133
2174
|
relatedEntryIds: [],
|
|
2134
2175
|
createdAt: timestamp,
|
|
2176
|
+
stageId,
|
|
2177
|
+
attempt: checkpointAttempt,
|
|
2135
2178
|
};
|
|
2136
2179
|
store.saveWorkflowTransition(transition, undefined, transitionEntry);
|
|
2137
2180
|
counters.journalEntries += 1;
|
|
@@ -2148,6 +2191,8 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2148
2191
|
evidence: [`class:transition_budget_exhausted`, `stage:${stageId}`],
|
|
2149
2192
|
relatedEntryIds: [],
|
|
2150
2193
|
createdAt: timestamp,
|
|
2194
|
+
stageId,
|
|
2195
|
+
attempt: checkpointAttempt,
|
|
2151
2196
|
};
|
|
2152
2197
|
store.saveWorkflowTransition(transition, undefined, exhaustEntry);
|
|
2153
2198
|
counters.journalEntries += 1;
|
|
@@ -2165,6 +2210,8 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2165
2210
|
evidence: workflowEvidenceStrings(evidence),
|
|
2166
2211
|
relatedEntryIds: [],
|
|
2167
2212
|
createdAt: transition.updatedAt,
|
|
2213
|
+
stageId,
|
|
2214
|
+
attempt: checkpointAttempt,
|
|
2168
2215
|
};
|
|
2169
2216
|
} else if (result.degraded) {
|
|
2170
2217
|
entry = {
|
|
@@ -2181,6 +2228,8 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2181
2228
|
],
|
|
2182
2229
|
relatedEntryIds: [],
|
|
2183
2230
|
createdAt: transition.updatedAt,
|
|
2231
|
+
stageId,
|
|
2232
|
+
attempt: checkpointAttempt,
|
|
2184
2233
|
};
|
|
2185
2234
|
}
|
|
2186
2235
|
store.saveWorkflowTransition(transition, undefined, entry);
|
|
@@ -2226,10 +2275,25 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2226
2275
|
);
|
|
2227
2276
|
}
|
|
2228
2277
|
const category = parseJournalCategory(body.category);
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2278
|
+
// Stage provenance first: the stage supplies the default area, and
|
|
2279
|
+
// the hub (never the caller) derives the attempt from its state.
|
|
2280
|
+
let stage: WorkflowStageState | undefined;
|
|
2281
|
+
if (body.stageId !== undefined && body.stageId !== null) {
|
|
2282
|
+
const requestedStageId = requireString(body.stageId, "stageId", { max: 128 });
|
|
2283
|
+
stage = run.stages.find((candidate) => candidate.id === requestedStageId);
|
|
2284
|
+
if (!stage) {
|
|
2285
|
+
throw new ProtocolError(400, `stageId ${requestedStageId} is not part of this workflow run`, "invalid_journal_relation");
|
|
2286
|
+
}
|
|
2232
2287
|
}
|
|
2288
|
+
const area = (body.area == null ? stage?.area : requireString(body.area, "area", { max: 24 })) as ImprovementArea | undefined;
|
|
2289
|
+
if (area === undefined || !IMPROVEMENT_AREAS.includes(area)) {
|
|
2290
|
+
throw new ProtocolError(
|
|
2291
|
+
400,
|
|
2292
|
+
"area is required unless stageId names a stage that declares an area",
|
|
2293
|
+
"invalid_improvement_area",
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
const attempt = stage ? journalAttemptFor(stage) : undefined;
|
|
2233
2297
|
const severity = requireString(body.severity ?? "info", "severity", { max: 16 });
|
|
2234
2298
|
if (severity !== "info" && severity !== "warning" && severity !== "error") {
|
|
2235
2299
|
throw new ProtocolError(400, "severity must be info, warning, or error", "invalid_journal_severity");
|
|
@@ -2250,16 +2314,6 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2250
2314
|
"journal_evidence_required",
|
|
2251
2315
|
);
|
|
2252
2316
|
}
|
|
2253
|
-
let stageId: string | undefined;
|
|
2254
|
-
let attempt: number | undefined;
|
|
2255
|
-
if (body.stageId !== undefined && body.stageId !== null) {
|
|
2256
|
-
stageId = requireString(body.stageId, "stageId", { max: 128 });
|
|
2257
|
-
const stage = run.stages.find((candidate) => candidate.id === stageId);
|
|
2258
|
-
if (!stage) {
|
|
2259
|
-
throw new ProtocolError(400, `stageId ${stageId} is not part of this workflow run`, "invalid_journal_relation");
|
|
2260
|
-
}
|
|
2261
|
-
attempt = stage.attempts + 1;
|
|
2262
|
-
}
|
|
2263
2317
|
const entry: WorkflowJournalEntry = {
|
|
2264
2318
|
id: newId("journal"),
|
|
2265
2319
|
runId: run.id,
|
|
@@ -2272,11 +2326,13 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2272
2326
|
evidence,
|
|
2273
2327
|
relatedEntryIds,
|
|
2274
2328
|
createdAt: nowIso(),
|
|
2275
|
-
...(
|
|
2329
|
+
...(stage !== undefined ? { stageId: stage.id } : {}),
|
|
2276
2330
|
...(attempt !== undefined ? { attempt } : {}),
|
|
2277
2331
|
};
|
|
2278
2332
|
store.saveJournalEntry(entry);
|
|
2279
2333
|
counters.journalEntries += 1;
|
|
2334
|
+
// A late entry on a terminal run refreshes its exported retrospective.
|
|
2335
|
+
exportTerminalRetrospective(run);
|
|
2280
2336
|
logger({
|
|
2281
2337
|
event: "workflow_journal_recorded",
|
|
2282
2338
|
workflowRunId: run.id,
|
|
@@ -2292,13 +2348,18 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2292
2348
|
if (method === "GET" && url.pathname === "/v1/improvements") {
|
|
2293
2349
|
const agent = requireAgent(request);
|
|
2294
2350
|
requireProjectAuth(request, agent.project);
|
|
2295
|
-
const
|
|
2351
|
+
const projectRuns = new Map(
|
|
2296
2352
|
[...workflowRuns.values()]
|
|
2297
2353
|
.filter((run) => run.project === agent.project)
|
|
2298
|
-
.map((run) => run.id),
|
|
2354
|
+
.map((run) => [run.id, run] as const),
|
|
2299
2355
|
);
|
|
2356
|
+
const visibleRuns = new Set(projectRuns.keys());
|
|
2300
2357
|
const entries = [...journal.values()].filter((entry) => visibleRuns.has(entry.runId));
|
|
2301
|
-
json(response, 200, {
|
|
2358
|
+
json(response, 200, {
|
|
2359
|
+
reports: improvementReport(entries),
|
|
2360
|
+
signals: rankImprovementSignals(entries, projectRuns),
|
|
2361
|
+
entries: entries.length,
|
|
2362
|
+
});
|
|
2302
2363
|
return;
|
|
2303
2364
|
}
|
|
2304
2365
|
|
|
@@ -2755,6 +2816,7 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2755
2816
|
publishOps(message.project, "messages");
|
|
2756
2817
|
const workflowRun = [...workflowRuns.values()].find((run) => run.messageId === message.id);
|
|
2757
2818
|
if (workflowRun?.status === "running") {
|
|
2819
|
+
const settledStage = workflowRun.stages.find((candidate) => candidate.id === workflowRun.currentStage);
|
|
2758
2820
|
workflowRun.status = "failed";
|
|
2759
2821
|
delete workflowRun.currentStage;
|
|
2760
2822
|
workflowRun.updatedAt = message.repliedAt;
|
|
@@ -2771,6 +2833,7 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2771
2833
|
evidence: [`message:${message.id}`],
|
|
2772
2834
|
relatedEntryIds: [],
|
|
2773
2835
|
createdAt: message.repliedAt,
|
|
2836
|
+
...(settledStage ? { stageId: settledStage.id, attempt: settledStage.attempts + 1 } : {}),
|
|
2774
2837
|
};
|
|
2775
2838
|
store.saveJournalEntry(entry);
|
|
2776
2839
|
counters.journalEntries += 1;
|