@kontextmind/kxm 0.7.112 → 0.7.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/agents/implementer.yaml +1 -1
  3. package/.kxm/roles/writer.yaml +1 -1
  4. package/docs/contributing/assignment-runner.md +3 -3
  5. package/package.json +1 -1
  6. package/packages/core/tui/dist/index.js +478 -0
  7. package/packages/core/tui/src/adapters/claude.ts +131 -0
  8. package/packages/core/tui/src/adapters/historyExport.ts +35 -0
  9. package/packages/core/tui/src/adapters/omp.ts +174 -0
  10. package/packages/core/tui/src/adapters/pi.ts +34 -0
  11. package/packages/core/tui/src/exports/index.ts +8 -0
  12. package/packages/core/tui/src/tui/history.ts +106 -0
  13. package/packages/core/tui/src/tui/modelSelector.ts +43 -0
  14. package/packages/core/tui/src/tui/optimizer.ts +154 -0
  15. package/packages/core/tui/src/tui/queue.ts +90 -0
  16. package/packages/core/tui/src/tui/roleBudget.ts +104 -0
  17. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  18. package/plugins/kxm/dist/cli.js +220 -22
  19. package/plugins/kxm/dist/mcp-server.js +1 -1
  20. package/plugins/kxm/dist/runtime-supervisor.js +36 -4
  21. package/plugins/kxm/dist/runtime.js +36 -4
  22. package/plugins/kxm/dist/server.js +22 -0
  23. package/plugins/kxm/package.json +1 -1
  24. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +6 -0
  25. package/plugins/kxm/src/autocomplete.ts +3 -0
  26. package/plugins/kxm/src/cli/plugins.ts +201 -0
  27. package/plugins/kxm/src/cli.ts +14 -0
  28. package/plugins/kxm/src/harness.ts +36 -4
  29. package/plugins/kxm/src/mcp-server.ts +1 -1
  30. package/scripts/assignment-run.d.mts +7 -0
  31. package/scripts/assignment-run.mjs +128 -1
  32. package/scripts/demo-overlay.mjs +175 -0
  33. package/scripts/harness-run.mjs +2 -2
  34. package/scripts/native-critic.mjs +7 -7
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Claude Code TUI & Channel Adapter.
3
+ *
4
+ * Formats KXM goals, tasks, upcoming lookaheads, retries, and workflow
5
+ * histories for Claude Code's terminal output, hook contexts, and channel events.
6
+ *
7
+ * Supports:
8
+ * - Single-line and boxed HUD banners for terminal output
9
+ * - Lookahead upcoming queue with dependency annotations
10
+ * - Formatted history timeline for channel delivery and SessionStart hooks
11
+ * - Plan optimizer proposals formatting
12
+ */
13
+
14
+ import { type HistoryEventItem, renderWorkflowHistoryMarkdown } from "../tui/history.ts";
15
+ import { type QueuedTaskItem } from "../tui/queue.ts";
16
+ import { type OptimizerResult } from "../tui/optimizer.ts";
17
+ import { type RoleBudgetState, type RoleBudgetConfig } from "../tui/roleBudget.ts";
18
+
19
+ export interface ClaudeTaskHudOptions {
20
+ readonly goal: string;
21
+ readonly stageName: string;
22
+ readonly stageIndex: number;
23
+ readonly totalStages: number;
24
+ readonly completedTasks: number;
25
+ readonly totalTasks: number;
26
+ readonly activeTaskTitle?: string | undefined;
27
+ readonly activeRole?: string | undefined;
28
+ readonly activeModel?: string | undefined;
29
+ readonly attempt?: number | undefined;
30
+ readonly maxAttempts?: number | undefined;
31
+ readonly elapsedSeconds?: number | undefined;
32
+ readonly tokensIn?: number | undefined;
33
+ readonly tokensOut?: number | undefined;
34
+ readonly autoDispatch?: boolean | undefined;
35
+ }
36
+
37
+ /**
38
+ * Format a high-visibility, single-line or framed HUD for Claude Code terminal output.
39
+ */
40
+ export function formatClaudeTaskHud(options: ClaudeTaskHudOptions, compact = false): string {
41
+ const percent = options.totalTasks > 0
42
+ ? Math.round((options.completedTasks / options.totalTasks) * 100)
43
+ : 0;
44
+ const filled = Math.round((percent / 100) * 10);
45
+ const bar = `[${"█".repeat(filled)}${"░".repeat(10 - filled)}] ${percent}%`;
46
+ const modeTag = options.autoDispatch ? "[AUTO]" : "[STEP]";
47
+ const attemptTag = options.attempt && options.attempt > 1 ? ` (Attempt ${options.attempt}/${options.maxAttempts ?? 3})` : "";
48
+ const timeTag = options.elapsedSeconds !== undefined ? ` ⏱ ${options.elapsedSeconds}s` : "";
49
+
50
+ if (compact) {
51
+ return `[KXM ${modeTag}] ${options.goal} ── Stage ${options.stageIndex}/${options.totalStages} ${bar}${attemptTag}${timeTag}`;
52
+ }
53
+
54
+ const activeTask = options.activeTaskTitle
55
+ ? `\n│ Active Task: ${options.activeTaskTitle}${attemptTag} [${options.activeRole ?? "agent"} (${options.activeModel ?? "default"})]`
56
+ : "";
57
+
58
+ return [
59
+ `┌── KXM TASK HUD ─────────────────────────────────────────────────────────────┐`,
60
+ `│ Goal: ${options.goal}`,
61
+ `│ Stage ${options.stageIndex}/${options.totalStages}: ${options.stageName} │ Progress: ${bar}${timeTag} │ Mode: ${modeTag}${activeTask}`,
62
+ `└─────────────────────────────────────────────────────────────────────────────┘`,
63
+ ].join("\n");
64
+ }
65
+
66
+ /**
67
+ * Format the upcoming lookahead queue for Claude Code context and channel streams.
68
+ */
69
+ export function formatClaudeUpcomingQueue(tasks: readonly QueuedTaskItem[]): string {
70
+ if (tasks.length === 0) return "No upcoming tasks in queue.";
71
+
72
+ const lines = ["### KXM Upcoming Task Queue:"];
73
+ for (let i = 0; i < tasks.length; i++) {
74
+ const t = tasks[i]!;
75
+ const deps = t.dependencies.length > 0 ? ` (Requires: ${t.dependencies.join(", ")})` : " (Ready)";
76
+ lines.push(`${i + 1}. [${t.status.toUpperCase()}] **${t.title}** (\`${t.id}\`)`);
77
+ lines.push(` Role: \`${t.role}\` [${t.harness}/${t.model}]${deps}`);
78
+ }
79
+ return lines.join("\n");
80
+ }
81
+
82
+ /**
83
+ * Format plan optimizer proposals for Claude Code review prompts.
84
+ */
85
+ export function formatClaudeOptimizerReport(result: OptimizerResult): string {
86
+ if (result.proposals.length === 0) {
87
+ return "Plan Optimizer: No pending optimization proposals.";
88
+ }
89
+
90
+ const lines = [
91
+ `### KXM Plan Optimizer Proposals:`,
92
+ `Total Projected Savings: -${result.totalProjectedTimeSavingsSeconds}s execution time, -${result.totalProjectedCostSavingsPercent}% token cost`,
93
+ "",
94
+ ];
95
+
96
+ for (const p of result.proposals) {
97
+ const icon = p.category === "concurrency" ? "⚡" : "💰";
98
+ lines.push(`${icon} **${p.title}** (\`${p.id}\`)`);
99
+ lines.push(` ${p.description}`);
100
+ if (p.suggestedModel) {
101
+ lines.push(` Suggested Routing: \`${p.suggestedHarness ?? "pi"}/${p.suggestedModel}\``);
102
+ }
103
+ lines.push("");
104
+ }
105
+
106
+ return lines.join("\n").trim();
107
+ }
108
+
109
+ /**
110
+ * Format role budget and subscription status for Claude Code channel info.
111
+ */
112
+ export function formatClaudeRoleBudget(config: RoleBudgetConfig, state: RoleBudgetState): string {
113
+ const runCap = config.runSpendCapUsd !== undefined ? `$${config.runSpendCapUsd.toFixed(2)}` : "Unmetered";
114
+ const monthlyCap = config.monthlySpendCapUsd !== undefined ? `$${config.monthlySpendCapUsd.toFixed(2)}` : "Unmetered";
115
+ const currentRun = `$${state.currentRunSpendUsd.toFixed(4)}`;
116
+ const currentMonthly = `$${state.currentMonthlySpendUsd.toFixed(2)}`;
117
+
118
+ return [
119
+ `Role: ${config.roleId} (${config.type})`,
120
+ `Run Spend: ${currentRun} / ${runCap}`,
121
+ `Monthly Spend: ${currentMonthly} / ${monthlyCap}`,
122
+ `On Exhausted: ${config.onExhausted}${config.fallbackModel ? ` (Fallback: ${config.fallbackModel})` : ""}`,
123
+ ].join(" │ ");
124
+ }
125
+
126
+ /**
127
+ * Format history for Claude Code channel updates.
128
+ */
129
+ export function formatClaudeWorkflowHistory(runId: string, events: readonly HistoryEventItem[]): string {
130
+ return renderWorkflowHistoryMarkdown(runId, events);
131
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * History export adapter for local disk persistence.
3
+ */
4
+
5
+ import { mkdirSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import {
8
+ renderWorkflowHistoryMarkdown,
9
+ formatWorkflowHistoryJsonl,
10
+ type HistoryEventItem,
11
+ } from "../tui/history.ts";
12
+
13
+ export interface ExportedHistoryFiles {
14
+ readonly mdPath: string;
15
+ readonly jsonlPath: string;
16
+ }
17
+
18
+ /**
19
+ * Export workflow history into both Markdown retrospective and JSONL audit logs on disk.
20
+ */
21
+ export function exportWorkflowHistory(
22
+ runId: string,
23
+ events: readonly HistoryEventItem[],
24
+ destinationDir: string,
25
+ ): ExportedHistoryFiles {
26
+ mkdirSync(destinationDir, { recursive: true });
27
+ const sanitizedRunId = runId.replaceAll(/[^a-zA-Z0-9_-]/g, "_");
28
+ const mdPath = join(destinationDir, `retrospective-${sanitizedRunId}.md`);
29
+ const jsonlPath = join(destinationDir, `history-${sanitizedRunId}.jsonl`);
30
+
31
+ writeFileSync(mdPath, renderWorkflowHistoryMarkdown(runId, events), "utf8");
32
+ writeFileSync(jsonlPath, formatWorkflowHistoryJsonl(runId, events), "utf8");
33
+
34
+ return { mdPath, jsonlPath };
35
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * OMP (Oh My Pi) overlay adapter.
3
+ *
4
+ * Provides a collapsible, differential-rendered task & plan overlay
5
+ * designed for OMP's overlay stack and custom TUI components.
6
+ *
7
+ * Supports:
8
+ * - Collapsed status HUD (single-line strip) vs Expanded 3-pane dashboard
9
+ * - Interactive task reordering (Shift+Up / Shift+Down)
10
+ * - Auto-dispatch flag toggling ('a' key)
11
+ * - On-demand plan optimizer ('o' key)
12
+ * - Full workflow history view ('h' key) with dual markdown + JSONL export ('x' key)
13
+ */
14
+
15
+ import { KxmTuiPanelComponent } from "../tui/panelComponent.ts";
16
+ import type { KxmTuiRegistry } from "../services/registry.ts";
17
+ import { createKxmTuiThemeFromPi, type PiThemeLike, type KxmTuiTheme } from "../tui/theme.ts";
18
+ import type { KxmTuiCustomComponent, KxmTuiCustomTui } from "./pi.ts";
19
+
20
+ export type OverlayMode = "collapsed" | "expanded" | "history" | "optimizer";
21
+
22
+ export interface KxmTuiOmpOverlayOptions {
23
+ readonly registry: KxmTuiRegistry;
24
+ readonly tui: KxmTuiCustomTui;
25
+ readonly theme: PiThemeLike | KxmTuiTheme;
26
+ readonly title: string;
27
+ readonly goal?: string | undefined;
28
+ readonly initialMode?: OverlayMode | undefined;
29
+ readonly autoDispatch?: boolean | undefined;
30
+ readonly onToggleAutoDispatch?: ((auto: boolean) => void) | undefined;
31
+ readonly onReorderTasks?: ((fromIndex: number, toIndex: number) => void) | undefined;
32
+ readonly onRunOptimizer?: (() => Promise<{ savingsTokens: number; savingsSeconds: number } | void>) | undefined;
33
+ readonly onExportHistory?: ((format: "both" | "md" | "jsonl") => Promise<{ mdPath?: string; jsonlPath?: string }>) | undefined;
34
+ readonly width?: (() => number) | undefined;
35
+ readonly done?: ((closed: true) => void) | undefined;
36
+ }
37
+
38
+ export interface KxmTuiOmpOverlayComponent extends KxmTuiCustomComponent {
39
+ getMode(): OverlayMode;
40
+ setMode(mode: OverlayMode): void;
41
+ isAutoDispatch(): boolean;
42
+ toggleAutoDispatch(): void;
43
+ dispose(): void;
44
+ }
45
+
46
+ function resolveKxmTheme(theme: PiThemeLike | KxmTuiTheme): KxmTuiTheme {
47
+ if ("headerBg" in theme && typeof (theme as KxmTuiTheme).headerBg === "function") {
48
+ return theme as KxmTuiTheme;
49
+ }
50
+ return createKxmTuiThemeFromPi(theme as PiThemeLike);
51
+ }
52
+
53
+ export function createKxmTuiOmpOverlay(options: KxmTuiOmpOverlayOptions): KxmTuiOmpOverlayComponent {
54
+ let closed = false;
55
+ let mode: OverlayMode = options.initialMode ?? "expanded";
56
+ let autoDispatch = Boolean(options.autoDispatch);
57
+ const theme: KxmTuiTheme = resolveKxmTheme(options.theme);
58
+
59
+ const panel = new KxmTuiPanelComponent({
60
+ registry: options.registry,
61
+ theme,
62
+ title: options.title,
63
+ ...(options.goal ? { breadcrumb: `Goal: ${options.goal}` } : {}),
64
+ helpLines: [
65
+ "[Ctrl+O/F2] Toggle Fold [h] History [a] Toggle Auto [o] Optimize [Shift+↑/↓] Reorder",
66
+ ],
67
+ requestRender: () => options.tui.requestRender(),
68
+ onQuit: () => {
69
+ if (closed) return;
70
+ closed = true;
71
+ panel.dispose();
72
+ options.done?.(true);
73
+ },
74
+ ...(options.width === undefined ? {} : { getWidth: options.width }),
75
+ });
76
+
77
+ function renderCollapsed(width: number): string[] {
78
+ const goalText = options.goal ? `Goal: ${options.goal}` : options.title;
79
+ const modeBadge = autoDispatch ? theme.accent("[AUTO: ON]") : theme.dim("[STEP MODE]");
80
+ const hint = theme.dim("[Ctrl+O to Expand]");
81
+ const content = ` [KXM] ${goalText} ── ${modeBadge} ── ${hint}`;
82
+ const pad = Math.max(0, width - content.length);
83
+ return [content + " ".repeat(pad)];
84
+ }
85
+
86
+ function handleInput(data: string): void {
87
+ if (closed) return;
88
+
89
+ // Toggle collapse: Ctrl+O (\x0f) or F2 (\x1bOQ or \x1b[12~)
90
+ if (data === "\x0f" || data === "\x1bOQ" || data === "\x1b[12~") {
91
+ mode = mode === "collapsed" ? "expanded" : "collapsed";
92
+ options.tui.requestRender();
93
+ return;
94
+ }
95
+
96
+ // When collapsed, only toggle/expand is handled
97
+ if (mode === "collapsed") {
98
+ if (data === "\r" || data === " " || data === "o" || data === "O") {
99
+ mode = "expanded";
100
+ options.tui.requestRender();
101
+ }
102
+ return;
103
+ }
104
+
105
+ // Toggle Auto-dispatch flag
106
+ if (data === "a" || data === "A") {
107
+ autoDispatch = !autoDispatch;
108
+ options.onToggleAutoDispatch?.(autoDispatch);
109
+ options.tui.requestRender();
110
+ return;
111
+ }
112
+
113
+ // History mode toggle
114
+ if (data === "h" || data === "H") {
115
+ mode = mode === "history" ? "expanded" : "history";
116
+ options.tui.requestRender();
117
+ return;
118
+ }
119
+
120
+ // Optimizer invocation
121
+ if (data === "o" || data === "O") {
122
+ mode = "optimizer";
123
+ options.onRunOptimizer?.();
124
+ options.tui.requestRender();
125
+ return;
126
+ }
127
+
128
+ // Export history in both formats
129
+ if ((data === "x" || data === "X") && mode === "history") {
130
+ options.onExportHistory?.("both");
131
+ options.tui.requestRender();
132
+ return;
133
+ }
134
+
135
+ // Escape in sub-modes returns to expanded
136
+ if (data === "\x1b" && mode !== "expanded") {
137
+ mode = "expanded";
138
+ options.tui.requestRender();
139
+ return;
140
+ }
141
+
142
+ // Forward to underlying KXM panel component
143
+ panel.handleInput(data);
144
+ options.tui.requestRender();
145
+ }
146
+
147
+ return {
148
+ getMode: () => mode,
149
+ setMode: (nextMode: OverlayMode) => {
150
+ mode = nextMode;
151
+ options.tui.requestRender();
152
+ },
153
+ isAutoDispatch: () => autoDispatch,
154
+ toggleAutoDispatch: () => {
155
+ autoDispatch = !autoDispatch;
156
+ options.onToggleAutoDispatch?.(autoDispatch);
157
+ options.tui.requestRender();
158
+ },
159
+ render: (width: number) => {
160
+ if (mode === "collapsed") {
161
+ return renderCollapsed(width);
162
+ }
163
+ return panel.render(width);
164
+ },
165
+ handleInput,
166
+ invalidate: () => panel.invalidate(),
167
+ dispose: () => {
168
+ if (closed) return;
169
+ closed = true;
170
+ panel.dispose();
171
+ options.done?.(true);
172
+ },
173
+ };
174
+ }
@@ -13,6 +13,11 @@
13
13
  import { KxmTuiPanelComponent } from "../tui/panelComponent.ts";
14
14
  import type { KxmTuiRegistry } from "../services/registry.ts";
15
15
  import { createKxmTuiThemeFromPi, type PiThemeLike } from "../tui/theme.ts";
16
+ import {
17
+ createKxmTuiOmpOverlay,
18
+ type KxmTuiOmpOverlayComponent,
19
+ type KxmTuiOmpOverlayOptions,
20
+ } from "./omp.ts";
16
21
 
17
22
  /** The renderer handle Pi passes to a custom component. */
18
23
  export interface KxmTuiCustomTui {
@@ -38,6 +43,35 @@ export interface KxmTuiPiPanelOptions {
38
43
  readonly done?: (closed: true) => void;
39
44
  }
40
45
 
46
+ export interface KxmTuiPiOverlayOptions extends KxmTuiOmpOverlayOptions {}
47
+ export type KxmTuiPiOverlayComponent = KxmTuiOmpOverlayComponent;
48
+
49
+ /**
50
+ * Create a collapsible Task & Plan overlay for Pi.
51
+ *
52
+ * Reuses the differential rendering engine and OMP overlay model,
53
+ * compatible with Pi's `ctx.ui.custom` hook and keybinding stack.
54
+ */
55
+ export const createKxmTuiPiOverlay = createKxmTuiOmpOverlay;
56
+
57
+ /**
58
+ * Format widget lines for Pi's `ctx.ui.setWidget`.
59
+ */
60
+ export function renderPiWorkflowWidget(options: {
61
+ readonly goal: string;
62
+ readonly stage: string;
63
+ readonly progressPercent: number;
64
+ readonly activeTask?: string | undefined;
65
+ readonly autoDispatch?: boolean | undefined;
66
+ }): string[] {
67
+ const filled = Math.min(10, Math.max(0, Math.round((options.progressPercent / 100) * 10)));
68
+ const bar = `[${"█".repeat(filled)}${"░".repeat(10 - filled)}] ${options.progressPercent}%`;
69
+ const mode = options.autoDispatch ? "AUTO" : "STEP";
70
+ const line1 = `[KXM] Goal: ${options.goal} ── Stage: ${options.stage} (${bar}) [${mode}]`;
71
+ const line2 = options.activeTask ? ` Active: ${options.activeTask}` : ` Waiting for next dispatch (use /kxm progress)`;
72
+ return [line1, line2];
73
+ }
74
+
41
75
  /**
42
76
  * Build the object to return from `ctx.ui.custom`.
43
77
  *
@@ -25,6 +25,14 @@ export * from "../tui/theme.ts";
25
25
  export * from "../tui/panel.ts";
26
26
  export * from "../tui/render.ts";
27
27
  export * from "../tui/panelComponent.ts";
28
+ export * from "../tui/optimizer.ts";
29
+ export * from "../tui/history.ts";
30
+ export * from "../tui/queue.ts";
31
+ export * from "../tui/roleBudget.ts";
32
+ export * from "../tui/modelSelector.ts";
28
33
  export * from "../services/registry.ts";
29
34
  export * from "../adapters/terminal.ts";
30
35
  export * from "../adapters/pi.ts";
36
+ export * from "../adapters/omp.ts";
37
+ export * from "../adapters/claude.ts";
38
+ export * from "../adapters/historyExport.ts";
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Workflow History & Journal View Engine.
3
+ *
4
+ * Tracks the complete chronological lifecycle of the active run:
5
+ * - Event classification (Tasks, Gates, Retries, Peers, State)
6
+ * - Filtering by category and search query
7
+ * - Dual format generators: clean Markdown retrospective + JSONL audit ledger
8
+ */
9
+
10
+ export type HistoryCategory = "all" | "task" | "gate" | "retry" | "peer" | "state";
11
+
12
+ export interface HistoryEventItem {
13
+ readonly id: string;
14
+ readonly timestamp: string;
15
+ readonly category: Exclude<HistoryCategory, "all">;
16
+ readonly title: string;
17
+ readonly stageId?: string | undefined;
18
+ readonly taskId?: string | undefined;
19
+ readonly role?: string | undefined;
20
+ readonly harness?: string | undefined;
21
+ readonly model?: string | undefined;
22
+ readonly attempt?: number | undefined;
23
+ readonly status: "passed" | "failed" | "warning" | "in_flight" | "degraded";
24
+ readonly durationMs?: number | undefined;
25
+ readonly tokensIn?: number | undefined;
26
+ readonly tokensOut?: number | undefined;
27
+ readonly costUsd?: number | undefined;
28
+ readonly artifacts?: readonly string[] | undefined;
29
+ readonly evidenceHashes?: readonly string[] | undefined;
30
+ readonly journalSummary?: string | undefined;
31
+ }
32
+
33
+ export interface HistoryFilterOptions {
34
+ readonly category?: HistoryCategory | undefined;
35
+ readonly query?: string | undefined;
36
+ }
37
+
38
+ /**
39
+ * Filter history items by category and substring query.
40
+ */
41
+ export function filterHistoryEvents(
42
+ events: readonly HistoryEventItem[],
43
+ options: HistoryFilterOptions = {},
44
+ ): HistoryEventItem[] {
45
+ const category = options.category ?? "all";
46
+ const query = options.query?.trim().toLowerCase();
47
+
48
+ return events.filter((ev) => {
49
+ if (category !== "all" && ev.category !== category) return false;
50
+ if (!query) return true;
51
+ const matchTarget = `${ev.id} ${ev.title} ${ev.stageId ?? ""} ${ev.taskId ?? ""} ${ev.role ?? ""} ${ev.model ?? ""} ${ev.journalSummary ?? ""}`.toLowerCase();
52
+ return matchTarget.includes(query);
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Render retrospective markdown text from workflow events.
58
+ */
59
+ export function renderWorkflowHistoryMarkdown(
60
+ runId: string,
61
+ events: readonly HistoryEventItem[],
62
+ ): string {
63
+ const totalSpend = events.reduce((acc, ev) => acc + (ev.costUsd ?? 0), 0);
64
+ const passedCount = events.filter((ev) => ev.status === "passed").length;
65
+ const failedCount = events.filter((ev) => ev.status === "failed").length;
66
+
67
+ let mdContent = `# KXM Workflow Retrospective: ${runId}\n\n`;
68
+ mdContent += `**Total Events:** ${events.length} (Passed: ${passedCount}, Failed: ${failedCount})\n`;
69
+ mdContent += `**Total Spend:** $${totalSpend.toFixed(4)} USD\n\n`;
70
+ mdContent += `## Chronological Event Journal\n\n`;
71
+ mdContent += `| Timestamp | Category | Title | Status | Role / Model | Duration | Spend |\n`;
72
+ mdContent += `|---|---|---|---|---|---|---|\n`;
73
+
74
+ for (const ev of events) {
75
+ const elapsed = ev.durationMs !== undefined ? `${(ev.durationMs / 1000).toFixed(1)}s` : "-";
76
+ const spend = ev.costUsd !== undefined ? `$${ev.costUsd.toFixed(4)}` : "-";
77
+ const roleModel = ev.role ? `${ev.role} (${ev.harness ?? "-"}/${ev.model ?? "-"})` : "-";
78
+ mdContent += `| ${ev.timestamp} | ${ev.category} | ${ev.title} | ${ev.status} | ${roleModel} | ${elapsed} | ${spend} |\n`;
79
+ }
80
+
81
+ mdContent += `\n## Produced Artifacts & Proofs\n\n`;
82
+ for (const ev of events) {
83
+ if (ev.artifacts && ev.artifacts.length > 0) {
84
+ mdContent += `### ${ev.title} (${ev.id})\n`;
85
+ for (const art of ev.artifacts) {
86
+ mdContent += `- \`${art}\`\n`;
87
+ }
88
+ }
89
+ }
90
+
91
+ return mdContent;
92
+ }
93
+
94
+ /**
95
+ * Format JSONL audit ledger lines from workflow events.
96
+ */
97
+ export function formatWorkflowHistoryJsonl(
98
+ runId: string,
99
+ events: readonly HistoryEventItem[],
100
+ ): string {
101
+ return events.map((ev) => JSON.stringify({
102
+ schema: "kxm.workflow-history-event.v1",
103
+ runId,
104
+ ...ev,
105
+ })).join("\n") + "\n";
106
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Model Selector & Provider Authentication Model.
3
+ *
4
+ * Provides fast fuzzy filtering, capability inspection, and
5
+ * provider readiness status across all supported harnesses.
6
+ */
7
+
8
+ export interface ModelOptionItem {
9
+ readonly id: string;
10
+ readonly selector: string;
11
+ readonly name: string;
12
+ readonly provider: string;
13
+ readonly harness: string;
14
+ readonly contextWindow: number;
15
+ readonly maxTokens?: number | undefined;
16
+ readonly reasoning: boolean;
17
+ readonly thinkingLevels?: readonly string[] | undefined;
18
+ readonly cost: {
19
+ readonly inputPerMillion: number;
20
+ readonly outputPerMillion: number;
21
+ readonly cacheReadPerMillion?: number | undefined;
22
+ };
23
+ readonly authStatus: "ready" | "missing_auth" | "unprobed";
24
+ readonly authHint?: string | undefined;
25
+ }
26
+
27
+ export function filterModelOptions(
28
+ models: readonly ModelOptionItem[],
29
+ query?: string | undefined,
30
+ providerFilter?: string | undefined,
31
+ ): ModelOptionItem[] {
32
+ const q = query?.trim().toLowerCase();
33
+ const prov = providerFilter?.trim().toLowerCase();
34
+
35
+ return models.filter((m) => {
36
+ if (prov && m.provider.toLowerCase() !== prov && m.harness.toLowerCase() !== prov) {
37
+ return false;
38
+ }
39
+ if (!q) return true;
40
+ const matchTarget = `${m.id} ${m.selector} ${m.name} ${m.provider} ${m.harness}`.toLowerCase();
41
+ return matchTarget.includes(q);
42
+ });
43
+ }