@kontextmind/kxm 0.7.60 → 0.7.63

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.
@@ -11,7 +11,7 @@
11
11
  "name": "kxm",
12
12
  "source": "./plugins/kxm",
13
13
  "description": "Durable workflows, peer agents, and kxm tui",
14
- "version": "0.7.60",
14
+ "version": "0.7.63",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
@@ -100,7 +100,7 @@ created → accepted → dispatched → executing → result_recorded → termin
100
100
  | `dispatched` | Attempt identity and dispatch intent persisted before process start |
101
101
  | `executing` | Invocation admitted and the attempt capability is usable. This slice is in-process (no OS process identity yet); later recovery still requires exact process/session proof |
102
102
  | `reattaching` | Runtime is verifying the same process/session and event cursor |
103
- | `result_recorded` | Structured attempt result and referenced receipts are durably stored |
103
+ | `result_recorded` | Structured attempt result and referenced receipts are durably stored. When a producer **refused** (`resultClass: producer_rejected`), the payload may carry `producerError`: a **machine code only** (lowercase-underscore identifier, ≤64 chars, schema-enforced; capability material is redacted before extraction, and anything unclassifiable — including prose — records as `producer_error`). The routing record names the producer that actually ran: a live drive that fails authentication must never read in the log as a simulation |
104
104
  | `retry_pending` | Policy permits another physical attempt after the prior attempt became terminal |
105
105
  | `blocked_uncertain` | A dependent effect cannot be reconciled safely |
106
106
  | `terminal` | The logical assignment outcome is final and immutable: passed, failed, or cancelled |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.60",
3
+ "version": "0.7.63",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "kxm",
4
4
  "displayName": "KXM",
5
- "version": "0.7.60",
5
+ "version": "0.7.63",
6
6
  "description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
7
7
  "author": {
8
8
  "name": "KontextMind",
@@ -17121,7 +17121,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
17121
17121
  }
17122
17122
 
17123
17123
  // plugins/kxm/src/mcp-server.ts
17124
- var VERSION = "0.7.60";
17124
+ var VERSION = "0.7.63";
17125
17125
  var inbox = /* @__PURE__ */ new Map();
17126
17126
  var notifiedInbox = /* @__PURE__ */ new Set();
17127
17127
  var meshClient;
@@ -24601,15 +24601,29 @@ var KxmRunScheduler = class _KxmRunScheduler {
24601
24601
  }
24602
24602
  };
24603
24603
  function invokeProducer(producer, request) {
24604
+ const reason = (error) => {
24605
+ try {
24606
+ const raw = error instanceof Error ? error.message : error;
24607
+ const message = typeof raw === "string" ? raw : "";
24608
+ let text = message;
24609
+ if (request.capability) text = text.split(request.capability).join("[redacted]");
24610
+ text = text.replace(/kxmcap_[A-Za-z0-9_-]+/g, "[redacted]");
24611
+ const match = /^[a-z][a-z0-9]*(?:_[a-z0-9]+){1,3}/.exec(text);
24612
+ const code = match?.[0] ?? "";
24613
+ return code.length > 0 && code.length <= 64 ? code : "producer_error";
24614
+ } catch {
24615
+ return "producer_error";
24616
+ }
24617
+ };
24604
24618
  let pending;
24605
24619
  try {
24606
24620
  pending = Promise.resolve(producer.produce(request));
24607
- } catch {
24608
- return Promise.resolve({ error: true });
24621
+ } catch (error) {
24622
+ return Promise.resolve({ error: true, errorCode: reason(error) });
24609
24623
  }
24610
24624
  return pending.then(
24611
24625
  (result) => ({ result, error: false }),
24612
- () => ({ error: true })
24626
+ (error) => ({ error: true, errorCode: reason(error) })
24613
24627
  );
24614
24628
  }
24615
24629
  var kxmGateDispatchSeams = {};
@@ -24883,6 +24897,21 @@ function resolveProducerRoute(projectRoot, step, agentId) {
24883
24897
  const parsed = (0, import_yaml5.parse)(readFileSync7(agentFile, "utf8"));
24884
24898
  if (typeof parsed?.model === "string") {
24885
24899
  agentModel = parsed.model;
24900
+ } else if (parsed?.model && typeof parsed.model === "object" && !Array.isArray(parsed.model)) {
24901
+ const declared = parsed.model;
24902
+ const providerOk = typeof declared.provider === "string" && declared.provider.length > 0 && !declared.provider.includes("/");
24903
+ const modelOk = typeof declared.model === "string" && declared.model.length > 0 && !declared.model.startsWith("/") && !declared.model.endsWith("/");
24904
+ if (providerOk && modelOk) {
24905
+ agentModel = `${declared.provider}/${declared.model}`;
24906
+ } else {
24907
+ return {
24908
+ error: {
24909
+ reason: "step_unsupported",
24910
+ field: "model",
24911
+ detail: "producer_route_unsupported: invalid model declaration"
24912
+ }
24913
+ };
24914
+ }
24886
24915
  }
24887
24916
  } catch {
24888
24917
  }
@@ -25413,7 +25442,7 @@ async function drivePanel(context, panel, producer) {
25413
25442
  if (kxmPanelDispatchSeams.failSettleMember?.(member)) {
25414
25443
  throw runtimeError("run_events_illegal", runId, "member settlement write failed");
25415
25444
  }
25416
- settleMember(context, member, produced.result, produced.error);
25445
+ settleMember(context, member, produced.result, produced.error, produced.errorCode);
25417
25446
  });
25418
25447
  return true;
25419
25448
  } catch (err) {
@@ -25568,7 +25597,7 @@ function producerRoutingRecord(context, dispatch, result, now) {
25568
25597
  }
25569
25598
  return parseRoutingRecordV2(record2);
25570
25599
  }
25571
- function settleMember(context, dispatch, result, produceError) {
25600
+ function settleMember(context, dispatch, result, produceError, produceErrorCode) {
25572
25601
  const run = requireRun(context, dispatch.run.runId);
25573
25602
  const state = foldStoredKxmRun(context, run);
25574
25603
  const capability = context.eventStore.capabilityByAttempt(dispatch.attemptId);
@@ -25619,7 +25648,10 @@ function settleMember(context, dispatch, result, produceError) {
25619
25648
  stepId: dispatch.stepId,
25620
25649
  assignmentId: dispatch.assignmentId,
25621
25650
  attemptId: dispatch.attemptId,
25622
- harness: result?.harness ?? (dispatch.producerId === "pi" ? "pi" : "driver-simulated"),
25651
+ // Label the producer that actually ran: the old fallback printed "driver-simulated"
25652
+ // for every producer that was not literally "pi", so a live oneshot drive that
25653
+ // failed authentication read in the log as a simulation.
25654
+ harness: result?.harness ?? dispatch.producerId,
25623
25655
  provider: result?.provider ?? "simulated",
25624
25656
  requestedModel: result?.requestedModel ?? "simulated",
25625
25657
  effectiveModel: result?.effectiveModel ?? result?.requestedModel ?? "simulated",
@@ -25633,7 +25665,8 @@ function settleMember(context, dispatch, result, produceError) {
25633
25665
  push("assignment.result_recorded", {
25634
25666
  assignmentId: dispatch.assignmentId,
25635
25667
  resultClass: "producer_rejected",
25636
- status: "result_recorded"
25668
+ status: "result_recorded",
25669
+ ...produceErrorCode !== void 0 ? { producerError: produceErrorCode } : {}
25637
25670
  });
25638
25671
  push("attempt.status_changed", { attemptId: dispatch.attemptId, status: "terminal" });
25639
25672
  push("assignment.terminal", { assignmentId: dispatch.assignmentId, outcome: "failed", status: "terminal" });
@@ -25113,15 +25113,29 @@ var KxmRunScheduler = class _KxmRunScheduler {
25113
25113
  }
25114
25114
  };
25115
25115
  function invokeProducer(producer, request) {
25116
+ const reason = (error) => {
25117
+ try {
25118
+ const raw = error instanceof Error ? error.message : error;
25119
+ const message = typeof raw === "string" ? raw : "";
25120
+ let text = message;
25121
+ if (request.capability) text = text.split(request.capability).join("[redacted]");
25122
+ text = text.replace(/kxmcap_[A-Za-z0-9_-]+/g, "[redacted]");
25123
+ const match = /^[a-z][a-z0-9]*(?:_[a-z0-9]+){1,3}/.exec(text);
25124
+ const code = match?.[0] ?? "";
25125
+ return code.length > 0 && code.length <= 64 ? code : "producer_error";
25126
+ } catch {
25127
+ return "producer_error";
25128
+ }
25129
+ };
25116
25130
  let pending;
25117
25131
  try {
25118
25132
  pending = Promise.resolve(producer.produce(request));
25119
- } catch {
25120
- return Promise.resolve({ error: true });
25133
+ } catch (error) {
25134
+ return Promise.resolve({ error: true, errorCode: reason(error) });
25121
25135
  }
25122
25136
  return pending.then(
25123
25137
  (result) => ({ result, error: false }),
25124
- () => ({ error: true })
25138
+ (error) => ({ error: true, errorCode: reason(error) })
25125
25139
  );
25126
25140
  }
25127
25141
  var kxmGateDispatchSeams = {};
@@ -25395,6 +25409,21 @@ function resolveProducerRoute(projectRoot, step, agentId) {
25395
25409
  const parsed = (0, import_yaml5.parse)(readFileSync7(agentFile, "utf8"));
25396
25410
  if (typeof parsed?.model === "string") {
25397
25411
  agentModel = parsed.model;
25412
+ } else if (parsed?.model && typeof parsed.model === "object" && !Array.isArray(parsed.model)) {
25413
+ const declared = parsed.model;
25414
+ const providerOk = typeof declared.provider === "string" && declared.provider.length > 0 && !declared.provider.includes("/");
25415
+ const modelOk = typeof declared.model === "string" && declared.model.length > 0 && !declared.model.startsWith("/") && !declared.model.endsWith("/");
25416
+ if (providerOk && modelOk) {
25417
+ agentModel = `${declared.provider}/${declared.model}`;
25418
+ } else {
25419
+ return {
25420
+ error: {
25421
+ reason: "step_unsupported",
25422
+ field: "model",
25423
+ detail: "producer_route_unsupported: invalid model declaration"
25424
+ }
25425
+ };
25426
+ }
25398
25427
  }
25399
25428
  } catch {
25400
25429
  }
@@ -25925,7 +25954,7 @@ async function drivePanel(context, panel, producer) {
25925
25954
  if (kxmPanelDispatchSeams.failSettleMember?.(member)) {
25926
25955
  throw runtimeError("run_events_illegal", runId, "member settlement write failed");
25927
25956
  }
25928
- settleMember(context, member, produced.result, produced.error);
25957
+ settleMember(context, member, produced.result, produced.error, produced.errorCode);
25929
25958
  });
25930
25959
  return true;
25931
25960
  } catch (err) {
@@ -26080,7 +26109,7 @@ function producerRoutingRecord(context, dispatch, result, now) {
26080
26109
  }
26081
26110
  return parseRoutingRecordV2(record2);
26082
26111
  }
26083
- function settleMember(context, dispatch, result, produceError) {
26112
+ function settleMember(context, dispatch, result, produceError, produceErrorCode) {
26084
26113
  const run = requireRun(context, dispatch.run.runId);
26085
26114
  const state = foldStoredKxmRun(context, run);
26086
26115
  const capability = context.eventStore.capabilityByAttempt(dispatch.attemptId);
@@ -26131,7 +26160,10 @@ function settleMember(context, dispatch, result, produceError) {
26131
26160
  stepId: dispatch.stepId,
26132
26161
  assignmentId: dispatch.assignmentId,
26133
26162
  attemptId: dispatch.attemptId,
26134
- harness: result?.harness ?? (dispatch.producerId === "pi" ? "pi" : "driver-simulated"),
26163
+ // Label the producer that actually ran: the old fallback printed "driver-simulated"
26164
+ // for every producer that was not literally "pi", so a live oneshot drive that
26165
+ // failed authentication read in the log as a simulation.
26166
+ harness: result?.harness ?? dispatch.producerId,
26135
26167
  provider: result?.provider ?? "simulated",
26136
26168
  requestedModel: result?.requestedModel ?? "simulated",
26137
26169
  effectiveModel: result?.effectiveModel ?? result?.requestedModel ?? "simulated",
@@ -26145,7 +26177,8 @@ function settleMember(context, dispatch, result, produceError) {
26145
26177
  push("assignment.result_recorded", {
26146
26178
  assignmentId: dispatch.assignmentId,
26147
26179
  resultClass: "producer_rejected",
26148
- status: "result_recorded"
26180
+ status: "result_recorded",
26181
+ ...produceErrorCode !== void 0 ? { producerError: produceErrorCode } : {}
26149
26182
  });
26150
26183
  push("attempt.status_changed", { attemptId: dispatch.attemptId, status: "terminal" });
26151
26184
  push("assignment.terminal", { assignmentId: dispatch.assignmentId, outcome: "failed", status: "terminal" });
@@ -28212,6 +28245,9 @@ function createKxmPiProducer(options = {}) {
28212
28245
  }
28213
28246
  function resolveModelForRequest(request) {
28214
28247
  if (request.model) {
28248
+ if (request.provider) {
28249
+ return { provider: request.provider.toLowerCase(), model: request.model, thinking: request.thinking };
28250
+ }
28215
28251
  const parsed2 = parseModelString(request.model);
28216
28252
  return {
28217
28253
  provider: request.provider?.toLowerCase() ?? parsed2.provider,
@@ -28222,6 +28258,9 @@ function createKxmPiProducer(options = {}) {
28222
28258
  if (options.resolveModel) {
28223
28259
  const resolved = options.resolveModel(request.agentId, request.runId);
28224
28260
  if (resolved && resolved.model) {
28261
+ if (resolved.provider) {
28262
+ return { provider: resolved.provider.toLowerCase(), model: resolved.model, thinking: resolved.thinking };
28263
+ }
28225
28264
  const parsed2 = parseModelString(resolved.model);
28226
28265
  return {
28227
28266
  provider: resolved.provider?.toLowerCase() ?? parsed2.provider,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.60",
3
+ "version": "0.7.63",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -854,15 +854,41 @@ function invokeProducer(
854
854
  producer: KxmProducer,
855
855
  request: KxmProducerRequest,
856
856
  ): Promise<{ result?: KxmProducerResult; error: boolean; errorCode?: string }> {
857
+ // The reason a producer refused is the single most useful fact about a failed attempt,
858
+ // and it used to be discarded here: a live drive that could not authenticate read in the
859
+ // event log exactly like a simulation that ran fine. But the message is producer-
860
+ // controlled text and must never reach durable records as-is — a producer that throws
861
+ // its capability (the hygiene contract tests exactly that) would persist the secret.
862
+ // So: redact the attempt capability and anything capability-shaped, then keep only a
863
+ // leading machine-code identifier (lowercase segments joined by underscores). Anything
864
+ // else — prose, free text, a redacted-to-empty message — records as producer_error.
865
+ const reason = (error: unknown): string => {
866
+ // Never throw while classifying a throw: a producer can reject with anything
867
+ // (Object.create(null) defeats String(), an Error whose message is not a string
868
+ // defeats split()), and a throwing classifier would reroute a producer_rejected
869
+ // settle into the execution-error path — the exact invisibility this exists to fix.
870
+ try {
871
+ const raw = error instanceof Error ? error.message : error;
872
+ const message = typeof raw === "string" ? raw : "";
873
+ let text = message;
874
+ if (request.capability) text = text.split(request.capability).join("[redacted]");
875
+ text = text.replace(/kxmcap_[A-Za-z0-9_-]+/g, "[redacted]");
876
+ const match = /^[a-z][a-z0-9]*(?:_[a-z0-9]+){1,3}/.exec(text);
877
+ const code = match?.[0] ?? "";
878
+ return code.length > 0 && code.length <= 64 ? code : "producer_error";
879
+ } catch {
880
+ return "producer_error";
881
+ }
882
+ };
857
883
  let pending: Promise<KxmProducerResult>;
858
884
  try {
859
885
  pending = Promise.resolve(producer.produce(request));
860
- } catch {
861
- return Promise.resolve({ error: true });
886
+ } catch (error) {
887
+ return Promise.resolve({ error: true, errorCode: reason(error) });
862
888
  }
863
889
  return pending.then(
864
890
  (result) => ({ result, error: false }),
865
- () => ({ error: true }),
891
+ (error) => ({ error: true, errorCode: reason(error) }),
866
892
  );
867
893
  }
868
894
 
@@ -1284,6 +1310,29 @@ function resolveProducerRoute(
1284
1310
  const parsed = parse(readFileSync(agentFile, "utf8")) as Record<string, unknown>;
1285
1311
  if (typeof parsed?.model === "string") {
1286
1312
  agentModel = parsed.model;
1313
+ } else if (parsed?.model && typeof parsed.model === "object" && !Array.isArray(parsed.model)) {
1314
+ // The agent schema requires the object form (`{provider, model}`); reading only the
1315
+ // string form left every agent without a route unless it was literally named
1316
+ // `implementer`, which alone hit the hard-coded fallback below. A declared model
1317
+ // must actually drive the producer route. Model ids may themselves be namespaced
1318
+ // (`qwen/qwen3-coder-plus` under `openrouter`) — the provider is the only field
1319
+ // that may not contain the separator, because it is what the selector splits on.
1320
+ // A malformed declaration is an error, not an absence: falling through to the
1321
+ // fallback would silently route an agent that declared `openrouter` to Grok.
1322
+ const declared = parsed.model as { provider?: unknown; model?: unknown };
1323
+ const providerOk = typeof declared.provider === "string" && declared.provider.length > 0 && !declared.provider.includes("/");
1324
+ const modelOk = typeof declared.model === "string" && declared.model.length > 0 && !declared.model.startsWith("/") && !declared.model.endsWith("/");
1325
+ if (providerOk && modelOk) {
1326
+ agentModel = `${declared.provider}/${declared.model}`;
1327
+ } else {
1328
+ return {
1329
+ error: {
1330
+ reason: "step_unsupported",
1331
+ field: "model",
1332
+ detail: "producer_route_unsupported: invalid model declaration",
1333
+ },
1334
+ };
1335
+ }
1287
1336
  }
1288
1337
  } catch {
1289
1338
  // ignore
@@ -1755,7 +1804,7 @@ function appendExecuting(context: KxmRuntimeContext, dispatch: PreparedDispatch)
1755
1804
  }
1756
1805
 
1757
1806
  type MemberWork =
1758
- | { attemptId: string; invoked: true; produced: { result?: KxmProducerResult; error: boolean } }
1807
+ | { attemptId: string; invoked: true; produced: { result?: KxmProducerResult; error: boolean; errorCode?: string } }
1759
1808
  | { attemptId: string; invoked: false; skipped: true }
1760
1809
  | { attemptId: string; invoked: false; appendFailed: true; error: unknown };
1761
1810
 
@@ -1896,7 +1945,7 @@ async function drivePanel(
1896
1945
  });
1897
1946
  };
1898
1947
 
1899
- const settleInvoked = (member: PreparedDispatch, produced: { result?: KxmProducerResult; error: boolean }): boolean => {
1948
+ const settleInvoked = (member: PreparedDispatch, produced: { result?: KxmProducerResult; error: boolean; errorCode?: string }): boolean => {
1900
1949
  if (produced.result?.effectUncertain) {
1901
1950
  settlementFailed = true;
1902
1951
  stopBirths = true;
@@ -1907,7 +1956,7 @@ async function drivePanel(
1907
1956
  if (kxmPanelDispatchSeams.failSettleMember?.(member)) {
1908
1957
  throw runtimeError("run_events_illegal", runId, "member settlement write failed");
1909
1958
  }
1910
- settleMember(context, member, produced.result, produced.error);
1959
+ settleMember(context, member, produced.result, produced.error, produced.errorCode);
1911
1960
  });
1912
1961
  return true;
1913
1962
  } catch (err) {
@@ -2069,6 +2118,7 @@ function settleMember(
2069
2118
  dispatch: PreparedDispatch,
2070
2119
  result: KxmProducerResult | undefined,
2071
2120
  produceError: boolean,
2121
+ produceErrorCode?: string,
2072
2122
  ): void {
2073
2123
  const run = requireRun(context, dispatch.run.runId);
2074
2124
  const state = foldStoredKxmRun(context, run);
@@ -2123,7 +2173,10 @@ function settleMember(
2123
2173
  stepId: dispatch.stepId,
2124
2174
  assignmentId: dispatch.assignmentId,
2125
2175
  attemptId: dispatch.attemptId,
2126
- harness: result?.harness ?? (dispatch.producerId === "pi" ? "pi" : "driver-simulated"),
2176
+ // Label the producer that actually ran: the old fallback printed "driver-simulated"
2177
+ // for every producer that was not literally "pi", so a live oneshot drive that
2178
+ // failed authentication read in the log as a simulation.
2179
+ harness: result?.harness ?? dispatch.producerId,
2127
2180
  provider: result?.provider ?? "simulated",
2128
2181
  requestedModel: result?.requestedModel ?? "simulated",
2129
2182
  effectiveModel: result?.effectiveModel ?? result?.requestedModel ?? "simulated",
@@ -2139,6 +2192,7 @@ function settleMember(
2139
2192
  assignmentId: dispatch.assignmentId,
2140
2193
  resultClass: "producer_rejected",
2141
2194
  status: "result_recorded",
2195
+ ...(produceErrorCode !== undefined ? { producerError: produceErrorCode } : {}),
2142
2196
  });
2143
2197
  push("attempt.status_changed", { attemptId: dispatch.attemptId, status: "terminal" });
2144
2198
  push("assignment.terminal", { assignmentId: dispatch.assignmentId, outcome: "failed", status: "terminal" });
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
8
8
  import { deliverInboxNotification } from "./inbox.ts";
9
9
  import type { HubEvent, MessageRecord } from "./protocol.ts";
10
10
 
11
- const VERSION = "0.7.60";
11
+ const VERSION = "0.7.63";
12
12
  const inbox = new Map<string, MessageRecord>();
13
13
  const notifiedInbox = new Set<string>();
14
14
  let meshClient: HubClient | undefined;
@@ -462,6 +462,13 @@ export function createKxmPiProducer(options: KxmPiProducerOptions = {}): KxmPiPr
462
462
 
463
463
  function resolveModelForRequest(request: KxmProducerRequest): { provider: string; model: string; thinking?: string | undefined } {
464
464
  if (request.model) {
465
+ // A separate provider field means the model id is bare and may itself be
466
+ // namespaced (`qwen/qwen3-coder-plus` under `openrouter`); reparsing it as a
467
+ // provider/model selector strips the namespace and sends the wrong model. Only
468
+ // parse when the provider must come from the string itself.
469
+ if (request.provider) {
470
+ return { provider: request.provider.toLowerCase(), model: request.model, thinking: request.thinking };
471
+ }
465
472
  const parsed = parseModelString(request.model);
466
473
  return {
467
474
  provider: request.provider?.toLowerCase() ?? parsed.provider,
@@ -472,6 +479,9 @@ export function createKxmPiProducer(options: KxmPiProducerOptions = {}): KxmPiPr
472
479
  if (options.resolveModel) {
473
480
  const resolved = options.resolveModel(request.agentId, request.runId);
474
481
  if (resolved && resolved.model) {
482
+ if (resolved.provider) {
483
+ return { provider: resolved.provider.toLowerCase(), model: resolved.model, thinking: resolved.thinking };
484
+ }
475
485
  const parsed = parseModelString(resolved.model);
476
486
  return {
477
487
  provider: resolved.provider?.toLowerCase() ?? parsed.provider,
@@ -364,6 +364,7 @@
364
364
  "runPlanHash": { "$ref": "common.schema.json#/$defs/sha256" },
365
365
  "capabilityHash": { "$ref": "common.schema.json#/$defs/sha256" },
366
366
  "resultClass": { "enum": ["outcome", "outcome_unknown", "producer_rejected", "cancelled"] },
367
+ "producerError": { "type": "string", "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+){1,3}$", "maxLength": 64, "description": "Machine-code reason a producer refused (produce threw), present only for resultClass producer_rejected. Capability material is redacted before extraction; anything unclassifiable records as producer_error." },
367
368
  "status": { "$ref": "common.schema.json#/$defs/lifecycleStatus" },
368
369
  "previousStatus": { "$ref": "common.schema.json#/$defs/lifecycleStatus" },
369
370
  "outcome": { "$ref": "common.schema.json#/$defs/identifier" },