@kontextmind/kxm 0.7.112 → 0.7.118

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 (34) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/agents/implementer.yaml +1 -1
  3. package/.kxm/roles/writer.yaml +1 -1
  4. package/docs/contributing/assignment-runner.md +3 -3
  5. package/package.json +1 -1
  6. package/packages/core/tui/dist/index.js +478 -0
  7. package/packages/core/tui/src/adapters/claude.ts +131 -0
  8. package/packages/core/tui/src/adapters/historyExport.ts +35 -0
  9. package/packages/core/tui/src/adapters/omp.ts +174 -0
  10. package/packages/core/tui/src/adapters/pi.ts +34 -0
  11. package/packages/core/tui/src/exports/index.ts +8 -0
  12. package/packages/core/tui/src/tui/history.ts +106 -0
  13. package/packages/core/tui/src/tui/modelSelector.ts +43 -0
  14. package/packages/core/tui/src/tui/optimizer.ts +154 -0
  15. package/packages/core/tui/src/tui/queue.ts +90 -0
  16. package/packages/core/tui/src/tui/roleBudget.ts +104 -0
  17. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  18. package/plugins/kxm/dist/cli.js +220 -22
  19. package/plugins/kxm/dist/mcp-server.js +1 -1
  20. package/plugins/kxm/dist/runtime-supervisor.js +36 -4
  21. package/plugins/kxm/dist/runtime.js +36 -4
  22. package/plugins/kxm/dist/server.js +22 -0
  23. package/plugins/kxm/package.json +1 -1
  24. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +6 -0
  25. package/plugins/kxm/src/autocomplete.ts +3 -0
  26. package/plugins/kxm/src/cli/plugins.ts +201 -0
  27. package/plugins/kxm/src/cli.ts +14 -0
  28. package/plugins/kxm/src/harness.ts +36 -4
  29. package/plugins/kxm/src/mcp-server.ts +1 -1
  30. package/scripts/assignment-run.d.mts +7 -0
  31. package/scripts/assignment-run.mjs +128 -1
  32. package/scripts/demo-overlay.mjs +175 -0
  33. package/scripts/harness-run.mjs +2 -2
  34. package/scripts/native-critic.mjs +7 -7
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.112",
3
+ "version": "0.7.118",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,6 +23,12 @@ Issuing a token is an operator action. When kxm_* tools fail with
23
23
  `No active session token found in env or disk` for such a file even though it
24
24
  still blocks the tools.
25
25
 
26
+ ## Plugins
27
+
28
+ | Command | Purpose | Options |
29
+ |---|---|---|
30
+ | `kxm plugin install` | Install KXM plugin for discovered or specified harnesses | `--all`, `--claude`, `--omp`, `--pi`, `--dry-run`, `--json` |
31
+
26
32
  ## Updates
27
33
 
28
34
  | Command | Purpose |
@@ -8,6 +8,7 @@ const TOP_LEVEL_COMMANDS = [
8
8
  "runs",
9
9
  "tenant",
10
10
  "harness",
11
+ "plugin",
11
12
  "update",
12
13
  "runtime",
13
14
  "trust",
@@ -38,6 +39,7 @@ const SUBCOMMANDS: Record<string, string[]> = {
38
39
  runs: ["status", "cancel", "list"],
39
40
  tenant: ["status"],
40
41
  harness: ["list"],
42
+ plugin: ["install"],
41
43
  runtime: ["start", "status", "stop"],
42
44
  trust: ["diff", "check"],
43
45
  agent: ["worker"],
@@ -137,6 +139,7 @@ _kxm() {
137
139
  'run:Create a KXM workflow run'
138
140
  'runs:Inspect KXM runs'
139
141
  'harness:Detect coding-agent harnesses and auth'
142
+ 'plugin:Install or manage KXM harness plugins'
140
143
  'update:Update kxm, harness CLIs, and model catalogs'
141
144
  'runtime:Manage the KXM Runtime supervisor'
142
145
  'trust:Permission-diff trust review'
@@ -0,0 +1,201 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { findKxmRepoRoot } from "../repo-root.ts";
3
+ import { probeHarnesses } from "../harness.ts";
4
+ import { print, type Runtime } from "./types.ts";
5
+
6
+ export type PluginHarnessId = "pi" | "omp" | "claude";
7
+
8
+ export const PLUGIN_SUPPORTED_HARNESSES: readonly PluginHarnessId[] = Object.freeze(["pi", "omp", "claude"]);
9
+
10
+ export interface PluginInstallOptions {
11
+ all?: boolean | undefined;
12
+ claude?: boolean | undefined;
13
+ omp?: boolean | undefined;
14
+ pi?: boolean | undefined;
15
+ }
16
+
17
+ export interface PluginInstallHarnessResult {
18
+ harness: string;
19
+ status: "passed" | "failed" | "would" | "skipped";
20
+ command?: string | undefined;
21
+ args?: readonly string[] | undefined;
22
+ detail?: string | undefined;
23
+ error?: string | undefined;
24
+ }
25
+
26
+ export async function cmdPluginInstall(
27
+ runtime: Runtime,
28
+ options: PluginInstallOptions,
29
+ ): Promise<number> {
30
+ const repoRoot = findKxmRepoRoot(import.meta.url);
31
+ const env = runtime.env.PATH ? runtime.env : { ...process.env, ...runtime.env };
32
+ const inventory = probeHarnesses({ env });
33
+
34
+ const isExplicit = Boolean(options.claude || options.omp || options.pi);
35
+ const requestedHarnesses: PluginHarnessId[] = [];
36
+ if (options.pi) requestedHarnesses.push("pi");
37
+ if (options.omp) requestedHarnesses.push("omp");
38
+ if (options.claude) requestedHarnesses.push("claude");
39
+
40
+ const targetHarnesses: readonly PluginHarnessId[] = isExplicit
41
+ ? requestedHarnesses
42
+ : PLUGIN_SUPPORTED_HARNESSES;
43
+
44
+ const results: PluginInstallHarnessResult[] = [];
45
+
46
+ for (const harnessId of targetHarnesses) {
47
+ const status = inventory.harnesses.find((h) => h.id === harnessId);
48
+ const detected = Boolean(status?.detected && status?.command);
49
+
50
+ if (!detected) {
51
+ if (isExplicit) {
52
+ results.push({
53
+ harness: harnessId,
54
+ status: "failed",
55
+ error: "harness_not_detected",
56
+ detail: `${harnessId} harness is not installed or not found on PATH`,
57
+ });
58
+ }
59
+ continue;
60
+ }
61
+
62
+ const command = status!.command!;
63
+
64
+ if (harnessId === "pi") {
65
+ const args = ["install", repoRoot, "-a"];
66
+ if (runtime.dryRun) {
67
+ results.push({
68
+ harness: "pi",
69
+ status: "would",
70
+ command,
71
+ args,
72
+ detail: `would install kxm plugin into pi: ${command} ${args.join(" ")}`,
73
+ });
74
+ } else {
75
+ const run = runtime.io.spawnSync
76
+ ? runtime.io.spawnSync(command, args)
77
+ : spawnSync(command, args, { encoding: "utf8", windowsHide: true, env: runtime.env });
78
+ if (run.error || (run.status !== 0 && run.status !== null)) {
79
+ results.push({
80
+ harness: "pi",
81
+ status: "failed",
82
+ command,
83
+ args,
84
+ error: run.error?.message || "install_failed",
85
+ detail: (run.stderr || run.stdout || "pi install exited with error").trim(),
86
+ });
87
+ } else {
88
+ results.push({
89
+ harness: "pi",
90
+ status: "passed",
91
+ command,
92
+ args,
93
+ detail: "installed kxm plugin into pi",
94
+ });
95
+ }
96
+ }
97
+ } else if (harnessId === "omp") {
98
+ const args = ["plugin", "install", repoRoot];
99
+ if (runtime.dryRun) {
100
+ results.push({
101
+ harness: "omp",
102
+ status: "would",
103
+ command,
104
+ args,
105
+ detail: `would install kxm plugin into omp: ${command} ${args.join(" ")}`,
106
+ });
107
+ } else {
108
+ const run = runtime.io.spawnSync
109
+ ? runtime.io.spawnSync(command, args)
110
+ : spawnSync(command, args, { encoding: "utf8", windowsHide: true, env: runtime.env });
111
+ if (run.error || (run.status !== 0 && run.status !== null)) {
112
+ results.push({
113
+ harness: "omp",
114
+ status: "failed",
115
+ command,
116
+ args,
117
+ error: run.error?.message || "install_failed",
118
+ detail: (run.stderr || run.stdout || "omp plugin install exited with error").trim(),
119
+ });
120
+ } else {
121
+ results.push({
122
+ harness: "omp",
123
+ status: "passed",
124
+ command,
125
+ args,
126
+ detail: "installed kxm plugin into omp",
127
+ });
128
+ }
129
+ }
130
+ } else if (harnessId === "claude") {
131
+ const marketplaceArgs = ["plugin", "marketplace", "add", repoRoot];
132
+ const installArgs = ["plugin", "install", "kxm", "-y"];
133
+ if (runtime.dryRun) {
134
+ results.push({
135
+ harness: "claude",
136
+ status: "would",
137
+ command,
138
+ args: installArgs,
139
+ detail: `would add marketplace and install kxm plugin into claude: ${command} ${installArgs.join(" ")}`,
140
+ });
141
+ } else {
142
+ if (runtime.io.spawnSync) {
143
+ runtime.io.spawnSync(command, marketplaceArgs);
144
+ } else {
145
+ spawnSync(command, marketplaceArgs, { encoding: "utf8", windowsHide: true, env: runtime.env });
146
+ }
147
+ const run = runtime.io.spawnSync
148
+ ? runtime.io.spawnSync(command, installArgs)
149
+ : spawnSync(command, installArgs, { encoding: "utf8", windowsHide: true, env: runtime.env });
150
+ if (run.error || (run.status !== 0 && run.status !== null)) {
151
+ results.push({
152
+ harness: "claude",
153
+ status: "failed",
154
+ command,
155
+ args: installArgs,
156
+ error: run.error?.message || "install_failed",
157
+ detail: (run.stderr || run.stdout || "claude plugin install exited with error").trim(),
158
+ });
159
+ } else {
160
+ results.push({
161
+ harness: "claude",
162
+ status: "passed",
163
+ command,
164
+ args: installArgs,
165
+ detail: "installed kxm plugin into claude",
166
+ });
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ if (results.length === 0) {
173
+ const errorMsg = "no supported harnesses (pi, omp, claude) detected on PATH";
174
+ print(
175
+ runtime.io,
176
+ runtime.json,
177
+ { ok: false, command: "plugin install", error: "no_harnesses_detected", results: [] },
178
+ errorMsg,
179
+ );
180
+ return 1;
181
+ }
182
+
183
+ const ok = results.every((r) => r.status === "passed" || r.status === "would");
184
+ const summary = results
185
+ .map((r) => `${r.harness}: ${r.status}${r.detail ? ` (${r.detail})` : ""}`)
186
+ .join("\n");
187
+
188
+ print(
189
+ runtime.io,
190
+ runtime.json,
191
+ {
192
+ ok,
193
+ command: "plugin install",
194
+ ...(runtime.dryRun ? { dryRun: true } : {}),
195
+ results,
196
+ },
197
+ summary,
198
+ );
199
+
200
+ return ok ? 0 : 1;
201
+ }
@@ -120,6 +120,8 @@ cmdBackup,
120
120
  cmdKxmRuntime,
121
121
  } from "./cli/project.ts";
122
122
 
123
+ import { cmdPluginInstall } from "./cli/plugins.ts";
124
+
123
125
  import {
124
126
  cmdStatus,
125
127
  cmdDash,
@@ -185,6 +187,7 @@ const USAGE_ERROR_CODES = new Set([
185
187
  */
186
188
  const DRY_RUN_COMMANDS: ReadonlySet<string> = new Set([
187
189
  "init", "backup", "restore", "run", "explain", "suggest", "update", "dash", "completion", "completion install",
190
+ "plugin install", "plugin",
188
191
  "runs status", "runs drive", "runs receipt", "runs cancel", "runs list",
189
192
  "tenant status", "models inventory-refresh", "harness list", "auth token",
190
193
  "routes list", "routes count", "routes admit", "routes disable",
@@ -506,6 +509,17 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
506
509
  harnessCmd.helpCommand("help", "Show harness help");
507
510
  addGlobalOptions(harnessCmd.command("list").description("Show installed harnesses, auth, and native updaters")).action(bind(cmdHarnessList));
508
511
 
512
+ const pluginCmd = addGlobalOptions(program.command("plugin").description("Install or manage KXM harness plugins"));
513
+ pluginCmd.helpCommand("help", "Show plugin help");
514
+ addGlobalOptions(pluginCmd.command("install", { isDefault: true }).description("Install the KXM plugin into discovered or specified harnesses"))
515
+ .option("--all", "Install plugin for all discovered harnesses (default)")
516
+ .option("--claude", "Install plugin for Claude Code")
517
+ .option("--omp", "Install plugin for Oh My Pi (OMP)")
518
+ .option("--pi", "Install plugin for Pi")
519
+ .action(async function pluginInstallAction(this: Command, options: { all?: boolean; claude?: boolean; omp?: boolean; pi?: boolean }) {
520
+ result.code = await cmdPluginInstall(runtimeFrom(ctx, this), options);
521
+ });
522
+
509
523
  const authCmd = addGlobalOptions(program.command("auth").description("Manage credentials, tokens, and authorization"));
510
524
  authCmd.helpCommand("help", "Show auth help");
511
525
  addGlobalOptions(authCmd.command("token").description("Inspect, issue, or clear local disk session tokens"))
@@ -508,6 +508,7 @@ const READ_ONLY_ONESHOT_ARGS = Object.freeze({
508
508
  // AGENTS.md/CLAUDE.md context discovery, and an ephemeral session. `--no-tools` alone
509
509
  // is not enough: extensions and hooks can still run with their own permissions.
510
510
  pi: Object.freeze(["--no-tools", "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-context-files", "--no-session"]),
511
+ omp: Object.freeze(["--no-tools", "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-context-files", "--no-session"]),
511
512
  claude: Object.freeze(["--tools", "Read,Glob,Grep", "--restricted", "--safe-mode", "--permission-mode", "plan", "--permission-prompts", "none", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--disable-slash-commands", "--no-session-persistence"]),
512
513
  codex: Object.freeze(["--sandbox", "read-only", "--ignore-user-config", "-c", 'approval_policy="never"']),
513
514
  grok: Object.freeze(["--sandbox", "read-only", "--permission-mode", "plan", "--tools", "Read,Glob,Grep", "--no-subagents", "--disable-web-search"]),
@@ -537,6 +538,7 @@ export function oneShotReadOnlyArgs(harness: string): readonly string[] | undefi
537
538
  */
538
539
  const WRITER_ONESHOT_ARGS = Object.freeze({
539
540
  pi: Object.freeze(["-a", "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-session"]),
541
+ omp: Object.freeze(["-a", "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-session"]),
540
542
  grok: Object.freeze(["--always-approve", "--no-subagents", "--disable-web-search"]),
541
543
  });
542
544
 
@@ -568,6 +570,26 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
568
570
  usageParser: parsePiOneShotUsage,
569
571
  },
570
572
  },
573
+ {
574
+ id: "omp",
575
+ label: "Oh My Pi",
576
+ default: false,
577
+ mode: "headless",
578
+ commands: ["omp"],
579
+ versionArgs: ["--version"],
580
+ authArgs: ["models", "--json"],
581
+ update: {
582
+ self: ["update"],
583
+ extensions: ["plugin", "upgrade"],
584
+ models: ["models", "refresh"],
585
+ },
586
+ oneShot: {
587
+ argv: ["-p", ...oneShotReadOnlyArgs("omp")!, "--mode", "json"],
588
+ promptVia: "arg",
589
+ outputFormat: "json",
590
+ usageParser: parsePiOneShotUsage,
591
+ },
592
+ },
571
593
  {
572
594
  id: "claude",
573
595
  label: "Claude Code",
@@ -857,6 +879,16 @@ function interpretAuth(id: string, result: HarnessCommandResult): Pick<HarnessSt
857
879
  if (commandSucceeded(result) && lines.some((line) => line === GROK_LOGIN_LINE)) return { authenticated: true, issues: [] };
858
880
  return { authenticated: null, issues: ["auth_unparsed"] };
859
881
  }
882
+ if (id === "omp") {
883
+ if (commandSucceeded(result)) {
884
+ const payload = parseJsonObject(result.stdout);
885
+ if (payload && Array.isArray(payload.models) && payload.models.length > 0) {
886
+ return { authenticated: true, issues: [] };
887
+ }
888
+ if (commandSucceeded(result)) return { authenticated: true, issues: [] };
889
+ }
890
+ return { authenticated: null, issues: ["auth_unparsed"] };
891
+ }
860
892
  if (id === "agy") {
861
893
  const text = `${result.stdout}\n${result.stderr}`;
862
894
  if (!commandSucceeded(result) || !text.trim()) {
@@ -1118,7 +1150,7 @@ export function validateHarnessModelPair(
1118
1150
  return { valid: true };
1119
1151
  }
1120
1152
 
1121
- if (harnessId === "pi") {
1153
+ if (harnessId === "pi" || harnessId === "omp") {
1122
1154
  if (!provider && model?.includes("/")) {
1123
1155
  const idx = model.indexOf("/");
1124
1156
  provider = model.slice(0, idx).toLowerCase();
@@ -1127,17 +1159,17 @@ export function validateHarnessModelPair(
1127
1159
  if (!provider) return { valid: true };
1128
1160
  const blocked = (message: string): HarnessModelValidation => ({ valid: false, issue: "pi_native_impersonation_blocked", message });
1129
1161
  if (PI_NATIVE_BRAKE_PROVIDERS.includes(provider)) {
1130
- return blocked(`pi must not impersonate native provider ${provider}; use the native harness`);
1162
+ return blocked(`${harnessId} must not impersonate native provider ${provider}; use the native harness`);
1131
1163
  }
1132
1164
  if (provider === "antigravity" && model && PI_ANTIGRAVITY_MODEL_ID.test(model)) return { valid: true };
1133
1165
  const owned = Object.hasOwn(PI_VENDOR_OWNED_PROVIDERS, provider) ? PI_VENDOR_OWNED_PROVIDERS[provider] : undefined;
1134
1166
  if (owned) {
1135
- return blocked(`pi provider ${provider} bills native vendor ${owned} for ${model ?? "this model"}; use the native harness`);
1167
+ return blocked(`${harnessId} provider ${provider} bills native vendor ${owned} for ${model ?? "this model"}; use the native harness`);
1136
1168
  }
1137
1169
  const segment = model?.includes("/") ? model.slice(0, model.indexOf("/")).toLowerCase() : undefined;
1138
1170
  const vendor = segment && Object.hasOwn(PI_VENDOR_SEGMENT_ALIASES, segment) ? PI_VENDOR_SEGMENT_ALIASES[segment] : segment;
1139
1171
  if (vendor && PI_NATIVE_BRAKE_PROVIDERS.includes(vendor)) {
1140
- return blocked(`pi must not bill native vendor ${vendor} through ${provider}; use the native harness`);
1172
+ return blocked(`${harnessId} must not bill native vendor ${vendor} through ${provider}; use the native harness`);
1141
1173
  }
1142
1174
  return { valid: true };
1143
1175
  }
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
11
11
  import type { HubEvent, MessageRecord } from "./protocol.ts";
12
12
  import { sessionTokenFixHint } from "./session-token-hint.ts";
13
13
 
14
- const VERSION = "0.7.112";
14
+ const VERSION = "0.7.118";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();
@@ -339,6 +339,13 @@ export function changeReport(request: { taskDir: string }, deps?: Record<string,
339
339
  ranking: "not-ranked";
340
340
  orchestration_usage: "unavailable";
341
341
  };
342
+ export const ROUTING_EXPORT_SCHEMA: "kxm.assignment-routing-export.v1";
343
+ export function exportAssignmentRoutingRecords(request: { taskDir: string }, deps?: Record<string, unknown>): {
344
+ schema: "kxm.assignment-routing-export.v1";
345
+ task_id: string;
346
+ records: Array<Record<string, any>>;
347
+ skipped: Array<{ assignment_id: string | null; reason: "acceptance_record_invalid" | "no_routing_record" | "routing_record_invalid" | "manifest_invalid" }>;
348
+ };
342
349
  export function writeCurrentPlan(request: {
343
350
  taskDir: string; plan: string; sha256: string; baseCommit: string; expectedGeneration: number; settledDecisions?: string[];
344
351
  }, deps?: Record<string, unknown>): {
@@ -2330,7 +2330,7 @@ export async function observeAssignment(outputDir, deps = {}) {
2330
2330
  return appended;
2331
2331
  }
2332
2332
 
2333
- const CLI_USAGE = "usage: assignment-run.mjs run --manifest <absolute-path> | observe --record-dir <absolute-path> | witness --record-dir <absolute-path> | accept --task-dir <absolute-path> --commit <commit> --record-dir <absolute-path> --critic <absolute-path> --critic <absolute-path> [--observed-pr <id>] [--observed-ci <id>] | attribute --task-dir <absolute-path> --record-dir <absolute-path> --class <orchestration|model|environment|unclassified> --explanation-file <absolute-path> | observe-cost --task-dir <absolute-path> --input <absolute-path> | plan-current --task-dir <absolute-path> --plan <absolute-path> --sha256 <hex64> --base-commit <commit> --expected-generation <n> | change-report --task-dir <absolute-path>";
2333
+ const CLI_USAGE = "usage: assignment-run.mjs run --manifest <absolute-path> | observe --record-dir <absolute-path> | witness --record-dir <absolute-path> | accept --task-dir <absolute-path> --commit <commit> --record-dir <absolute-path> --critic <absolute-path> --critic <absolute-path> [--observed-pr <id>] [--observed-ci <id>] | attribute --task-dir <absolute-path> --record-dir <absolute-path> --class <orchestration|model|environment|unclassified> --explanation-file <absolute-path> | observe-cost --task-dir <absolute-path> --input <absolute-path> | plan-current --task-dir <absolute-path> --plan <absolute-path> --sha256 <hex64> --base-commit <commit> --expected-generation <n> | change-report --task-dir <absolute-path> | routing-export --task-dir <absolute-path>";
2334
2334
 
2335
2335
  const WITNESS_RECEIPT_KEYS = Object.freeze([
2336
2336
  "schema",
@@ -4293,8 +4293,135 @@ export function writeCurrentPlan(request, deps = {}) {
4293
4293
  } catch (error) { throw observationFailure(error, "plan_ref_invalid"); }
4294
4294
  }
4295
4295
 
4296
+ export const ROUTING_EXPORT_SCHEMA = "kxm.assignment-routing-export.v1";
4297
+
4298
+ function contractExportable(contract) {
4299
+ return Boolean(contract)
4300
+ && typeof contract === "object"
4301
+ && !Array.isArray(contract)
4302
+ && typeof contract.boundary === "string"
4303
+ && Array.isArray(contract.deliverables)
4304
+ && contract.witness
4305
+ && typeof contract.witness === "object"
4306
+ && !Array.isArray(contract.witness)
4307
+ && typeof contract.witness.id === "string"
4308
+ && Array.isArray(contract.deferred);
4309
+ }
4310
+
4311
+ export function exportAssignmentRoutingRecords(request, deps = {}) {
4312
+ const io = ioDeps(deps);
4313
+ const taskDir = observationTask(request.taskDir, io);
4314
+ const taskId = basename(taskDir);
4315
+ const skipped = [];
4316
+ const acceptedIds = new Set();
4317
+ const acceptedPath = join(taskDir, ACCEPTED_FILENAME);
4318
+ if (lstatOrNull(acceptedPath, io.lstatSync)) {
4319
+ try {
4320
+ const value = privateRecord(acceptedPath, io).value;
4321
+ const valid = value.schema === ACCEPTED_SCHEMA
4322
+ && value.task_id === taskId
4323
+ && typeof value.writer?.assignment_id === "string"
4324
+ && Array.isArray(value.critics);
4325
+ if (!valid) throw new Error("acceptance invalid");
4326
+ acceptedIds.add(value.writer.assignment_id);
4327
+ for (const critic of value.critics) {
4328
+ if (typeof critic?.assignment_id === "string") acceptedIds.add(critic.assignment_id);
4329
+ }
4330
+ } catch {
4331
+ skipped.push({ assignment_id: null, reason: "acceptance_record_invalid" });
4332
+ }
4333
+ }
4334
+ const collected = [];
4335
+ const dirs = listDirectAssignmentDirs(taskDir, io).sort((left, right) => left.assignment_id < right.assignment_id ? -1 : left.assignment_id > right.assignment_id ? 1 : 0);
4336
+ for (const { assignment_id, path } of dirs) {
4337
+ const routingPath = join(path, "routing-record.json");
4338
+ if (!lstatOrNull(routingPath, io.lstatSync)) {
4339
+ skipped.push({ assignment_id, reason: "no_routing_record" });
4340
+ continue;
4341
+ }
4342
+ let record;
4343
+ try {
4344
+ record = parseRoutingRecord(privateRecord(routingPath, io).value);
4345
+ if (record.workflowRunId !== assignment_id) throw new Error("workflowRunId");
4346
+ } catch {
4347
+ skipped.push({ assignment_id, reason: "routing_record_invalid" });
4348
+ continue;
4349
+ }
4350
+ let contract;
4351
+ try {
4352
+ const manifest = privateRecord(join(path, "manifest.json"), io).value;
4353
+ contract = manifest.contract;
4354
+ if (!contractExportable(contract)) throw new Error("contract");
4355
+ } catch {
4356
+ skipped.push({ assignment_id, reason: "manifest_invalid" });
4357
+ continue;
4358
+ }
4359
+ collected.push({ id: assignment_id, record, contract });
4360
+ }
4361
+ const supersededBy = new Map();
4362
+ for (const entry of collected) {
4363
+ const reworkOf = entry.record.providerMetadata?.rework_of;
4364
+ if (typeof reworkOf === "string" && !supersededBy.has(reworkOf)) supersededBy.set(reworkOf, entry.id);
4365
+ }
4366
+ const records = [];
4367
+ for (const entry of collected) {
4368
+ const kind = entry.record.stageId;
4369
+ const role = entry.record.agentRole;
4370
+ const askSha256 = sha256Bytes(Buffer.from(JSON.stringify(["kxm.assignment-ask.v1", kind, role])));
4371
+ const objectiveSha256 = sha256Bytes(Buffer.from(JSON.stringify([
4372
+ "kxm.assignment-brief.v1",
4373
+ kind,
4374
+ entry.contract.boundary,
4375
+ entry.contract.deliverables,
4376
+ entry.contract.witness.id,
4377
+ entry.contract.deferred,
4378
+ ])));
4379
+ const finalOutcome = acceptedIds.has(entry.id) ? "accepted" : supersededBy.has(entry.id) ? "failed" : "pending";
4380
+ try {
4381
+ records.push(parseRoutingRecord({
4382
+ ...entry.record,
4383
+ workflowRunId: taskId,
4384
+ finalOutcome,
4385
+ providerMetadata: {
4386
+ ...entry.record.providerMetadata,
4387
+ workflowId: "assignment-runner",
4388
+ askSha256,
4389
+ objectiveSha256,
4390
+ stepWrites: WRITER_KINDS.includes(kind),
4391
+ assignmentId: entry.id,
4392
+ ...(finalOutcome === "failed" ? { supersededBy: supersededBy.get(entry.id) } : {}),
4393
+ },
4394
+ }));
4395
+ } catch {
4396
+ skipped.push({ assignment_id: entry.id, reason: "routing_record_invalid" });
4397
+ }
4398
+ }
4399
+ return { schema: ROUTING_EXPORT_SCHEMA, task_id: taskId, records, skipped };
4400
+ }
4401
+
4296
4402
  export async function main(argv = process.argv, io = { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr }) {
4297
4403
  const args = argv.slice(2).filter((arg) => arg !== "--");
4404
+ if (args[0] === "routing-export") {
4405
+ try {
4406
+ const values = {};
4407
+ for (let i = 1; i < args.length; i += 2) {
4408
+ if (args[i] !== "--task-dir" || values[args[i]] !== undefined || args[i + 1] === undefined) throw failClosed("invalid report/plan arguments", "manifest_invalid");
4409
+ values[args[i]] = args[i + 1];
4410
+ }
4411
+ if (values["--task-dir"] === undefined) throw failClosed("missing report/plan arguments", "manifest_invalid");
4412
+ const result = exportAssignmentRoutingRecords({ taskDir: values["--task-dir"] });
4413
+ for (const record of result.records) io.stdout.write(`${JSON.stringify(record)}\n`);
4414
+ io.stderr.write(`${JSON.stringify({ schema: result.schema, task_id: result.task_id, records: result.records.length, skipped: result.skipped })}\n`);
4415
+ process.exitCode = 0;
4416
+ return result;
4417
+ } catch (error) {
4418
+ const failure = observationFailure(error);
4419
+ io.stderr.write(`${failure.runnerCode}: assignment routing export refused\n`);
4420
+ process.exitCode = 1;
4421
+ throw failure;
4422
+ }
4423
+ }
4424
+
4298
4425
  if (["change-report", "plan-current"].includes(args[0])) {
4299
4426
  try {
4300
4427
  const flags = args[0] === "change-report" ? ["--task-dir"] : ["--task-dir", "--plan", "--sha256", "--base-commit", "--expected-generation"];