@kontextmind/kxm 0.6.0 → 0.7.0

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 (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/agents/coordinator.yaml +9 -0
  3. package/.kxm/agents/critic-arch.yaml +13 -0
  4. package/.kxm/agents/critic-cli.yaml +13 -0
  5. package/.kxm/agents/implementer.yaml +13 -0
  6. package/.kxm/gates.yaml +8 -0
  7. package/.kxm/producers.yaml +22 -0
  8. package/.kxm/project.yaml +15 -0
  9. package/.kxm/roles/writer.yaml +5 -0
  10. package/.kxm/workflows/default.yaml +47 -0
  11. package/CHANGELOG.md +39 -7
  12. package/README.md +1 -0
  13. package/docs/README.md +2 -0
  14. package/docs/agent-skills.md +118 -0
  15. package/docs/architecture.md +1 -1
  16. package/docs/assignment-runner.md +21 -8
  17. package/docs/configuration.md +2 -2
  18. package/docs/kxm-handbook.md +3 -3
  19. package/docs/operator-pi-packages.md +63 -0
  20. package/docs/skills/repo-work-delivery.md +102 -0
  21. package/docs/test-matrix.md +4 -3
  22. package/docs/troubleshooting.md +19 -0
  23. package/docs/vnext/validation.md +9 -0
  24. package/docs/webhook-workflows.md +2 -2
  25. package/examples/README.md +1 -1
  26. package/package.json +16 -17
  27. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  28. package/plugins/kxm/README.md +1 -1
  29. package/plugins/kxm/dist/cli.js +6875 -3672
  30. package/plugins/kxm/dist/core.js +214 -34
  31. package/plugins/kxm/dist/extension.js +7721 -85
  32. package/plugins/kxm/dist/mcp-server.js +75 -21
  33. package/plugins/kxm/dist/runtime.js +5403 -1014
  34. package/plugins/kxm/dist/server.js +3008 -2268
  35. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5751 -631
  36. package/plugins/kxm/package.json +1 -1
  37. package/plugins/kxm/skills/SUITE.md +5 -0
  38. package/plugins/kxm/skills/hints.json +73 -0
  39. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  40. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  41. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  42. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  43. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  44. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +34 -0
  45. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  46. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  47. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  48. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +35 -0
  49. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  50. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  51. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  52. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  53. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  54. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  55. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  56. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  57. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  58. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  59. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  60. package/plugins/kxm/src/autocomplete.ts +9 -3
  61. package/plugins/kxm/src/cli.ts +1077 -11
  62. package/plugins/kxm/src/commands.ts +150 -8
  63. package/plugins/kxm/src/config.ts +7 -4
  64. package/plugins/kxm/src/context-packet.ts +172 -0
  65. package/plugins/kxm/src/extension.ts +36 -1
  66. package/plugins/kxm/src/external-effects.ts +356 -7
  67. package/plugins/kxm/src/hub.ts +2 -4
  68. package/plugins/kxm/src/improve.ts +72 -0
  69. package/plugins/kxm/src/mcp-server.ts +1 -1
  70. package/plugins/kxm/src/model-inventory.ts +127 -0
  71. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  72. package/plugins/kxm/src/policy-draft.mjs +565 -0
  73. package/plugins/kxm/src/price-calc.ts +17 -18
  74. package/plugins/kxm/src/prices.ts +32 -16
  75. package/plugins/kxm/src/producers.ts +71 -0
  76. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  77. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  78. package/plugins/kxm/src/role.ts +375 -0
  79. package/plugins/kxm/src/routing.ts +99 -1
  80. package/plugins/kxm/src/session-work.ts +9 -2
  81. package/plugins/kxm/src/studio-layout.ts +660 -17
  82. package/plugins/kxm/src/suggest.ts +7 -13
  83. package/plugins/kxm/src/telemetry.ts +82 -0
  84. package/plugins/kxm/src/tui.ts +140 -0
  85. package/plugins/kxm/src/vnext-config.ts +15 -110
  86. package/plugins/kxm/src/vnext-engine.ts +198 -62
  87. package/plugins/kxm/src/vnext-harness.ts +263 -81
  88. package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
  89. package/plugins/kxm/src/vnext-oneshot-process.ts +149 -0
  90. package/plugins/kxm/src/vnext-oneshot-producer.ts +170 -233
  91. package/plugins/kxm/src/vnext-runtime-store.ts +35 -1
  92. package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
  93. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  94. package/plugins/kxm/src/workflow-manager.ts +392 -0
  95. package/plugins/kxm/src/workflow-tui.ts +255 -0
  96. package/schemas/policy-draft/README.md +17 -0
  97. package/schemas/policy-draft/model.v2.schema.json +140 -0
  98. package/schemas/policy-draft/role.v2.schema.json +91 -0
  99. package/schemas/vnext/role.schema.json +76 -0
  100. package/schemas/vnext/run-event.schema.json +1 -0
  101. package/scripts/assignment-run.d.mts +1 -1
  102. package/scripts/assignment-run.mjs +44 -35
  103. package/scripts/check-generated.mjs +33 -9
  104. package/scripts/emit-codex-artifacts.mjs +255 -11
  105. package/scripts/harness-run.d.mts +12 -4
  106. package/scripts/harness-run.mjs +65 -17
  107. package/scripts/kxm-hub.mjs +6 -0
  108. package/scripts/native-critic.d.mts +5 -0
  109. package/scripts/native-critic.mjs +60 -0
  110. package/.kxm/config/README.md +0 -5
  111. package/.kxm/config/agents.json +0 -43
  112. package/.kxm/config/env.example +0 -56
  113. package/.kxm/config/update.example.yaml +0 -9
  114. package/.kxm/config/workflows/fix.json +0 -160
  115. package/.kxm/config/workflows/jira-development.json +0 -116
  116. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  117. package/.kxm/config/workflows/v04-dogfood.json +0 -72
@@ -1,7 +1,13 @@
1
1
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { parse } from "yaml";
5
+ import { isProducerAdmitted, listRoleBindings } from "./producers.ts";
2
6
  import {
3
7
  buildFormalContextPacket,
8
+ buildHandoffManifest,
4
9
  formatContextPacketForPrompt,
10
+ pruneContextPacket,
5
11
  type FormalContextPacketV2,
6
12
  type HandoffManifestV1,
7
13
  } from "./context-packet.ts";
@@ -116,6 +122,8 @@ export interface VnextProducerRequest {
116
122
 
117
123
  export interface VnextProducerResult {
118
124
  readonly outcome: string;
125
+ /** Host cannot attest termination; retain the issued attempt, never replay or settle it. */
126
+ readonly effectUncertain?: true;
119
127
  readonly costBasis?: "metered" | "unmetered" | "unknown";
120
128
  readonly costUsd?: number | null;
121
129
  readonly tokensIn?: number | null;
@@ -136,7 +144,7 @@ export interface VnextProducerResult {
136
144
  }
137
145
 
138
146
  export interface VnextProducer {
139
- readonly id: "driver-simulated" | "pi" | "oneshot" | string;
147
+ readonly id: "driver-simulated" | "pi" | "oneshot";
140
148
  produce(request: VnextProducerRequest): Promise<VnextProducerResult>;
141
149
  }
142
150
 
@@ -332,7 +340,7 @@ export async function driveVnextRun(
332
340
  context: VnextRuntimeContext,
333
341
  runId: string,
334
342
  producer: VnextProducer,
335
- options: { allowLimits?: boolean } = {},
343
+ options: { allowLimits?: boolean; liveMode?: boolean } = {},
336
344
  ): Promise<VnextRunDriveResult> {
337
345
  requireTrustedProducer(producer);
338
346
  const run = requireRun(context, runId);
@@ -347,7 +355,7 @@ async function driveAdmitted(
347
355
  runId: string,
348
356
  producer: VnextProducer,
349
357
  token: string,
350
- options: { allowLimits?: boolean } = {},
358
+ options: { allowLimits?: boolean; liveMode?: boolean } = {},
351
359
  ): Promise<VnextRunDriveResult> {
352
360
  const run = requireRun(context, runId);
353
361
  if (run.status === "preparing") {
@@ -407,7 +415,11 @@ export class VnextRunScheduler {
407
415
  return new VnextRunScheduler(context, bundle.configRevision);
408
416
  }
409
417
 
410
- enqueue(runId: string, producer: VnextProducer): Promise<VnextRunDriveResult> {
418
+ enqueue(
419
+ runId: string,
420
+ producer: VnextProducer,
421
+ options: { allowLimits?: boolean; liveMode?: boolean } = {},
422
+ ): Promise<VnextRunDriveResult> {
411
423
  requireTrustedProducer(producer);
412
424
  const policy = vnextSchedulerPolicy(this.context.eventStore.path);
413
425
  if (!policy || policy.configRevision !== this.configRevision) {
@@ -421,7 +433,7 @@ export class VnextRunScheduler {
421
433
  runId,
422
434
  envelope.revisions.config,
423
435
  envelope.projectLimits.maxConcurrentRuns,
424
- (token) => driveAdmitted(this.context, runId, producer, token),
436
+ (token) => driveAdmitted(this.context, runId, producer, token, options),
425
437
  ) as Promise<VnextRunDriveResult>;
426
438
  } catch (error) {
427
439
  return Promise.reject(error);
@@ -471,7 +483,7 @@ export function verifyVnextAttemptCapability(
471
483
  function invokeProducer(
472
484
  producer: VnextProducer,
473
485
  request: VnextProducerRequest,
474
- ): Promise<{ result?: VnextProducerResult; error: boolean }> {
486
+ ): Promise<{ result?: VnextProducerResult; error: boolean; errorCode?: string }> {
475
487
  let pending: Promise<VnextProducerResult>;
476
488
  try {
477
489
  pending = Promise.resolve(producer.produce(request));
@@ -884,6 +896,79 @@ function unreconciledPanelAttemptId(state: VnextRunState): string | undefined {
884
896
  return undefined;
885
897
  }
886
898
 
899
+ function resolveProducerRoute(
900
+ projectRoot: string,
901
+ step: VnextCompiledStep,
902
+ agentId: string,
903
+ ): { provider: string; model: string; selector: string } | { error: Omit<VnextRunHandoff, "stepId"> } {
904
+ let agentModel: string | undefined;
905
+ const agentFile = join(projectRoot, ".kxm", "agents", `${agentId}.yaml`);
906
+ if (existsSync(agentFile)) {
907
+ try {
908
+ const parsed = parse(readFileSync(agentFile, "utf8")) as Record<string, unknown>;
909
+ if (typeof parsed?.model === "string") {
910
+ agentModel = parsed.model;
911
+ }
912
+ } catch {
913
+ // ignore
914
+ }
915
+ }
916
+
917
+ let selector = typeof step.model === "string" ? step.model : agentModel;
918
+ if (!selector) {
919
+ if (agentId === "implementer") {
920
+ selector = "xai/grok-4.6";
921
+ }
922
+ }
923
+ if (!selector) {
924
+ return {
925
+ error: {
926
+ reason: "step_unsupported",
927
+ field: "model",
928
+ detail: "producer_route_unsupported: agent or step has no model declared",
929
+ },
930
+ };
931
+ }
932
+
933
+ const slash = selector.indexOf("/");
934
+ if (slash <= 0 || slash === selector.length - 1) {
935
+ return {
936
+ error: {
937
+ reason: "step_unsupported",
938
+ field: "model",
939
+ detail: `producer_route_unsupported: invalid selector '${selector}'`,
940
+ },
941
+ };
942
+ }
943
+
944
+ if (!isProducerAdmitted(projectRoot, selector)) {
945
+ return {
946
+ error: {
947
+ reason: "step_unsupported",
948
+ field: "model",
949
+ detail: `producer_route_unsupported: model '${selector}' is not admitted`,
950
+ },
951
+ };
952
+ }
953
+
954
+ const role = agentId === "implementer" ? "writer" : agentId;
955
+ const roleBindings = listRoleBindings(projectRoot);
956
+ const roster = roleBindings[role];
957
+ if (roster && !roster.includes(selector)) {
958
+ return {
959
+ error: {
960
+ reason: "step_unsupported",
961
+ field: "model",
962
+ detail: `producer_route_unsupported: model '${selector}' not in role '${role}' roster`,
963
+ },
964
+ };
965
+ }
966
+
967
+ const provider = selector.slice(0, slash);
968
+ const model = selector.slice(slash + 1);
969
+ return { provider, model, selector };
970
+ }
971
+
887
972
  function prepareDispatch(
888
973
  context: VnextRuntimeContext,
889
974
  runId: string,
@@ -1022,8 +1107,22 @@ function prepareDispatch(
1022
1107
  };
1023
1108
  }
1024
1109
 
1025
- const unsupported = unsupportedStep(plan, step);
1110
+ const unsupported = unsupportedStep(plan, step, producerId);
1026
1111
  if (unsupported) return { kind: "return", state, handoff: { ...unsupported, stepId } };
1112
+
1113
+ let resolvedRoute: { provider: string; model: string; selector: string } | undefined;
1114
+ if (producerId !== "driver-simulated") {
1115
+ const allowed = step.assignments.allowedAgents;
1116
+ const agentId = (allowed && allowed.length > 0 && allowed[0])
1117
+ ? allowed[0]!
1118
+ : (step.kind === "agent" || step.kind === "moa" ? step.agent : "coordinator");
1119
+ const routeResult = resolveProducerRoute(context.projectRoot, step, agentId);
1120
+ if ("error" in routeResult) {
1121
+ return { kind: "return", state, handoff: { ...routeResult.error, stepId } };
1122
+ }
1123
+ resolvedRoute = routeResult;
1124
+ }
1125
+
1027
1126
  const used = state.stepAttempts[stepId] ?? 0;
1028
1127
  if (used >= step.maxAttempts) {
1029
1128
  return { kind: "return", ...failBudget(context, run, plan, state, "budget_step_attempts", stepId) };
@@ -1056,6 +1155,7 @@ function prepareDispatch(
1056
1155
  stepAttempt,
1057
1156
  enterRunning: true,
1058
1157
  producerId,
1158
+ resolvedRoute,
1059
1159
  });
1060
1160
  return {
1061
1161
  kind: "panel",
@@ -1114,6 +1214,7 @@ function birthMember(
1114
1214
  stepAttempt: number;
1115
1215
  enterRunning?: boolean | undefined;
1116
1216
  producerId?: ("driver-simulated" | "pi" | string) | undefined;
1217
+ resolvedRoute?: { provider: string; model: string; selector: string } | undefined;
1117
1218
  },
1118
1219
  ): PreparedDispatch {
1119
1220
  const run = requireRun(context, input.run.runId);
@@ -1121,11 +1222,27 @@ function birthMember(
1121
1222
  if (!birthAllowed(folded, input.step)) {
1122
1223
  throw runtimeError("run_events_illegal", run.runId, "member birth is not legal");
1123
1224
  }
1225
+ const promptText = context.eventStore.getRunPrompt(run.runId);
1226
+ if (promptText === undefined) {
1227
+ throw runtimeError("run_prompt_mismatch", run.runId, "prompt text missing from accepted run prompt store");
1228
+ }
1229
+ const promptHash = createHash("sha256").update(promptText, "utf8").digest("hex");
1230
+ const expectedHash = run.promptSha256.replace(/^sha256:/, "");
1231
+ if (promptHash !== expectedHash) {
1232
+ throw runtimeError("run_prompt_mismatch", run.runId, "prompt text does not match accepted promptSha256");
1233
+ }
1124
1234
  const born = folded.currentStep?.panel.order.length ?? 0;
1125
1235
  const allowed = input.step.assignments.allowedAgents;
1126
1236
  const agentId = (allowed && allowed.length > born && allowed[born])
1127
1237
  ? allowed[born]!
1128
1238
  : (input.step.kind === "agent" || input.step.kind === "moa" ? input.step.agent : "coordinator");
1239
+ let resolvedRoute = input.resolvedRoute;
1240
+ if (!resolvedRoute && input.producerId && input.producerId !== "driver-simulated") {
1241
+ const routeResult = resolveProducerRoute(context.projectRoot, input.step, agentId);
1242
+ if (!("error" in routeResult)) {
1243
+ resolvedRoute = routeResult;
1244
+ }
1245
+ }
1129
1246
  const assignmentId = newVnextAssignmentId();
1130
1247
  const attemptId = newVnextAttemptId();
1131
1248
  const minted = mintCapabilitySecret();
@@ -1147,7 +1264,12 @@ function birthMember(
1147
1264
  push("assignment.created", { assignmentId, stepId: input.stepId, stepAttempt: input.stepAttempt, agentId, status: "created" });
1148
1265
  push("assignment.accepted", { assignmentId, status: "accepted" });
1149
1266
  push("attempt.created", { attemptId, assignmentId, status: "created" });
1150
- push("assignment.dispatched", { assignmentId, capabilityHash: minted.hash, status: "dispatched" });
1267
+ push("assignment.dispatched", {
1268
+ assignmentId,
1269
+ capabilityHash: minted.hash,
1270
+ status: "dispatched",
1271
+ ...(resolvedRoute ? { model: { provider: resolvedRoute.provider, model: resolvedRoute.model } } : {}),
1272
+ });
1151
1273
  push("attempt.status_changed", { attemptId, status: "starting" });
1152
1274
  if (input.enterRunning) {
1153
1275
  push("step.status_changed", { stepId: input.stepId, status: "running", previousStatus: "preparing" });
@@ -1165,16 +1287,16 @@ function birthMember(
1165
1287
  });
1166
1288
  const next = foldStoredVnextRun(context, run);
1167
1289
  persistVnextRunState(context, run.runId, next, events[events.length - 1]!.sequence);
1168
- const contextPacket = buildFormalContextPacket({
1290
+ const rawContextPacket = buildFormalContextPacket({
1169
1291
  project: run.projectId,
1170
1292
  targetRole: agentId,
1171
1293
  task: {
1172
1294
  taskId: run.runId,
1173
1295
  stepId: input.stepId,
1174
1296
  stepAttempt: input.stepAttempt,
1175
- objective: input.step.description ?? input.step.instructions ?? input.stepId,
1297
+ objective: promptText,
1176
1298
  allowedOutcomes: [...input.step.outcomes],
1177
- permissionCeiling: "edit",
1299
+ permissionCeiling: Object.values(input.step.repositories).some((access) => access === "write") ? "edit" : "read-only",
1178
1300
  },
1179
1301
  acceptanceCriteria: input.step.requiredEvidence.map((ev) => ({
1180
1302
  id: ev.key,
@@ -1189,6 +1311,7 @@ function birthMember(
1189
1311
  settledDecisions: [],
1190
1312
  },
1191
1313
  });
1314
+ const { packet: contextPacket } = pruneContextPacket(rawContextPacket);
1192
1315
  const generatedPrompt = formatContextPacketForPrompt(contextPacket);
1193
1316
 
1194
1317
  const member: PreparedDispatch = {
@@ -1215,7 +1338,9 @@ function birthMember(
1215
1338
  allowedOutcomes: input.step.outcomes,
1216
1339
  signal: controller.signal,
1217
1340
  prompt: input.step.instructions ? `${input.step.instructions}\n\n${generatedPrompt}` : generatedPrompt,
1341
+ thinking: input.stepAttempt <= 1 ? "low" : "medium",
1218
1342
  contextPacket,
1343
+ ...(resolvedRoute ? { provider: resolvedRoute.provider, model: resolvedRoute.model } : {}),
1219
1344
  },
1220
1345
  controller,
1221
1346
  state: next,
@@ -1391,6 +1516,11 @@ async function drivePanel(
1391
1516
  };
1392
1517
 
1393
1518
  const settleInvoked = (member: PreparedDispatch, produced: { result?: VnextProducerResult; error: boolean }): boolean => {
1519
+ if (produced.result?.effectUncertain) {
1520
+ settlementFailed = true;
1521
+ stopBirths = true;
1522
+ return false;
1523
+ }
1394
1524
  try {
1395
1525
  context.eventStore.transaction(() => {
1396
1526
  if (vnextPanelDispatchSeams.failSettleMember?.(member)) {
@@ -1515,6 +1645,43 @@ async function drivePanel(
1515
1645
  }
1516
1646
  }
1517
1647
 
1648
+ function producerRoutingRecord(context: VnextRuntimeContext, dispatch: PreparedDispatch, result: VnextProducerResult, now: string): RoutingRecordV2 {
1649
+ const run = requireRun(context, dispatch.run.runId);
1650
+ if (result.costBasis === undefined || result.costBasis === null) {
1651
+ throw runtimeError("settle_missing_cost_basis", run.runId, `attempt settlement rejected: missing required costBasis for attempt ${dispatch.attemptId}`);
1652
+ }
1653
+ if (result.costBasis !== "metered" && result.costBasis !== "unmetered" && result.costBasis !== "unknown") {
1654
+ throw runtimeError("settle_invalid_cost_basis", run.runId, `attempt settlement rejected: invalid costBasis ${String(result.costBasis)}`);
1655
+ }
1656
+ if (result.costBasis === "metered" && (typeof result.costUsd !== "number" || !Number.isFinite(result.costUsd) || result.costUsd < 0)) {
1657
+ throw runtimeError("settle_invalid_cost", run.runId, "attempt settlement rejected: metered costBasis requires non-negative finite costUsd");
1658
+ }
1659
+ const envelope = loadVnextRunPlanEnvelope(context.eventStore, run);
1660
+ const rawHash = result.behavioralSha256 ?? behavioralConfigHash({
1661
+ requestedModel: result.requestedModel ?? "simulated",
1662
+ effectiveModel: result.effectiveModel ?? result.requestedModel ?? "simulated",
1663
+ agentRole: dispatch.step.kind === "agent" || dispatch.step.kind === "moa" ? dispatch.step.agent : undefined,
1664
+ toolPolicyVersion: envelope.revisions.toolPolicy,
1665
+ });
1666
+ const record: Record<string, unknown> = {
1667
+ schema: ROUTING_RECORD_V2_SCHEMA, recordedAt: now,
1668
+ project: run.projectId, runId: run.runId, stepId: dispatch.stepId,
1669
+ assignmentId: dispatch.assignmentId, attemptId: dispatch.attemptId,
1670
+ harness: result.harness ?? (dispatch.producerId === "pi" ? "pi" : "driver-simulated"),
1671
+ provider: result.provider ?? "simulated", requestedModel: result.requestedModel ?? "simulated",
1672
+ effectiveModel: result.effectiveModel ?? result.requestedModel ?? "simulated",
1673
+ behavioralSha256: rawHash.startsWith("sha256:") ? rawHash : `sha256:${rawHash}`,
1674
+ latencyMs: typeof result.latencyMs === "number" ? result.latencyMs : 0,
1675
+ costBasis: result.costBasis, costUsd: result.costUsd ?? null,
1676
+ retries: Math.max(0, dispatch.stepAttempt - 1),
1677
+ thinking: result.thinking ?? dispatch.request.thinking,
1678
+ };
1679
+ for (const field of ["agentRole", "contextTokens", "tokensIn", "tokensOut", "cacheReadTokens", "cacheWriteTokens", "priceRef", "providerMetadata"] as const) {
1680
+ if (result[field] !== undefined) record[field] = result[field];
1681
+ }
1682
+ return parseRoutingRecordV2(record);
1683
+ }
1684
+
1518
1685
  function settleMember(
1519
1686
  context: VnextRuntimeContext,
1520
1687
  dispatch: PreparedDispatch,
@@ -1551,6 +1718,8 @@ function settleMember(
1551
1718
  };
1552
1719
  push("attempt.status_changed", { attemptId: dispatch.attemptId, status: "settling" });
1553
1720
  if (cancelling) {
1721
+ push("routing.attempt.recorded", { routing: producerRoutingRecord(context, dispatch,
1722
+ result ?? { outcome: "cancelled", costBasis: "unknown", costUsd: null }, now) });
1554
1723
  push("assignment.result_recorded", { assignmentId: dispatch.assignmentId, resultClass: "cancelled", status: "result_recorded" });
1555
1724
  push("attempt.status_changed", { attemptId: dispatch.attemptId, status: "terminal" });
1556
1725
  push("assignment.terminal", { assignmentId: dispatch.assignmentId, outcome: "cancelled", status: "terminal" });
@@ -1592,57 +1761,10 @@ function settleMember(
1592
1761
  push("attempt.status_changed", { attemptId: dispatch.attemptId, status: "terminal" });
1593
1762
  push("assignment.terminal", { assignmentId: dispatch.assignmentId, outcome: "failed", status: "terminal" });
1594
1763
  } else {
1595
- if (!result || result.costBasis === undefined || result.costBasis === null) {
1764
+ if (!result) {
1596
1765
  throw runtimeError("settle_missing_cost_basis", run.runId, `attempt settlement rejected: missing required costBasis for attempt ${dispatch.attemptId}`);
1597
1766
  }
1598
- if (result.costBasis !== "metered" && result.costBasis !== "unmetered" && result.costBasis !== "unknown") {
1599
- throw runtimeError("settle_invalid_cost_basis", run.runId, `attempt settlement rejected: invalid costBasis ${String(result.costBasis)}`);
1600
- }
1601
- if (result.costBasis === "metered") {
1602
- if (typeof result.costUsd !== "number" || !Number.isFinite(result.costUsd) || result.costUsd < 0) {
1603
- throw runtimeError("settle_invalid_cost", run.runId, "attempt settlement rejected: metered costBasis requires non-negative finite costUsd");
1604
- }
1605
- }
1606
-
1607
- const envelope = loadVnextRunPlanEnvelope(context.eventStore, run);
1608
- const rawHash = result.behavioralSha256 ?? behavioralConfigHash({
1609
- requestedModel: result.requestedModel ?? "simulated",
1610
- effectiveModel: result.effectiveModel ?? result.requestedModel ?? "simulated",
1611
- agentRole: dispatch.step.kind === "agent" || dispatch.step.kind === "moa" ? dispatch.step.agent : undefined,
1612
- toolPolicyVersion: envelope.revisions.toolPolicy,
1613
- });
1614
- const behavioralSha256 = rawHash.startsWith("sha256:") ? rawHash : `sha256:${rawHash}`;
1615
-
1616
- const routingRecord: RoutingRecordV2 = {
1617
- schema: ROUTING_RECORD_V2_SCHEMA,
1618
- recordedAt: now,
1619
- project: run.projectId,
1620
- runId: run.runId,
1621
- stepId: dispatch.stepId,
1622
- assignmentId: dispatch.assignmentId,
1623
- attemptId: dispatch.attemptId,
1624
- harness: result.harness ?? (dispatch.producerId === "pi" ? "pi" : "driver-simulated"),
1625
- provider: result.provider ?? "simulated",
1626
- requestedModel: result.requestedModel ?? "simulated",
1627
- effectiveModel: result.effectiveModel ?? result.requestedModel ?? "simulated",
1628
- behavioralSha256,
1629
- latencyMs: typeof result.latencyMs === "number" ? result.latencyMs : 0,
1630
- costBasis: result.costBasis,
1631
- costUsd: result.costBasis === "metered" ? (result.costUsd ?? 0) : (result.costUsd ?? null),
1632
- retries: Math.max(0, dispatch.stepAttempt - 1),
1633
- };
1634
-
1635
- if (result.thinking !== undefined) routingRecord.thinking = result.thinking;
1636
- if (result.agentRole !== undefined) routingRecord.agentRole = result.agentRole;
1637
- if (result.contextTokens !== undefined) routingRecord.contextTokens = result.contextTokens;
1638
- if (result.tokensIn !== undefined) routingRecord.tokensIn = result.tokensIn;
1639
- if (result.tokensOut !== undefined) routingRecord.tokensOut = result.tokensOut;
1640
- if (result.cacheReadTokens !== undefined) routingRecord.cacheReadTokens = result.cacheReadTokens;
1641
- if (result.cacheWriteTokens !== undefined) routingRecord.cacheWriteTokens = result.cacheWriteTokens;
1642
- if (result.priceRef !== undefined) routingRecord.priceRef = result.priceRef;
1643
- if (result.providerMetadata !== undefined) routingRecord.providerMetadata = result.providerMetadata;
1644
-
1645
- push("routing.attempt.recorded", { routing: parseRoutingRecordV2(routingRecord) });
1767
+ push("routing.attempt.recorded", { routing: producerRoutingRecord(context, dispatch, result, now) });
1646
1768
 
1647
1769
  const outcome = typeof result.outcome === "string" ? result.outcome : undefined;
1648
1770
  const known = outcome !== undefined && dispatch.step.outcomes.includes(outcome);
@@ -1844,7 +1966,11 @@ function unsupportedLimit(envelope: VnextRunPlanEnvelope): VnextRunHandoff | und
1844
1966
  return undefined;
1845
1967
  }
1846
1968
 
1847
- function unsupportedStep(plan: VnextCompiledPlan, step: VnextCompiledStep): Omit<VnextRunHandoff, "stepId"> | undefined {
1969
+ function unsupportedStep(
1970
+ plan: VnextCompiledPlan,
1971
+ step: VnextCompiledStep,
1972
+ producerId?: "driver-simulated" | "pi" | string,
1973
+ ): Omit<VnextRunHandoff, "stepId"> | undefined {
1848
1974
  if (step.kind === "gate") throw runtimeError("run_plan_corrupt", plan.workflowId, `unsupportedStep called on gate without envelope; use unsupportedGateStep instead`);
1849
1975
  if (step.kind !== "agent" && step.kind !== "moa" && step.kind !== "approval" && step.kind !== "wait") {
1850
1976
  return { reason: "step_unsupported", field: "kind", detail: `step kind ${step.kind} is not executed in this slice` };
@@ -1883,6 +2009,13 @@ function unsupportedStep(plan: VnextCompiledPlan, step: VnextCompiledStep): Omit
1883
2009
  return { reason: "step_unsupported", field: "repositories", detail: `invalid repository access '${access}' on ${repoId}` };
1884
2010
  }
1885
2011
  }
2012
+ if (producerId !== "driver-simulated" && Object.values(step.repositories).some((access) => access === "write")) {
2013
+ return {
2014
+ reason: "step_unsupported",
2015
+ field: "repositories",
2016
+ detail: "live write steps are unsupported until writer sandboxing witness passes",
2017
+ };
2018
+ }
1886
2019
  return undefined;
1887
2020
  }
1888
2021
 
@@ -2364,9 +2497,12 @@ export function recoverVnextRun(
2364
2497
  const run = requireRun(context, runId);
2365
2498
  let state = foldStoredVnextRun(context, run);
2366
2499
 
2367
- if (state.status !== "blocked_uncertain" && state.currentStep?.effectState !== "blocked_uncertain") {
2500
+ if (state.status !== "blocked_uncertain" && state.status !== "cancelling" && state.currentStep?.effectState !== "blocked_uncertain") {
2368
2501
  throw runtimeError("run_events_illegal", runId, `run ${runId} is not in blocked_uncertain status`);
2369
2502
  }
2503
+ if (state.status === "cancelling" && state.currentStep?.effectState === "blocked_uncertain") {
2504
+ state = { ...state, status: "blocked_uncertain" };
2505
+ }
2370
2506
 
2371
2507
  const now = new Date().toISOString();
2372
2508
  let sequence = context.eventStore.nextSequence(runId);