@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.
Files changed (175) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/agents/coordinator.yaml +9 -0
  3. package/.kxm/agents/critic-arch.yaml +13 -0
  4. package/.kxm/agents/critic-cli.yaml +13 -0
  5. package/.kxm/agents/implementer.yaml +13 -0
  6. package/.kxm/gates.yaml +8 -0
  7. package/.kxm/producers.yaml +22 -0
  8. package/.kxm/project.yaml +15 -0
  9. package/.kxm/roles/writer.yaml +7 -0
  10. package/.kxm/workflows/default.yaml +47 -0
  11. package/CHANGELOG.md +39 -7
  12. package/README.md +1 -0
  13. package/docs/README.md +5 -0
  14. package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
  15. package/docs/agent-skills.md +135 -0
  16. package/docs/architecture.md +1 -1
  17. package/docs/assignment-runner.md +21 -8
  18. package/docs/browser-automation.md +116 -0
  19. package/docs/configuration.md +11 -2
  20. package/docs/getting-started.md +21 -0
  21. package/docs/kb/how-credentials-retrieved-safely.md +31 -0
  22. package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
  23. package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
  24. package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
  25. package/docs/kb/how-to-resume-after-mfa.md +28 -0
  26. package/docs/kb/how-to-take-over-session.md +32 -0
  27. package/docs/kb/why-authentication-disappeared.md +32 -0
  28. package/docs/kb/why-automation-opened-different-browser.md +32 -0
  29. package/docs/kb/why-session-viewer-cannot-control.md +31 -0
  30. package/docs/kxm-handbook.md +3 -3
  31. package/docs/operations.md +24 -0
  32. package/docs/operator-pi-packages.md +63 -0
  33. package/docs/prompts/browser-annotate-feedback.md +41 -0
  34. package/docs/prompts/browser-diagnose-recover.md +38 -0
  35. package/docs/prompts/browser-explore.md +42 -0
  36. package/docs/prompts/browser-repro-fix.md +48 -0
  37. package/docs/prompts/browser-start.md +41 -0
  38. package/docs/prompts/browser-takeover.md +50 -0
  39. package/docs/skills/repo-work-delivery.md +107 -0
  40. package/docs/skills.md +2 -0
  41. package/docs/test-matrix.md +4 -3
  42. package/docs/troubleshooting.md +41 -1
  43. package/docs/vnext/validation.md +9 -0
  44. package/docs/webhook-workflows.md +2 -2
  45. package/examples/README.md +1 -1
  46. package/package.json +16 -17
  47. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  48. package/plugins/kxm/README.md +1 -1
  49. package/plugins/kxm/dist/cli.js +41620 -35578
  50. package/plugins/kxm/dist/core.js +271 -34
  51. package/plugins/kxm/dist/extension.js +7759 -86
  52. package/plugins/kxm/dist/mcp-server.js +75 -21
  53. package/plugins/kxm/dist/runtime.js +8218 -2328
  54. package/plugins/kxm/dist/server.js +3125 -2260
  55. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
  56. package/plugins/kxm/package.json +1 -1
  57. package/plugins/kxm/skills/SUITE.md +5 -0
  58. package/plugins/kxm/skills/hints.json +103 -0
  59. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  60. package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
  61. package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
  62. package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
  63. package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
  64. package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
  65. package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
  66. package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
  67. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  68. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  69. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  70. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  71. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
  72. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  73. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  74. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  75. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
  76. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  77. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  78. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  79. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  80. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  81. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  82. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  83. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  84. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  85. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  86. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  87. package/plugins/kxm/src/autocomplete.ts +9 -3
  88. package/plugins/kxm/src/browser.ts +603 -0
  89. package/plugins/kxm/src/cli/context-skills.ts +373 -0
  90. package/plugins/kxm/src/cli/hub.ts +614 -0
  91. package/plugins/kxm/src/cli/roles.ts +615 -0
  92. package/plugins/kxm/src/cli/system.ts +906 -0
  93. package/plugins/kxm/src/cli/tasks.ts +364 -0
  94. package/plugins/kxm/src/cli/types.ts +270 -0
  95. package/plugins/kxm/src/cli/vnext.ts +698 -0
  96. package/plugins/kxm/src/cli/workflows.ts +699 -0
  97. package/plugins/kxm/src/cli.ts +362 -2849
  98. package/plugins/kxm/src/commands.ts +150 -8
  99. package/plugins/kxm/src/completion-install.ts +223 -0
  100. package/plugins/kxm/src/config.ts +7 -4
  101. package/plugins/kxm/src/context-packet.ts +172 -0
  102. package/plugins/kxm/src/database.ts +1 -1
  103. package/plugins/kxm/src/extension.ts +36 -1
  104. package/plugins/kxm/src/external-effects.ts +357 -8
  105. package/plugins/kxm/src/hub-env.ts +193 -0
  106. package/plugins/kxm/src/hub.ts +2 -4
  107. package/plugins/kxm/src/improve.ts +72 -0
  108. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  109. package/plugins/kxm/src/local-snapshot.ts +1 -1
  110. package/plugins/kxm/src/mcp-server.ts +1 -1
  111. package/plugins/kxm/src/model-inventory.ts +127 -0
  112. package/plugins/kxm/src/modes.ts +348 -0
  113. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  114. package/plugins/kxm/src/policy-draft.mjs +565 -0
  115. package/plugins/kxm/src/price-calc.ts +17 -18
  116. package/plugins/kxm/src/prices.ts +32 -16
  117. package/plugins/kxm/src/producers.ts +71 -0
  118. package/plugins/kxm/src/protocol.ts +111 -0
  119. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  120. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  121. package/plugins/kxm/src/role.ts +710 -0
  122. package/plugins/kxm/src/routing.ts +99 -1
  123. package/plugins/kxm/src/runtime.ts +4 -0
  124. package/plugins/kxm/src/safety-integrity.ts +76 -0
  125. package/plugins/kxm/src/session-work.ts +9 -2
  126. package/plugins/kxm/src/sqlite.ts +76 -0
  127. package/plugins/kxm/src/ssh-remote.ts +560 -0
  128. package/plugins/kxm/src/store.ts +1 -1
  129. package/plugins/kxm/src/studio-layout.ts +660 -17
  130. package/plugins/kxm/src/subagent-control.ts +312 -0
  131. package/plugins/kxm/src/suggest.ts +7 -13
  132. package/plugins/kxm/src/telemetry.ts +82 -0
  133. package/plugins/kxm/src/tui.ts +140 -0
  134. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  135. package/plugins/kxm/src/vnext-config.ts +53 -111
  136. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  137. package/plugins/kxm/src/vnext-engine.ts +214 -62
  138. package/plugins/kxm/src/vnext-harness.ts +336 -84
  139. package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
  140. package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
  141. package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
  142. package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
  143. package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
  144. package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
  145. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  146. package/plugins/kxm/src/workflow-manager.ts +392 -0
  147. package/plugins/kxm/src/workflow-tui.ts +255 -0
  148. package/plugins/kxm/src/workflow.ts +144 -0
  149. package/schemas/policy-draft/README.md +17 -0
  150. package/schemas/policy-draft/model.v2.schema.json +140 -0
  151. package/schemas/policy-draft/role.v2.schema.json +91 -0
  152. package/schemas/vnext/modes.schema.json +56 -0
  153. package/schemas/vnext/role.schema.json +76 -0
  154. package/schemas/vnext/run-event.schema.json +1 -0
  155. package/scripts/assignment-run.d.mts +1 -1
  156. package/scripts/assignment-run.mjs +44 -35
  157. package/scripts/check-generated.mjs +33 -9
  158. package/scripts/emit-codex-artifacts.mjs +255 -11
  159. package/scripts/harness-run.d.mts +12 -4
  160. package/scripts/harness-run.mjs +65 -17
  161. package/scripts/kxm-bump-version.mjs +146 -0
  162. package/scripts/kxm-hub.mjs +150 -2
  163. package/scripts/kxm-publish-npm.mjs +3 -1
  164. package/scripts/kxm-release-github.mjs +3 -1
  165. package/scripts/kxm.mjs +0 -0
  166. package/scripts/native-critic.d.mts +5 -0
  167. package/scripts/native-critic.mjs +60 -0
  168. package/.kxm/config/README.md +0 -5
  169. package/.kxm/config/agents.json +0 -43
  170. package/.kxm/config/env.example +0 -56
  171. package/.kxm/config/update.example.yaml +0 -9
  172. package/.kxm/config/workflows/fix.json +0 -160
  173. package/.kxm/config/workflows/jira-development.json +0 -116
  174. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  175. 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;