@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,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supervisory agent control, in-process real-time steering, and work-splitting.
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* - Dual-tool control plane: `agent` (spawn) and `agent_control` (manage, poll, steer, stop).
|
|
6
|
+
* - Strict capability narrowing via `allowed_tools[]` allowlists.
|
|
7
|
+
* - Mid-flight steering instruction injection.
|
|
8
|
+
* - Subagent state lifecycle management (working, blocked, idle, done, error).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { assertCommandSeatbelt } from "./safety-integrity.ts";
|
|
13
|
+
|
|
14
|
+
export type SubagentType = "general" | "Explore" | "Plan" | "Reviewer" | "Auditor";
|
|
15
|
+
export type SubagentStatus = "pending" | "running" | "blocked" | "completed" | "stopped" | "failed";
|
|
16
|
+
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high";
|
|
17
|
+
|
|
18
|
+
export interface AgentSpawnParams {
|
|
19
|
+
prompt: string;
|
|
20
|
+
description: string;
|
|
21
|
+
type?: SubagentType | undefined;
|
|
22
|
+
model?: string | undefined;
|
|
23
|
+
thinking?: ThinkingLevel | undefined;
|
|
24
|
+
allowed_tools?: string[] | undefined;
|
|
25
|
+
turns?: number | undefined;
|
|
26
|
+
background?: boolean | undefined;
|
|
27
|
+
parent_tools?: readonly string[] | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AgentControlParams {
|
|
31
|
+
action: "info" | "result" | "steer" | "stop";
|
|
32
|
+
kind?: "types" | "models" | "active" | undefined;
|
|
33
|
+
agent_id?: string | undefined;
|
|
34
|
+
message?: string | undefined;
|
|
35
|
+
turns?: number | undefined;
|
|
36
|
+
verbose?: boolean | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SubagentRecord {
|
|
40
|
+
id: string;
|
|
41
|
+
description: string;
|
|
42
|
+
type: SubagentType;
|
|
43
|
+
model: string;
|
|
44
|
+
thinking: ThinkingLevel;
|
|
45
|
+
allowedTools: string[];
|
|
46
|
+
status: SubagentStatus;
|
|
47
|
+
turnsMax: number;
|
|
48
|
+
turnsCompleted: number;
|
|
49
|
+
prompt: string;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
updatedAt: string;
|
|
52
|
+
steeringMessages: Array<{ text: string; injectedAt: string; processed: boolean }>;
|
|
53
|
+
output?: string | undefined;
|
|
54
|
+
error?: string | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SubagentExecutionReceipt {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
action: string;
|
|
60
|
+
agent_id?: string | undefined;
|
|
61
|
+
status?: SubagentStatus | undefined;
|
|
62
|
+
activeAgents?: Array<{ id: string; description: string; type: SubagentType; status: SubagentStatus }> | undefined;
|
|
63
|
+
types?: readonly SubagentType[] | undefined;
|
|
64
|
+
result?: string | undefined;
|
|
65
|
+
steered?: boolean | undefined;
|
|
66
|
+
stopped?: boolean | undefined;
|
|
67
|
+
error?: string | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const SUBAGENT_TYPES: readonly SubagentType[] = Object.freeze([
|
|
71
|
+
"general",
|
|
72
|
+
"Explore",
|
|
73
|
+
"Plan",
|
|
74
|
+
"Reviewer",
|
|
75
|
+
"Auditor",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
export const DEFAULT_SUBAGENT_MODELS: Readonly<Record<SubagentType, string>> = Object.freeze({
|
|
79
|
+
general: "grok/grok-4.6",
|
|
80
|
+
Explore: "openrouter/qwen/qwen3-coder-plus",
|
|
81
|
+
Plan: "claude/fable",
|
|
82
|
+
Reviewer: "openai/gpt-5.6-sol",
|
|
83
|
+
Auditor: "claude/fable",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validates and intersects child allowed tools against the parent tool surface.
|
|
88
|
+
* Enforces the safety invariant that a child can only narrow, never expand authority.
|
|
89
|
+
*/
|
|
90
|
+
export function narrowSubagentTools(
|
|
91
|
+
requestedTools: readonly string[] | undefined,
|
|
92
|
+
parentTools?: readonly string[] | undefined,
|
|
93
|
+
): string[] {
|
|
94
|
+
if (!requestedTools || requestedTools.length === 0) {
|
|
95
|
+
return parentTools ? [...parentTools] : ["read", "grep", "find"];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!parentTools || parentTools.length === 0) {
|
|
99
|
+
return [...requestedTools];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const parentSet = new Set(parentTools);
|
|
103
|
+
const narrowed: string[] = [];
|
|
104
|
+
|
|
105
|
+
for (const tool of requestedTools) {
|
|
106
|
+
if (parentSet.has(tool)) {
|
|
107
|
+
narrowed.push(tool);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (narrowed.length === 0 && requestedTools.length > 0) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Cannot spawn subagent: requested tools [${requestedTools.join(", ")}] expand beyond parent capabilities.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return narrowed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* In-Process Subagent Manager.
|
|
122
|
+
*/
|
|
123
|
+
export class SubagentManager {
|
|
124
|
+
private readonly subagents = new Map<string, SubagentRecord>();
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Spawns a new managed subagent with strict capability boundaries.
|
|
128
|
+
*/
|
|
129
|
+
spawn(params: AgentSpawnParams): SubagentRecord {
|
|
130
|
+
if (!params.prompt || !params.prompt.trim()) {
|
|
131
|
+
throw new Error('Subagent "prompt" is required.');
|
|
132
|
+
}
|
|
133
|
+
if (!params.description || !params.description.trim()) {
|
|
134
|
+
throw new Error('Subagent "description" is required.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const type = params.type ?? "general";
|
|
138
|
+
const model = params.model ?? DEFAULT_SUBAGENT_MODELS[type] ?? "grok/grok-4.6";
|
|
139
|
+
const thinking = params.thinking ?? "low";
|
|
140
|
+
const allowedTools = narrowSubagentTools(params.allowed_tools, params.parent_tools);
|
|
141
|
+
const id = `ag_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
142
|
+
const now = new Date().toISOString();
|
|
143
|
+
|
|
144
|
+
const record: SubagentRecord = {
|
|
145
|
+
id,
|
|
146
|
+
description: params.description.trim(),
|
|
147
|
+
type,
|
|
148
|
+
model,
|
|
149
|
+
thinking,
|
|
150
|
+
allowedTools,
|
|
151
|
+
status: params.background === false ? "completed" : "running",
|
|
152
|
+
turnsMax: params.turns ?? 10,
|
|
153
|
+
turnsCompleted: 0,
|
|
154
|
+
prompt: params.prompt.trim(),
|
|
155
|
+
createdAt: now,
|
|
156
|
+
updatedAt: now,
|
|
157
|
+
steeringMessages: [],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
this.subagents.set(id, record);
|
|
161
|
+
return record;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Dispatches an action from the `agent_control` tool.
|
|
166
|
+
*/
|
|
167
|
+
control(params: AgentControlParams): SubagentExecutionReceipt {
|
|
168
|
+
const action = params.action;
|
|
169
|
+
|
|
170
|
+
// 1. Action: "info"
|
|
171
|
+
if (action === "info") {
|
|
172
|
+
const kind = params.kind ?? "active";
|
|
173
|
+
if (kind === "types") {
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
action: "info",
|
|
177
|
+
types: SUBAGENT_TYPES,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (kind === "models") {
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
action: "info",
|
|
184
|
+
result: JSON.stringify(DEFAULT_SUBAGENT_MODELS),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const active = Array.from(this.subagents.values()).map((ag) => ({
|
|
188
|
+
id: ag.id,
|
|
189
|
+
description: ag.description,
|
|
190
|
+
type: ag.type,
|
|
191
|
+
status: ag.status,
|
|
192
|
+
}));
|
|
193
|
+
return {
|
|
194
|
+
ok: true,
|
|
195
|
+
action: "info",
|
|
196
|
+
activeAgents: active,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!params.agent_id) {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
action,
|
|
204
|
+
error: 'Parameter "agent_id" is required for result, steer, and stop actions.',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const agent = this.subagents.get(params.agent_id);
|
|
209
|
+
if (!agent) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
action,
|
|
213
|
+
agent_id: params.agent_id,
|
|
214
|
+
error: `Subagent "${params.agent_id}" not found.`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 2. Action: "result"
|
|
219
|
+
if (action === "result") {
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
action: "result",
|
|
223
|
+
agent_id: agent.id,
|
|
224
|
+
status: agent.status,
|
|
225
|
+
result: agent.output ?? `[Subagent is currently ${agent.status}]`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 3. Action: "steer"
|
|
230
|
+
if (action === "steer") {
|
|
231
|
+
if (!params.message || !params.message.trim()) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
action: "steer",
|
|
235
|
+
agent_id: agent.id,
|
|
236
|
+
error: 'Parameter "message" is required for steer action.',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (agent.status === "completed" || agent.status === "stopped" || agent.status === "failed") {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
action: "steer",
|
|
244
|
+
agent_id: agent.id,
|
|
245
|
+
error: `Cannot steer subagent "${agent.id}" because it is already ${agent.status}.`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
agent.steeringMessages.push({
|
|
250
|
+
text: params.message.trim(),
|
|
251
|
+
injectedAt: new Date().toISOString(),
|
|
252
|
+
processed: false,
|
|
253
|
+
});
|
|
254
|
+
agent.updatedAt = new Date().toISOString();
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
ok: true,
|
|
258
|
+
action: "steer",
|
|
259
|
+
agent_id: agent.id,
|
|
260
|
+
status: agent.status,
|
|
261
|
+
steered: true,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 4. Action: "stop"
|
|
266
|
+
if (action === "stop") {
|
|
267
|
+
agent.status = "stopped";
|
|
268
|
+
agent.updatedAt = new Date().toISOString();
|
|
269
|
+
return {
|
|
270
|
+
ok: true,
|
|
271
|
+
action: "stop",
|
|
272
|
+
agent_id: agent.id,
|
|
273
|
+
status: "stopped",
|
|
274
|
+
stopped: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
action,
|
|
281
|
+
error: `Unknown control action "${action}".`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Retrieves an agent record by ID.
|
|
287
|
+
*/
|
|
288
|
+
get(agentId: string): SubagentRecord | undefined {
|
|
289
|
+
return this.subagents.get(agentId);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Marks a subagent as completed with output.
|
|
294
|
+
*/
|
|
295
|
+
complete(agentId: string, output: string): void {
|
|
296
|
+
const agent = this.subagents.get(agentId);
|
|
297
|
+
if (agent) {
|
|
298
|
+
agent.status = "completed";
|
|
299
|
+
agent.output = output;
|
|
300
|
+
agent.updatedAt = new Date().toISOString();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Returns all active running subagents.
|
|
306
|
+
*/
|
|
307
|
+
listActive(): SubagentRecord[] {
|
|
308
|
+
return Array.from(this.subagents.values()).filter(
|
|
309
|
+
(ag) => ag.status === "running" || ag.status === "pending" || ag.status === "blocked",
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -32,49 +32,49 @@ const WORKFLOW_PATTERNS: WorkflowPattern[] = [
|
|
|
32
32
|
id: "software-engineering/bug-fix",
|
|
33
33
|
area: "software-engineering",
|
|
34
34
|
keywords: ["fix", "bug", "flaky", "failure", "timeout", "error", "repro", "crash", "broken", "hang"],
|
|
35
|
-
skills: ["troubleshooting", "
|
|
35
|
+
skills: ["troubleshooting", "kxm", "kxm-query", "kxm-work"],
|
|
36
36
|
defaultCommand: (p) => `kxm run software-engineering/bug-fix "${p}"`,
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
id: "software-engineering/feature-implementation",
|
|
40
40
|
area: "software-engineering",
|
|
41
41
|
keywords: ["feature", "implement", "add", "build", "create", "develop", "support", "endpoint", "ui", "tui"],
|
|
42
|
-
skills: ["modern-web-guidance", "kxm"],
|
|
42
|
+
skills: ["modern-web-guidance", "kxm", "kxm-mind", "kxm-query"],
|
|
43
43
|
defaultCommand: (p) => `kxm run software-engineering/feature-implementation "${p}"`,
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
id: "software-engineering/refactoring",
|
|
47
47
|
area: "software-engineering",
|
|
48
48
|
keywords: ["refactor", "cleanup", "reorganize", "modularize", "deduplicate", "split", "simplify", "deprecate"],
|
|
49
|
-
skills: ["kxm"],
|
|
49
|
+
skills: ["kxm", "kxm-mind", "kxm-query"],
|
|
50
50
|
defaultCommand: (p) => `kxm run software-engineering/refactoring "${p}"`,
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
id: "security-reliability/vulnerability-remediation",
|
|
54
54
|
area: "security-reliability",
|
|
55
55
|
keywords: ["cve", "vulnerability", "security", "exploit", "sanitize", "leak", "secret", "injection", "redact", "auth"],
|
|
56
|
-
skills: ["kxm"],
|
|
56
|
+
skills: ["kxm", "kxm-protocol", "kxm-triage"],
|
|
57
57
|
defaultCommand: (p) => `kxm run security-reliability/vulnerability-remediation "${p}"`,
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
id: "security-reliability/reliability-hardening",
|
|
61
61
|
area: "security-reliability",
|
|
62
62
|
keywords: ["idempotency", "retry", "circuit-breaker", "cas", "lock", "concurrency", "deadlock", "race", "crash-recovery"],
|
|
63
|
-
skills: ["kxm"],
|
|
63
|
+
skills: ["kxm", "kxm-protocol"],
|
|
64
64
|
defaultCommand: (p) => `kxm run security-reliability/reliability-hardening "${p}"`,
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
67
|
id: "data-analytics/pipeline-migration",
|
|
68
68
|
area: "data-analytics",
|
|
69
69
|
keywords: ["database", "sqlite", "migration", "pipeline", "schema", "transform", "table", "wal", "foreign", "cascading"],
|
|
70
|
-
skills: ["kxm"],
|
|
70
|
+
skills: ["kxm", "kxm-query"],
|
|
71
71
|
defaultCommand: (p) => `kxm run data-analytics/pipeline-migration "${p}"`,
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
id: "research-strategy/architecture-spike",
|
|
75
75
|
area: "research-strategy",
|
|
76
76
|
keywords: ["spike", "investigate", "prototype", "research", "feasibility", "benchmark", "explore", "evaluate"],
|
|
77
|
-
skills: ["kxm-session"],
|
|
77
|
+
skills: ["kxm-session", "kxm-mind", "kxm-query", "kxm-insights"],
|
|
78
78
|
defaultCommand: (p) => `kxm run research-strategy/architecture-spike "${p}"`,
|
|
79
79
|
},
|
|
80
80
|
];
|
|
@@ -110,36 +110,30 @@ export function suggestWorkflowAndRoles(
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
if (bestScore <= 0) {
|
|
113
|
-
// Default fallback to feature-implementation
|
|
114
113
|
bestPattern = WORKFLOW_PATTERNS[1]!;
|
|
115
114
|
matchedReasons.push("Default fallback: general feature implementation workflow");
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
// Determine active harnesses
|
|
119
117
|
const activeHarnesses = new Set(
|
|
120
118
|
(options.availableHarnesses ?? [])
|
|
121
119
|
.filter((h) => h.auth === "active" || h.auth === "ready" || h.auth === "configured")
|
|
122
120
|
.map((h) => h.harness.toLowerCase()),
|
|
123
121
|
);
|
|
124
122
|
|
|
125
|
-
// If none specified or active, assume standard default rotation
|
|
126
123
|
const hasClaude = activeHarnesses.size === 0 || activeHarnesses.has("claude");
|
|
127
124
|
const hasGrok = activeHarnesses.size === 0 || activeHarnesses.has("grok");
|
|
128
125
|
const hasCodex = activeHarnesses.size === 0 || activeHarnesses.has("codex");
|
|
129
126
|
|
|
130
|
-
// Planner
|
|
131
127
|
const planner: RoleSpec = hasClaude
|
|
132
128
|
? { harness: "claude", model: "fable", role: "planner" }
|
|
133
129
|
: { harness: "codex", model: "gpt-5.6-sol", role: "planner" };
|
|
134
130
|
|
|
135
|
-
// Writer (Starting rotation: Grok 4.6 headless)
|
|
136
131
|
const writer: RoleSpec = hasGrok
|
|
137
132
|
? { harness: "grok", model: "grok-4.6", role: "writer" }
|
|
138
133
|
: (activeHarnesses.has("agy")
|
|
139
134
|
? { harness: "agy", model: "gemini-2.5-pro", role: "writer" }
|
|
140
135
|
: { harness: "pi", model: "qwen/qwen3-coder-plus", role: "writer" });
|
|
141
136
|
|
|
142
|
-
// Critics (Vendor independent: Fable Arch + Codex CLI)
|
|
143
137
|
const critics: RoleSpec[] = [
|
|
144
138
|
{ harness: "claude", model: "fable", role: "reviewer-arch" },
|
|
145
139
|
{ harness: "codex", model: "gpt-5.6-sol", role: "reviewer-cli" },
|
|
@@ -114,3 +114,85 @@ export function readRoutingRecords(path: string): Array<{ recordedAt: string; ro
|
|
|
114
114
|
}
|
|
115
115
|
return records;
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
export const FEDERATED_METRIC_SCHEMA = "kxm.federated-metric.v1" as const;
|
|
119
|
+
|
|
120
|
+
export interface FederatedModelMetric {
|
|
121
|
+
schema: typeof FEDERATED_METRIC_SCHEMA;
|
|
122
|
+
harness: string;
|
|
123
|
+
model: string;
|
|
124
|
+
timestamp: string;
|
|
125
|
+
latencyMs: number;
|
|
126
|
+
tokensIn?: number | null | undefined;
|
|
127
|
+
tokensOut?: number | null | undefined;
|
|
128
|
+
costUsd?: number | null | undefined;
|
|
129
|
+
verifierOutcome?: "passed" | "warning" | "failed" | undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Exports anonymized model telemetry metrics to federated user directory (Decision Q15).
|
|
134
|
+
* Strictly strips repository paths, prompts, code diffs, run IDs, and memory items.
|
|
135
|
+
*/
|
|
136
|
+
export function exportFederatedTelemetry(
|
|
137
|
+
records: Array<RoutingRecord | RoutingRecordV2>,
|
|
138
|
+
globalDir?: string,
|
|
139
|
+
): number {
|
|
140
|
+
const targetDir = globalDir
|
|
141
|
+
? join(globalDir, "telemetry")
|
|
142
|
+
: join(process.env.XDG_CONFIG_HOME || join(process.env.HOME || "~", ".config"), "kxm", "telemetry");
|
|
143
|
+
|
|
144
|
+
mkdirSync(targetDir, { recursive: true });
|
|
145
|
+
const metricsPath = join(targetDir, "model-metrics.jsonl");
|
|
146
|
+
|
|
147
|
+
let exported = 0;
|
|
148
|
+
for (const r of records) {
|
|
149
|
+
const isV2 = r.schema === "kxm.routing-record.v2";
|
|
150
|
+
const v2 = isV2 ? (r as RoutingRecordV2) : undefined;
|
|
151
|
+
const harness = (v2 ? v2.harness : (r.providerMetadata?.harness as string | undefined)) || "unknown";
|
|
152
|
+
const model = (v2 ? (v2.effectiveModel || v2.requestedModel) : (r.effectiveModel || r.requestedModel)) || "unknown";
|
|
153
|
+
const timestamp = (r as any).recordedAt || new Date().toISOString();
|
|
154
|
+
const latencyMs = v2 ? v2.latencyMs : (typeof r.providerMetadata?.latencyMs === "number" ? (r.providerMetadata.latencyMs as number) : 0);
|
|
155
|
+
|
|
156
|
+
const metric: FederatedModelMetric = {
|
|
157
|
+
schema: FEDERATED_METRIC_SCHEMA,
|
|
158
|
+
harness,
|
|
159
|
+
model,
|
|
160
|
+
timestamp,
|
|
161
|
+
latencyMs,
|
|
162
|
+
tokensIn: r.tokensIn ?? null,
|
|
163
|
+
tokensOut: r.tokensOut ?? null,
|
|
164
|
+
costUsd: r.costUsd ?? null,
|
|
165
|
+
verifierOutcome: r.verifierOutcome,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
appendFileSync(metricsPath, `${JSON.stringify(metric)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
169
|
+
exported++;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return exported;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function readFederatedTelemetry(globalDir?: string): FederatedModelMetric[] {
|
|
176
|
+
const targetDir = globalDir
|
|
177
|
+
? join(globalDir, "telemetry")
|
|
178
|
+
: join(process.env.XDG_CONFIG_HOME || join(process.env.HOME || "~", ".config"), "kxm", "telemetry");
|
|
179
|
+
const metricsPath = join(targetDir, "model-metrics.jsonl");
|
|
180
|
+
try {
|
|
181
|
+
const raw = readFileSync(metricsPath, "utf8");
|
|
182
|
+
const metrics: FederatedModelMetric[] = [];
|
|
183
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
184
|
+
if (!line.trim()) continue;
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(line) as FederatedModelMetric;
|
|
187
|
+
if (parsed.schema === FEDERATED_METRIC_SCHEMA) {
|
|
188
|
+
metrics.push(parsed);
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// Skip malformed
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return metrics;
|
|
195
|
+
} catch {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
}
|
package/plugins/kxm/src/tui.ts
CHANGED
|
@@ -15,7 +15,11 @@ import {
|
|
|
15
15
|
type Terminal,
|
|
16
16
|
type TUI,
|
|
17
17
|
} from "@earendil-works/pi-tui";
|
|
18
|
+
import { mkdirSync } from "node:fs";
|
|
19
|
+
import { join, resolve, dirname } from "node:path";
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
18
21
|
import type { AgentRecord } from "./protocol.ts";
|
|
22
|
+
import { slugifyBranchPart } from "./external-effects.ts";
|
|
19
23
|
import {
|
|
20
24
|
loadLocalMeshSnapshot,
|
|
21
25
|
summarizeMeshRun,
|
|
@@ -138,6 +142,114 @@ export function applyMeshTuiKey(view: MeshTuiView, key: string, itemCount = 0):
|
|
|
138
142
|
return view;
|
|
139
143
|
}
|
|
140
144
|
|
|
145
|
+
export interface DegradeWorktreeResult {
|
|
146
|
+
ok: boolean;
|
|
147
|
+
branchName: string;
|
|
148
|
+
worktreePath: string;
|
|
149
|
+
jumpCommand: string;
|
|
150
|
+
copiedToClipboard: boolean;
|
|
151
|
+
error?: string | undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function copyToClipboard(text: string): boolean {
|
|
155
|
+
try {
|
|
156
|
+
if (process.platform === "darwin") {
|
|
157
|
+
const proc = spawnSync("pbcopy", { input: text, encoding: "utf8", windowsHide: true });
|
|
158
|
+
return proc.status === 0;
|
|
159
|
+
}
|
|
160
|
+
if (process.platform === "win32") {
|
|
161
|
+
const proc = spawnSync("clip", { input: text, encoding: "utf8", windowsHide: true });
|
|
162
|
+
return proc.status === 0;
|
|
163
|
+
}
|
|
164
|
+
const wl = spawnSync("wl-copy", [text], { encoding: "utf8", windowsHide: true });
|
|
165
|
+
if (wl.status === 0) return true;
|
|
166
|
+
const xclip = spawnSync("xclip", ["-selection", "clipboard"], { input: text, encoding: "utf8", windowsHide: true });
|
|
167
|
+
return xclip.status === 0;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Automated Degrade Worktree Spawn (Decision Q1).
|
|
175
|
+
* Spawns an isolated git worktree for human operator intervention on key 'd'.
|
|
176
|
+
* Detaches the AI worker, copies directory path to clipboard, and prints jump command.
|
|
177
|
+
*/
|
|
178
|
+
export function spawnDegradeWorktree(
|
|
179
|
+
repoRoot: string,
|
|
180
|
+
runId: string,
|
|
181
|
+
options?: {
|
|
182
|
+
description?: string | undefined;
|
|
183
|
+
execFn?: ((cmd: string, args: string[]) => { status: number; stdout: string; stderr: string }) | undefined;
|
|
184
|
+
clipboardFn?: ((text: string) => boolean) | undefined;
|
|
185
|
+
},
|
|
186
|
+
): DegradeWorktreeResult {
|
|
187
|
+
const runner = options?.execFn ?? ((cmd: string, args: string[]) => {
|
|
188
|
+
const res = spawnSync(cmd, args, {
|
|
189
|
+
cwd: repoRoot,
|
|
190
|
+
encoding: "utf8",
|
|
191
|
+
windowsHide: true,
|
|
192
|
+
env: Object.fromEntries(
|
|
193
|
+
Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("GIT_")),
|
|
194
|
+
),
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
status: res.status ?? 1,
|
|
198
|
+
stdout: res.stdout || "",
|
|
199
|
+
stderr: res.stderr || "",
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const cleanRunId = runId.replace(/^run_/, "");
|
|
204
|
+
const desc = options?.description ? slugifyBranchPart(options.description, 30) : "degrade";
|
|
205
|
+
const branchName = `kxm/run-${cleanRunId}-${desc}`;
|
|
206
|
+
const relativeWtPath = join(".kxm", "worktrees", `run-${cleanRunId}-${desc}`);
|
|
207
|
+
const absoluteWtPath = resolve(repoRoot, relativeWtPath);
|
|
208
|
+
const jumpCommand = `cd "${absoluteWtPath}"`;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
mkdirSync(dirname(absoluteWtPath), { recursive: true });
|
|
212
|
+
|
|
213
|
+
const branchCheck = runner("git", ["rev-parse", "--verify", `refs/heads/${branchName}`]);
|
|
214
|
+
let wtRes: { status: number; stdout: string; stderr: string };
|
|
215
|
+
if (branchCheck.status === 0) {
|
|
216
|
+
wtRes = runner("git", ["worktree", "add", absoluteWtPath, branchName]);
|
|
217
|
+
} else {
|
|
218
|
+
wtRes = runner("git", ["worktree", "add", "-b", branchName, absoluteWtPath]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (wtRes.status !== 0) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
branchName,
|
|
225
|
+
worktreePath: absoluteWtPath,
|
|
226
|
+
jumpCommand,
|
|
227
|
+
copiedToClipboard: false,
|
|
228
|
+
error: `failed_to_add_worktree: ${wtRes.stderr.trim() || wtRes.stdout.trim()}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const clipFn = options?.clipboardFn ?? copyToClipboard;
|
|
233
|
+
const copied = clipFn(absoluteWtPath);
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
branchName,
|
|
237
|
+
worktreePath: absoluteWtPath,
|
|
238
|
+
jumpCommand,
|
|
239
|
+
copiedToClipboard: copied,
|
|
240
|
+
};
|
|
241
|
+
} catch (err: unknown) {
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
branchName,
|
|
245
|
+
worktreePath: absoluteWtPath,
|
|
246
|
+
jumpCommand,
|
|
247
|
+
copiedToClipboard: false,
|
|
248
|
+
error: `degrade_worktree_spawn_error: ${(err as Error).message}`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
141
253
|
export interface MeshTuiSnapshot extends LocalMeshSnapshot {
|
|
142
254
|
serverUrl: string;
|
|
143
255
|
healthOk: boolean;
|
|
@@ -534,6 +646,12 @@ export class KxmDashboard implements Component {
|
|
|
534
646
|
this.requestRender();
|
|
535
647
|
}
|
|
536
648
|
|
|
649
|
+
setStatusMessage(message: string): void {
|
|
650
|
+
this.view = { ...this.view, statusMessage: message };
|
|
651
|
+
this.rebuild();
|
|
652
|
+
this.requestRender();
|
|
653
|
+
}
|
|
654
|
+
|
|
537
655
|
private activeScroll(): ScrollView {
|
|
538
656
|
return this.view.pane === "detail" ? this.detailScroll : this.listScroll;
|
|
539
657
|
}
|
|
@@ -886,6 +1004,28 @@ export async function runMeshTui(input: {
|
|
|
886
1004
|
body: JSON.stringify({ signalKey: "operator-approval", status: "failed", summary: "Rejected by operator in kxm dash" }),
|
|
887
1005
|
signal: abort.signal,
|
|
888
1006
|
});
|
|
1007
|
+
} else if (action.action === "degrade") {
|
|
1008
|
+
await input.fetchImpl(`${base}/v1/runs/${encodeURIComponent(targetId)}/signal?project=${encodeURIComponent(input.project)}`, {
|
|
1009
|
+
method: "POST",
|
|
1010
|
+
headers: headers(),
|
|
1011
|
+
body: JSON.stringify({ signalKey: "operator-degrade", status: "degraded", summary: "Degraded to operator worktree from kxm dash" }),
|
|
1012
|
+
signal: abort.signal,
|
|
1013
|
+
});
|
|
1014
|
+
const repoRoot = process.cwd();
|
|
1015
|
+
const degradeRes = spawnDegradeWorktree(repoRoot, targetId, {
|
|
1016
|
+
description: "operator-degrade",
|
|
1017
|
+
});
|
|
1018
|
+
if (dashboard) {
|
|
1019
|
+
if (degradeRes.ok) {
|
|
1020
|
+
dashboard.setStatusMessage(
|
|
1021
|
+
`[DEGRADE] Worktree: ${degradeRes.worktreePath} (cd command copied to clipboard)`,
|
|
1022
|
+
);
|
|
1023
|
+
} else {
|
|
1024
|
+
dashboard.setStatusMessage(
|
|
1025
|
+
`[DEGRADE] Signal sent, worktree failed: ${degradeRes.error}`,
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
889
1029
|
}
|
|
890
1030
|
} catch {
|
|
891
1031
|
// fail-soft on dashboard network errors during key press
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
writeFileSync,
|
|
16
16
|
} from "node:fs";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
|
-
import { DatabaseSync } from "
|
|
18
|
+
import { DatabaseSync } from "./sqlite.ts";
|
|
19
19
|
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
20
20
|
import {
|
|
21
21
|
VnextConfigError,
|