@kontextmind/kxm 0.6.0 → 0.7.4

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 (136) 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 +7 -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 +4 -0
  14. package/docs/agent-skills.md +121 -0
  15. package/docs/architecture.md +1 -1
  16. package/docs/assignment-runner.md +21 -8
  17. package/docs/configuration.md +11 -2
  18. package/docs/getting-started.md +21 -0
  19. package/docs/kxm-handbook.md +3 -3
  20. package/docs/operations.md +24 -0
  21. package/docs/operator-pi-packages.md +63 -0
  22. package/docs/skills/repo-work-delivery.md +107 -0
  23. package/docs/skills.md +2 -0
  24. package/docs/test-matrix.md +4 -3
  25. package/docs/troubleshooting.md +41 -1
  26. package/docs/vnext/validation.md +9 -0
  27. package/docs/webhook-workflows.md +2 -2
  28. package/examples/README.md +1 -1
  29. package/package.json +16 -17
  30. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  31. package/plugins/kxm/README.md +1 -1
  32. package/plugins/kxm/dist/cli.js +8955 -3934
  33. package/plugins/kxm/dist/core.js +271 -34
  34. package/plugins/kxm/dist/extension.js +7759 -86
  35. package/plugins/kxm/dist/mcp-server.js +75 -21
  36. package/plugins/kxm/dist/runtime.js +5637 -1125
  37. package/plugins/kxm/dist/server.js +3125 -2260
  38. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5808 -565
  39. package/plugins/kxm/package.json +1 -1
  40. package/plugins/kxm/skills/SUITE.md +5 -0
  41. package/plugins/kxm/skills/hints.json +73 -0
  42. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  43. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  44. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  45. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  46. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  47. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
  48. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  49. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  50. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  51. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
  52. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  53. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  54. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  55. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  56. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  57. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  58. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  59. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  60. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  61. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  62. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  63. package/plugins/kxm/src/autocomplete.ts +9 -3
  64. package/plugins/kxm/src/cli.ts +1637 -73
  65. package/plugins/kxm/src/commands.ts +150 -8
  66. package/plugins/kxm/src/completion-install.ts +223 -0
  67. package/plugins/kxm/src/config.ts +7 -4
  68. package/plugins/kxm/src/context-packet.ts +172 -0
  69. package/plugins/kxm/src/database.ts +1 -1
  70. package/plugins/kxm/src/extension.ts +36 -1
  71. package/plugins/kxm/src/external-effects.ts +357 -8
  72. package/plugins/kxm/src/hub-env.ts +193 -0
  73. package/plugins/kxm/src/hub.ts +2 -4
  74. package/plugins/kxm/src/improve.ts +72 -0
  75. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  76. package/plugins/kxm/src/local-snapshot.ts +1 -1
  77. package/plugins/kxm/src/mcp-server.ts +1 -1
  78. package/plugins/kxm/src/model-inventory.ts +127 -0
  79. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  80. package/plugins/kxm/src/policy-draft.mjs +565 -0
  81. package/plugins/kxm/src/price-calc.ts +17 -18
  82. package/plugins/kxm/src/prices.ts +32 -16
  83. package/plugins/kxm/src/producers.ts +71 -0
  84. package/plugins/kxm/src/protocol.ts +111 -0
  85. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  86. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  87. package/plugins/kxm/src/role.ts +710 -0
  88. package/plugins/kxm/src/routing.ts +99 -1
  89. package/plugins/kxm/src/safety-integrity.ts +76 -0
  90. package/plugins/kxm/src/session-work.ts +9 -2
  91. package/plugins/kxm/src/sqlite.ts +76 -0
  92. package/plugins/kxm/src/store.ts +1 -1
  93. package/plugins/kxm/src/studio-layout.ts +660 -17
  94. package/plugins/kxm/src/suggest.ts +7 -13
  95. package/plugins/kxm/src/telemetry.ts +82 -0
  96. package/plugins/kxm/src/tui.ts +140 -0
  97. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  98. package/plugins/kxm/src/vnext-config.ts +53 -111
  99. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  100. package/plugins/kxm/src/vnext-engine.ts +214 -62
  101. package/plugins/kxm/src/vnext-harness.ts +267 -83
  102. package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
  103. package/plugins/kxm/src/vnext-oneshot-process.ts +166 -0
  104. package/plugins/kxm/src/vnext-oneshot-producer.ts +179 -224
  105. package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
  106. package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
  107. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  108. package/plugins/kxm/src/workflow-manager.ts +392 -0
  109. package/plugins/kxm/src/workflow-tui.ts +255 -0
  110. package/plugins/kxm/src/workflow.ts +144 -0
  111. package/schemas/policy-draft/README.md +17 -0
  112. package/schemas/policy-draft/model.v2.schema.json +140 -0
  113. package/schemas/policy-draft/role.v2.schema.json +91 -0
  114. package/schemas/vnext/role.schema.json +76 -0
  115. package/schemas/vnext/run-event.schema.json +1 -0
  116. package/scripts/assignment-run.d.mts +1 -1
  117. package/scripts/assignment-run.mjs +44 -35
  118. package/scripts/check-generated.mjs +33 -9
  119. package/scripts/emit-codex-artifacts.mjs +255 -11
  120. package/scripts/harness-run.d.mts +12 -4
  121. package/scripts/harness-run.mjs +65 -17
  122. package/scripts/kxm-bump-version.mjs +146 -0
  123. package/scripts/kxm-hub.mjs +150 -2
  124. package/scripts/kxm-publish-npm.mjs +3 -1
  125. package/scripts/kxm-release-github.mjs +3 -1
  126. package/scripts/kxm.mjs +0 -0
  127. package/scripts/native-critic.d.mts +5 -0
  128. package/scripts/native-critic.mjs +60 -0
  129. package/.kxm/config/README.md +0 -5
  130. package/.kxm/config/agents.json +0 -43
  131. package/.kxm/config/env.example +0 -56
  132. package/.kxm/config/update.example.yaml +0 -9
  133. package/.kxm/config/workflows/fix.json +0 -160
  134. package/.kxm/config/workflows/jira-development.json +0 -116
  135. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  136. 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";
@@ -88,6 +94,21 @@ import {
88
94
  parseRoutingRecordV2,
89
95
  behavioralConfigHash,
90
96
  } from "./routing.ts";
97
+ import {
98
+ assertCommandSeatbelt,
99
+ DESTRUCTIVE_COMMAND_PATTERNS,
100
+ isRtkBypassRequired,
101
+ assertPinnedSshHostKeyPolicy,
102
+ type RtkBypassContext,
103
+ } from "./safety-integrity.ts";
104
+
105
+ export {
106
+ assertCommandSeatbelt,
107
+ DESTRUCTIVE_COMMAND_PATTERNS,
108
+ isRtkBypassRequired,
109
+ assertPinnedSshHostKeyPolicy,
110
+ type RtkBypassContext,
111
+ };
91
112
 
92
113
  const trustedProducers = new WeakSet<object>();
93
114
  const CAPABILITY_PREFIX = "kxm-attempt-capability\0";
@@ -116,6 +137,8 @@ export interface VnextProducerRequest {
116
137
 
117
138
  export interface VnextProducerResult {
118
139
  readonly outcome: string;
140
+ /** Host cannot attest termination; retain the issued attempt, never replay or settle it. */
141
+ readonly effectUncertain?: true;
119
142
  readonly costBasis?: "metered" | "unmetered" | "unknown";
120
143
  readonly costUsd?: number | null;
121
144
  readonly tokensIn?: number | null;
@@ -136,7 +159,7 @@ export interface VnextProducerResult {
136
159
  }
137
160
 
138
161
  export interface VnextProducer {
139
- readonly id: "driver-simulated" | "pi" | "oneshot" | string;
162
+ readonly id: "driver-simulated" | "pi" | "oneshot";
140
163
  produce(request: VnextProducerRequest): Promise<VnextProducerResult>;
141
164
  }
142
165
 
@@ -332,7 +355,7 @@ export async function driveVnextRun(
332
355
  context: VnextRuntimeContext,
333
356
  runId: string,
334
357
  producer: VnextProducer,
335
- options: { allowLimits?: boolean } = {},
358
+ options: { allowLimits?: boolean; liveMode?: boolean } = {},
336
359
  ): Promise<VnextRunDriveResult> {
337
360
  requireTrustedProducer(producer);
338
361
  const run = requireRun(context, runId);
@@ -347,7 +370,7 @@ async function driveAdmitted(
347
370
  runId: string,
348
371
  producer: VnextProducer,
349
372
  token: string,
350
- options: { allowLimits?: boolean } = {},
373
+ options: { allowLimits?: boolean; liveMode?: boolean } = {},
351
374
  ): Promise<VnextRunDriveResult> {
352
375
  const run = requireRun(context, runId);
353
376
  if (run.status === "preparing") {
@@ -407,7 +430,11 @@ export class VnextRunScheduler {
407
430
  return new VnextRunScheduler(context, bundle.configRevision);
408
431
  }
409
432
 
410
- enqueue(runId: string, producer: VnextProducer): Promise<VnextRunDriveResult> {
433
+ enqueue(
434
+ runId: string,
435
+ producer: VnextProducer,
436
+ options: { allowLimits?: boolean; liveMode?: boolean } = {},
437
+ ): Promise<VnextRunDriveResult> {
411
438
  requireTrustedProducer(producer);
412
439
  const policy = vnextSchedulerPolicy(this.context.eventStore.path);
413
440
  if (!policy || policy.configRevision !== this.configRevision) {
@@ -421,7 +448,7 @@ export class VnextRunScheduler {
421
448
  runId,
422
449
  envelope.revisions.config,
423
450
  envelope.projectLimits.maxConcurrentRuns,
424
- (token) => driveAdmitted(this.context, runId, producer, token),
451
+ (token) => driveAdmitted(this.context, runId, producer, token, options),
425
452
  ) as Promise<VnextRunDriveResult>;
426
453
  } catch (error) {
427
454
  return Promise.reject(error);
@@ -471,7 +498,7 @@ export function verifyVnextAttemptCapability(
471
498
  function invokeProducer(
472
499
  producer: VnextProducer,
473
500
  request: VnextProducerRequest,
474
- ): Promise<{ result?: VnextProducerResult; error: boolean }> {
501
+ ): Promise<{ result?: VnextProducerResult; error: boolean; errorCode?: string }> {
475
502
  let pending: Promise<VnextProducerResult>;
476
503
  try {
477
504
  pending = Promise.resolve(producer.produce(request));
@@ -545,6 +572,7 @@ async function runPreparedCommandGate(
545
572
  definition: { readonly kind: "command"; readonly argv: readonly string[]; readonly timeoutMs: number; readonly cwd?: "control" },
546
573
  token: string,
547
574
  ): Promise<VnextRunDriveResult> {
575
+ assertCommandSeatbelt(definition.argv.join(" "));
548
576
  const storePath = context.eventStore.path;
549
577
  const observer = createCommandObserver({
550
578
  definition,
@@ -884,6 +912,79 @@ function unreconciledPanelAttemptId(state: VnextRunState): string | undefined {
884
912
  return undefined;
885
913
  }
886
914
 
915
+ function resolveProducerRoute(
916
+ projectRoot: string,
917
+ step: VnextCompiledStep,
918
+ agentId: string,
919
+ ): { provider: string; model: string; selector: string } | { error: Omit<VnextRunHandoff, "stepId"> } {
920
+ let agentModel: string | undefined;
921
+ const agentFile = join(projectRoot, ".kxm", "agents", `${agentId}.yaml`);
922
+ if (existsSync(agentFile)) {
923
+ try {
924
+ const parsed = parse(readFileSync(agentFile, "utf8")) as Record<string, unknown>;
925
+ if (typeof parsed?.model === "string") {
926
+ agentModel = parsed.model;
927
+ }
928
+ } catch {
929
+ // ignore
930
+ }
931
+ }
932
+
933
+ let selector = typeof step.model === "string" ? step.model : agentModel;
934
+ if (!selector) {
935
+ if (agentId === "implementer") {
936
+ selector = "xai/grok-4.6";
937
+ }
938
+ }
939
+ if (!selector) {
940
+ return {
941
+ error: {
942
+ reason: "step_unsupported",
943
+ field: "model",
944
+ detail: "producer_route_unsupported: agent or step has no model declared",
945
+ },
946
+ };
947
+ }
948
+
949
+ const slash = selector.indexOf("/");
950
+ if (slash <= 0 || slash === selector.length - 1) {
951
+ return {
952
+ error: {
953
+ reason: "step_unsupported",
954
+ field: "model",
955
+ detail: `producer_route_unsupported: invalid selector '${selector}'`,
956
+ },
957
+ };
958
+ }
959
+
960
+ if (!isProducerAdmitted(projectRoot, selector)) {
961
+ return {
962
+ error: {
963
+ reason: "step_unsupported",
964
+ field: "model",
965
+ detail: `producer_route_unsupported: model '${selector}' is not admitted`,
966
+ },
967
+ };
968
+ }
969
+
970
+ const role = agentId === "implementer" ? "writer" : agentId;
971
+ const roleBindings = listRoleBindings(projectRoot);
972
+ const roster = roleBindings[role];
973
+ if (roster && !roster.includes(selector)) {
974
+ return {
975
+ error: {
976
+ reason: "step_unsupported",
977
+ field: "model",
978
+ detail: `producer_route_unsupported: model '${selector}' not in role '${role}' roster`,
979
+ },
980
+ };
981
+ }
982
+
983
+ const provider = selector.slice(0, slash);
984
+ const model = selector.slice(slash + 1);
985
+ return { provider, model, selector };
986
+ }
987
+
887
988
  function prepareDispatch(
888
989
  context: VnextRuntimeContext,
889
990
  runId: string,
@@ -1022,8 +1123,22 @@ function prepareDispatch(
1022
1123
  };
1023
1124
  }
1024
1125
 
1025
- const unsupported = unsupportedStep(plan, step);
1126
+ const unsupported = unsupportedStep(plan, step, producerId);
1026
1127
  if (unsupported) return { kind: "return", state, handoff: { ...unsupported, stepId } };
1128
+
1129
+ let resolvedRoute: { provider: string; model: string; selector: string } | undefined;
1130
+ if (producerId !== "driver-simulated") {
1131
+ const allowed = step.assignments.allowedAgents;
1132
+ const agentId = (allowed && allowed.length > 0 && allowed[0])
1133
+ ? allowed[0]!
1134
+ : (step.kind === "agent" || step.kind === "moa" ? step.agent : "coordinator");
1135
+ const routeResult = resolveProducerRoute(context.projectRoot, step, agentId);
1136
+ if ("error" in routeResult) {
1137
+ return { kind: "return", state, handoff: { ...routeResult.error, stepId } };
1138
+ }
1139
+ resolvedRoute = routeResult;
1140
+ }
1141
+
1027
1142
  const used = state.stepAttempts[stepId] ?? 0;
1028
1143
  if (used >= step.maxAttempts) {
1029
1144
  return { kind: "return", ...failBudget(context, run, plan, state, "budget_step_attempts", stepId) };
@@ -1056,6 +1171,7 @@ function prepareDispatch(
1056
1171
  stepAttempt,
1057
1172
  enterRunning: true,
1058
1173
  producerId,
1174
+ resolvedRoute,
1059
1175
  });
1060
1176
  return {
1061
1177
  kind: "panel",
@@ -1114,6 +1230,7 @@ function birthMember(
1114
1230
  stepAttempt: number;
1115
1231
  enterRunning?: boolean | undefined;
1116
1232
  producerId?: ("driver-simulated" | "pi" | string) | undefined;
1233
+ resolvedRoute?: { provider: string; model: string; selector: string } | undefined;
1117
1234
  },
1118
1235
  ): PreparedDispatch {
1119
1236
  const run = requireRun(context, input.run.runId);
@@ -1121,11 +1238,27 @@ function birthMember(
1121
1238
  if (!birthAllowed(folded, input.step)) {
1122
1239
  throw runtimeError("run_events_illegal", run.runId, "member birth is not legal");
1123
1240
  }
1241
+ const promptText = context.eventStore.getRunPrompt(run.runId);
1242
+ if (promptText === undefined) {
1243
+ throw runtimeError("run_prompt_mismatch", run.runId, "prompt text missing from accepted run prompt store");
1244
+ }
1245
+ const promptHash = createHash("sha256").update(promptText, "utf8").digest("hex");
1246
+ const expectedHash = run.promptSha256.replace(/^sha256:/, "");
1247
+ if (promptHash !== expectedHash) {
1248
+ throw runtimeError("run_prompt_mismatch", run.runId, "prompt text does not match accepted promptSha256");
1249
+ }
1124
1250
  const born = folded.currentStep?.panel.order.length ?? 0;
1125
1251
  const allowed = input.step.assignments.allowedAgents;
1126
1252
  const agentId = (allowed && allowed.length > born && allowed[born])
1127
1253
  ? allowed[born]!
1128
1254
  : (input.step.kind === "agent" || input.step.kind === "moa" ? input.step.agent : "coordinator");
1255
+ let resolvedRoute = input.resolvedRoute;
1256
+ if (!resolvedRoute && input.producerId && input.producerId !== "driver-simulated") {
1257
+ const routeResult = resolveProducerRoute(context.projectRoot, input.step, agentId);
1258
+ if (!("error" in routeResult)) {
1259
+ resolvedRoute = routeResult;
1260
+ }
1261
+ }
1129
1262
  const assignmentId = newVnextAssignmentId();
1130
1263
  const attemptId = newVnextAttemptId();
1131
1264
  const minted = mintCapabilitySecret();
@@ -1147,7 +1280,12 @@ function birthMember(
1147
1280
  push("assignment.created", { assignmentId, stepId: input.stepId, stepAttempt: input.stepAttempt, agentId, status: "created" });
1148
1281
  push("assignment.accepted", { assignmentId, status: "accepted" });
1149
1282
  push("attempt.created", { attemptId, assignmentId, status: "created" });
1150
- push("assignment.dispatched", { assignmentId, capabilityHash: minted.hash, status: "dispatched" });
1283
+ push("assignment.dispatched", {
1284
+ assignmentId,
1285
+ capabilityHash: minted.hash,
1286
+ status: "dispatched",
1287
+ ...(resolvedRoute ? { model: { provider: resolvedRoute.provider, model: resolvedRoute.model } } : {}),
1288
+ });
1151
1289
  push("attempt.status_changed", { attemptId, status: "starting" });
1152
1290
  if (input.enterRunning) {
1153
1291
  push("step.status_changed", { stepId: input.stepId, status: "running", previousStatus: "preparing" });
@@ -1165,16 +1303,16 @@ function birthMember(
1165
1303
  });
1166
1304
  const next = foldStoredVnextRun(context, run);
1167
1305
  persistVnextRunState(context, run.runId, next, events[events.length - 1]!.sequence);
1168
- const contextPacket = buildFormalContextPacket({
1306
+ const rawContextPacket = buildFormalContextPacket({
1169
1307
  project: run.projectId,
1170
1308
  targetRole: agentId,
1171
1309
  task: {
1172
1310
  taskId: run.runId,
1173
1311
  stepId: input.stepId,
1174
1312
  stepAttempt: input.stepAttempt,
1175
- objective: input.step.description ?? input.step.instructions ?? input.stepId,
1313
+ objective: promptText,
1176
1314
  allowedOutcomes: [...input.step.outcomes],
1177
- permissionCeiling: "edit",
1315
+ permissionCeiling: Object.values(input.step.repositories).some((access) => access === "write") ? "edit" : "read-only",
1178
1316
  },
1179
1317
  acceptanceCriteria: input.step.requiredEvidence.map((ev) => ({
1180
1318
  id: ev.key,
@@ -1189,6 +1327,7 @@ function birthMember(
1189
1327
  settledDecisions: [],
1190
1328
  },
1191
1329
  });
1330
+ const { packet: contextPacket } = pruneContextPacket(rawContextPacket);
1192
1331
  const generatedPrompt = formatContextPacketForPrompt(contextPacket);
1193
1332
 
1194
1333
  const member: PreparedDispatch = {
@@ -1215,7 +1354,9 @@ function birthMember(
1215
1354
  allowedOutcomes: input.step.outcomes,
1216
1355
  signal: controller.signal,
1217
1356
  prompt: input.step.instructions ? `${input.step.instructions}\n\n${generatedPrompt}` : generatedPrompt,
1357
+ thinking: input.stepAttempt <= 1 ? "low" : "medium",
1218
1358
  contextPacket,
1359
+ ...(resolvedRoute ? { provider: resolvedRoute.provider, model: resolvedRoute.model } : {}),
1219
1360
  },
1220
1361
  controller,
1221
1362
  state: next,
@@ -1391,6 +1532,11 @@ async function drivePanel(
1391
1532
  };
1392
1533
 
1393
1534
  const settleInvoked = (member: PreparedDispatch, produced: { result?: VnextProducerResult; error: boolean }): boolean => {
1535
+ if (produced.result?.effectUncertain) {
1536
+ settlementFailed = true;
1537
+ stopBirths = true;
1538
+ return false;
1539
+ }
1394
1540
  try {
1395
1541
  context.eventStore.transaction(() => {
1396
1542
  if (vnextPanelDispatchSeams.failSettleMember?.(member)) {
@@ -1515,6 +1661,43 @@ async function drivePanel(
1515
1661
  }
1516
1662
  }
1517
1663
 
1664
+ function producerRoutingRecord(context: VnextRuntimeContext, dispatch: PreparedDispatch, result: VnextProducerResult, now: string): RoutingRecordV2 {
1665
+ const run = requireRun(context, dispatch.run.runId);
1666
+ if (result.costBasis === undefined || result.costBasis === null) {
1667
+ throw runtimeError("settle_missing_cost_basis", run.runId, `attempt settlement rejected: missing required costBasis for attempt ${dispatch.attemptId}`);
1668
+ }
1669
+ if (result.costBasis !== "metered" && result.costBasis !== "unmetered" && result.costBasis !== "unknown") {
1670
+ throw runtimeError("settle_invalid_cost_basis", run.runId, `attempt settlement rejected: invalid costBasis ${String(result.costBasis)}`);
1671
+ }
1672
+ if (result.costBasis === "metered" && (typeof result.costUsd !== "number" || !Number.isFinite(result.costUsd) || result.costUsd < 0)) {
1673
+ throw runtimeError("settle_invalid_cost", run.runId, "attempt settlement rejected: metered costBasis requires non-negative finite costUsd");
1674
+ }
1675
+ const envelope = loadVnextRunPlanEnvelope(context.eventStore, run);
1676
+ const rawHash = result.behavioralSha256 ?? behavioralConfigHash({
1677
+ requestedModel: result.requestedModel ?? "simulated",
1678
+ effectiveModel: result.effectiveModel ?? result.requestedModel ?? "simulated",
1679
+ agentRole: dispatch.step.kind === "agent" || dispatch.step.kind === "moa" ? dispatch.step.agent : undefined,
1680
+ toolPolicyVersion: envelope.revisions.toolPolicy,
1681
+ });
1682
+ const record: Record<string, unknown> = {
1683
+ schema: ROUTING_RECORD_V2_SCHEMA, recordedAt: now,
1684
+ project: run.projectId, runId: run.runId, stepId: dispatch.stepId,
1685
+ assignmentId: dispatch.assignmentId, attemptId: dispatch.attemptId,
1686
+ harness: result.harness ?? (dispatch.producerId === "pi" ? "pi" : "driver-simulated"),
1687
+ provider: result.provider ?? "simulated", requestedModel: result.requestedModel ?? "simulated",
1688
+ effectiveModel: result.effectiveModel ?? result.requestedModel ?? "simulated",
1689
+ behavioralSha256: rawHash.startsWith("sha256:") ? rawHash : `sha256:${rawHash}`,
1690
+ latencyMs: typeof result.latencyMs === "number" ? result.latencyMs : 0,
1691
+ costBasis: result.costBasis, costUsd: result.costUsd ?? null,
1692
+ retries: Math.max(0, dispatch.stepAttempt - 1),
1693
+ thinking: result.thinking ?? dispatch.request.thinking,
1694
+ };
1695
+ for (const field of ["agentRole", "contextTokens", "tokensIn", "tokensOut", "cacheReadTokens", "cacheWriteTokens", "priceRef", "providerMetadata"] as const) {
1696
+ if (result[field] !== undefined) record[field] = result[field];
1697
+ }
1698
+ return parseRoutingRecordV2(record);
1699
+ }
1700
+
1518
1701
  function settleMember(
1519
1702
  context: VnextRuntimeContext,
1520
1703
  dispatch: PreparedDispatch,
@@ -1551,6 +1734,8 @@ function settleMember(
1551
1734
  };
1552
1735
  push("attempt.status_changed", { attemptId: dispatch.attemptId, status: "settling" });
1553
1736
  if (cancelling) {
1737
+ push("routing.attempt.recorded", { routing: producerRoutingRecord(context, dispatch,
1738
+ result ?? { outcome: "cancelled", costBasis: "unknown", costUsd: null }, now) });
1554
1739
  push("assignment.result_recorded", { assignmentId: dispatch.assignmentId, resultClass: "cancelled", status: "result_recorded" });
1555
1740
  push("attempt.status_changed", { attemptId: dispatch.attemptId, status: "terminal" });
1556
1741
  push("assignment.terminal", { assignmentId: dispatch.assignmentId, outcome: "cancelled", status: "terminal" });
@@ -1592,57 +1777,10 @@ function settleMember(
1592
1777
  push("attempt.status_changed", { attemptId: dispatch.attemptId, status: "terminal" });
1593
1778
  push("assignment.terminal", { assignmentId: dispatch.assignmentId, outcome: "failed", status: "terminal" });
1594
1779
  } else {
1595
- if (!result || result.costBasis === undefined || result.costBasis === null) {
1780
+ if (!result) {
1596
1781
  throw runtimeError("settle_missing_cost_basis", run.runId, `attempt settlement rejected: missing required costBasis for attempt ${dispatch.attemptId}`);
1597
1782
  }
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) });
1783
+ push("routing.attempt.recorded", { routing: producerRoutingRecord(context, dispatch, result, now) });
1646
1784
 
1647
1785
  const outcome = typeof result.outcome === "string" ? result.outcome : undefined;
1648
1786
  const known = outcome !== undefined && dispatch.step.outcomes.includes(outcome);
@@ -1844,7 +1982,11 @@ function unsupportedLimit(envelope: VnextRunPlanEnvelope): VnextRunHandoff | und
1844
1982
  return undefined;
1845
1983
  }
1846
1984
 
1847
- function unsupportedStep(plan: VnextCompiledPlan, step: VnextCompiledStep): Omit<VnextRunHandoff, "stepId"> | undefined {
1985
+ function unsupportedStep(
1986
+ plan: VnextCompiledPlan,
1987
+ step: VnextCompiledStep,
1988
+ producerId?: "driver-simulated" | "pi" | string,
1989
+ ): Omit<VnextRunHandoff, "stepId"> | undefined {
1848
1990
  if (step.kind === "gate") throw runtimeError("run_plan_corrupt", plan.workflowId, `unsupportedStep called on gate without envelope; use unsupportedGateStep instead`);
1849
1991
  if (step.kind !== "agent" && step.kind !== "moa" && step.kind !== "approval" && step.kind !== "wait") {
1850
1992
  return { reason: "step_unsupported", field: "kind", detail: `step kind ${step.kind} is not executed in this slice` };
@@ -1883,6 +2025,13 @@ function unsupportedStep(plan: VnextCompiledPlan, step: VnextCompiledStep): Omit
1883
2025
  return { reason: "step_unsupported", field: "repositories", detail: `invalid repository access '${access}' on ${repoId}` };
1884
2026
  }
1885
2027
  }
2028
+ if (producerId !== "driver-simulated" && Object.values(step.repositories).some((access) => access === "write")) {
2029
+ return {
2030
+ reason: "step_unsupported",
2031
+ field: "repositories",
2032
+ detail: "live write steps are unsupported until writer sandboxing witness passes",
2033
+ };
2034
+ }
1886
2035
  return undefined;
1887
2036
  }
1888
2037
 
@@ -2364,9 +2513,12 @@ export function recoverVnextRun(
2364
2513
  const run = requireRun(context, runId);
2365
2514
  let state = foldStoredVnextRun(context, run);
2366
2515
 
2367
- if (state.status !== "blocked_uncertain" && state.currentStep?.effectState !== "blocked_uncertain") {
2516
+ if (state.status !== "blocked_uncertain" && state.status !== "cancelling" && state.currentStep?.effectState !== "blocked_uncertain") {
2368
2517
  throw runtimeError("run_events_illegal", runId, `run ${runId} is not in blocked_uncertain status`);
2369
2518
  }
2519
+ if (state.status === "cancelling" && state.currentStep?.effectState === "blocked_uncertain") {
2520
+ state = { ...state, status: "blocked_uncertain" };
2521
+ }
2370
2522
 
2371
2523
  const now = new Date().toISOString();
2372
2524
  let sequence = context.eventStore.nextSequence(runId);