@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.
Files changed (92) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/workflows/default.yaml +1 -1
  3. package/CHANGELOG.md +212 -0
  4. package/README.md +3 -0
  5. package/docs/README.md +3 -0
  6. package/docs/agent-skills.md +123 -60
  7. package/docs/architecture.md +5 -2
  8. package/docs/cli-reference.md +3527 -0
  9. package/docs/config-reference.md +1943 -0
  10. package/docs/configuration.md +30 -4
  11. package/docs/continuous-improvement.md +122 -10
  12. package/docs/contracts/routing.md +95 -11
  13. package/docs/harness-routing.md +616 -0
  14. package/docs/kxm-handbook.md +106 -19
  15. package/docs/templates/README.md +1 -1
  16. package/docs/test-matrix.md +12 -6
  17. package/docs/troubleshooting.md +2 -2
  18. package/examples/project/.kxm/workflows/fix.yaml +1 -1
  19. package/examples/project/.kxm/workflows/improve.yaml +1 -1
  20. package/package.json +1 -1
  21. package/plugins/kxm/.claude-plugin/plugin.json +9 -10
  22. package/plugins/kxm/README.md +238 -56
  23. package/plugins/kxm/dist/claude-hook.js +10083 -0
  24. package/plugins/kxm/dist/cli.js +2487 -1848
  25. package/plugins/kxm/dist/client.js +64 -0
  26. package/plugins/kxm/dist/core.js +102 -9
  27. package/plugins/kxm/dist/extension.js +210 -68
  28. package/plugins/kxm/dist/mcp-server.js +217 -40
  29. package/plugins/kxm/dist/runtime-supervisor.js +1628 -157
  30. package/plugins/kxm/dist/runtime.js +1874 -298
  31. package/plugins/kxm/dist/server.js +416 -82
  32. package/plugins/kxm/package.json +1 -1
  33. package/plugins/kxm/skills/hints.json +1 -1
  34. package/plugins/kxm/skills/kxm/SKILL.md +48 -24
  35. package/plugins/kxm/skills/kxm/references/protocol.md +3 -3
  36. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +67 -21
  37. package/plugins/kxm/skills/kxm-definitions/SKILL.md +9 -0
  38. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +82 -16
  39. package/plugins/kxm/skills/kxm-harvest/SKILL.md +1 -1
  40. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +55 -27
  41. package/plugins/kxm/skills/kxm-insights/SKILL.md +1 -1
  42. package/plugins/kxm/skills/kxm-mind/SKILL.md +2 -2
  43. package/plugins/kxm/skills/{kxm-setup → kxm-mind-setup}/SKILL.md +4 -4
  44. package/plugins/kxm/skills/kxm-peer/SKILL.md +68 -93
  45. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +156 -23
  46. package/plugins/kxm/skills/kxm-projects/SKILL.md +1 -1
  47. package/plugins/kxm/skills/kxm-protocol/SKILL.md +1 -1
  48. package/plugins/kxm/skills/kxm-query/SKILL.md +1 -1
  49. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +74 -15
  50. package/plugins/kxm/skills/kxm-runs/SKILL.md +46 -17
  51. package/plugins/kxm/skills/kxm-session/SKILL.md +64 -36
  52. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +44 -15
  53. package/plugins/kxm/skills/kxm-tasks/SKILL.md +16 -4
  54. package/plugins/kxm/skills/kxm-triage/SKILL.md +1 -1
  55. package/plugins/kxm/skills/kxm-work/SKILL.md +1 -1
  56. package/plugins/kxm/skills/kxm-workflow/SKILL.md +60 -19
  57. package/plugins/kxm/src/arbiter.ts +67 -22
  58. package/plugins/kxm/src/autocomplete.ts +1 -1
  59. package/plugins/kxm/src/claude-hook.ts +192 -0
  60. package/plugins/kxm/src/cli/project.ts +11 -5
  61. package/plugins/kxm/src/cli/system.ts +85 -13
  62. package/plugins/kxm/src/cli/types.ts +4 -1
  63. package/plugins/kxm/src/cli/workflows.ts +18 -16
  64. package/plugins/kxm/src/cli.ts +23 -13
  65. package/plugins/kxm/src/client.ts +15 -4
  66. package/plugins/kxm/src/commands.ts +19 -9
  67. package/plugins/kxm/src/config.ts +42 -7
  68. package/plugins/kxm/src/context-packet.ts +14 -2
  69. package/plugins/kxm/src/context.ts +16 -5
  70. package/plugins/kxm/src/dispatch-context.ts +286 -0
  71. package/plugins/kxm/src/engine-plan.ts +40 -0
  72. package/plugins/kxm/src/engine.ts +138 -6
  73. package/plugins/kxm/src/hub-env.ts +17 -1
  74. package/plugins/kxm/src/hub.ts +92 -29
  75. package/plugins/kxm/src/improve-sources.ts +228 -0
  76. package/plugins/kxm/src/improve.ts +325 -140
  77. package/plugins/kxm/src/local-snapshot.ts +101 -42
  78. package/plugins/kxm/src/mcp-server.ts +129 -30
  79. package/plugins/kxm/src/memory.ts +43 -20
  80. package/plugins/kxm/src/project-config.ts +25 -0
  81. package/plugins/kxm/src/protocol.ts +11 -0
  82. package/plugins/kxm/src/relevance.ts +138 -0
  83. package/plugins/kxm/src/retrospective.ts +16 -10
  84. package/plugins/kxm/src/runtime-service.ts +8 -1
  85. package/plugins/kxm/src/runtime-supervisor.ts +16 -2
  86. package/plugins/kxm/src/session-token-hint.ts +17 -0
  87. package/plugins/kxm/src/suggest.ts +7 -7
  88. package/plugins/kxm/src/workflow-manager.ts +80 -78
  89. package/plugins/kxm/src/workflow.ts +202 -12
  90. package/scripts/build-runtime.mjs +7 -1
  91. package/scripts/check-generated.mjs +1 -0
  92. 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 prepared = context.eventStore.transaction(() => prepareDispatch(context, runId, producer.id));
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?: "driver-simulated" | "pi" | string,
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
- function producerRoutingRecord(context: KxmRuntimeContext, dispatch: PreparedDispatch, result: KxmProducerResult, now: string): RoutingRecordV2 {
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 ["agentRole", "contextTokens", "tokensIn", "tokensOut", "cacheReadTokens", "cacheWriteTokens", "priceRef", "providerMetadata"] as const) {
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, MCP server, dashboards).
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
@@ -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
- "At every stage, record material plans, decisions, contradictions, errors, and lessons with kxm_workflow_record.",
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 plans, decisions, contradictions, errors, and lessons.",
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
- ...outcome.audit.request,
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 }).toLowerCase();
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 recalled = pool
1831
+ const live = pool
1809
1832
  .filter((item) => item.status !== "superseded" && item.status !== "rejected")
1810
- .filter((item) => kinds === undefined || kinds.includes(item.kind))
1811
- .filter((item) => query === "" || item.summary.toLowerCase().includes(query) || (item.stateKey ?? "").toLowerCase().includes(query))
1812
- .sort((left, right) => left.id.localeCompare(right.id))
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({ event: "context_recall", project: callerProject, query, limit, results: recalled.length });
1816
- json(response, 200, { items: recalled.map(contextItemAuditMetadata), unresolvedGaps: recalled.length === 0 ? ["no matching context records"] : [] });
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
- publishOps(entry.runId, "workflows");
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
- const area = requireString(body.area, "area", { max: 24 }) as ImprovementArea;
2230
- if (!["harness", "gates", "implementation", "workflow", "documentation", "security", "other"].includes(area)) {
2231
- throw new ProtocolError(400, "invalid improvement area", "invalid_improvement_area");
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
- ...(stageId !== undefined ? { stageId } : {}),
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 visibleRuns = new Set(
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, { reports: improvementReport(entries), entries: entries.length });
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;