@kontextmind/kxm 0.7.115 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/packages/core/tui/dist/index.js +478 -0
- package/packages/core/tui/src/adapters/claude.ts +131 -0
- package/packages/core/tui/src/adapters/historyExport.ts +35 -0
- package/packages/core/tui/src/adapters/omp.ts +174 -0
- package/packages/core/tui/src/adapters/pi.ts +34 -0
- package/packages/core/tui/src/exports/index.ts +8 -0
- package/packages/core/tui/src/tui/history.ts +106 -0
- package/packages/core/tui/src/tui/modelSelector.ts +43 -0
- package/packages/core/tui/src/tui/optimizer.ts +154 -0
- package/packages/core/tui/src/tui/queue.ts +90 -0
- package/packages/core/tui/src/tui/roleBudget.ts +104 -0
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +220 -22
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +36 -4
- package/plugins/kxm/dist/runtime.js +36 -4
- package/plugins/kxm/dist/server.js +22 -0
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +6 -0
- package/plugins/kxm/src/autocomplete.ts +3 -0
- package/plugins/kxm/src/cli/plugins.ts +201 -0
- package/plugins/kxm/src/cli.ts +14 -0
- package/plugins/kxm/src/harness.ts +36 -4
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/scripts/demo-overlay.mjs +175 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Optimizer Engine.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes upcoming tasks against dependency graphs, token footprints,
|
|
5
|
+
* and harness/model capabilities to generate concrete proposals for:
|
|
6
|
+
* 1. Concurrency (parallel fanout for independent tasks)
|
|
7
|
+
* 2. Cost (downscaling lightweight tasks to faster/cheaper models)
|
|
8
|
+
* 3. Reordering (failing fast on syntax/lint before heavier passes)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface OptimizerTaskInput {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly title: string;
|
|
14
|
+
readonly stageId: string;
|
|
15
|
+
readonly role: string;
|
|
16
|
+
readonly harness: string;
|
|
17
|
+
readonly model: string;
|
|
18
|
+
readonly dependencies: readonly string[];
|
|
19
|
+
readonly estimatedTokens?: number | undefined;
|
|
20
|
+
readonly category?: "implementation" | "test" | "docs" | "lint" | "security" | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface OptimizationProposal {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly category: "concurrency" | "cost" | "reorder" | "slice";
|
|
26
|
+
readonly title: string;
|
|
27
|
+
readonly description: string;
|
|
28
|
+
readonly targetTaskIds: readonly string[];
|
|
29
|
+
readonly projectedTimeSavingsSeconds: number;
|
|
30
|
+
readonly projectedCostSavingsPercent: number;
|
|
31
|
+
readonly suggestedModel?: string | undefined;
|
|
32
|
+
readonly suggestedHarness?: string | undefined;
|
|
33
|
+
readonly selected: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OptimizerResult {
|
|
37
|
+
readonly proposals: readonly OptimizationProposal[];
|
|
38
|
+
readonly totalProjectedTimeSavingsSeconds: number;
|
|
39
|
+
readonly totalProjectedCostSavingsPercent: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Analyze a task queue and generate optimization proposals.
|
|
44
|
+
*/
|
|
45
|
+
export function evaluatePlanOptimizations(tasks: readonly OptimizerTaskInput[]): OptimizerResult {
|
|
46
|
+
const proposals: OptimizationProposal[] = [];
|
|
47
|
+
|
|
48
|
+
// 1. Detect Parallel Fanout Opportunities (tasks with no mutual dependencies in the same stage)
|
|
49
|
+
const byStage = new Map<string, OptimizerTaskInput[]>();
|
|
50
|
+
for (const task of tasks) {
|
|
51
|
+
const list = byStage.get(task.stageId) ?? [];
|
|
52
|
+
list.push(task);
|
|
53
|
+
byStage.set(task.stageId, list);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const [stageId, stageTasks] of byStage) {
|
|
57
|
+
if (stageTasks.length >= 2) {
|
|
58
|
+
const independent = stageTasks.filter((t) => t.dependencies.length === 0 || t.dependencies.every((dep) => !stageTasks.some((st) => st.id === dep)));
|
|
59
|
+
if (independent.length >= 2) {
|
|
60
|
+
proposals.push({
|
|
61
|
+
id: `opt_fanout_${stageId}`,
|
|
62
|
+
category: "concurrency",
|
|
63
|
+
title: `Parallelize ${independent.length} tasks in stage "${stageId}"`,
|
|
64
|
+
description: `Tasks [${independent.map((t) => t.id).join(", ")}] have zero mutual dependencies and can execute concurrently via kxm_fanout.`,
|
|
65
|
+
targetTaskIds: independent.map((t) => t.id),
|
|
66
|
+
projectedTimeSavingsSeconds: Math.round(independent.length * 15),
|
|
67
|
+
projectedCostSavingsPercent: 0,
|
|
68
|
+
selected: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. Detect Cost/Model Downscaling Opportunities (Docs or Lint on top-tier frontier models)
|
|
75
|
+
for (const task of tasks) {
|
|
76
|
+
const isFrontier = /claude-3-5-sonnet|claude-3-opus|gpt-4o|gpt-5/i.test(task.model);
|
|
77
|
+
if (isFrontier && (task.category === "docs" || /doc|readme|markdown/i.test(task.title))) {
|
|
78
|
+
proposals.push({
|
|
79
|
+
id: `opt_cost_${task.id}`,
|
|
80
|
+
category: "cost",
|
|
81
|
+
title: `Downscale model for documentation task "${task.title}"`,
|
|
82
|
+
description: `Task "${task.id}" is documentation-focused. Routing to a high-throughput lightweight model saves significant token budget.`,
|
|
83
|
+
targetTaskIds: [task.id],
|
|
84
|
+
projectedTimeSavingsSeconds: 5,
|
|
85
|
+
projectedCostSavingsPercent: 65,
|
|
86
|
+
suggestedHarness: "pi",
|
|
87
|
+
suggestedModel: "qwen/qwen3-coder-plus",
|
|
88
|
+
selected: true,
|
|
89
|
+
});
|
|
90
|
+
} else if (isFrontier && (task.category === "lint" || /lint|format|style/i.test(task.title))) {
|
|
91
|
+
proposals.push({
|
|
92
|
+
id: `opt_lint_${task.id}`,
|
|
93
|
+
category: "cost",
|
|
94
|
+
title: `Downscale model for formatting/lint task "${task.title}"`,
|
|
95
|
+
description: `Task "${task.id}" performs deterministic formatting/linting. Use a fast local/tier-1 helper.`,
|
|
96
|
+
targetTaskIds: [task.id],
|
|
97
|
+
projectedTimeSavingsSeconds: 8,
|
|
98
|
+
projectedCostSavingsPercent: 80,
|
|
99
|
+
suggestedHarness: "pi",
|
|
100
|
+
suggestedModel: "google/gemini-2.5-flash",
|
|
101
|
+
selected: true,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const totalTimeSavings = proposals.reduce((acc, p) => acc + p.projectedTimeSavingsSeconds, 0);
|
|
107
|
+
const totalCostSavings = proposals.length > 0
|
|
108
|
+
? Math.round(proposals.reduce((acc, p) => acc + p.projectedCostSavingsPercent, 0) / proposals.length)
|
|
109
|
+
: 0;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
proposals,
|
|
113
|
+
totalProjectedTimeSavingsSeconds: totalTimeSavings,
|
|
114
|
+
totalProjectedCostSavingsPercent: totalCostSavings,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Apply selected proposals to the task queue.
|
|
120
|
+
*/
|
|
121
|
+
export function applyPlanOptimizations<T extends OptimizerTaskInput>(
|
|
122
|
+
tasks: readonly T[],
|
|
123
|
+
proposals: readonly OptimizationProposal[],
|
|
124
|
+
): { tasks: T[]; appliedCount: number } {
|
|
125
|
+
const selected = proposals.filter((p) => p.selected);
|
|
126
|
+
if (selected.length === 0) return { tasks: [...tasks], appliedCount: 0 };
|
|
127
|
+
|
|
128
|
+
const modelOverrides = new Map<string, { harness?: string; model?: string }>();
|
|
129
|
+
for (const proposal of selected) {
|
|
130
|
+
if (proposal.category === "cost" && proposal.suggestedModel) {
|
|
131
|
+
for (const targetId of proposal.targetTaskIds) {
|
|
132
|
+
modelOverrides.set(targetId, {
|
|
133
|
+
...(proposal.suggestedHarness ? { harness: proposal.suggestedHarness } : {}),
|
|
134
|
+
model: proposal.suggestedModel,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const updated = tasks.map((task) => {
|
|
141
|
+
const override = modelOverrides.get(task.id);
|
|
142
|
+
if (!override) return task;
|
|
143
|
+
return {
|
|
144
|
+
...task,
|
|
145
|
+
...(override.harness ? { harness: override.harness } : {}),
|
|
146
|
+
...(override.model ? { model: override.model } : {}),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
tasks: updated,
|
|
152
|
+
appliedCount: selected.length,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Queue & Topological Reordering Engine.
|
|
3
|
+
*
|
|
4
|
+
* Manages upcoming tasks in the execution queue with safety guards:
|
|
5
|
+
* - Detects prerequisite violations when moving a task before its dependencies
|
|
6
|
+
* - Prevents circular dependency cycles
|
|
7
|
+
* - Shifts tasks safely up or down
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface QueuedTaskItem {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly title: string;
|
|
13
|
+
readonly stageId: string;
|
|
14
|
+
readonly role: string;
|
|
15
|
+
readonly harness: string;
|
|
16
|
+
readonly model: string;
|
|
17
|
+
readonly status: "completed" | "in_flight" | "pending" | "blocked" | "failed";
|
|
18
|
+
readonly dependencies: readonly string[];
|
|
19
|
+
readonly attempt: number;
|
|
20
|
+
readonly maxAttempts: number;
|
|
21
|
+
readonly estimatedDurationSec?: number | undefined;
|
|
22
|
+
readonly estimatedCostUsd?: number | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ReorderResult {
|
|
26
|
+
readonly success: boolean;
|
|
27
|
+
readonly tasks: readonly QueuedTaskItem[];
|
|
28
|
+
readonly warning?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reorder a task in the upcoming queue with dependency validation.
|
|
33
|
+
*/
|
|
34
|
+
export function reorderQueuedTasks(
|
|
35
|
+
tasks: readonly QueuedTaskItem[],
|
|
36
|
+
fromIndex: number,
|
|
37
|
+
toIndex: number,
|
|
38
|
+
): ReorderResult {
|
|
39
|
+
if (
|
|
40
|
+
fromIndex < 0 ||
|
|
41
|
+
fromIndex >= tasks.length ||
|
|
42
|
+
toIndex < 0 ||
|
|
43
|
+
toIndex >= tasks.length ||
|
|
44
|
+
fromIndex === toIndex
|
|
45
|
+
) {
|
|
46
|
+
return { success: false, tasks, warning: "invalid_indices" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const target = tasks[fromIndex]!;
|
|
50
|
+
|
|
51
|
+
// Cannot reorder in-flight or completed tasks
|
|
52
|
+
if (target.status === "in_flight" || target.status === "completed") {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
tasks,
|
|
56
|
+
warning: `cannot move ${target.status} task "${target.id}"`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create mutable copy and move
|
|
61
|
+
const draft = [...tasks];
|
|
62
|
+
const [removed] = draft.splice(fromIndex, 1);
|
|
63
|
+
draft.splice(toIndex, 0, removed!);
|
|
64
|
+
|
|
65
|
+
// Validate dependencies:
|
|
66
|
+
// For every task at index i, none of its dependencies can appear at index j > i.
|
|
67
|
+
const idToIndex = new Map<string, number>();
|
|
68
|
+
for (let i = 0; i < draft.length; i++) {
|
|
69
|
+
idToIndex.set(draft[i]!.id, i);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < draft.length; i++) {
|
|
73
|
+
const task = draft[i]!;
|
|
74
|
+
for (const depId of task.dependencies) {
|
|
75
|
+
const depIndex = idToIndex.get(depId);
|
|
76
|
+
if (depIndex !== undefined && depIndex > i) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
tasks,
|
|
80
|
+
warning: `Prerequisite violation: "${task.id}" depends on "${depId}", which is scheduled later at position ${depIndex + 1}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
tasks: draft,
|
|
89
|
+
};
|
|
90
|
+
}
|