@kontextmind/kxm 0.7.91 → 0.7.93
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/workflows/default.yaml +1 -1
- package/CHANGELOG.md +212 -0
- package/README.md +3 -0
- package/docs/README.md +3 -0
- package/docs/agent-skills.md +123 -60
- package/docs/architecture.md +5 -2
- package/docs/cli-reference.md +3527 -0
- package/docs/config-reference.md +1943 -0
- package/docs/configuration.md +30 -4
- package/docs/continuous-improvement.md +122 -10
- package/docs/contracts/routing.md +95 -11
- package/docs/harness-routing.md +616 -0
- package/docs/kxm-handbook.md +106 -19
- package/docs/templates/README.md +1 -1
- package/docs/test-matrix.md +12 -6
- package/docs/troubleshooting.md +2 -2
- package/examples/project/.kxm/workflows/fix.yaml +1 -1
- package/examples/project/.kxm/workflows/improve.yaml +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +9 -10
- package/plugins/kxm/README.md +238 -56
- package/plugins/kxm/dist/claude-hook.js +10083 -0
- package/plugins/kxm/dist/cli.js +2487 -1848
- package/plugins/kxm/dist/client.js +64 -0
- package/plugins/kxm/dist/core.js +102 -9
- package/plugins/kxm/dist/extension.js +210 -68
- package/plugins/kxm/dist/mcp-server.js +217 -40
- package/plugins/kxm/dist/runtime-supervisor.js +1628 -157
- package/plugins/kxm/dist/runtime.js +1874 -298
- package/plugins/kxm/dist/server.js +416 -82
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/hints.json +1 -1
- package/plugins/kxm/skills/kxm/SKILL.md +48 -24
- package/plugins/kxm/skills/kxm/references/protocol.md +3 -3
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +67 -21
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +9 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +82 -16
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +55 -27
- package/plugins/kxm/skills/kxm-insights/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-mind/SKILL.md +2 -2
- package/plugins/kxm/skills/{kxm-setup → kxm-mind-setup}/SKILL.md +4 -4
- package/plugins/kxm/skills/kxm-peer/SKILL.md +68 -93
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +156 -23
- package/plugins/kxm/skills/kxm-projects/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-query/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +74 -15
- package/plugins/kxm/skills/kxm-runs/SKILL.md +46 -17
- package/plugins/kxm/skills/kxm-session/SKILL.md +64 -36
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +44 -15
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +16 -4
- package/plugins/kxm/skills/kxm-triage/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-work/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +60 -19
- package/plugins/kxm/src/arbiter.ts +67 -22
- package/plugins/kxm/src/autocomplete.ts +1 -1
- package/plugins/kxm/src/claude-hook.ts +192 -0
- package/plugins/kxm/src/cli/project.ts +11 -5
- package/plugins/kxm/src/cli/system.ts +85 -13
- package/plugins/kxm/src/cli/types.ts +4 -1
- package/plugins/kxm/src/cli/workflows.ts +18 -16
- package/plugins/kxm/src/cli.ts +23 -13
- package/plugins/kxm/src/client.ts +15 -4
- package/plugins/kxm/src/commands.ts +19 -9
- package/plugins/kxm/src/config.ts +42 -7
- package/plugins/kxm/src/context-packet.ts +14 -2
- package/plugins/kxm/src/context.ts +16 -5
- package/plugins/kxm/src/dispatch-context.ts +286 -0
- package/plugins/kxm/src/engine-plan.ts +40 -0
- package/plugins/kxm/src/engine.ts +138 -6
- package/plugins/kxm/src/hub-env.ts +17 -1
- package/plugins/kxm/src/hub.ts +92 -29
- package/plugins/kxm/src/improve-sources.ts +228 -0
- package/plugins/kxm/src/improve.ts +325 -140
- package/plugins/kxm/src/local-snapshot.ts +101 -42
- package/plugins/kxm/src/mcp-server.ts +129 -30
- package/plugins/kxm/src/memory.ts +43 -20
- package/plugins/kxm/src/project-config.ts +25 -0
- package/plugins/kxm/src/protocol.ts +11 -0
- package/plugins/kxm/src/relevance.ts +138 -0
- package/plugins/kxm/src/retrospective.ts +16 -10
- package/plugins/kxm/src/runtime-service.ts +8 -1
- package/plugins/kxm/src/runtime-supervisor.ts +16 -2
- package/plugins/kxm/src/session-token-hint.ts +17 -0
- package/plugins/kxm/src/suggest.ts +7 -7
- package/plugins/kxm/src/workflow-manager.ts +80 -78
- package/plugins/kxm/src/workflow.ts +202 -12
- package/scripts/build-runtime.mjs +7 -1
- package/scripts/check-generated.mjs +1 -0
- package/scripts/emit-codex-artifacts.mjs +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ContextItem } from "./context.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic lexical relevance (BM25) for context ranking and recall.
|
|
5
|
+
*
|
|
6
|
+
* No model, no clock, no randomness: the same query and documents always
|
|
7
|
+
* produce bit-identical scores, so arbiter packets and recall results stay
|
|
8
|
+
* reproducible and testable.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Words that carry no task signal. Dropped before scoring. */
|
|
12
|
+
export const RELEVANCE_STOPWORDS: ReadonlySet<string> = Object.freeze(new Set([
|
|
13
|
+
"a", "an", "and", "are", "as", "at", "be", "been", "but", "by", "can", "could",
|
|
14
|
+
"did", "do", "does", "for", "from", "had", "has", "have", "how", "if", "in",
|
|
15
|
+
"into", "is", "it", "its", "of", "on", "or", "our", "should", "so", "than",
|
|
16
|
+
"that", "the", "their", "them", "then", "there", "these", "they", "this",
|
|
17
|
+
"those", "to", "was", "we", "were", "what", "when", "where", "which", "while",
|
|
18
|
+
"who", "why", "will", "with", "would", "you", "your",
|
|
19
|
+
]));
|
|
20
|
+
|
|
21
|
+
/** BM25 term-frequency saturation. */
|
|
22
|
+
export const RELEVANCE_K1 = 1.2;
|
|
23
|
+
/** BM25 document-length normalization. */
|
|
24
|
+
export const RELEVANCE_B = 0.75;
|
|
25
|
+
|
|
26
|
+
const MIN_TOKEN_CHARS = 2;
|
|
27
|
+
const MAX_TOKEN_CHARS = 64;
|
|
28
|
+
|
|
29
|
+
function foldPlural(token: string): string {
|
|
30
|
+
if (/^\p{N}+$/u.test(token)) return token;
|
|
31
|
+
if (token.length > 4 && token.endsWith("ies")) return `${token.slice(0, -3)}y`;
|
|
32
|
+
if (token.length > 4 && token.endsWith("sses")) return token.slice(0, -2);
|
|
33
|
+
if (
|
|
34
|
+
token.length > 3
|
|
35
|
+
&& token.endsWith("s")
|
|
36
|
+
&& !token.endsWith("ss")
|
|
37
|
+
&& !token.endsWith("us")
|
|
38
|
+
&& !token.endsWith("is")
|
|
39
|
+
) {
|
|
40
|
+
return token.slice(0, -1);
|
|
41
|
+
}
|
|
42
|
+
return token;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** NFKC-normalized, lowercased, stopword-free, plural-folded tokens in text
|
|
46
|
+
* order, duplicates kept. */
|
|
47
|
+
export function relevanceTokens(text: string): string[] {
|
|
48
|
+
const tokens: string[] = [];
|
|
49
|
+
for (const raw of text.normalize("NFKC").toLowerCase().split(/[^\p{L}\p{N}]+/u)) {
|
|
50
|
+
if (raw.length < MIN_TOKEN_CHARS || raw.length > MAX_TOKEN_CHARS) continue;
|
|
51
|
+
if (RELEVANCE_STOPWORDS.has(raw)) continue;
|
|
52
|
+
tokens.push(foldPlural(raw));
|
|
53
|
+
}
|
|
54
|
+
return tokens;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Index-aligned BM25 scores of each document against the query. Terms are
|
|
58
|
+
* summed in first-occurrence query order so the float result is bit-stable. */
|
|
59
|
+
export function scoreRelevance(query: string, documents: readonly string[]): number[] {
|
|
60
|
+
const scores = documents.map(() => 0);
|
|
61
|
+
const terms = [...new Set(relevanceTokens(query))];
|
|
62
|
+
if (terms.length === 0 || documents.length === 0) return scores;
|
|
63
|
+
|
|
64
|
+
const indexed = documents.map((document) => {
|
|
65
|
+
const tokens = relevanceTokens(document);
|
|
66
|
+
const frequencies = new Map<string, number>();
|
|
67
|
+
for (const token of tokens) frequencies.set(token, (frequencies.get(token) ?? 0) + 1);
|
|
68
|
+
return { length: tokens.length, frequencies };
|
|
69
|
+
});
|
|
70
|
+
const count = indexed.length;
|
|
71
|
+
let totalLength = 0;
|
|
72
|
+
for (const document of indexed) totalLength += document.length;
|
|
73
|
+
const averageLength = totalLength > 0 ? totalLength / count : 1;
|
|
74
|
+
|
|
75
|
+
const inverseFrequency = new Map<string, number>();
|
|
76
|
+
for (const term of terms) {
|
|
77
|
+
let documentFrequency = 0;
|
|
78
|
+
for (const document of indexed) {
|
|
79
|
+
if (document.frequencies.has(term)) documentFrequency += 1;
|
|
80
|
+
}
|
|
81
|
+
inverseFrequency.set(term, Math.log(1 + (count - documentFrequency + 0.5) / (documentFrequency + 0.5)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
indexed.forEach((document, index) => {
|
|
85
|
+
let score = 0;
|
|
86
|
+
for (const term of terms) {
|
|
87
|
+
const frequency = document.frequencies.get(term) ?? 0;
|
|
88
|
+
if (frequency === 0) continue;
|
|
89
|
+
const lengthNorm = 1 - RELEVANCE_B + RELEVANCE_B * (document.length / averageLength);
|
|
90
|
+
score += inverseFrequency.get(term)! * ((frequency * (RELEVANCE_K1 + 1)) / (frequency + RELEVANCE_K1 * lengthNorm));
|
|
91
|
+
}
|
|
92
|
+
scores[index] = score;
|
|
93
|
+
});
|
|
94
|
+
return scores;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Relevance rounded to three decimals for audits and responses. */
|
|
98
|
+
export function roundRelevance(score: number): number {
|
|
99
|
+
return Math.round(score * 1000) / 1000;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The text a context item is ranked on: its summary plus its state key.
|
|
103
|
+
* Kind, id and sourceRef are excluded. */
|
|
104
|
+
export function contextItemRelevanceText(item: ContextItem): string {
|
|
105
|
+
return item.stateKey !== undefined ? `${item.summary} ${item.stateKey}` : item.summary;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Locale-independent code-unit order for identifiers. */
|
|
109
|
+
export function compareCodeUnitIds(left: string, right: string): number {
|
|
110
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface RankedRecallItem {
|
|
114
|
+
item: ContextItem;
|
|
115
|
+
relevance: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Rank recall candidates: exact-phrase hits first, then BM25 any-token hits,
|
|
119
|
+
* then id. An empty query is a phrase hit for every item (id order). Items
|
|
120
|
+
* that neither contain the phrase nor share a token are dropped. */
|
|
121
|
+
export function rankRecall(query: string, items: readonly ContextItem[], limit: number): RankedRecallItem[] {
|
|
122
|
+
const needle = query.toLowerCase();
|
|
123
|
+
const scores = scoreRelevance(query, items.map(contextItemRelevanceText));
|
|
124
|
+
const ranked: { item: ContextItem; score: number; phraseHit: boolean }[] = [];
|
|
125
|
+
items.forEach((item, index) => {
|
|
126
|
+
const score = scores[index] ?? 0;
|
|
127
|
+
const phraseHit = needle === ""
|
|
128
|
+
|| item.summary.toLowerCase().includes(needle)
|
|
129
|
+
|| (item.stateKey ?? "").toLowerCase().includes(needle);
|
|
130
|
+
if (phraseHit || score > 0) ranked.push({ item, score, phraseHit });
|
|
131
|
+
});
|
|
132
|
+
ranked.sort((left, right) =>
|
|
133
|
+
(right.phraseHit ? 1 : 0) - (left.phraseHit ? 1 : 0)
|
|
134
|
+
|| right.score - left.score
|
|
135
|
+
|| compareCodeUnitIds(left.item.id, right.item.id),
|
|
136
|
+
);
|
|
137
|
+
return ranked.slice(0, limit).map(({ item, score }) => ({ item, relevance: roundRelevance(score) }));
|
|
138
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
canonicalWorkflowEvidenceKey,
|
|
6
6
|
journalPromotionState,
|
|
7
7
|
normalizeVerifiedWorkflowEvidence,
|
|
8
|
+
rankImprovementSignals,
|
|
8
9
|
type WorkflowJournalEntry,
|
|
9
10
|
type WorkflowRun,
|
|
10
11
|
} from "./workflow.ts";
|
|
@@ -248,12 +249,14 @@ export function buildRetrospective(
|
|
|
248
249
|
const byCategory: Record<string, number> = {};
|
|
249
250
|
const byArea: Record<string, number> = {};
|
|
250
251
|
const byClass: Record<string, number> = {};
|
|
252
|
+
const byErrorClass: Record<string, number> = {};
|
|
251
253
|
for (const entry of entries) {
|
|
252
254
|
increment(byCategory, entry.category);
|
|
253
255
|
increment(byArea, entry.area);
|
|
254
256
|
increment(byClass, classFromEvidence(entry.evidence));
|
|
257
|
+
if (entry.category === "error") increment(byErrorClass, classFromEvidence(entry.evidence));
|
|
255
258
|
}
|
|
256
|
-
const recurringErrorClasses = Object.entries(
|
|
259
|
+
const recurringErrorClasses = Object.entries(byErrorClass)
|
|
257
260
|
.map(([errorClass, count]) => ({ class: errorClass, count }))
|
|
258
261
|
.sort((left, right) => right.count - left.count || left.class.localeCompare(right.class));
|
|
259
262
|
const resolvedContradictions = new Set(entries.filter((entry) => entry.category === "decision" || entry.category === "lesson").flatMap((entry) => entry.relatedEntryIds));
|
|
@@ -263,15 +266,18 @@ export function buildRetrospective(
|
|
|
263
266
|
const decisions = entries
|
|
264
267
|
.filter((entry) => entry.category === "decision")
|
|
265
268
|
.map((entry) => ({ id: entry.id, summary: entry.summary, area: entry.area }));
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
269
|
+
// Ranked, merged error and lesson signals over this run's raw entries. The
|
|
270
|
+
// category filter runs before the limit so up to 12 of them survive.
|
|
271
|
+
const proposedImprovements = rankImprovementSignals(
|
|
272
|
+
journal.filter((entry) => entry.runId === run.id && (entry.category === "error" || entry.category === "lesson")),
|
|
273
|
+
new Map([[run.id, run]]),
|
|
274
|
+
12,
|
|
275
|
+
).map((signal) => ({
|
|
276
|
+
area: signal.area,
|
|
277
|
+
summary: signal.summary,
|
|
278
|
+
successMeasure: redactSecrets(`no recurrence of ${signal.key} in the next ${run.definitionId} run`),
|
|
279
|
+
status: "proposed" as const,
|
|
280
|
+
}));
|
|
275
281
|
const evidenceAudit = buildEvidenceAudit(run);
|
|
276
282
|
const degradedStageIds = run.stages
|
|
277
283
|
.filter((stage) => stage.degraded)
|
|
@@ -117,7 +117,7 @@ function collectMemoryFiles(dir: string, baseDir: string, ignoreSubdirs: Set<str
|
|
|
117
117
|
* the promoted-state snapshot.
|
|
118
118
|
*/
|
|
119
119
|
export function computeKxmMemoryRevision(
|
|
120
|
-
bundle: KxmProjectBundle,
|
|
120
|
+
bundle: Pick<KxmProjectBundle, "projectRoot">,
|
|
121
121
|
options: KxmMemoryRevisionOptions = {},
|
|
122
122
|
): string {
|
|
123
123
|
const hash = createHash("sha256");
|
|
@@ -248,6 +248,11 @@ export interface KxmRuntimeContext {
|
|
|
248
248
|
* freeze a budget by supplying a stale clock.
|
|
249
249
|
*/
|
|
250
250
|
readonly budgetClock?: () => string;
|
|
251
|
+
/**
|
|
252
|
+
* Optional structured logger (the Runtime supervisor's). The engine writes
|
|
253
|
+
* metadata-only lines here, ids and counts, never task or memory text.
|
|
254
|
+
*/
|
|
255
|
+
readonly logger?: (entry: Record<string, unknown>) => void;
|
|
251
256
|
}
|
|
252
257
|
|
|
253
258
|
const closedRuntimeContexts = new WeakSet<KxmRuntimeContext>();
|
|
@@ -286,6 +291,7 @@ export function openKxmRuntimeContext(
|
|
|
286
291
|
now?: string;
|
|
287
292
|
homeRuntimeId: string;
|
|
288
293
|
budgetClock?: () => string;
|
|
294
|
+
logger?: (entry: Record<string, unknown>) => void;
|
|
289
295
|
},
|
|
290
296
|
): KxmRuntimeContext {
|
|
291
297
|
const paths = kxmRuntimePaths(options.stateRoot !== undefined ? { stateRoot: options.stateRoot } : {});
|
|
@@ -310,6 +316,7 @@ export function openKxmRuntimeContext(
|
|
|
310
316
|
eventStore,
|
|
311
317
|
configRevision: bundle.configRevision,
|
|
312
318
|
...(options.budgetClock ? { budgetClock: options.budgetClock } : {}),
|
|
319
|
+
...(options.logger ? { logger: options.logger } : {}),
|
|
313
320
|
};
|
|
314
321
|
} catch (error) {
|
|
315
322
|
registry.close();
|
|
@@ -683,7 +683,7 @@ async function startKxmRuntimeSupervisorInner(
|
|
|
683
683
|
const key = projectRuntimeKey(projectRoot);
|
|
684
684
|
const existing = contexts.get(key);
|
|
685
685
|
if (existing) return existing;
|
|
686
|
-
const context = openKxmRuntimeContext(projectRoot, { homeRuntimeId: activeRuntimeId, stateRoot: paths.stateRoot });
|
|
686
|
+
const context = openKxmRuntimeContext(projectRoot, { homeRuntimeId: activeRuntimeId, stateRoot: paths.stateRoot, logger });
|
|
687
687
|
registerSyncCredentials(context);
|
|
688
688
|
contexts.set(key, context);
|
|
689
689
|
return context;
|
|
@@ -1315,7 +1315,21 @@ export async function kxmRuntimeRequest(
|
|
|
1315
1315
|
if (!response.ok) {
|
|
1316
1316
|
const code = typeof payload.error === "string" ? payload.error : "runtime_request_failed";
|
|
1317
1317
|
const message = typeof payload.message === "string" ? payload.message : `runtime request failed with HTTP ${response.status}`;
|
|
1318
|
-
throw runtimeError(code, path, message);
|
|
1318
|
+
throw runtimeError(code, path, `${message}${handoffSuffix(payload.handoff)}`);
|
|
1319
1319
|
}
|
|
1320
1320
|
return payload;
|
|
1321
1321
|
}
|
|
1322
|
+
|
|
1323
|
+
const HANDOFF_TEXT_MAX = 200;
|
|
1324
|
+
|
|
1325
|
+
/** Why the Runtime handed a run off, from a `run_handoff_required` body: the
|
|
1326
|
+
* reason, field and detail, each capped so a refusal stays one readable line. */
|
|
1327
|
+
function handoffSuffix(handoff: unknown): string {
|
|
1328
|
+
if (!handoff || typeof handoff !== "object" || Array.isArray(handoff)) return "";
|
|
1329
|
+
const parts: string[] = [];
|
|
1330
|
+
for (const key of ["reason", "field", "detail"] as const) {
|
|
1331
|
+
const value = (handoff as Record<string, unknown>)[key];
|
|
1332
|
+
if (typeof value === "string" && value.length > 0) parts.push(`${key} ${value.slice(0, HANDOFF_TEXT_MAX)}`);
|
|
1333
|
+
}
|
|
1334
|
+
return parts.length > 0 ? ` (handoff ${parts.join("; ")})` : "";
|
|
1335
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** User-directed fix for a `session_token_invalid` tool-policy denial.
|
|
2
|
+
*
|
|
3
|
+
* `enforceToolPolicy` (commands.ts) returns that error from three places: an
|
|
4
|
+
* invalid `KXM_SESSION_TOKEN` in the environment (detail starts with
|
|
5
|
+
* `KXM_SESSION_TOKEN`), and an unreadable or malformed/expired session token
|
|
6
|
+
* file on disk. The Claude SessionStart hook and the MCP server both use this
|
|
7
|
+
* text, so a session sees one explanation. Neither text prints or reissues a
|
|
8
|
+
* token: `kxm session token --issue` and bare `kxm session token` print one. */
|
|
9
|
+
|
|
10
|
+
const ENV_TEXT = "KXM_SESSION_TOKEN in the environment Claude Code was launched from is malformed or expired, so every kxm_* tool fails with tool_policy_denied. Ask the user to unset or replace KXM_SESSION_TOKEN in the environment Claude Code was launched from, then restart Claude Code.";
|
|
11
|
+
|
|
12
|
+
const DISK_TEXT = "The KXM session token file on this machine is expired, malformed or unreadable, so every kxm_* tool fails with tool_policy_denied. Ask the user to run `kxm session token --clear` in their own terminal. `kxm session token --status` reports No active session token found for an expired file even though the file still blocks tools. The kxm plugin no longer refreshes that 24-hour token.";
|
|
13
|
+
|
|
14
|
+
export function sessionTokenFixHint(policy: { error?: string | undefined; detail?: string | undefined }): string | undefined {
|
|
15
|
+
if (policy.error !== "session_token_invalid") return undefined;
|
|
16
|
+
return policy.detail?.startsWith("KXM_SESSION_TOKEN") ? ENV_TEXT : DISK_TEXT;
|
|
17
|
+
}
|
|
@@ -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: ["
|
|
35
|
+
skills: ["kxm-workflow", "kxm-runs", "kxm-context-memory"],
|
|
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: ["
|
|
42
|
+
skills: ["kxm-workflow", "kxm-peer", "kxm-context-memory"],
|
|
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-workflow", "kxm-runs"],
|
|
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-workflow", "kxm-definitions"],
|
|
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", "kxm-
|
|
63
|
+
skills: ["kxm-workflow", "kxm-peer"],
|
|
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", "kxm-
|
|
70
|
+
skills: ["kxm-runs", "kxm-context-memory"],
|
|
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", "kxm-
|
|
77
|
+
skills: ["kxm-session", "kxm-context-memory", "kxm-routing-improve"],
|
|
78
78
|
defaultCommand: (p) => `kxm run research-strategy/architecture-spike "${p}"`,
|
|
79
79
|
},
|
|
80
80
|
];
|
|
@@ -18,10 +18,40 @@ export interface WorkflowDefSummary {
|
|
|
18
18
|
roles: string[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Fresh objects per use: YAML stringify turns a repeated object into an
|
|
22
|
+
// anchor and alias, which the restricted project loader refuses.
|
|
23
|
+
const completed = () => ({ target: "$terminal", terminalStatus: "completed" });
|
|
24
|
+
const failed = () => ({ target: "$terminal", terminalStatus: "failed" });
|
|
25
|
+
|
|
26
|
+
/** The project's `test` gate from `kxm init`. A gate step settles on `passed` or
|
|
27
|
+
* `implementation-failure`, never `failed`. `maxAttempts` counts entries into a
|
|
28
|
+
* step, so every step a back-edge re-enters allows one entry per transition. */
|
|
29
|
+
function verifyStep(retryStep: string): Record<string, unknown> {
|
|
30
|
+
return {
|
|
31
|
+
id: "verify",
|
|
32
|
+
kind: "gate",
|
|
33
|
+
gate: "test",
|
|
34
|
+
expect: "pass",
|
|
35
|
+
maxAttempts: 3,
|
|
36
|
+
repositories: { control: "write" },
|
|
37
|
+
on: {
|
|
38
|
+
passed: completed(),
|
|
39
|
+
"implementation-failure": { target: retryStep, maxTransitions: 2 },
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Built-in `kxm workflow add --template` definitions. Each is a complete
|
|
46
|
+
* kxm.workflow.v1 document (the file name is the workflow id) that uses only
|
|
47
|
+
* what `kxm init` creates: the coordinator and implementer agents, the control
|
|
48
|
+
* repository, and the `test` gate. Agent steps declare `failed` because the
|
|
49
|
+
* producer falls back to it.
|
|
50
|
+
*/
|
|
21
51
|
export const WORKFLOW_TEMPLATES: Record<string, Record<string, unknown>> = {
|
|
22
52
|
"implement-and-verify": {
|
|
23
53
|
schema: "kxm.workflow.v1",
|
|
24
|
-
description: "
|
|
54
|
+
description: "Implement a change, then run the project's test gate; a failing gate sends the work back to implement.",
|
|
25
55
|
coordinator: "coordinator",
|
|
26
56
|
limits: {
|
|
27
57
|
maxTransitions: 8,
|
|
@@ -30,38 +60,20 @@ export const WORKFLOW_TEMPLATES: Record<string, Record<string, unknown>> = {
|
|
|
30
60
|
{
|
|
31
61
|
id: "implement",
|
|
32
62
|
kind: "agent",
|
|
33
|
-
|
|
34
|
-
maxAttempts:
|
|
63
|
+
agent: "implementer",
|
|
64
|
+
maxAttempts: 3,
|
|
65
|
+
repositories: { control: "write" },
|
|
35
66
|
on: {
|
|
36
67
|
passed: "verify",
|
|
37
|
-
failed:
|
|
38
|
-
target: "$terminal",
|
|
39
|
-
terminalStatus: "failed",
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: "verify",
|
|
45
|
-
kind: "gate",
|
|
46
|
-
gate: "verify-gate",
|
|
47
|
-
expect: "pass",
|
|
48
|
-
maxAttempts: 1,
|
|
49
|
-
on: {
|
|
50
|
-
passed: {
|
|
51
|
-
target: "$terminal",
|
|
52
|
-
terminalStatus: "completed",
|
|
53
|
-
},
|
|
54
|
-
failed: {
|
|
55
|
-
target: "implement",
|
|
56
|
-
maxTransitions: 2,
|
|
57
|
-
},
|
|
68
|
+
failed: failed(),
|
|
58
69
|
},
|
|
59
70
|
},
|
|
71
|
+
verifyStep("implement"),
|
|
60
72
|
],
|
|
61
73
|
},
|
|
62
74
|
"dual-critic-review": {
|
|
63
75
|
schema: "kxm.workflow.v1",
|
|
64
|
-
description: "
|
|
76
|
+
description: "Implement, review twice, then run the project's test gate. Both reviews run as the coordinator agent; for independent critics, add agents under .kxm/agents and point review-arch and review-cli at them.",
|
|
65
77
|
coordinator: "coordinator",
|
|
66
78
|
limits: {
|
|
67
79
|
maxTransitions: 12,
|
|
@@ -70,102 +82,92 @@ export const WORKFLOW_TEMPLATES: Record<string, Record<string, unknown>> = {
|
|
|
70
82
|
{
|
|
71
83
|
id: "implement",
|
|
72
84
|
kind: "agent",
|
|
73
|
-
|
|
74
|
-
maxAttempts:
|
|
85
|
+
agent: "implementer",
|
|
86
|
+
maxAttempts: 3,
|
|
87
|
+
repositories: { control: "write" },
|
|
75
88
|
on: {
|
|
76
89
|
passed: "review-arch",
|
|
77
|
-
failed:
|
|
78
|
-
target: "$terminal",
|
|
79
|
-
terminalStatus: "failed",
|
|
80
|
-
},
|
|
90
|
+
failed: failed(),
|
|
81
91
|
},
|
|
82
92
|
},
|
|
83
93
|
{
|
|
84
94
|
id: "review-arch",
|
|
85
95
|
kind: "agent",
|
|
86
|
-
|
|
87
|
-
maxAttempts:
|
|
96
|
+
agent: "coordinator",
|
|
97
|
+
maxAttempts: 3,
|
|
98
|
+
repositories: { control: "read" },
|
|
88
99
|
on: {
|
|
89
100
|
passed: "review-cli",
|
|
90
|
-
failed: {
|
|
91
|
-
target: "implement",
|
|
92
|
-
maxTransitions: 2,
|
|
93
|
-
},
|
|
101
|
+
failed: { target: "implement", maxTransitions: 2 },
|
|
94
102
|
},
|
|
95
103
|
},
|
|
96
104
|
{
|
|
97
105
|
id: "review-cli",
|
|
98
106
|
kind: "agent",
|
|
99
|
-
|
|
100
|
-
maxAttempts:
|
|
107
|
+
agent: "coordinator",
|
|
108
|
+
maxAttempts: 3,
|
|
109
|
+
repositories: { control: "read" },
|
|
101
110
|
on: {
|
|
102
111
|
passed: "verify",
|
|
103
|
-
failed: {
|
|
104
|
-
target: "implement",
|
|
105
|
-
maxTransitions: 2,
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
id: "verify",
|
|
111
|
-
kind: "gate",
|
|
112
|
-
gate: "verify-gate",
|
|
113
|
-
expect: "pass",
|
|
114
|
-
maxAttempts: 1,
|
|
115
|
-
on: {
|
|
116
|
-
passed: {
|
|
117
|
-
target: "$terminal",
|
|
118
|
-
terminalStatus: "completed",
|
|
119
|
-
},
|
|
120
|
-
failed: {
|
|
121
|
-
target: "implement",
|
|
122
|
-
maxTransitions: 2,
|
|
123
|
-
},
|
|
112
|
+
failed: { target: "implement", maxTransitions: 2 },
|
|
124
113
|
},
|
|
125
114
|
},
|
|
115
|
+
verifyStep("implement"),
|
|
126
116
|
],
|
|
127
117
|
},
|
|
128
118
|
"spec-and-plan": {
|
|
129
119
|
schema: "kxm.workflow.v1",
|
|
130
|
-
description: "
|
|
120
|
+
description: "Plan a change, then review the plan. Both steps run as the coordinator agent and only read the repository.",
|
|
131
121
|
coordinator: "coordinator",
|
|
132
122
|
limits: {
|
|
133
|
-
maxTransitions:
|
|
123
|
+
maxTransitions: 8,
|
|
134
124
|
},
|
|
135
125
|
steps: [
|
|
136
126
|
{
|
|
137
127
|
id: "plan",
|
|
138
128
|
kind: "agent",
|
|
139
|
-
|
|
140
|
-
maxAttempts:
|
|
129
|
+
agent: "coordinator",
|
|
130
|
+
maxAttempts: 3,
|
|
131
|
+
repositories: { control: "read" },
|
|
141
132
|
on: {
|
|
142
133
|
passed: "review-arch",
|
|
143
|
-
failed:
|
|
144
|
-
target: "$terminal",
|
|
145
|
-
terminalStatus: "failed",
|
|
146
|
-
},
|
|
134
|
+
failed: failed(),
|
|
147
135
|
},
|
|
148
136
|
},
|
|
149
137
|
{
|
|
150
138
|
id: "review-arch",
|
|
151
139
|
kind: "agent",
|
|
152
|
-
|
|
153
|
-
maxAttempts:
|
|
140
|
+
agent: "coordinator",
|
|
141
|
+
maxAttempts: 3,
|
|
142
|
+
repositories: { control: "read" },
|
|
154
143
|
on: {
|
|
155
|
-
passed:
|
|
156
|
-
|
|
157
|
-
terminalStatus: "completed",
|
|
158
|
-
},
|
|
159
|
-
failed: {
|
|
160
|
-
target: "plan",
|
|
161
|
-
maxTransitions: 2,
|
|
162
|
-
},
|
|
144
|
+
passed: completed(),
|
|
145
|
+
failed: { target: "plan", maxTransitions: 2 },
|
|
163
146
|
},
|
|
164
147
|
},
|
|
165
148
|
],
|
|
166
149
|
},
|
|
167
150
|
};
|
|
168
151
|
|
|
152
|
+
/** The one-step definition `kxm workflow add <id>` writes without a template or file. */
|
|
153
|
+
export function scaffoldWorkflowDefinition(description: string): Record<string, unknown> {
|
|
154
|
+
return {
|
|
155
|
+
schema: "kxm.workflow.v1",
|
|
156
|
+
description,
|
|
157
|
+
coordinator: "coordinator",
|
|
158
|
+
limits: { maxTransitions: 8 },
|
|
159
|
+
steps: [
|
|
160
|
+
{
|
|
161
|
+
id: "step-1",
|
|
162
|
+
kind: "agent",
|
|
163
|
+
agent: "implementer",
|
|
164
|
+
repositories: { control: "write" },
|
|
165
|
+
on: { passed: completed(), failed: failed() },
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
169
171
|
export const DEFAULT_WORKFLOW_TEMPLATE = WORKFLOW_TEMPLATES["implement-and-verify"]!;
|
|
170
172
|
|
|
171
173
|
export function workflowsDirectory(
|