@kontextmind/kxm 0.6.0 → 0.7.10
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/agents/coordinator.yaml +9 -0
- package/.kxm/agents/critic-arch.yaml +13 -0
- package/.kxm/agents/critic-cli.yaml +13 -0
- package/.kxm/agents/implementer.yaml +13 -0
- package/.kxm/gates.yaml +8 -0
- package/.kxm/producers.yaml +22 -0
- package/.kxm/project.yaml +15 -0
- package/.kxm/roles/writer.yaml +7 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +5 -0
- package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
- package/docs/agent-skills.md +135 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/browser-automation.md +116 -0
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +21 -0
- package/docs/kb/how-credentials-retrieved-safely.md +31 -0
- package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
- package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
- package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
- package/docs/kb/how-to-resume-after-mfa.md +28 -0
- package/docs/kb/how-to-take-over-session.md +32 -0
- package/docs/kb/why-authentication-disappeared.md +32 -0
- package/docs/kb/why-automation-opened-different-browser.md +32 -0
- package/docs/kb/why-session-viewer-cannot-control.md +31 -0
- package/docs/kxm-handbook.md +3 -3
- package/docs/operations.md +24 -0
- package/docs/operator-pi-packages.md +63 -0
- package/docs/prompts/browser-annotate-feedback.md +41 -0
- package/docs/prompts/browser-diagnose-recover.md +38 -0
- package/docs/prompts/browser-explore.md +42 -0
- package/docs/prompts/browser-repro-fix.md +48 -0
- package/docs/prompts/browser-start.md +41 -0
- package/docs/prompts/browser-takeover.md +50 -0
- package/docs/skills/repo-work-delivery.md +107 -0
- package/docs/skills.md +2 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +41 -1
- package/docs/vnext/validation.md +9 -0
- package/docs/webhook-workflows.md +2 -2
- package/examples/README.md +1 -1
- package/package.json +16 -17
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/README.md +1 -1
- package/plugins/kxm/dist/cli.js +41620 -35578
- package/plugins/kxm/dist/core.js +271 -34
- package/plugins/kxm/dist/extension.js +7759 -86
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +8218 -2328
- package/plugins/kxm/dist/server.js +3125 -2260
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/SUITE.md +5 -0
- package/plugins/kxm/skills/hints.json +103 -0
- package/plugins/kxm/skills/kxm/SKILL.md +30 -83
- package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
- package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
- package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
- package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
- package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
- package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
- package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
- package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
- package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
- package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
- package/plugins/kxm/src/autocomplete.ts +9 -3
- package/plugins/kxm/src/browser.ts +603 -0
- package/plugins/kxm/src/cli/context-skills.ts +373 -0
- package/plugins/kxm/src/cli/hub.ts +614 -0
- package/plugins/kxm/src/cli/roles.ts +615 -0
- package/plugins/kxm/src/cli/system.ts +906 -0
- package/plugins/kxm/src/cli/tasks.ts +364 -0
- package/plugins/kxm/src/cli/types.ts +270 -0
- package/plugins/kxm/src/cli/vnext.ts +698 -0
- package/plugins/kxm/src/cli/workflows.ts +699 -0
- package/plugins/kxm/src/cli.ts +362 -2849
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +357 -8
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/init-guide-setup.ts +547 -0
- package/plugins/kxm/src/local-snapshot.ts +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +127 -0
- package/plugins/kxm/src/modes.ts +348 -0
- package/plugins/kxm/src/policy-draft.d.mts +55 -0
- package/plugins/kxm/src/policy-draft.mjs +565 -0
- package/plugins/kxm/src/price-calc.ts +17 -18
- package/plugins/kxm/src/prices.ts +32 -16
- package/plugins/kxm/src/producers.ts +71 -0
- package/plugins/kxm/src/protocol.ts +111 -0
- package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
- package/plugins/kxm/src/restricted-yaml.mjs +145 -0
- package/plugins/kxm/src/role.ts +710 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/runtime.ts +4 -0
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/ssh-remote.ts +560 -0
- package/plugins/kxm/src/store.ts +1 -1
- package/plugins/kxm/src/studio-layout.ts +660 -17
- package/plugins/kxm/src/subagent-control.ts +312 -0
- package/plugins/kxm/src/suggest.ts +7 -13
- package/plugins/kxm/src/telemetry.ts +82 -0
- package/plugins/kxm/src/tui.ts +140 -0
- package/plugins/kxm/src/vnext-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +53 -111
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +214 -62
- package/plugins/kxm/src/vnext-harness.ts +336 -84
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
- package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
- package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
- package/plugins/kxm/src/vnext-runtime.ts +14 -0
- package/plugins/kxm/src/workflow-manager.ts +392 -0
- package/plugins/kxm/src/workflow-tui.ts +255 -0
- package/plugins/kxm/src/workflow.ts +144 -0
- package/schemas/policy-draft/README.md +17 -0
- package/schemas/policy-draft/model.v2.schema.json +140 -0
- package/schemas/policy-draft/role.v2.schema.json +91 -0
- package/schemas/vnext/modes.schema.json +56 -0
- package/schemas/vnext/role.schema.json +76 -0
- package/schemas/vnext/run-event.schema.json +1 -0
- package/scripts/assignment-run.d.mts +1 -1
- package/scripts/assignment-run.mjs +44 -35
- package/scripts/check-generated.mjs +33 -9
- package/scripts/emit-codex-artifacts.mjs +255 -11
- package/scripts/harness-run.d.mts +12 -4
- package/scripts/harness-run.mjs +65 -17
- package/scripts/kxm-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +150 -2
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -0
- package/scripts/native-critic.d.mts +5 -0
- package/scripts/native-critic.mjs +60 -0
- package/.kxm/config/README.md +0 -5
- package/.kxm/config/agents.json +0 -43
- package/.kxm/config/env.example +0 -56
- package/.kxm/config/update.example.yaml +0 -9
- package/.kxm/config/workflows/fix.json +0 -160
- package/.kxm/config/workflows/jira-development.json +0 -116
- package/.kxm/config/workflows/provenance-quorum.json +0 -150
- package/.kxm/config/workflows/v04-dogfood.json +0 -72
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { stringify } from "yaml";
|
|
4
|
+
import { defaultSpawn } from "./vnext-oneshot-process.ts";
|
|
5
|
+
|
|
6
|
+
export interface InventoryPrice {
|
|
7
|
+
inputPerMillion: number | null;
|
|
8
|
+
outputPerMillion: number | null;
|
|
9
|
+
cacheReadPerMillion: number | null;
|
|
10
|
+
cacheWritePerMillion: number | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface InventoryCapabilities {
|
|
14
|
+
thinking: { supported: boolean | null; levels?: string[]; default?: string; source: string };
|
|
15
|
+
speed: { fast: boolean | null; tiers?: string[]; source: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface InventoryModel {
|
|
19
|
+
id: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
contextLength?: number;
|
|
22
|
+
capabilities: InventoryCapabilities;
|
|
23
|
+
standard?: InventoryPrice;
|
|
24
|
+
discount?: InventoryPrice;
|
|
25
|
+
sources: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ModelInventory {
|
|
29
|
+
schema: "kxm.model-inventory.v1";
|
|
30
|
+
fetchedAt: string;
|
|
31
|
+
currency: "USD";
|
|
32
|
+
sources: Record<string, { url: string; ok: boolean; error?: string }>;
|
|
33
|
+
models: InventoryModel[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const OR_URL = "https://openrouter.ai/api/v1/models";
|
|
37
|
+
const NOUS_URL = "https://inference-api.nousresearch.com/v1/models";
|
|
38
|
+
|
|
39
|
+
function price(value: unknown): number | null {
|
|
40
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value * 1_000_000;
|
|
41
|
+
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)) && Number(value) >= 0) return Number(value) * 1_000_000;
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function rates(raw: any): InventoryPrice | undefined {
|
|
45
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
46
|
+
const input = price(raw.prompt ?? raw.input);
|
|
47
|
+
const output = price(raw.completion ?? raw.output);
|
|
48
|
+
if (input === null && output === null) return undefined;
|
|
49
|
+
return {
|
|
50
|
+
inputPerMillion: input,
|
|
51
|
+
outputPerMillion: output,
|
|
52
|
+
cacheReadPerMillion: price(raw.input_cache_read ?? raw.cache_read),
|
|
53
|
+
cacheWritePerMillion: price(raw.input_cache_write ?? raw.cache_write),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function modelId(id: string): string {
|
|
57
|
+
return id.trim().toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
async function fetchModels(url: string, authorization?: string): Promise<{ data: any[]; error?: string }> {
|
|
60
|
+
try {
|
|
61
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
62
|
+
if (authorization) headers.Authorization = `Bearer ${authorization}`;
|
|
63
|
+
const response = await fetch(url, { headers, signal: AbortSignal.timeout(15_000) });
|
|
64
|
+
if (!response.ok) return { data: [], error: `HTTP ${response.status}` };
|
|
65
|
+
const payload = await response.json() as any;
|
|
66
|
+
if (Array.isArray(payload?.data)) return { data: payload.data };
|
|
67
|
+
return { data: [], error: "response has no data array" };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return { data: [], error: error instanceof Error ? error.message : "request failed" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Refreshes a provenance-bearing model inventory. Aggregator prices are never treated as native auth evidence. */
|
|
74
|
+
export async function refreshModelInventory(options: { outputRoot: string; env?: NodeJS.ProcessEnv; now?: Date }): Promise<ModelInventory> {
|
|
75
|
+
const env = options.env ?? process.env;
|
|
76
|
+
const fetchedAt = (options.now ?? new Date()).toISOString();
|
|
77
|
+
const sources: ModelInventory["sources"] = {};
|
|
78
|
+
const byId = new Map<string, InventoryModel>();
|
|
79
|
+
const add = (item: any, source: string, kind: "standard" | "discount") => {
|
|
80
|
+
if (!item || typeof item.id !== "string" || !item.id.trim()) return;
|
|
81
|
+
const key = modelId(item.id);
|
|
82
|
+
const current: InventoryModel = byId.get(key) ?? { id: item.id, ...(typeof item.name === "string" ? { name: item.name } : {}), ...(typeof item.context_length === "number" ? { contextLength: item.context_length } : {}), capabilities: { thinking: { supported: null, source: "unknown" }, speed: { fast: null, source: "unknown" } }, sources: [] };
|
|
83
|
+
if (!current.sources.includes(source)) current.sources.push(source);
|
|
84
|
+
const parsed = rates(item.pricing);
|
|
85
|
+
if (parsed) {
|
|
86
|
+
if (kind === "standard") current.standard = parsed;
|
|
87
|
+
else current.discount = parsed;
|
|
88
|
+
}
|
|
89
|
+
if (!current.name && typeof item.name === "string") current.name = item.name;
|
|
90
|
+
if (!current.contextLength && typeof item.context_length === "number") current.contextLength = item.context_length;
|
|
91
|
+
const parameters = Array.isArray(item.supported_parameters) ? item.supported_parameters.filter((value: unknown): value is string => typeof value === "string") : [];
|
|
92
|
+
if (parameters.some((value: string) => /reasoning|thinking/i.test(value))) current.capabilities.thinking = { supported: true, source };
|
|
93
|
+
if (parameters.some((value: string) => /speed|service.?tier|fast/i.test(value))) current.capabilities.speed = { fast: true, tiers: ["fast"], source };
|
|
94
|
+
if (Array.isArray(item.thinkingLevels) && item.thinkingLevels.length > 0) current.capabilities.thinking = { supported: true, levels: item.thinkingLevels, ...(item.defaultThinking ? { default: item.defaultThinking } : {}), source };
|
|
95
|
+
if (item.fast === true) current.capabilities.speed = { fast: true, tiers: ["fast"], source };
|
|
96
|
+
byId.set(key, current);
|
|
97
|
+
};
|
|
98
|
+
const native = async (command: string, args: string[], source: string) => {
|
|
99
|
+
const result = await defaultSpawn(command, args, { env, timeoutMs: 20_000 });
|
|
100
|
+
const ok = result.code === 0 && !result.error;
|
|
101
|
+
sources[source] = { url: `${command} ${args.join(" ")}`, ok, ...(ok ? {} : { error: (result.stderr || result.error?.message || "command failed").trim().slice(0, 240) }) };
|
|
102
|
+
if (!ok) return;
|
|
103
|
+
for (const line of (result.stdout || "").split(/\r?\n/)) {
|
|
104
|
+
const parts = line.trim().split(/\s+/);
|
|
105
|
+
const id = source === "pi" && parts.length >= 2 && parts[0] !== "provider" ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
106
|
+
if (id && /^[a-z0-9][a-z0-9._/-]+$/i.test(id) && id !== "Available") {
|
|
107
|
+
const effortMatch = source === "agy" ? id.match(/-(low|medium|high)$/i) : null;
|
|
108
|
+
add({ id, ...(effortMatch?.[1] ? { thinkingLevels: [effortMatch[1].toLowerCase()] } : {}) }, source, "standard");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
await native("pi", ["--list-models"], "pi");
|
|
113
|
+
await native("grok", ["models"], "grok");
|
|
114
|
+
await native("agy", ["models"], "agy");
|
|
115
|
+
const openrouter = await fetchModels(env.KXM_OPENROUTER_MODELS_URL?.trim() || OR_URL, env.OPENROUTER_API_KEY);
|
|
116
|
+
const openrouterUrl = env.KXM_OPENROUTER_MODELS_URL?.trim() || OR_URL;
|
|
117
|
+
sources.openrouter = { url: openrouterUrl, ok: !openrouter.error, ...(openrouter.error ? { error: openrouter.error } : {}) };
|
|
118
|
+
for (const item of openrouter.data) add(item, "openrouter", "standard");
|
|
119
|
+
const nousUrl = env.KXM_NOUS_MODELS_URL?.trim() || NOUS_URL;
|
|
120
|
+
const nous = await fetchModels(nousUrl, env.NOUS_API_KEY);
|
|
121
|
+
sources.nous = { url: nousUrl, ok: !nous.error, ...(nous.error ? { error: nous.error } : {}) };
|
|
122
|
+
for (const item of nous.data) add(item, "nous", "discount");
|
|
123
|
+
const inventory: ModelInventory = { schema: "kxm.model-inventory.v1", fetchedAt, currency: "USD", sources, models: [...byId.values()].sort((a, b) => a.id.localeCompare(b.id)) };
|
|
124
|
+
mkdirSync(join(options.outputRoot, ".kxm", "models"), { recursive: true });
|
|
125
|
+
writeFileSync(join(options.outputRoot, ".kxm", "models", "inventory.yaml"), stringify(inventory), "utf8");
|
|
126
|
+
return inventory;
|
|
127
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KXM Declarative Workflow Modes, Domain Isolation, and Pre-Flight Explain
|
|
3
|
+
*
|
|
4
|
+
* Implements major modes (base role + tools + prompt) and stackable domain
|
|
5
|
+
* toolkits to eliminate prompt bloat and calculate pre-flight token costs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { parse } from "yaml";
|
|
11
|
+
import { calculateModelCost, type PriceCatalog, loadPriceCatalog } from "./prices.ts";
|
|
12
|
+
|
|
13
|
+
export interface MajorMode {
|
|
14
|
+
description?: string | undefined;
|
|
15
|
+
baseTools: string[];
|
|
16
|
+
contextFiles?: string[] | undefined;
|
|
17
|
+
thinkingLevel?: "low" | "medium" | "high" | "xhigh" | undefined;
|
|
18
|
+
model?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Domain {
|
|
22
|
+
description?: string | undefined;
|
|
23
|
+
tools: string[];
|
|
24
|
+
contextFiles?: string[] | undefined;
|
|
25
|
+
promptSnippet?: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ModesConfig {
|
|
29
|
+
schema: "kxm.modes.v1";
|
|
30
|
+
majorModes: Record<string, MajorMode>;
|
|
31
|
+
domains?: Record<string, Domain> | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ResolvedMode {
|
|
35
|
+
majorMode: string;
|
|
36
|
+
enabledDomains: string[];
|
|
37
|
+
tools: string[];
|
|
38
|
+
contextFiles: string[];
|
|
39
|
+
promptSnippets: string[];
|
|
40
|
+
thinkingLevel?: string | undefined;
|
|
41
|
+
model?: string | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ContextBreakdownItem {
|
|
45
|
+
name: string;
|
|
46
|
+
chars: number;
|
|
47
|
+
estimatedTokens: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PromptFootprint {
|
|
51
|
+
majorMode: string;
|
|
52
|
+
enabledDomains: string[];
|
|
53
|
+
model: string;
|
|
54
|
+
breakdown: ContextBreakdownItem[];
|
|
55
|
+
totalChars: number;
|
|
56
|
+
totalTokens: number;
|
|
57
|
+
contextWindowRatio: number;
|
|
58
|
+
projectedCost: {
|
|
59
|
+
inputCostUsd: number | null;
|
|
60
|
+
cacheReadCostUsd: number | null;
|
|
61
|
+
outputCostEstimateUsd: number | null;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const DEFAULT_MODES_CONFIG: ModesConfig = Object.freeze({
|
|
66
|
+
schema: "kxm.modes.v1",
|
|
67
|
+
majorModes: {
|
|
68
|
+
coder: {
|
|
69
|
+
description: "First-pass implementation, bug fixing, and test authoring",
|
|
70
|
+
baseTools: ["read", "edit", "write", "bash"],
|
|
71
|
+
contextFiles: ["AGENTS.md"],
|
|
72
|
+
thinkingLevel: "medium" as const,
|
|
73
|
+
model: "grok/grok-4.6",
|
|
74
|
+
},
|
|
75
|
+
planner: {
|
|
76
|
+
description: "High-level architectural planning, scoping, and decomposition",
|
|
77
|
+
baseTools: ["read", "grep", "find"],
|
|
78
|
+
contextFiles: ["plans/implementation-plan.md"],
|
|
79
|
+
thinkingLevel: "high" as const,
|
|
80
|
+
model: "claude/fable",
|
|
81
|
+
},
|
|
82
|
+
auditor: {
|
|
83
|
+
description: "Security, compliance, and code quality verification",
|
|
84
|
+
baseTools: ["read", "grep"],
|
|
85
|
+
contextFiles: ["SECURITY.md"],
|
|
86
|
+
thinkingLevel: "high" as const,
|
|
87
|
+
model: "codex/gpt-5.6-sol",
|
|
88
|
+
},
|
|
89
|
+
browser: {
|
|
90
|
+
description: "Web application exploration, screenshotting, and UI testing",
|
|
91
|
+
baseTools: ["read", "bash"],
|
|
92
|
+
contextFiles: ["docs/browser-automation.md"],
|
|
93
|
+
thinkingLevel: "medium" as const,
|
|
94
|
+
model: "grok/grok-4.6",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
domains: {
|
|
98
|
+
git: {
|
|
99
|
+
description: "Git version control operations",
|
|
100
|
+
tools: ["git_status", "git_diff", "git_commit"],
|
|
101
|
+
promptSnippet: "Follow git branch conventions; never commit directly to main.",
|
|
102
|
+
},
|
|
103
|
+
k8s: {
|
|
104
|
+
description: "Kubernetes cluster inspection and deployment",
|
|
105
|
+
tools: ["kubectl_get", "kubectl_describe"],
|
|
106
|
+
promptSnippet: "Target local dev cluster; verify namespaces before mutating.",
|
|
107
|
+
},
|
|
108
|
+
database: {
|
|
109
|
+
description: "Database queries and schema verification",
|
|
110
|
+
tools: ["sqlite_query", "sqlite_schema"],
|
|
111
|
+
promptSnippet: "Database is SQLite at .kxm/state/kxm.db; use read-only queries.",
|
|
112
|
+
},
|
|
113
|
+
browser: {
|
|
114
|
+
description: "Remote Steel browser sessions and visual testing",
|
|
115
|
+
tools: ["steel_session", "steel_scrape", "steel_screenshot"],
|
|
116
|
+
promptSnippet: "Use Steel on DOKS for browser automation; invoke takeover on MFA.",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Estimate tokens from character count using standard ~4 chars/token heuristic.
|
|
123
|
+
*/
|
|
124
|
+
export function estimateTokens(charCount: number): number {
|
|
125
|
+
return Math.ceil(charCount / 3.8);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Load and validate `.kxm/modes.yaml` from project root or return defaults.
|
|
130
|
+
*/
|
|
131
|
+
export function loadModesConfig(projectRoot?: string): ModesConfig {
|
|
132
|
+
if (!projectRoot) {
|
|
133
|
+
return DEFAULT_MODES_CONFIG;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const modesPath = join(projectRoot, ".kxm", "modes.yaml");
|
|
137
|
+
if (!existsSync(modesPath)) {
|
|
138
|
+
return DEFAULT_MODES_CONFIG;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const raw = readFileSync(modesPath, "utf8");
|
|
143
|
+
const parsed = parse(raw) as Partial<ModesConfig>;
|
|
144
|
+
if (parsed && parsed.schema === "kxm.modes.v1" && parsed.majorModes) {
|
|
145
|
+
return {
|
|
146
|
+
schema: "kxm.modes.v1",
|
|
147
|
+
majorModes: { ...DEFAULT_MODES_CONFIG.majorModes, ...parsed.majorModes },
|
|
148
|
+
domains: { ...DEFAULT_MODES_CONFIG.domains, ...parsed.domains },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
// Malformed config falls back to default safely
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return DEFAULT_MODES_CONFIG;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolve active tools, context files, and prompt snippets for a major mode and domain set.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveActiveMode(
|
|
162
|
+
config: ModesConfig,
|
|
163
|
+
majorModeName = "coder",
|
|
164
|
+
domainNames: string[] = []
|
|
165
|
+
): ResolvedMode {
|
|
166
|
+
const major = config.majorModes[majorModeName] || config.majorModes["coder"] || DEFAULT_MODES_CONFIG.majorModes["coder"]!;
|
|
167
|
+
const tools = new Set<string>(major.baseTools);
|
|
168
|
+
const contextFiles = new Set<string>(major.contextFiles || []);
|
|
169
|
+
const promptSnippets: string[] = [];
|
|
170
|
+
|
|
171
|
+
const enabledDomains: string[] = [];
|
|
172
|
+
|
|
173
|
+
if (config.domains) {
|
|
174
|
+
for (const dName of domainNames) {
|
|
175
|
+
const d = config.domains[dName.toLowerCase().trim()];
|
|
176
|
+
if (d) {
|
|
177
|
+
enabledDomains.push(dName.toLowerCase().trim());
|
|
178
|
+
d.tools.forEach((t) => tools.add(t));
|
|
179
|
+
(d.contextFiles || []).forEach((f) => contextFiles.add(f));
|
|
180
|
+
if (d.promptSnippet) {
|
|
181
|
+
promptSnippets.push(d.promptSnippet);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
majorMode: majorModeName,
|
|
189
|
+
enabledDomains,
|
|
190
|
+
tools: Array.from(tools),
|
|
191
|
+
contextFiles: Array.from(contextFiles),
|
|
192
|
+
promptSnippets,
|
|
193
|
+
thinkingLevel: major.thinkingLevel,
|
|
194
|
+
model: major.model || "grok/grok-4.6",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Calculate the exact prompt token footprint and projected costs for a mode configuration.
|
|
200
|
+
*/
|
|
201
|
+
export function calculatePromptFootprint(
|
|
202
|
+
resolved: ResolvedMode,
|
|
203
|
+
projectRoot: string = process.cwd(),
|
|
204
|
+
catalog?: PriceCatalog
|
|
205
|
+
): PromptFootprint {
|
|
206
|
+
const breakdown: ContextBreakdownItem[] = [];
|
|
207
|
+
|
|
208
|
+
// 1. Base System Prompt
|
|
209
|
+
const baseSystemPromptChars = 3200; // ~850 tokens base system prompt
|
|
210
|
+
breakdown.push({
|
|
211
|
+
name: `Base System Prompt (${resolved.majorMode})`,
|
|
212
|
+
chars: baseSystemPromptChars,
|
|
213
|
+
estimatedTokens: estimateTokens(baseSystemPromptChars),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// 2. Context Files
|
|
217
|
+
for (const relPath of resolved.contextFiles) {
|
|
218
|
+
const fullPath = join(projectRoot, relPath);
|
|
219
|
+
let chars = 0;
|
|
220
|
+
if (existsSync(fullPath)) {
|
|
221
|
+
try {
|
|
222
|
+
chars = readFileSync(fullPath, "utf8").length;
|
|
223
|
+
} catch {
|
|
224
|
+
chars = 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (chars === 0) {
|
|
228
|
+
chars = 1500; // Estimated fallback if missing
|
|
229
|
+
}
|
|
230
|
+
breakdown.push({
|
|
231
|
+
name: `Context File: ${relPath}`,
|
|
232
|
+
chars,
|
|
233
|
+
estimatedTokens: estimateTokens(chars),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 3. Domain Prompts
|
|
238
|
+
if (resolved.promptSnippets.length > 0) {
|
|
239
|
+
const snippetChars = resolved.promptSnippets.join("\n").length;
|
|
240
|
+
breakdown.push({
|
|
241
|
+
name: `Domain Prompts (${resolved.enabledDomains.join(", ")})`,
|
|
242
|
+
chars: snippetChars,
|
|
243
|
+
estimatedTokens: estimateTokens(snippetChars),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 4. Tool Schemas
|
|
248
|
+
// Each MCP / standard tool schema consumes ~150-200 tokens
|
|
249
|
+
const toolSchemaChars = resolved.tools.length * 650;
|
|
250
|
+
breakdown.push({
|
|
251
|
+
name: `Tool Schemas (${resolved.tools.length} active tools)`,
|
|
252
|
+
chars: toolSchemaChars,
|
|
253
|
+
estimatedTokens: estimateTokens(toolSchemaChars),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const totalChars = breakdown.reduce((sum, item) => sum + item.chars, 0);
|
|
257
|
+
const totalTokens = breakdown.reduce((sum, item) => sum + item.estimatedTokens, 0);
|
|
258
|
+
const maxContextWindow = 200000;
|
|
259
|
+
const contextWindowRatio = Math.round((totalTokens / maxContextWindow) * 1000) / 10;
|
|
260
|
+
|
|
261
|
+
// 5. Projected Costs
|
|
262
|
+
const cat = catalog || loadPriceCatalog(projectRoot);
|
|
263
|
+
const rawModel = resolved.model || "grok/grok-4.6";
|
|
264
|
+
const [prov, mod] = rawModel.includes("/") ? rawModel.split("/", 2) : [undefined, rawModel];
|
|
265
|
+
|
|
266
|
+
const inputCost = cat
|
|
267
|
+
? calculateModelCost(cat, {
|
|
268
|
+
model: mod || rawModel,
|
|
269
|
+
...(prov !== undefined ? { provider: prov } : {}),
|
|
270
|
+
tokensIn: totalTokens,
|
|
271
|
+
tokensOut: 0,
|
|
272
|
+
cacheReadTokens: 0,
|
|
273
|
+
cacheWriteTokens: 0,
|
|
274
|
+
})
|
|
275
|
+
: undefined;
|
|
276
|
+
|
|
277
|
+
const cacheReadCost = cat
|
|
278
|
+
? calculateModelCost(cat, {
|
|
279
|
+
model: mod || rawModel,
|
|
280
|
+
...(prov !== undefined ? { provider: prov } : {}),
|
|
281
|
+
tokensIn: 0,
|
|
282
|
+
tokensOut: 0,
|
|
283
|
+
cacheReadTokens: totalTokens,
|
|
284
|
+
cacheWriteTokens: 0,
|
|
285
|
+
})
|
|
286
|
+
: undefined;
|
|
287
|
+
|
|
288
|
+
const outputEstimateCost = cat
|
|
289
|
+
? calculateModelCost(cat, {
|
|
290
|
+
model: mod || rawModel,
|
|
291
|
+
...(prov !== undefined ? { provider: prov } : {}),
|
|
292
|
+
tokensIn: 0,
|
|
293
|
+
tokensOut: 1000,
|
|
294
|
+
cacheReadTokens: 0,
|
|
295
|
+
cacheWriteTokens: 0,
|
|
296
|
+
})
|
|
297
|
+
: undefined;
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
majorMode: resolved.majorMode,
|
|
301
|
+
enabledDomains: resolved.enabledDomains,
|
|
302
|
+
model: resolved.model || "grok/grok-4.6",
|
|
303
|
+
breakdown,
|
|
304
|
+
totalChars,
|
|
305
|
+
totalTokens,
|
|
306
|
+
contextWindowRatio,
|
|
307
|
+
projectedCost: {
|
|
308
|
+
inputCostUsd: inputCost?.costUsd ?? null,
|
|
309
|
+
cacheReadCostUsd: cacheReadCost?.costUsd ?? null,
|
|
310
|
+
outputCostEstimateUsd: outputEstimateCost?.costUsd ?? null,
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Format a human-readable ASCII report for `kxm explain`.
|
|
317
|
+
*/
|
|
318
|
+
export function formatModesExplainReport(footprint: PromptFootprint): string {
|
|
319
|
+
const divider = "═".repeat(60);
|
|
320
|
+
const subDivider = "─".repeat(60);
|
|
321
|
+
|
|
322
|
+
let out = `\n${divider}\n`;
|
|
323
|
+
out += `KXM PRE-FLIGHT CONTEXT EXPLAIN\n`;
|
|
324
|
+
out += `${divider}\n`;
|
|
325
|
+
out += `Major Mode: ${footprint.majorMode}\n`;
|
|
326
|
+
out += `Enabled Domains: ${footprint.enabledDomains.length > 0 ? footprint.enabledDomains.join(", ") : "(none)"}\n`;
|
|
327
|
+
out += `Target Model: ${footprint.model}\n\n`;
|
|
328
|
+
|
|
329
|
+
out += `CONTEXT BREAKDOWN:\n`;
|
|
330
|
+
for (const item of footprint.breakdown) {
|
|
331
|
+
const padName = item.name.padEnd(38, " ");
|
|
332
|
+
const tokenStr = `${item.estimatedTokens.toLocaleString()} tokens`.padStart(16, " ");
|
|
333
|
+
out += ` • ${padName} ${tokenStr}\n`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
out += `${subDivider}\n`;
|
|
337
|
+
const totalPad = `TOTAL PROMPT FOOTPRINT:`.padEnd(38, " ");
|
|
338
|
+
const totalStr = `${footprint.totalTokens.toLocaleString()} tokens`.padStart(16, " ");
|
|
339
|
+
out += ` ${totalPad} ${totalStr} (${footprint.contextWindowRatio}% of 200k window)\n\n`;
|
|
340
|
+
|
|
341
|
+
out += `PROJECTED COSTS (per turn):\n`;
|
|
342
|
+
out += ` • Initial Turn Input Cost: ${footprint.projectedCost.inputCostUsd !== null ? `$${footprint.projectedCost.inputCostUsd.toFixed(4)}` : "unmetered/unknown"}\n`;
|
|
343
|
+
out += ` • Subsequent Cache-Read Cost: ${footprint.projectedCost.cacheReadCostUsd !== null ? `$${footprint.projectedCost.cacheReadCostUsd.toFixed(4)} (approx 90% savings)` : "unmetered/unknown"}\n`;
|
|
344
|
+
out += ` • Output Estimate (1k tokens): ${footprint.projectedCost.outputCostEstimateUsd !== null ? `$${footprint.projectedCost.outputCostEstimateUsd.toFixed(4)}` : "unmetered/unknown"}\n`;
|
|
345
|
+
out += `${divider}\n`;
|
|
346
|
+
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { JsonObject } from "./restricted-yaml.mjs";
|
|
2
|
+
|
|
3
|
+
export const POLICY_DRAFT_MODEL_SCHEMA: "kxm.model.v2";
|
|
4
|
+
export const POLICY_DRAFT_ROLE_SCHEMA: "kxm.role.v2";
|
|
5
|
+
export const POLICY_DRAFT_PURPOSES: readonly [
|
|
6
|
+
"writer",
|
|
7
|
+
"planner",
|
|
8
|
+
"reviewer-arch",
|
|
9
|
+
"reviewer-cli",
|
|
10
|
+
"experiment",
|
|
11
|
+
];
|
|
12
|
+
export const POLICY_DRAFT_PERMISSIONS: readonly ["edit", "read-only"];
|
|
13
|
+
export const POLICY_DRAFT_STATUSES: readonly ["admitted", "candidate", "retired"];
|
|
14
|
+
|
|
15
|
+
export interface PolicyDraftIssue {
|
|
16
|
+
phase: "parse" | "schema" | "reference" | "semantic";
|
|
17
|
+
code: string;
|
|
18
|
+
file: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PolicyDraftCeiling {
|
|
23
|
+
readonly provider: string;
|
|
24
|
+
readonly roles?: readonly string[];
|
|
25
|
+
readonly permissions: readonly string[];
|
|
26
|
+
readonly models?: readonly string[];
|
|
27
|
+
readonly efforts?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PolicyDraftOptions {
|
|
31
|
+
readonly ceilings: Readonly<Record<string, PolicyDraftCeiling>>;
|
|
32
|
+
readonly nativePiBrakeProviders: readonly string[];
|
|
33
|
+
readonly piAllowedProviders: readonly string[];
|
|
34
|
+
readonly vendorAliases?: Readonly<Record<string, string>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type PolicyDraftDocument = JsonObject | string | Uint8Array;
|
|
38
|
+
|
|
39
|
+
export interface PolicyDraftInput {
|
|
40
|
+
readonly models?: Readonly<Record<string, PolicyDraftDocument>> | readonly PolicyDraftDocument[];
|
|
41
|
+
readonly roles?: Readonly<Record<string, PolicyDraftDocument>> | readonly PolicyDraftDocument[];
|
|
42
|
+
readonly evidence?: Readonly<Record<string, string | Uint8Array>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PolicyDraftData {
|
|
46
|
+
readonly models: Readonly<Record<string, JsonObject>>;
|
|
47
|
+
readonly roles: Readonly<Record<string, JsonObject>>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type PolicyDraftResult =
|
|
51
|
+
| { readonly ok: true; readonly data: PolicyDraftData }
|
|
52
|
+
| { readonly ok: false; readonly issues: readonly PolicyDraftIssue[] };
|
|
53
|
+
|
|
54
|
+
/** Pure draft validation. Not admission, not a trusted loader, and not file I/O. */
|
|
55
|
+
export function validatePolicyDraft(input: PolicyDraftInput, options: PolicyDraftOptions): PolicyDraftResult;
|