@kontextmind/kxm 0.7.97 → 0.7.98
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.kxm/workflows/default.yaml +2 -0
- package/CHANGELOG.md +39 -2
- package/docs/concepts/architecture.md +1 -1
- package/docs/concepts/data-and-storage.md +1 -1
- package/docs/contracts/routing.md +1 -1
- package/docs/contributing/test-matrix.md +6 -5
- package/docs/operations/backup-and-restore.md +43 -24
- package/docs/operations/deploy.md +1 -1
- package/docs/reference/cli-reference.md +59 -24
- package/docs/reference/config-reference.md +24 -13
- package/docs/reference/harness-routing.md +3 -3
- package/docs/reference/http-api.md +1 -1
- package/docs/start/first-workflow.md +4 -4
- package/docs/start/quickstart-claude-code.md +2 -2
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +393 -154
- package/plugins/kxm/dist/core.js +5 -2
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +231 -48
- package/plugins/kxm/dist/runtime.js +415 -92
- package/plugins/kxm/dist/server.js +7 -0
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +4 -3
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +3 -2
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +3 -0
- package/plugins/kxm/skills/kxm-runs/SKILL.md +8 -7
- package/plugins/kxm/src/cli/project.ts +22 -13
- package/plugins/kxm/src/cli/system.ts +22 -1
- package/plugins/kxm/src/cli.ts +11 -3
- package/plugins/kxm/src/database.ts +210 -36
- package/plugins/kxm/src/engine.ts +117 -2
- package/plugins/kxm/src/harness.ts +29 -0
- package/plugins/kxm/src/init-guide-setup.ts +43 -28
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/oneshot-producer.ts +16 -8
- package/plugins/kxm/src/prices.ts +33 -2
- package/plugins/kxm/src/routing.ts +13 -7
- package/plugins/kxm/src/studio-layout.ts +5 -4
- package/plugins/kxm/src/template.ts +31 -0
- package/plugins/kxm/src/worktree-witness.ts +71 -0
- package/schemas/backup-manifest.schema.json +33 -0
|
@@ -3,6 +3,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
import { isRouteAdmitted, listRoleBindings } from "./routes.ts";
|
|
6
|
+
import { oneShotWriterArgs } from "./harness.ts";
|
|
7
|
+
import { applyAuthoringWitness, captureWorktreeWitness } from "./worktree-witness.ts";
|
|
6
8
|
import {
|
|
7
9
|
buildFormalContextPacket,
|
|
8
10
|
buildHandoffManifest,
|
|
@@ -153,6 +155,8 @@ export interface KxmProducerRequest {
|
|
|
153
155
|
readonly thinking?: string | undefined;
|
|
154
156
|
readonly agentRole?: string | undefined;
|
|
155
157
|
readonly harness?: string | undefined;
|
|
158
|
+
/** Live producers select an audited argv profile from this ceiling. */
|
|
159
|
+
readonly permission?: "read-only" | "edit" | undefined;
|
|
156
160
|
readonly contextPacket?: FormalContextPacketV2 | undefined;
|
|
157
161
|
readonly handoffManifest?: HandoffManifestV1 | undefined;
|
|
158
162
|
}
|
|
@@ -1593,6 +1597,14 @@ function prepareDispatch(
|
|
|
1593
1597
|
return { kind: "return", state, handoff: { ...routeResult.error, stepId } };
|
|
1594
1598
|
}
|
|
1595
1599
|
resolvedRoute = routeResult;
|
|
1600
|
+
const writeRefusal = unsupportedLiveWrite(
|
|
1601
|
+
context.projectRoot,
|
|
1602
|
+
step,
|
|
1603
|
+
agentId,
|
|
1604
|
+
resolvedRoute.selector,
|
|
1605
|
+
loadKxmRunPlanEnvelope(context.eventStore, run).projectLimits.maxConcurrentRuns,
|
|
1606
|
+
);
|
|
1607
|
+
if (writeRefusal) return { kind: "return", state, handoff: { ...writeRefusal, stepId } };
|
|
1596
1608
|
}
|
|
1597
1609
|
|
|
1598
1610
|
const used = state.stepAttempts[stepId] ?? 0;
|
|
@@ -1825,6 +1837,7 @@ function birthMember(
|
|
|
1825
1837
|
signal: controller.signal,
|
|
1826
1838
|
prompt: input.step.instructions ? `${input.step.instructions}\n\n${generatedPrompt}` : generatedPrompt,
|
|
1827
1839
|
thinking: input.stepAttempt <= 1 ? "low" : "medium",
|
|
1840
|
+
permission: Object.values(input.step.repositories).some((access) => access === "write") ? "edit" : "read-only",
|
|
1828
1841
|
contextPacket,
|
|
1829
1842
|
...(resolvedRoute ? { provider: resolvedRoute.provider, model: resolvedRoute.model } : {}),
|
|
1830
1843
|
},
|
|
@@ -1998,7 +2011,18 @@ async function drivePanel(
|
|
|
1998
2011
|
if (!executingBound()) return { attemptId: member.attemptId, invoked: false, skipped: true };
|
|
1999
2012
|
kxmPanelDispatchSeams.beforeInvoke?.(member);
|
|
2000
2013
|
if (!executingBound()) return { attemptId: member.attemptId, invoked: false, skipped: true };
|
|
2014
|
+
const live = member.producerId !== "driver-simulated";
|
|
2015
|
+
const writes = Object.values(member.step.repositories).some((access) => access === "write");
|
|
2016
|
+
const before = live ? captureWorktreeWitness(context.projectRoot) : undefined;
|
|
2001
2017
|
const produced = await invokeProducer(producer, member.request);
|
|
2018
|
+
if (live && produced.result && before) {
|
|
2019
|
+
const after = captureWorktreeWitness(context.projectRoot);
|
|
2020
|
+
return {
|
|
2021
|
+
attemptId: member.attemptId,
|
|
2022
|
+
invoked: true,
|
|
2023
|
+
produced: { ...produced, result: applyAuthoringWitness(produced.result, { writes, before, after }) },
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2002
2026
|
return { attemptId: member.attemptId, invoked: true, produced };
|
|
2003
2027
|
} catch (error) {
|
|
2004
2028
|
member.controller.abort();
|
|
@@ -2722,11 +2746,102 @@ function unsupportedStep(
|
|
|
2722
2746
|
return { reason: "step_unsupported", field: "repositories", detail: `invalid repository access '${access}' on ${repoId}` };
|
|
2723
2747
|
}
|
|
2724
2748
|
}
|
|
2725
|
-
|
|
2749
|
+
return undefined;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
function readYamlRecord(path: string): Record<string, unknown> | undefined {
|
|
2753
|
+
if (!existsSync(path)) return undefined;
|
|
2754
|
+
try {
|
|
2755
|
+
const parsed = parse(readFileSync(path, "utf8")) as unknown;
|
|
2756
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record<string, unknown>;
|
|
2757
|
+
} catch {
|
|
2758
|
+
return undefined;
|
|
2759
|
+
}
|
|
2760
|
+
return undefined;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
function agentHarness(projectRoot: string, agentId: string): string | undefined {
|
|
2764
|
+
const harness = readYamlRecord(join(projectRoot, ".kxm", "agents", `${agentId}.yaml`))?.harness;
|
|
2765
|
+
return typeof harness === "string" && harness.length > 0 ? harness : undefined;
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
function projectDefaultHarness(projectRoot: string): string {
|
|
2769
|
+
const harness = readYamlRecord(join(projectRoot, ".kxm", "project.yaml"))?.defaultHarness;
|
|
2770
|
+
return typeof harness === "string" && harness.length > 0 ? harness : "pi";
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
/**
|
|
2774
|
+
* Live write steps run only on an audited writer profile, and only when the
|
|
2775
|
+
* developer roster (when present) lists that harness and model as an edit writer.
|
|
2776
|
+
* A missing roster is a fresh project: route admission is the other gate.
|
|
2777
|
+
*
|
|
2778
|
+
* The authoring witness fingerprints the one project checkout around each
|
|
2779
|
+
* spawn, so it can only attribute a change to a lone writer. A write step with
|
|
2780
|
+
* more than one assignment would run several writers there (and member n runs
|
|
2781
|
+
* as allowedAgents[n], which the writer check below never sees), and a
|
|
2782
|
+
* project that admits concurrent runs lets another run's writer edit the tree
|
|
2783
|
+
* mid-attempt. Both hand off.
|
|
2784
|
+
*/
|
|
2785
|
+
function unsupportedLiveWrite(
|
|
2786
|
+
projectRoot: string,
|
|
2787
|
+
step: KxmCompiledStep,
|
|
2788
|
+
agentId: string,
|
|
2789
|
+
selector: string,
|
|
2790
|
+
maxConcurrentRuns: number,
|
|
2791
|
+
): Omit<KxmRunHandoff, "stepId"> | undefined {
|
|
2792
|
+
if (!Object.values(step.repositories).some((access) => access === "write")) return undefined;
|
|
2793
|
+
if (step.assignments.maximum !== 1) {
|
|
2794
|
+
return {
|
|
2795
|
+
reason: "step_unsupported",
|
|
2796
|
+
field: "assignments.maximum",
|
|
2797
|
+
detail: "live write steps run a single assignment; the checkout witness cannot attribute edits between writers",
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
if (maxConcurrentRuns !== 1) {
|
|
2801
|
+
return {
|
|
2802
|
+
reason: "step_unsupported",
|
|
2803
|
+
field: "limits.maxConcurrentRuns",
|
|
2804
|
+
detail: "live write steps require limits.maxConcurrentRuns of 1; concurrent runs share one checkout",
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
const harness = agentHarness(projectRoot, agentId) ?? projectDefaultHarness(projectRoot);
|
|
2808
|
+
if (!oneShotWriterArgs(harness)) {
|
|
2726
2809
|
return {
|
|
2727
2810
|
reason: "step_unsupported",
|
|
2728
2811
|
field: "repositories",
|
|
2729
|
-
detail:
|
|
2812
|
+
detail: `live write steps require an audited writer profile; ${harness} has none`,
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
const rosterPath = join(projectRoot, ".kxm", "roster.yaml");
|
|
2816
|
+
if (!existsSync(rosterPath)) return undefined;
|
|
2817
|
+
const roster = readYamlRecord(rosterPath);
|
|
2818
|
+
if (!roster || roster.schema !== "kxm.developer-roster.v1") {
|
|
2819
|
+
return { reason: "step_unsupported", field: "model", detail: "live write steps require a readable kxm.developer-roster.v1" };
|
|
2820
|
+
}
|
|
2821
|
+
const routes = roster.routes;
|
|
2822
|
+
const lineup = roster.lineup;
|
|
2823
|
+
const writerIds = lineup && typeof lineup === "object" && !Array.isArray(lineup)
|
|
2824
|
+
? (lineup as Record<string, unknown>).writer
|
|
2825
|
+
: undefined;
|
|
2826
|
+
if (!routes || typeof routes !== "object" || Array.isArray(routes) || !Array.isArray(writerIds)) {
|
|
2827
|
+
return { reason: "step_unsupported", field: "model", detail: "developer roster has no writer lineup" };
|
|
2828
|
+
}
|
|
2829
|
+
const allowed = writerIds.some((id) => {
|
|
2830
|
+
if (typeof id !== "string") return false;
|
|
2831
|
+
const route = (routes as Record<string, unknown>)[id];
|
|
2832
|
+
if (!route || typeof route !== "object" || Array.isArray(route)) return false;
|
|
2833
|
+
const record = route as Record<string, unknown>;
|
|
2834
|
+
if (record.harness !== harness || record.status !== "admitted") return false;
|
|
2835
|
+
if (!Array.isArray(record.permissions) || !record.permissions.includes("edit")) return false;
|
|
2836
|
+
const model = typeof record.model === "string" ? record.model : "";
|
|
2837
|
+
const vendor = typeof record.vendor === "string" ? record.vendor : "";
|
|
2838
|
+
return model === selector || (vendor.length > 0 && `${vendor}/${model}` === selector);
|
|
2839
|
+
});
|
|
2840
|
+
if (!allowed) {
|
|
2841
|
+
return {
|
|
2842
|
+
reason: "step_unsupported",
|
|
2843
|
+
field: "model",
|
|
2844
|
+
detail: `live write route ${selector} on ${harness} is not on the developer roster writer lineup`,
|
|
2730
2845
|
};
|
|
2731
2846
|
}
|
|
2732
2847
|
return undefined;
|
|
@@ -518,6 +518,35 @@ export function oneShotReadOnlyArgs(harness: string): readonly string[] | undefi
|
|
|
518
518
|
return Object.hasOwn(READ_ONLY_ONESHOT_ARGS, harness) ? READ_ONLY_ONESHOT_ARGS[harness as keyof typeof READ_ONLY_ONESHOT_ARGS] : undefined;
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Audited edit profiles. These are a writer containment, not read-only flags
|
|
523
|
+
* with the sandbox removed. They match the assignment helper
|
|
524
|
+
* (`scripts/harness-run.mjs` `buildArgv` for `permission: "edit"` with hooks
|
|
525
|
+
* and skills refused):
|
|
526
|
+
*
|
|
527
|
+
* - Pi: `-a` auto-approves tools so the process can edit the checkout, while
|
|
528
|
+
* extensions, skills, prompt templates, and session persistence stay off.
|
|
529
|
+
* `--no-tools` is the read-only profile and is not used here.
|
|
530
|
+
* - Grok: `--always-approve` so edits are not an interactive prompt, with
|
|
531
|
+
* subagents and web search disabled. Grok has no audited read-only edit
|
|
532
|
+
* mix; the read-only one-shot flags stay on the read-only profile.
|
|
533
|
+
*
|
|
534
|
+
* Claude, Codex, agy, and Kimi stay read-only. A live write step on a harness
|
|
535
|
+
* without an entry here hands off instead of spawning unconstrained.
|
|
536
|
+
*/
|
|
537
|
+
const WRITER_ONESHOT_ARGS = Object.freeze({
|
|
538
|
+
pi: Object.freeze(["-a", "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-session"]),
|
|
539
|
+
grok: Object.freeze(["--always-approve", "--no-subagents", "--disable-web-search"]),
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
export function oneShotWriterArgs(harness: string): readonly string[] | undefined {
|
|
543
|
+
return Object.hasOwn(WRITER_ONESHOT_ARGS, harness) ? WRITER_ONESHOT_ARGS[harness as keyof typeof WRITER_ONESHOT_ARGS] : undefined;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function oneShotPermissionArgs(harness: string, permission: "read-only" | "edit"): readonly string[] | undefined {
|
|
547
|
+
return permission === "edit" ? oneShotWriterArgs(harness) : oneShotReadOnlyArgs(harness);
|
|
548
|
+
}
|
|
549
|
+
|
|
521
550
|
export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
522
551
|
{
|
|
523
552
|
id: "pi",
|
|
@@ -10,15 +10,21 @@
|
|
|
10
10
|
* This module writes only current KXM project resources:
|
|
11
11
|
* - `.kxm/agents/<role-slug>.yaml` (kxm.agent.v1)
|
|
12
12
|
* - `.kxm/workflows/<slug>.yaml` (kxm.workflow.v1)
|
|
13
|
+
* - admitted selectors appended to `.kxm/routes.yaml`
|
|
13
14
|
* It never writes retired legacy authority (`.kxm/config`, retired
|
|
14
15
|
* `.kxm/roster.json`) or the trusted `.kxm/roster.yaml` policy
|
|
15
16
|
* and does not use the kxm.role.v1 subsystem.
|
|
17
|
+
*
|
|
18
|
+
* Guide research ids are not dispatch ids. Only the admitted map below is
|
|
19
|
+
* written, and only when that harness is authenticated. Google goes through
|
|
20
|
+
* the Pi `antigravity` provider. Unmapped ids are skipped.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
23
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
19
24
|
import { dirname, join } from "node:path";
|
|
20
25
|
import { stringify } from "yaml";
|
|
21
|
-
import {
|
|
26
|
+
import { type HarnessInventory } from "./harness.ts";
|
|
27
|
+
import { loadRoutePolicy } from "./routes.ts";
|
|
22
28
|
|
|
23
29
|
export interface GuideCandidate {
|
|
24
30
|
readonly vendor: string;
|
|
@@ -41,19 +47,17 @@ export interface GuideWorkflow {
|
|
|
41
47
|
readonly stages: readonly GuideStage[];
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
google: "
|
|
54
|
-
|
|
55
|
-
moonshot: "kimi",
|
|
56
|
-
deepseek: "deepseek",
|
|
50
|
+
/** Research id → the admitted harness/provider/model drive will actually accept.
|
|
51
|
+
* Anything absent is skipped, even when its harness is logged in. */
|
|
52
|
+
const ADMITTED_GUIDE_BINDINGS: Readonly<Record<string, AgentBinding>> = Object.freeze({
|
|
53
|
+
"anthropic/claude-fable-5.1": { harness: "claude", provider: "anthropic", model: "fable" },
|
|
54
|
+
"anthropic/fable": { harness: "claude", provider: "anthropic", model: "fable" },
|
|
55
|
+
"openai/gpt-5.6-sol": { harness: "codex", provider: "openai", model: "gpt-5.6-sol" },
|
|
56
|
+
"x-ai/grok-4.6": { harness: "grok", provider: "xai", model: "grok-4.6" },
|
|
57
|
+
"xai/grok-4.6": { harness: "grok", provider: "xai", model: "grok-4.6" },
|
|
58
|
+
"google/gemini-3.8-flash": { harness: "pi", provider: "antigravity", model: "gemini-3.8-flash-high" },
|
|
59
|
+
"google/gemini-3.8-flash-high": { harness: "pi", provider: "antigravity", model: "gemini-3.8-flash-high" },
|
|
60
|
+
"qwen/qwen3-coder-plus": { harness: "pi", provider: "openrouter", model: "qwen/qwen3-coder-plus" },
|
|
57
61
|
});
|
|
58
62
|
|
|
59
63
|
/** Guide candidates are ordered by preference; the first eligible wins. */
|
|
@@ -380,25 +384,17 @@ function authenticatedHarnesses(inventory: HarnessInventory): ReadonlySet<string
|
|
|
380
384
|
}
|
|
381
385
|
|
|
382
386
|
/**
|
|
383
|
-
* Resolve a guide candidate to
|
|
384
|
-
* candidate
|
|
385
|
-
* Pi OpenRouter provider (guide ids are already OpenRouter-style slugs).
|
|
387
|
+
* Resolve a guide candidate to an admitted dispatch binding, or undefined when
|
|
388
|
+
* no candidate is both on the admitted map and authenticated on its harness.
|
|
386
389
|
*/
|
|
387
390
|
export function resolveCandidate(
|
|
388
391
|
candidates: readonly GuideCandidate[],
|
|
389
392
|
eligible: ReadonlySet<string>,
|
|
390
393
|
): AgentBinding | undefined {
|
|
391
394
|
for (const candidate of candidates) {
|
|
392
|
-
const
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
return { harness: nativeHarness, provider: NATIVE_HARNESS_PROVIDERS[nativeHarness]!, model: candidate.model };
|
|
396
|
-
}
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
if (eligible.has("pi")) {
|
|
400
|
-
return { harness: "pi", provider: "openrouter", model: `${candidate.vendor}/${candidate.model}` };
|
|
401
|
-
}
|
|
395
|
+
const mapped = ADMITTED_GUIDE_BINDINGS[`${candidate.vendor}/${candidate.model}`];
|
|
396
|
+
if (!mapped || !eligible.has(mapped.harness)) continue;
|
|
397
|
+
return mapped;
|
|
402
398
|
}
|
|
403
399
|
return undefined;
|
|
404
400
|
}
|
|
@@ -424,7 +420,7 @@ export function planGuideSetup(options: {
|
|
|
424
420
|
workflow: workflow.slug,
|
|
425
421
|
stage: stage.slug,
|
|
426
422
|
role: stage.role,
|
|
427
|
-
reason: "no guide candidate has an authenticated harness (see `kxm harness list`)",
|
|
423
|
+
reason: "no admitted guide candidate has an authenticated harness (see `kxm harness list`)",
|
|
428
424
|
});
|
|
429
425
|
covered = false;
|
|
430
426
|
continue;
|
|
@@ -465,6 +461,7 @@ function workflowDocument(workflow: GuideWorkflow): Record<string, unknown> {
|
|
|
465
461
|
id: stage.slug,
|
|
466
462
|
kind: "agent",
|
|
467
463
|
agent: stage.role,
|
|
464
|
+
repositories: { control: isWriterRole(stage.role) ? "write" : "read" },
|
|
468
465
|
maxAttempts: 2,
|
|
469
466
|
on: {
|
|
470
467
|
passed: last ? { target: "$terminal", terminalStatus: "completed" } : workflow.stages[index + 1]!.slug,
|
|
@@ -508,6 +505,24 @@ export function renderGuideSetupFiles(projectRoot: string, plan: GuideSetupPlan)
|
|
|
508
505
|
return files;
|
|
509
506
|
}
|
|
510
507
|
|
|
508
|
+
/** Append the plan's admitted selectors to `.kxm/routes.yaml`. Does not write role files. */
|
|
509
|
+
export function mergeGuideRouteAdmission(projectRoot: string, plan: GuideSetupPlan): string[] {
|
|
510
|
+
const policy = loadRoutePolicy(projectRoot);
|
|
511
|
+
const added: string[] = [];
|
|
512
|
+
for (const binding of plan.agents.values()) {
|
|
513
|
+
const selector = `${binding.provider}/${binding.model}`;
|
|
514
|
+
if (policy.disabled.includes(selector) || policy.admitted.includes(selector)) continue;
|
|
515
|
+
policy.admitted.push(selector);
|
|
516
|
+
added.push(selector);
|
|
517
|
+
}
|
|
518
|
+
if (added.length === 0) return added;
|
|
519
|
+
policy.updatedAt = new Date().toISOString();
|
|
520
|
+
const path = join(projectRoot, ".kxm", "routes.yaml");
|
|
521
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
522
|
+
writeFileSync(path, stringify(policy), "utf8");
|
|
523
|
+
return added;
|
|
524
|
+
}
|
|
525
|
+
|
|
511
526
|
export interface WriteReport {
|
|
512
527
|
readonly written: string[];
|
|
513
528
|
readonly existed: string[];
|
|
@@ -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.
|
|
14
|
+
const VERSION = "0.7.98";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
BUILTIN_HARNESSES,
|
|
8
8
|
NATIVE_HARNESS_PROVIDERS,
|
|
9
9
|
probeHarnessAssignmentAsync,
|
|
10
|
-
|
|
10
|
+
oneShotPermissionArgs,
|
|
11
11
|
type HarnessAssignmentProbeOptions,
|
|
12
12
|
type HarnessStatus,
|
|
13
13
|
type HarnessCatalogEntry,
|
|
@@ -128,11 +128,11 @@ export function createKxmOneShotProducer(options: KxmOneShotProducerOptions = {}
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
if (options.defaultModel) {
|
|
132
|
+
const parsed = parseModelString(options.defaultModel, harness);
|
|
133
|
+
return { provider: parsed.provider, model: parsed.model, thinking: request.thinking };
|
|
134
|
+
}
|
|
135
|
+
throw new Error("producer_route_not_admitted");
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
async function checkAuth(harness: string, provider: string, model: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<HarnessStatus> {
|
|
@@ -167,8 +167,13 @@ export function createKxmOneShotProducer(options: KxmOneShotProducerOptions = {}
|
|
|
167
167
|
if (request.signal.aborted) return cancelled();
|
|
168
168
|
const catalogEntry = (options.catalog ?? BUILTIN_HARNESSES).find((h) => h.id === harness);
|
|
169
169
|
if (!catalogEntry?.oneShot) throw new Error(`oneshot_harness_unsupported: ${harness}`);
|
|
170
|
-
const
|
|
171
|
-
|
|
170
|
+
const permission = request.permission === "edit" || request.contextPacket?.task.permissionCeiling === "edit"
|
|
171
|
+
? "edit"
|
|
172
|
+
: "read-only";
|
|
173
|
+
const permissionArgs = oneShotPermissionArgs(harness, permission);
|
|
174
|
+
if (!permissionArgs) {
|
|
175
|
+
throw new Error(`oneshot_harness_unsupported: ${harness} ${permission === "edit" ? "writer_profile_unaudited" : "permission_profile_unaudited"}`);
|
|
176
|
+
}
|
|
172
177
|
// Pin the environment for auth and execution; don't observe subscription
|
|
173
178
|
// auth under one environment and then spawn under changed API-key settings.
|
|
174
179
|
const env = { ...(options.env ?? process.env) };
|
|
@@ -287,6 +292,9 @@ export function createKxmOneShotProducer(options: KxmOneShotProducerOptions = {}
|
|
|
287
292
|
const providerMetadata: Record<string, string | number | boolean> = {
|
|
288
293
|
processStatus: aborted ? "aborted" : transportFailed ? "failed" : "completed",
|
|
289
294
|
executionEvidenceId: evidence.id,
|
|
295
|
+
permission,
|
|
296
|
+
permissionProfile: permission === "edit" ? "writer" : "read-only",
|
|
297
|
+
authored: false,
|
|
290
298
|
};
|
|
291
299
|
if (procResult.code !== null && Number.isFinite(procResult.code)) providerMetadata.processExitCode = procResult.code;
|
|
292
300
|
if (procResult.signal) providerMetadata.processSignal = procResult.signal;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { parse } from "yaml";
|
|
4
|
+
import { parse, stringify } from "yaml";
|
|
5
5
|
|
|
6
6
|
export * from "./price-calc.ts";
|
|
7
7
|
import type { PriceCatalog, ModelPriceRow, PriceTier } from "./price-calc.ts";
|
|
@@ -167,3 +167,34 @@ export function loadPriceCatalogForEstimate(options?: {
|
|
|
167
167
|
return { catalog, unavailable: false, stale: false };
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Stamp the existing project list-price file as today's estimate.
|
|
172
|
+
* This does not fetch vendor rates. Until a catalog's date is today,
|
|
173
|
+
* `loadPriceCatalogForEstimate` keeps the estimate unknown.
|
|
174
|
+
*/
|
|
175
|
+
export function acknowledgePriceCatalog(projectRoot: string, now = new Date()): PriceCatalog {
|
|
176
|
+
const path = join(projectRoot, ".kxm", "prices.yaml");
|
|
177
|
+
const loaded = loadPriceCatalog(projectRoot);
|
|
178
|
+
if (!loaded) throw new Error("price catalog missing");
|
|
179
|
+
const date = now.toISOString().slice(0, 10);
|
|
180
|
+
const body = {
|
|
181
|
+
schema: loaded.schema,
|
|
182
|
+
date,
|
|
183
|
+
currency: loaded.currency ?? "USD",
|
|
184
|
+
models: loaded.models,
|
|
185
|
+
};
|
|
186
|
+
const stamped: PriceCatalog = { ...body, sha256: hashPriceCatalog(body) };
|
|
187
|
+
writeFileSync(path, stringify({
|
|
188
|
+
schema: stamped.schema,
|
|
189
|
+
date: stamped.date,
|
|
190
|
+
sha256: stamped.sha256,
|
|
191
|
+
currency: stamped.currency ?? "USD",
|
|
192
|
+
models: stamped.models,
|
|
193
|
+
}), "utf8");
|
|
194
|
+
const again = loadPriceCatalog(projectRoot);
|
|
195
|
+
if (!again || again.date !== date || again.sha256 !== stamped.sha256) {
|
|
196
|
+
throw new Error("price catalog acknowledge failed verification");
|
|
197
|
+
}
|
|
198
|
+
return again;
|
|
199
|
+
}
|
|
200
|
+
|
|
@@ -422,7 +422,8 @@ export interface RoutingComparison {
|
|
|
422
422
|
blocked: number;
|
|
423
423
|
failed: number;
|
|
424
424
|
reworkRate: number;
|
|
425
|
-
totalCostUsd: number;
|
|
425
|
+
totalCostUsd: number | null;
|
|
426
|
+
missingCostRuns: number;
|
|
426
427
|
totalTokensIn: number;
|
|
427
428
|
totalTokensOut: number;
|
|
428
429
|
totalHumanInterventions: number;
|
|
@@ -445,6 +446,8 @@ export function compareRoutingRecords(records: RoutingRecord[]): RoutingComparis
|
|
|
445
446
|
const blocked = settled.filter((record) => record.finalOutcome === "blocked").length;
|
|
446
447
|
const failed = settled.filter((record) => record.finalOutcome === "failed").length;
|
|
447
448
|
const reworked = records.filter((record) => record.retries > 0 || record.transitions > 0).length;
|
|
449
|
+
const missingCostRuns = records.filter((record) => typeof record.costUsd !== "number" || !Number.isFinite(record.costUsd)).length;
|
|
450
|
+
const summedCost = records.reduce((sum, record) => sum + (typeof record.costUsd === "number" && Number.isFinite(record.costUsd) ? record.costUsd : 0), 0);
|
|
448
451
|
return {
|
|
449
452
|
behavioralSha256,
|
|
450
453
|
runs: records.length,
|
|
@@ -452,7 +455,8 @@ export function compareRoutingRecords(records: RoutingRecord[]): RoutingComparis
|
|
|
452
455
|
blocked,
|
|
453
456
|
failed,
|
|
454
457
|
reworkRate: records.length === 0 ? 0 : Math.round((reworked / records.length) * 100) / 100,
|
|
455
|
-
totalCostUsd:
|
|
458
|
+
totalCostUsd: missingCostRuns > 0 ? null : Math.round(summedCost * 10_000) / 10_000,
|
|
459
|
+
missingCostRuns,
|
|
456
460
|
totalTokensIn: records.reduce((sum, record) => sum + (record.tokensIn ?? 0), 0),
|
|
457
461
|
totalTokensOut: records.reduce((sum, record) => sum + (record.tokensOut ?? 0), 0),
|
|
458
462
|
totalHumanInterventions: records.reduce((sum, record) => sum + record.humanInterventions, 0),
|
|
@@ -748,9 +752,11 @@ export function generateRoutingReport(
|
|
|
748
752
|
|
|
749
753
|
const meteredCostUsd = Math.round(meteredCostTotal * 10_000) / 10_000;
|
|
750
754
|
const costPerAcceptedUsd = acceptedCount > 0
|
|
751
|
-
? (
|
|
752
|
-
?
|
|
753
|
-
: (
|
|
755
|
+
? (unknownCostAttempts > 0
|
|
756
|
+
? null
|
|
757
|
+
: (meteredCostUsd > 0 || unmeteredAttempts > 0
|
|
758
|
+
? Math.round((meteredCostUsd / acceptedCount) * 10_000) / 10_000
|
|
759
|
+
: 0))
|
|
754
760
|
: null;
|
|
755
761
|
|
|
756
762
|
const flagged = unknownCostAttempts > 0;
|
|
@@ -817,8 +823,8 @@ export function generateRoutingReport(
|
|
|
817
823
|
return a.reworkRate - b.reworkRate;
|
|
818
824
|
}
|
|
819
825
|
|
|
820
|
-
// 2. Cost: unknown is never ranked cheapest. Any unknown-cost attempt
|
|
821
|
-
// route's cost
|
|
826
|
+
// 2. Cost: unknown is never ranked cheapest. Any unknown-cost attempt leaves the
|
|
827
|
+
// route's cost unknown (unmetered or metered attempts beside it do not price it).
|
|
822
828
|
const aCostUnknown = a.unknownCostAttempts > 0;
|
|
823
829
|
const bCostUnknown = b.unknownCostAttempts > 0;
|
|
824
830
|
if (aCostUnknown && !bCostUnknown) return 1;
|
|
@@ -420,13 +420,14 @@ export function createStudioServer(options: StudioServerOptions = {}): StudioSer
|
|
|
420
420
|
return;
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
-
res.writeHead(
|
|
423
|
+
res.writeHead(501, { "Content-Type": "application/json" });
|
|
424
424
|
res.end(JSON.stringify({
|
|
425
|
-
ok:
|
|
425
|
+
ok: false,
|
|
426
|
+
executed: false,
|
|
426
427
|
mutationId,
|
|
427
428
|
command,
|
|
428
|
-
mappedToCli:
|
|
429
|
-
|
|
429
|
+
mappedToCli: false,
|
|
430
|
+
error: "mutation_handler_missing",
|
|
430
431
|
}));
|
|
431
432
|
return;
|
|
432
433
|
}
|
|
@@ -174,6 +174,37 @@ function coreTemplate(projectId: string, projectName: string, variant: KxmTempla
|
|
|
174
174
|
}],
|
|
175
175
|
]);
|
|
176
176
|
if (variant === "v4-registry") {
|
|
177
|
+
const coordinator = files.get(".kxm/agents/coordinator.yaml");
|
|
178
|
+
if (coordinator) {
|
|
179
|
+
files.set(".kxm/agents/coordinator.yaml", {
|
|
180
|
+
...coordinator,
|
|
181
|
+
harness: "claude",
|
|
182
|
+
model: { provider: "anthropic", model: "fable" },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const implementer = files.get(".kxm/agents/implementer.yaml");
|
|
186
|
+
if (implementer) {
|
|
187
|
+
files.set(".kxm/agents/implementer.yaml", {
|
|
188
|
+
...implementer,
|
|
189
|
+
harness: "grok",
|
|
190
|
+
model: { provider: "xai", model: "grok-4.6" },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const workflow = files.get(".kxm/workflows/default.yaml");
|
|
194
|
+
if (workflow) {
|
|
195
|
+
const limits = { ...(workflow.limits as JsonObject) };
|
|
196
|
+
delete limits.maxAgentTimeMs;
|
|
197
|
+
files.set(".kxm/workflows/default.yaml", { ...workflow, limits });
|
|
198
|
+
}
|
|
199
|
+
// The two models the template names, and nothing else. A fresh project can
|
|
200
|
+
// be driven without falling through to an unadmitted default model.
|
|
201
|
+
files.set(".kxm/routes.yaml", {
|
|
202
|
+
schema: "kxm.routes.v2",
|
|
203
|
+
updatedAt: "2026-09-23T00:00:00.000Z",
|
|
204
|
+
admitted: ["anthropic/fable", "xai/grok-4.6"],
|
|
205
|
+
disabled: [],
|
|
206
|
+
roles: { implementer: ["xai/grok-4.6"] },
|
|
207
|
+
});
|
|
177
208
|
files.set(".kxm/gates.yaml", {
|
|
178
209
|
schema: "kxm.gate-registry.v1",
|
|
179
210
|
gates: { test: { kind: "command", argv: ["npm", "test"], timeoutMs: 3_600_000 } },
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import type { KxmProducerResult } from "./engine.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fingerprint of a checkout taken around a live producer spawn.
|
|
6
|
+
* Porcelain alone misses a content edit that keeps the same status line, so the
|
|
7
|
+
* witness also includes unstaged and staged diffs.
|
|
8
|
+
*/
|
|
9
|
+
export interface WorktreeWitness {
|
|
10
|
+
readonly unwitnessed: boolean;
|
|
11
|
+
readonly fingerprint: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function gitText(cwd: string, args: readonly string[]): string | undefined {
|
|
15
|
+
const result = spawnSync("git", ["-C", cwd, ...args], {
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
timeout: 15_000,
|
|
19
|
+
});
|
|
20
|
+
if (result.error || result.status !== 0) return undefined;
|
|
21
|
+
return result.stdout ?? "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function captureWorktreeWitness(cwd: string): WorktreeWitness {
|
|
25
|
+
const porcelain = gitText(cwd, ["status", "--porcelain=v1", "-uall"]);
|
|
26
|
+
const diff = gitText(cwd, ["diff", "--no-ext-diff"]);
|
|
27
|
+
const staged = gitText(cwd, ["diff", "--cached", "--no-ext-diff"]);
|
|
28
|
+
if (porcelain === undefined || diff === undefined || staged === undefined) {
|
|
29
|
+
return { unwitnessed: true, fingerprint: "" };
|
|
30
|
+
}
|
|
31
|
+
return { unwitnessed: false, fingerprint: `${porcelain}\0${diff}\0${staged}` };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function worktreeChanged(before: WorktreeWitness, after: WorktreeWitness): boolean {
|
|
35
|
+
if (before.unwitnessed || after.unwitnessed) return false;
|
|
36
|
+
return before.fingerprint !== after.fingerprint;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A live `passed` is an authoring success only when the step declared write
|
|
41
|
+
* and the checkout fingerprint changed. A read-only step that mutates the tree
|
|
42
|
+
* cannot stay `passed`. Metadata `authored` is set here, not trusted from the
|
|
43
|
+
* producer.
|
|
44
|
+
*/
|
|
45
|
+
export function applyAuthoringWitness(
|
|
46
|
+
result: KxmProducerResult,
|
|
47
|
+
input: { writes: boolean; before: WorktreeWitness; after: WorktreeWitness },
|
|
48
|
+
): KxmProducerResult {
|
|
49
|
+
const changed = worktreeChanged(input.before, input.after);
|
|
50
|
+
const unwitnessed = input.before.unwitnessed || input.after.unwitnessed;
|
|
51
|
+
const providerMetadata: Record<string, string | number | boolean> = { ...(result.providerMetadata ?? {}) };
|
|
52
|
+
if (input.writes) {
|
|
53
|
+
if (unwitnessed || !changed) {
|
|
54
|
+
providerMetadata.authored = false;
|
|
55
|
+
providerMetadata.authoringWitness = unwitnessed ? "unwitnessed" : "unchanged";
|
|
56
|
+
if (result.outcome === "passed") return { ...result, outcome: "failed", providerMetadata };
|
|
57
|
+
} else {
|
|
58
|
+
providerMetadata.authored = true;
|
|
59
|
+
providerMetadata.authoringWitness = "changed";
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
providerMetadata.authored = false;
|
|
63
|
+
if (!unwitnessed && changed) {
|
|
64
|
+
providerMetadata.authoringWitness = "readonly_mutated";
|
|
65
|
+
if (result.outcome === "passed") return { ...result, outcome: "failed", providerMetadata };
|
|
66
|
+
} else {
|
|
67
|
+
providerMetadata.authoringWitness = unwitnessed ? "unwitnessed" : "read-only";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { ...result, providerMetadata };
|
|
71
|
+
}
|