@kontextmind/kxm 0.6.0 → 0.7.0
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 +5 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +2 -0
- package/docs/agent-skills.md +118 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/configuration.md +2 -2
- package/docs/kxm-handbook.md +3 -3
- package/docs/operator-pi-packages.md +63 -0
- package/docs/skills/repo-work-delivery.md +102 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +19 -0
- 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 +6875 -3672
- package/plugins/kxm/dist/core.js +214 -34
- package/plugins/kxm/dist/extension.js +7721 -85
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +5403 -1014
- package/plugins/kxm/dist/server.js +3008 -2268
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5751 -631
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/SUITE.md +5 -0
- package/plugins/kxm/skills/hints.json +73 -0
- package/plugins/kxm/skills/kxm/SKILL.md +30 -83
- 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 +34 -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 +35 -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/cli.ts +1077 -11
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +356 -7
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +127 -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/restricted-yaml.d.mts +31 -0
- package/plugins/kxm/src/restricted-yaml.mjs +145 -0
- package/plugins/kxm/src/role.ts +375 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/studio-layout.ts +660 -17
- 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-config.ts +15 -110
- package/plugins/kxm/src/vnext-engine.ts +198 -62
- package/plugins/kxm/src/vnext-harness.ts +263 -81
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +149 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +170 -233
- package/plugins/kxm/src/vnext-runtime-store.ts +35 -1
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -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/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/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-hub.mjs +6 -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
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
5
|
import { HubClient, HubHttpError } from "./client.ts";
|
|
3
6
|
import type {
|
|
4
7
|
DeliveryMode,
|
|
@@ -62,6 +65,7 @@ export interface SessionTokenPayload {
|
|
|
62
65
|
agentName?: string;
|
|
63
66
|
toolPolicy?: ToolPolicy;
|
|
64
67
|
issuedAt: string;
|
|
68
|
+
expiresAt?: string;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
export interface CommandExecutionContext {
|
|
@@ -930,6 +934,13 @@ export function parseAttemptToken(token: string): AttemptTokenPayload | undefine
|
|
|
930
934
|
return undefined;
|
|
931
935
|
}
|
|
932
936
|
|
|
937
|
+
export function timingSafeStringCompare(a: string | undefined, b: string | undefined): boolean {
|
|
938
|
+
if (typeof a !== "string" || typeof b !== "string") return false;
|
|
939
|
+
const hashA = createHash("sha256").update(a).digest();
|
|
940
|
+
const hashB = createHash("sha256").update(b).digest();
|
|
941
|
+
return timingSafeEqual(hashA, hashB);
|
|
942
|
+
}
|
|
943
|
+
|
|
933
944
|
export function mintSessionToken(input?: {
|
|
934
945
|
sessionId?: string;
|
|
935
946
|
agentName?: string;
|
|
@@ -937,26 +948,53 @@ export function mintSessionToken(input?: {
|
|
|
937
948
|
preset?: string;
|
|
938
949
|
allowedTools?: string[];
|
|
939
950
|
deniedTools?: string[];
|
|
951
|
+
expiresAt?: string;
|
|
952
|
+
ttlMs?: number;
|
|
940
953
|
}): string {
|
|
941
954
|
const toolPolicy: ToolPolicy | undefined = input?.toolPolicy ?? (
|
|
942
955
|
input?.allowedTools || input?.deniedTools || input?.preset
|
|
943
956
|
? {
|
|
944
957
|
...(input?.preset ? { preset: input.preset } : {}),
|
|
945
|
-
...(input?.allowedTools ? { allow: input.allowedTools } : {}),
|
|
946
|
-
...(input?.deniedTools ? { deny: input.deniedTools } : {}),
|
|
958
|
+
...(input?.allowedTools ? { allow: input.allowedTools, allowedTools: input.allowedTools } : {}),
|
|
959
|
+
...(input?.deniedTools ? { deny: input.deniedTools, deniedTools: input.deniedTools } : {}),
|
|
947
960
|
}
|
|
948
961
|
: undefined
|
|
949
962
|
);
|
|
963
|
+
const issuedAt = new Date().toISOString();
|
|
964
|
+
// Decision Q2: Default 24-hour TTL for disk SessionToken
|
|
965
|
+
const defaultTtlMs = 24 * 60 * 60 * 1000;
|
|
966
|
+
const ttlMs = typeof input?.ttlMs === "number" && input.ttlMs > 0 ? input.ttlMs : defaultTtlMs;
|
|
967
|
+
const expiresAt = input?.expiresAt ?? new Date(Date.now() + ttlMs).toISOString();
|
|
968
|
+
|
|
950
969
|
const payload: SessionTokenPayload = {
|
|
951
970
|
schema: "kxm.session-token.v1",
|
|
952
971
|
sessionId: input?.sessionId ?? `session-${randomUUID()}`,
|
|
953
|
-
issuedAt
|
|
972
|
+
issuedAt,
|
|
973
|
+
expiresAt,
|
|
954
974
|
...(input?.agentName ? { agentName: input.agentName } : {}),
|
|
955
975
|
...(toolPolicy ? { toolPolicy } : {}),
|
|
956
976
|
};
|
|
957
977
|
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
958
978
|
}
|
|
959
979
|
|
|
980
|
+
export function isSessionTokenExpired(payloadOrToken: SessionTokenPayload | string): boolean {
|
|
981
|
+
let payload: SessionTokenPayload | undefined;
|
|
982
|
+
if (typeof payloadOrToken === "string") {
|
|
983
|
+
try {
|
|
984
|
+
const raw = Buffer.from(payloadOrToken.trim(), "base64url").toString("utf8");
|
|
985
|
+
payload = JSON.parse(raw) as SessionTokenPayload;
|
|
986
|
+
} catch {
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
} else {
|
|
990
|
+
payload = payloadOrToken;
|
|
991
|
+
}
|
|
992
|
+
if (!payload || !payload.expiresAt) return false;
|
|
993
|
+
const expiryTime = new Date(payload.expiresAt).getTime();
|
|
994
|
+
if (Number.isNaN(expiryTime)) return true;
|
|
995
|
+
return Date.now() >= expiryTime;
|
|
996
|
+
}
|
|
997
|
+
|
|
960
998
|
export function parseSessionToken(token: string): SessionTokenPayload | undefined {
|
|
961
999
|
try {
|
|
962
1000
|
const raw = Buffer.from(token.trim(), "base64url").toString("utf8");
|
|
@@ -967,6 +1005,12 @@ export function parseSessionToken(token: string): SessionTokenPayload | undefine
|
|
|
967
1005
|
&& parsed.schema === "kxm.session-token.v1"
|
|
968
1006
|
&& typeof parsed.sessionId === "string"
|
|
969
1007
|
) {
|
|
1008
|
+
if (parsed.expiresAt) {
|
|
1009
|
+
const expiryTime = new Date(parsed.expiresAt).getTime();
|
|
1010
|
+
if (!Number.isNaN(expiryTime) && Date.now() >= expiryTime) {
|
|
1011
|
+
return undefined; // Expired token fails closed
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
970
1014
|
return parsed as SessionTokenPayload;
|
|
971
1015
|
}
|
|
972
1016
|
} catch {
|
|
@@ -975,6 +1019,61 @@ export function parseSessionToken(token: string): SessionTokenPayload | undefine
|
|
|
975
1019
|
return undefined;
|
|
976
1020
|
}
|
|
977
1021
|
|
|
1022
|
+
function resolveUserConfigDirectory(overrideDir?: string | undefined): string {
|
|
1023
|
+
if (overrideDir) return resolve(overrideDir);
|
|
1024
|
+
return resolve(process.env.KXM_USER_CONFIG_DIR?.trim() || join(homedir(), ".config", "kxm"));
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
export function sessionTokenPath(userConfigDir?: string | undefined): string {
|
|
1028
|
+
return join(resolveUserConfigDirectory(userConfigDir), "session.token");
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
export function persistSessionTokenToDisk(
|
|
1032
|
+
token: string,
|
|
1033
|
+
options?: { userConfigDir?: string | undefined; mode?: number | undefined } | undefined,
|
|
1034
|
+
): string {
|
|
1035
|
+
const filePath = sessionTokenPath(options?.userConfigDir);
|
|
1036
|
+
const dir = dirname(filePath);
|
|
1037
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1038
|
+
const mode = options?.mode ?? 0o600;
|
|
1039
|
+
writeFileSync(filePath, `${token.trim()}\n`, { encoding: "utf8", mode });
|
|
1040
|
+
try {
|
|
1041
|
+
chmodSync(filePath, mode);
|
|
1042
|
+
} catch {
|
|
1043
|
+
// Ignore permissions error on filesystems that do not support POSIX modes
|
|
1044
|
+
}
|
|
1045
|
+
return filePath;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
export function readSessionTokenFromDisk(options?: {
|
|
1049
|
+
userConfigDir?: string | undefined;
|
|
1050
|
+
} | undefined): { token: string; payload: SessionTokenPayload } | undefined {
|
|
1051
|
+
const filePath = sessionTokenPath(options?.userConfigDir);
|
|
1052
|
+
if (!existsSync(filePath)) return undefined;
|
|
1053
|
+
try {
|
|
1054
|
+
const token = readFileSync(filePath, "utf8").trim();
|
|
1055
|
+
if (!token) return undefined;
|
|
1056
|
+
const payload = parseSessionToken(token);
|
|
1057
|
+
if (!payload) return undefined;
|
|
1058
|
+
return { token, payload };
|
|
1059
|
+
} catch {
|
|
1060
|
+
return undefined;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
export function clearSessionTokenFromDisk(options?: {
|
|
1065
|
+
userConfigDir?: string | undefined;
|
|
1066
|
+
} | undefined): boolean {
|
|
1067
|
+
const filePath = sessionTokenPath(options?.userConfigDir);
|
|
1068
|
+
if (!existsSync(filePath)) return false;
|
|
1069
|
+
try {
|
|
1070
|
+
unlinkSync(filePath);
|
|
1071
|
+
return true;
|
|
1072
|
+
} catch {
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
978
1077
|
function matchToolPattern(pattern: string, toolName: string): boolean {
|
|
979
1078
|
if (pattern === "*" || pattern === toolName) return true;
|
|
980
1079
|
if (pattern.endsWith("*")) {
|
|
@@ -1026,6 +1125,7 @@ export function isToolAllowed(commandName: string, policy?: ToolPolicy): boolean
|
|
|
1026
1125
|
export function enforceToolPolicy(
|
|
1027
1126
|
commandName: string,
|
|
1028
1127
|
env: NodeJS.ProcessEnv = process.env,
|
|
1128
|
+
options?: { runId?: string; stageId?: string },
|
|
1029
1129
|
): { allowed: boolean; error?: string; detail?: string } {
|
|
1030
1130
|
const attemptTokenRaw = env.KXM_ATTEMPT_TOKEN?.trim();
|
|
1031
1131
|
if (attemptTokenRaw) {
|
|
@@ -1040,14 +1140,56 @@ export function enforceToolPolicy(
|
|
|
1040
1140
|
detail: `command ${commandName} is denied by attempt tool policy`,
|
|
1041
1141
|
};
|
|
1042
1142
|
}
|
|
1143
|
+
// AttemptToken boundary: AttemptTokens are strictly worker tokens.
|
|
1144
|
+
// They cannot execute administrative state promotion unless explicitly allowed in toolPolicy
|
|
1145
|
+
if (commandName === "kxm_promote" || commandName === "promote") {
|
|
1146
|
+
const explicitAllow = attempt.toolPolicy?.allow ?? attempt.toolPolicy?.allowedTools;
|
|
1147
|
+
if (!Array.isArray(explicitAllow) || (!explicitAllow.includes("kxm_promote") && !explicitAllow.includes("promote") && !explicitAllow.includes("*"))) {
|
|
1148
|
+
return {
|
|
1149
|
+
allowed: false,
|
|
1150
|
+
error: "attempt_token_admin_denied",
|
|
1151
|
+
detail: "AttemptToken worker cannot perform operator state promotion without explicit policy grant",
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
// AttemptToken boundary: Scoped strictly to runId if provided in execution options
|
|
1156
|
+
if (options?.runId && attempt.runId && options.runId !== attempt.runId) {
|
|
1157
|
+
return {
|
|
1158
|
+
allowed: false,
|
|
1159
|
+
error: "attempt_token_scope_violation",
|
|
1160
|
+
detail: `attempt token runId ${attempt.runId} does not match request runId ${options.runId}`,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
return { allowed: true };
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const envSessionRaw = env.KXM_SESSION_TOKEN?.trim();
|
|
1167
|
+
if (envSessionRaw) {
|
|
1168
|
+
const session = parseSessionToken(envSessionRaw);
|
|
1169
|
+
if (!session) {
|
|
1170
|
+
return { allowed: false, error: "session_token_invalid", detail: "KXM_SESSION_TOKEN is malformed or expired" };
|
|
1171
|
+
}
|
|
1172
|
+
if (!isToolAllowed(commandName, session.toolPolicy)) {
|
|
1173
|
+
return {
|
|
1174
|
+
allowed: false,
|
|
1175
|
+
error: "tool_policy_denied",
|
|
1176
|
+
detail: `command ${commandName} is denied by session tool policy`,
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1043
1179
|
return { allowed: true };
|
|
1044
1180
|
}
|
|
1045
1181
|
|
|
1046
|
-
const
|
|
1047
|
-
if (
|
|
1048
|
-
|
|
1182
|
+
const tokenFile = sessionTokenPath(env.KXM_USER_CONFIG_DIR);
|
|
1183
|
+
if (existsSync(tokenFile)) {
|
|
1184
|
+
let tokenRaw: string | undefined;
|
|
1185
|
+
try {
|
|
1186
|
+
tokenRaw = readFileSync(tokenFile, "utf8").trim();
|
|
1187
|
+
} catch {
|
|
1188
|
+
return { allowed: false, error: "session_token_invalid", detail: "Session token file on disk could not be read" };
|
|
1189
|
+
}
|
|
1190
|
+
const session = tokenRaw ? parseSessionToken(tokenRaw) : undefined;
|
|
1049
1191
|
if (!session) {
|
|
1050
|
-
return { allowed: false, error: "session_token_invalid", detail: "
|
|
1192
|
+
return { allowed: false, error: "session_token_invalid", detail: "Session token on disk is malformed or expired" };
|
|
1051
1193
|
}
|
|
1052
1194
|
if (!isToolAllowed(commandName, session.toolPolicy)) {
|
|
1053
1195
|
return {
|
|
@@ -193,8 +193,8 @@ export function loadKxmConfig(
|
|
|
193
193
|
const text = readFileSync(userConfigFile, "utf8");
|
|
194
194
|
userRaw = (parse(text) as Record<string, unknown>) ?? {};
|
|
195
195
|
userLoadedPath = userConfigFile;
|
|
196
|
-
} catch {
|
|
197
|
-
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw new Error(`invalid user config YAML at ${userConfigFile}`, { cause: error });
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -205,8 +205,8 @@ export function loadKxmConfig(
|
|
|
205
205
|
const text = readFileSync(repoConfigFile, "utf8");
|
|
206
206
|
repoRaw = (parse(text) as Record<string, unknown>) ?? {};
|
|
207
207
|
repoLoadedPath = repoConfigFile;
|
|
208
|
-
} catch {
|
|
209
|
-
|
|
208
|
+
} catch (error) {
|
|
209
|
+
throw new Error(`invalid project config YAML at ${repoConfigFile}`, { cause: error });
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
@@ -264,6 +264,9 @@ export function setKxmConfigValue(
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
const parts = keyPath.split(".");
|
|
267
|
+
if (parts.some((part) => !part || part === "__proto__" || part === "prototype" || part === "constructor")) {
|
|
268
|
+
throw new Error("invalid config key path");
|
|
269
|
+
}
|
|
267
270
|
let cursor = existing;
|
|
268
271
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
269
272
|
const p = parts[i]!;
|
|
@@ -291,6 +291,178 @@ export function formatContextPacketForPrompt(packet: FormalContextPacketV2): str
|
|
|
291
291
|
return sections.join("\n").trim();
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
export interface PruningAudit {
|
|
295
|
+
initialTokens: number;
|
|
296
|
+
finalTokens: number;
|
|
297
|
+
budgetTokens: number;
|
|
298
|
+
prunedStages: Array<"L1_defaults" | "L2_episodes" | "L2_docs" | "L5_artifacts">;
|
|
299
|
+
prunedItemCounts: {
|
|
300
|
+
sharedDefaults: number;
|
|
301
|
+
episodes: number;
|
|
302
|
+
docs: number;
|
|
303
|
+
artifactSnippets: number;
|
|
304
|
+
};
|
|
305
|
+
unresolvedGaps: string[];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function estimateContextPacketTokens(packet: FormalContextPacketV2): number {
|
|
309
|
+
const prompt = formatContextPacketForPrompt(packet);
|
|
310
|
+
return Math.ceil(prompt.length / 4);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Deterministic Pruning Hierarchy (Decision Q4):
|
|
315
|
+
* L1 Defaults -> L2 Historical Episodes -> L2 Project Docs -> L5 Artifacts; L3 Task Objective is inviolable.
|
|
316
|
+
*/
|
|
317
|
+
export function pruneContextPacket(
|
|
318
|
+
packet: FormalContextPacketV2,
|
|
319
|
+
tokenBudget?: number,
|
|
320
|
+
): { packet: FormalContextPacketV2; audit: PruningAudit } {
|
|
321
|
+
const budget = tokenBudget ?? packet.budget.allocatedTokens;
|
|
322
|
+
let currentTokens = estimateContextPacketTokens(packet);
|
|
323
|
+
const initialTokens = currentTokens;
|
|
324
|
+
|
|
325
|
+
const prunedStages: Array<"L1_defaults" | "L2_episodes" | "L2_docs" | "L5_artifacts"> = [];
|
|
326
|
+
const prunedItemCounts = {
|
|
327
|
+
sharedDefaults: 0,
|
|
328
|
+
episodes: 0,
|
|
329
|
+
docs: 0,
|
|
330
|
+
artifactSnippets: 0,
|
|
331
|
+
};
|
|
332
|
+
const unresolvedGaps: string[] = [...packet.budget.unresolvedGaps];
|
|
333
|
+
|
|
334
|
+
if (currentTokens <= budget) {
|
|
335
|
+
return {
|
|
336
|
+
packet: {
|
|
337
|
+
...packet,
|
|
338
|
+
budget: {
|
|
339
|
+
...packet.budget,
|
|
340
|
+
allocatedTokens: budget,
|
|
341
|
+
estimatedTokens: currentTokens,
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
audit: {
|
|
345
|
+
initialTokens,
|
|
346
|
+
finalTokens: currentTokens,
|
|
347
|
+
budgetTokens: budget,
|
|
348
|
+
prunedStages,
|
|
349
|
+
prunedItemCounts,
|
|
350
|
+
unresolvedGaps,
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Clone packet environment and predecessors to prune deterministically
|
|
356
|
+
const environment: ContextPacketEnvironment = {
|
|
357
|
+
sharedDefaults: [...packet.environment.sharedDefaults],
|
|
358
|
+
projectKnowledge: [...packet.environment.projectKnowledge],
|
|
359
|
+
currentState: [...packet.environment.currentState],
|
|
360
|
+
contradictions: [...packet.environment.contradictions],
|
|
361
|
+
activeSkills: [...packet.environment.activeSkills],
|
|
362
|
+
};
|
|
363
|
+
const predecessors: ContextPacketPredecessor[] = packet.predecessors.map((p) => ({
|
|
364
|
+
...p,
|
|
365
|
+
artifacts: p.artifacts.map((a) => ({ ...a })),
|
|
366
|
+
}));
|
|
367
|
+
|
|
368
|
+
const workingPacket: FormalContextPacketV2 = {
|
|
369
|
+
...packet,
|
|
370
|
+
environment,
|
|
371
|
+
predecessors,
|
|
372
|
+
budget: { ...packet.budget, allocatedTokens: budget },
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Stage 1 (L1 Defaults): Drop shared defaults first
|
|
376
|
+
if (environment.sharedDefaults.length > 0 && currentTokens > budget) {
|
|
377
|
+
prunedStages.push("L1_defaults");
|
|
378
|
+
while (environment.sharedDefaults.length > 0 && currentTokens > budget) {
|
|
379
|
+
environment.sharedDefaults.pop();
|
|
380
|
+
prunedItemCounts.sharedDefaults += 1;
|
|
381
|
+
currentTokens = estimateContextPacketTokens(workingPacket);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Stage 2 (L2 Historical Episodes): Drop episode state items
|
|
386
|
+
if (currentTokens > budget) {
|
|
387
|
+
const episodeIndices: number[] = [];
|
|
388
|
+
for (let i = environment.currentState.length - 1; i >= 0; i--) {
|
|
389
|
+
if (environment.currentState[i]?.kind === "episode") {
|
|
390
|
+
episodeIndices.push(i);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (episodeIndices.length > 0) {
|
|
394
|
+
prunedStages.push("L2_episodes");
|
|
395
|
+
for (const idx of episodeIndices) {
|
|
396
|
+
if (currentTokens <= budget) break;
|
|
397
|
+
environment.currentState.splice(idx, 1);
|
|
398
|
+
prunedItemCounts.episodes += 1;
|
|
399
|
+
currentTokens = estimateContextPacketTokens(workingPacket);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Stage 3 (L2 Project Docs / Knowledge): Drop project knowledge/doc items
|
|
405
|
+
if (currentTokens > budget && environment.projectKnowledge.length > 0) {
|
|
406
|
+
prunedStages.push("L2_docs");
|
|
407
|
+
while (environment.projectKnowledge.length > 0 && currentTokens > budget) {
|
|
408
|
+
environment.projectKnowledge.pop();
|
|
409
|
+
prunedItemCounts.docs += 1;
|
|
410
|
+
currentTokens = estimateContextPacketTokens(workingPacket);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Stage 4 (L5 Artifacts): Prune artifact content snippets, then artifact entries if still overflowing
|
|
415
|
+
if (currentTokens > budget) {
|
|
416
|
+
const hasArtifacts = predecessors.some((p) => p.artifacts.length > 0);
|
|
417
|
+
if (hasArtifacts) {
|
|
418
|
+
prunedStages.push("L5_artifacts");
|
|
419
|
+
// First strip snippets
|
|
420
|
+
for (const p of predecessors) {
|
|
421
|
+
for (const a of p.artifacts) {
|
|
422
|
+
if (a.contentSnippet) {
|
|
423
|
+
a.contentSnippet = undefined;
|
|
424
|
+
prunedItemCounts.artifactSnippets += 1;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
currentTokens = estimateContextPacketTokens(workingPacket);
|
|
429
|
+
|
|
430
|
+
// If still overflowing, prune artifacts list
|
|
431
|
+
if (currentTokens > budget) {
|
|
432
|
+
for (const p of predecessors) {
|
|
433
|
+
p.artifacts = [];
|
|
434
|
+
}
|
|
435
|
+
currentTokens = estimateContextPacketTokens(workingPacket);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// L3 Task Objective Inviolable:
|
|
441
|
+
// task, plan, and acceptance criteria are inviolable and never stripped.
|
|
442
|
+
if (currentTokens > budget) {
|
|
443
|
+
unresolvedGaps.push("context_budget_exceeded_task_inviolable");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
workingPacket.budget = {
|
|
447
|
+
...packet.budget,
|
|
448
|
+
allocatedTokens: budget,
|
|
449
|
+
estimatedTokens: currentTokens,
|
|
450
|
+
unresolvedGaps,
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
packet: workingPacket,
|
|
455
|
+
audit: {
|
|
456
|
+
initialTokens,
|
|
457
|
+
finalTokens: currentTokens,
|
|
458
|
+
budgetTokens: budget,
|
|
459
|
+
prunedStages,
|
|
460
|
+
prunedItemCounts,
|
|
461
|
+
unresolvedGaps,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
294
466
|
/**
|
|
295
467
|
* Builds a structured handoff manifest between workflow roles.
|
|
296
468
|
*/
|
|
@@ -24,6 +24,11 @@ import {
|
|
|
24
24
|
type SessionHubStatus,
|
|
25
25
|
type SessionWorkItem,
|
|
26
26
|
} from "./session-work.ts";
|
|
27
|
+
import {
|
|
28
|
+
loadActiveWorkflowProgress,
|
|
29
|
+
renderWorkflowTuiText,
|
|
30
|
+
renderWorkflowWidgetLines,
|
|
31
|
+
} from "./workflow-tui.ts";
|
|
27
32
|
|
|
28
33
|
const SETTLEMENT_RETRY_BASE_MS = 250;
|
|
29
34
|
const SETTLEMENT_RETRY_MAX_MS = 30_000;
|
|
@@ -303,6 +308,10 @@ export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void>
|
|
|
303
308
|
const brief = await loadSessionBriefAsync(cwd, process.env, currentWork, hub);
|
|
304
309
|
ctx.ui.setStatus?.("kxm", brief.statusLine);
|
|
305
310
|
ctx.ui.setWidget?.("kxm-work", brief.widgetLines);
|
|
311
|
+
const progress = loadActiveWorkflowProgress(cwd);
|
|
312
|
+
if (progress) {
|
|
313
|
+
ctx.ui.setWidget?.("kxm-progress", renderWorkflowWidgetLines(progress));
|
|
314
|
+
}
|
|
306
315
|
if (!offerPicker || !sessionBriefPickerEnabled({
|
|
307
316
|
env: process.env,
|
|
308
317
|
...(ctx.mode ? { mode: ctx.mode } : {}),
|
|
@@ -866,8 +875,20 @@ export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void>
|
|
|
866
875
|
ctx.ui.notify(brief.statusLine, "info");
|
|
867
876
|
return;
|
|
868
877
|
}
|
|
878
|
+
if (command === "progress" || command === "workflow") {
|
|
879
|
+
const cwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
880
|
+
const progress = loadActiveWorkflowProgress(cwd);
|
|
881
|
+
if (progress) {
|
|
882
|
+
const lines = renderWorkflowWidgetLines(progress);
|
|
883
|
+
ctx.ui.setWidget?.("kxm-progress", lines);
|
|
884
|
+
ctx.ui.notify(renderWorkflowTuiText(progress), "info");
|
|
885
|
+
} else {
|
|
886
|
+
ctx.ui.notify("kxm: no active workflow run found in state.", "info");
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
869
890
|
if (command === "help") {
|
|
870
|
-
ctx.ui.notify("kxm: /kxm | /kxm status | /kxm hub | /kxm memory | /kxm help. CLI: kxm session brief, kxm hub view, kxm memory brief", "info");
|
|
891
|
+
ctx.ui.notify("kxm: /kxm | /kxm status | /kxm progress | /kxm workflow | /kxm hub | /kxm memory | /kxm help. CLI: kxm session brief, kxm hub view, kxm memory brief", "info");
|
|
871
892
|
return;
|
|
872
893
|
}
|
|
873
894
|
if (command === "memory") {
|
|
@@ -889,6 +910,20 @@ export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void>
|
|
|
889
910
|
},
|
|
890
911
|
});
|
|
891
912
|
|
|
913
|
+
pi.registerCommand("workflow", {
|
|
914
|
+
description: "Display active KXM workflow stage progress, assigned roles, and model metrics",
|
|
915
|
+
handler: async (_args, ctx) => {
|
|
916
|
+
const cwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
917
|
+
const progress = loadActiveWorkflowProgress(cwd);
|
|
918
|
+
if (progress) {
|
|
919
|
+
ctx.ui.setWidget?.("kxm-progress", renderWorkflowWidgetLines(progress));
|
|
920
|
+
ctx.ui.notify(renderWorkflowTuiText(progress), "info");
|
|
921
|
+
} else {
|
|
922
|
+
ctx.ui.notify("kxm: no active workflow run found in state.", "info");
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
|
|
892
927
|
return nousFactoryWork(pi, (report) => {
|
|
893
928
|
nousReport = report;
|
|
894
929
|
});
|