@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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
|
-
import type
|
|
4
|
+
import { computeDecayedWeight, type RoutingRecord, type RoutingRecordV2 } from "./routing.ts";
|
|
5
5
|
import { nowIso } from "./protocol.ts";
|
|
6
|
+
import { compareCodeUnitIds } from "./relevance.ts";
|
|
6
7
|
|
|
7
8
|
export const CANDIDATE_SCHEMA = "kxm.candidate.v1" as const;
|
|
8
9
|
export const IMPROVEMENT_REPORT_SCHEMA = "kxm.improvement-report.v2" as const;
|
|
@@ -32,22 +33,43 @@ export interface ImprovementCandidate {
|
|
|
32
33
|
createdAt: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/** Why a group that recurs is still not a coded-repeat candidate. */
|
|
37
|
+
export type ImprovementExcludedReason = "writes-repository" | "ask-not-repeated";
|
|
38
|
+
|
|
35
39
|
export interface ImprovementGroupRow {
|
|
36
40
|
workflowHash: string;
|
|
41
|
+
/** The engine's workflow id when the records carry one. */
|
|
42
|
+
workflowId?: string;
|
|
37
43
|
stepId: string;
|
|
38
44
|
agentRole: string;
|
|
39
45
|
promptHash: string;
|
|
40
46
|
recurrence: number;
|
|
47
|
+
/** Distinct runs among decided records. */
|
|
48
|
+
distinctRuns: number;
|
|
49
|
+
/** Most distinct decided runs that shared one objective (the same ask). */
|
|
50
|
+
askRecurrence: number;
|
|
51
|
+
undecidedRecords: number;
|
|
41
52
|
meanCost: number;
|
|
42
53
|
meanLatency: number;
|
|
54
|
+
/** Accepted share of decided records; a superseded retry never passes. */
|
|
43
55
|
verifyPassRate: number;
|
|
44
56
|
rework: number;
|
|
57
|
+
/** Recency-weighted recurrence. Orders rows; never decides candidacy. */
|
|
58
|
+
weightedRecurrence: number;
|
|
59
|
+
undatedRecords: number;
|
|
60
|
+
costSamples: number;
|
|
61
|
+
writesRepository: boolean;
|
|
45
62
|
evidenceRefs: string[];
|
|
46
63
|
isCandidate: boolean;
|
|
64
|
+
excludedReason?: ImprovementExcludedReason;
|
|
47
65
|
candidateKind?: CandidateKind;
|
|
48
66
|
candidateId?: string;
|
|
49
67
|
}
|
|
50
68
|
|
|
69
|
+
export interface ImprovementPromotionEntry extends PromotionReadiness {
|
|
70
|
+
candidateId: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
51
73
|
export interface ImprovementReport {
|
|
52
74
|
schema: typeof IMPROVEMENT_REPORT_SCHEMA;
|
|
53
75
|
createdAt: string;
|
|
@@ -55,6 +77,8 @@ export interface ImprovementReport {
|
|
|
55
77
|
recordsCount: number;
|
|
56
78
|
groups: ImprovementGroupRow[];
|
|
57
79
|
candidates: ImprovementCandidate[];
|
|
80
|
+
promotionPolicy: string;
|
|
81
|
+
promotion: ImprovementPromotionEntry[];
|
|
58
82
|
}
|
|
59
83
|
|
|
60
84
|
export function classifyCandidateKind(stepId: string, agentRole: string): CandidateKind {
|
|
@@ -69,8 +93,17 @@ export function classifyCandidateKind(stepId: string, agentRole: string): Candid
|
|
|
69
93
|
return "workflow-step";
|
|
70
94
|
}
|
|
71
95
|
|
|
72
|
-
function
|
|
73
|
-
|
|
96
|
+
function candidateSlug(value: string): string {
|
|
97
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "step";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function generateCandidateDiff(
|
|
101
|
+
kind: CandidateKind,
|
|
102
|
+
stepId: string,
|
|
103
|
+
agentRole: string,
|
|
104
|
+
workflowId?: string | undefined,
|
|
105
|
+
): { diff: string; declaredOutcome: string; measure: string; summary: string } {
|
|
106
|
+
const safeSlug = candidateSlug(stepId);
|
|
74
107
|
if (kind === "gate") {
|
|
75
108
|
const diff = [
|
|
76
109
|
"diff --git a/.kxm/gates.yaml b/.kxm/gates.yaml",
|
|
@@ -118,80 +151,159 @@ function generateCandidateDiff(kind: CandidateKind, stepId: string, agentRole: s
|
|
|
118
151
|
diff,
|
|
119
152
|
declaredOutcome: `Governed reusable skill for ${stepId}`,
|
|
120
153
|
measure: `Reduced prompt drift and consistent model guidance for ${stepId}`,
|
|
121
|
-
summary: `
|
|
154
|
+
summary: `Consolidate repeated instructions for '${stepId}' (${agentRole}) into a governed skill (consolidation, not a coded step)`,
|
|
122
155
|
};
|
|
123
156
|
}
|
|
124
|
-
|
|
157
|
+
// A workflow-step candidate proposes the coded step itself: the agent turn
|
|
158
|
+
// becomes a command gate the operator writes and reviews.
|
|
159
|
+
const rel = `.kxm/workflows/${workflowId !== undefined ? candidateSlug(workflowId) : safeSlug}.yaml`;
|
|
125
160
|
const diff = [
|
|
126
161
|
`diff --git a/${rel} b/${rel}`,
|
|
127
162
|
`--- a/${rel}`,
|
|
128
163
|
`+++ b/${rel}`,
|
|
129
|
-
"@@ -1,3 +1,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
164
|
+
"@@ -1,3 +1,3 @@",
|
|
165
|
+
` - id: ${stepId}`,
|
|
166
|
+
"- kind: agent",
|
|
167
|
+
`- agent: ${agentRole}`,
|
|
168
|
+
"+ kind: gate",
|
|
169
|
+
`+ gate: ${safeSlug}`,
|
|
170
|
+
"diff --git a/.kxm/gates.yaml b/.kxm/gates.yaml",
|
|
171
|
+
"--- a/.kxm/gates.yaml",
|
|
172
|
+
"+++ b/.kxm/gates.yaml",
|
|
173
|
+
"@@ -1,2 +1,6 @@",
|
|
174
|
+
" schema: kxm.gate-registry.v1",
|
|
175
|
+
" gates:",
|
|
176
|
+
`+ ${safeSlug}:`,
|
|
177
|
+
"+ kind: command",
|
|
178
|
+
`+ argv: [node, scripts/${safeSlug}.mjs]`,
|
|
179
|
+
"+ timeoutMs: 600000",
|
|
134
180
|
"",
|
|
135
181
|
].join("\n");
|
|
136
182
|
return {
|
|
137
183
|
diff,
|
|
138
|
-
declaredOutcome: `
|
|
139
|
-
measure: `
|
|
140
|
-
summary: `
|
|
184
|
+
declaredOutcome: `Coded gate replaces the model turn for ${stepId}; the operator writes scripts/${safeSlug}.mjs`,
|
|
185
|
+
measure: `Model cost and latency for ${stepId} drop to zero while its accepted rate holds`,
|
|
186
|
+
summary: `Replace the model turn for '${stepId}' (${agentRole}) with a coded gate step`,
|
|
141
187
|
};
|
|
142
188
|
}
|
|
143
189
|
|
|
190
|
+
/** A trimmed, non-empty string, or undefined. */
|
|
191
|
+
function nonEmptyText(value: unknown): string | undefined {
|
|
192
|
+
if (typeof value !== "string") return undefined;
|
|
193
|
+
const trimmed = value.trim();
|
|
194
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function roundThree(value: number): number {
|
|
198
|
+
return Number(value.toFixed(3));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface GroupAccumulator {
|
|
202
|
+
key: string;
|
|
203
|
+
workflowHash: string;
|
|
204
|
+
workflowId?: string;
|
|
205
|
+
stepId: string;
|
|
206
|
+
agentRole: string;
|
|
207
|
+
promptHash: string;
|
|
208
|
+
costs: number[];
|
|
209
|
+
latencies: number[];
|
|
210
|
+
total: number;
|
|
211
|
+
decided: number;
|
|
212
|
+
passed: number;
|
|
213
|
+
undecided: number;
|
|
214
|
+
reworkCount: number;
|
|
215
|
+
weight: number;
|
|
216
|
+
undated: number;
|
|
217
|
+
writes: boolean;
|
|
218
|
+
decidedRuns: Set<string>;
|
|
219
|
+
/** Decided runs per objective digest; one '(unkeyed)' bucket for records without one. */
|
|
220
|
+
askRuns: Map<string, Set<string>>;
|
|
221
|
+
evidenceRefs: Set<string>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface GroupRoutingOptions {
|
|
225
|
+
minRecurrence?: number | undefined;
|
|
226
|
+
minPassRate?: number | undefined;
|
|
227
|
+
/** improvement.telemetryHalfLifeDays; weights order rows and never decide candidacy. */
|
|
228
|
+
halfLifeDays?: number | undefined;
|
|
229
|
+
/** Epoch milliseconds the recency weights are measured against. */
|
|
230
|
+
now?: number | undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Group routing records by (workflowHash, step, role, promptHash) across runs.
|
|
235
|
+
*
|
|
236
|
+
* Identity: workflowHash is the engine's workflowId when present, then a
|
|
237
|
+
* workflow definition digest, then the run; promptHash is the engine's
|
|
238
|
+
* askSha256 (stable across runs, independent of the objective), then a v1
|
|
239
|
+
* rolePromptSha256. A group is a coded-repeat candidate only when the same
|
|
240
|
+
* objective was decided in at least minRecurrence runs, the accepted rate over
|
|
241
|
+
* decided records reaches minPassRate, and the step writes no repository.
|
|
242
|
+
*/
|
|
144
243
|
export function groupRoutingRecords(
|
|
145
244
|
records: Array<RoutingRecord | RoutingRecordV2>,
|
|
146
|
-
options:
|
|
245
|
+
options: GroupRoutingOptions = {},
|
|
147
246
|
): ImprovementGroupRow[] {
|
|
148
247
|
const minRecurrence = options.minRecurrence ?? 2;
|
|
149
248
|
const minPassRate = options.minPassRate ?? 0.75;
|
|
249
|
+
const halfLifeDays = options.halfLifeDays ?? 14;
|
|
250
|
+
const now = options.now ?? Date.now();
|
|
251
|
+
|
|
252
|
+
const raws = records.map((record) => record as unknown as Record<string, unknown>);
|
|
253
|
+
const runKeys = raws.map((raw, index) => nonEmptyText(raw.runId) ?? nonEmptyText(raw.workflowRunId) ?? `record:${index}`);
|
|
254
|
+
const stepIds = raws.map((raw) => nonEmptyText(raw.stepId) ?? nonEmptyText(raw.stageId) ?? "unknown");
|
|
255
|
+
const retriesOf = (raw: Record<string, unknown>): number =>
|
|
256
|
+
typeof raw.retries === "number" && Number.isFinite(raw.retries) ? raw.retries : 0;
|
|
257
|
+
// A retry of the same step in the same run supersedes every earlier attempt.
|
|
258
|
+
const maxRetries = new Map<string, number>();
|
|
259
|
+
raws.forEach((raw, index) => {
|
|
260
|
+
const attemptKey = `${runKeys[index]}\u0000${stepIds[index]}`;
|
|
261
|
+
maxRetries.set(attemptKey, Math.max(maxRetries.get(attemptKey) ?? 0, retriesOf(raw)));
|
|
262
|
+
});
|
|
150
263
|
|
|
151
|
-
const map = new Map<string,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
const workflowHash = (raw.workflowDefinitionSha256 as string)
|
|
168
|
-
|| (providerMeta.workflowDefinitionSha256 as string)
|
|
169
|
-
|| (raw.workflowRunId as string)
|
|
170
|
-
|| (raw.runId as string)
|
|
171
|
-
|| "standalone";
|
|
172
|
-
const stepId = (raw.stepId as string) || (raw.stageId as string) || "unknown";
|
|
173
|
-
const agentRole = (raw.agentRole as string)?.trim() || "agent";
|
|
174
|
-
const promptHash = (raw.rolePromptSha256 as string)?.trim()
|
|
175
|
-
|| (providerMeta.rolePromptSha256 as string)?.trim()
|
|
176
|
-
|| "none";
|
|
264
|
+
const map = new Map<string, GroupAccumulator>();
|
|
265
|
+
raws.forEach((raw, index) => {
|
|
266
|
+
const providerMeta = raw.providerMetadata && typeof raw.providerMetadata === "object" && !Array.isArray(raw.providerMetadata)
|
|
267
|
+
? raw.providerMetadata as Record<string, unknown>
|
|
268
|
+
: {};
|
|
269
|
+
const workflowId = nonEmptyText(providerMeta.workflowId);
|
|
270
|
+
const workflowHash = workflowId
|
|
271
|
+
?? nonEmptyText(raw.workflowDefinitionSha256)
|
|
272
|
+
?? nonEmptyText(providerMeta.workflowDefinitionSha256)
|
|
273
|
+
?? nonEmptyText(raw.workflowRunId)
|
|
274
|
+
?? nonEmptyText(raw.runId)
|
|
275
|
+
?? "standalone";
|
|
276
|
+
const stepId = stepIds[index]!;
|
|
277
|
+
const agentRole = nonEmptyText(raw.agentRole) ?? "agent";
|
|
278
|
+
const promptHash = nonEmptyText(providerMeta.askSha256) ?? nonEmptyText(raw.rolePromptSha256) ?? "none";
|
|
279
|
+
const runKey = runKeys[index]!;
|
|
177
280
|
|
|
178
281
|
const key = `${workflowHash}:${stepId}:${agentRole}:${promptHash}`;
|
|
179
282
|
let group = map.get(key);
|
|
180
283
|
if (!group) {
|
|
181
284
|
group = {
|
|
285
|
+
key,
|
|
182
286
|
workflowHash,
|
|
183
287
|
stepId,
|
|
184
288
|
agentRole,
|
|
185
289
|
promptHash,
|
|
186
290
|
costs: [],
|
|
187
291
|
latencies: [],
|
|
188
|
-
|
|
292
|
+
total: 0,
|
|
293
|
+
decided: 0,
|
|
294
|
+
passed: 0,
|
|
295
|
+
undecided: 0,
|
|
189
296
|
reworkCount: 0,
|
|
297
|
+
weight: 0,
|
|
298
|
+
undated: 0,
|
|
299
|
+
writes: false,
|
|
300
|
+
decidedRuns: new Set<string>(),
|
|
301
|
+
askRuns: new Map<string, Set<string>>(),
|
|
190
302
|
evidenceRefs: new Set<string>(),
|
|
191
|
-
total: 0,
|
|
192
303
|
};
|
|
193
304
|
map.set(key, group);
|
|
194
305
|
}
|
|
306
|
+
if (group.workflowId === undefined && workflowId !== undefined) group.workflowId = workflowId;
|
|
195
307
|
|
|
196
308
|
group.total += 1;
|
|
197
309
|
const cost = raw.costUsd;
|
|
@@ -205,8 +317,24 @@ export function groupRoutingRecords(
|
|
|
205
317
|
|
|
206
318
|
const verifierOutcome = raw.verifierOutcome;
|
|
207
319
|
const finalOutcome = raw.finalOutcome;
|
|
208
|
-
|
|
209
|
-
|
|
320
|
+
const decided = (verifierOutcome !== undefined && verifierOutcome !== null)
|
|
321
|
+
|| (finalOutcome !== undefined && finalOutcome !== null && finalOutcome !== "pending");
|
|
322
|
+
if (decided) {
|
|
323
|
+
group.decided += 1;
|
|
324
|
+
group.decidedRuns.add(runKey);
|
|
325
|
+
const ask = nonEmptyText(providerMeta.objectiveSha256) ?? "(unkeyed)";
|
|
326
|
+
let askRuns = group.askRuns.get(ask);
|
|
327
|
+
if (!askRuns) {
|
|
328
|
+
askRuns = new Set<string>();
|
|
329
|
+
group.askRuns.set(ask, askRuns);
|
|
330
|
+
}
|
|
331
|
+
askRuns.add(runKey);
|
|
332
|
+
const superseded = (maxRetries.get(`${runKey}\u0000${stepId}`) ?? 0) > retriesOf(raw);
|
|
333
|
+
if (!superseded && (verifierOutcome === "passed" || finalOutcome === "accepted" || finalOutcome === "completed")) {
|
|
334
|
+
group.passed += 1;
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
group.undecided += 1;
|
|
210
338
|
}
|
|
211
339
|
|
|
212
340
|
const retries = raw.retries;
|
|
@@ -214,13 +342,18 @@ export function groupRoutingRecords(
|
|
|
214
342
|
group.reworkCount += retries;
|
|
215
343
|
}
|
|
216
344
|
|
|
345
|
+
const recordedAt = nonEmptyText(raw.recordedAt);
|
|
346
|
+
if (recordedAt === undefined || !Number.isFinite(Date.parse(recordedAt))) group.undated += 1;
|
|
347
|
+
group.weight += computeDecayedWeight(recordedAt ?? "", halfLifeDays, now);
|
|
348
|
+
if (providerMeta.stepWrites === true) group.writes = true;
|
|
349
|
+
|
|
217
350
|
const ref = (raw.attemptId as string) || (raw.runId as string) || (raw.workflowRunId as string) || (raw.behavioralSha256 as string);
|
|
218
351
|
if (ref && group.evidenceRefs.size < 16) {
|
|
219
352
|
group.evidenceRefs.add(ref);
|
|
220
353
|
}
|
|
221
|
-
}
|
|
354
|
+
});
|
|
222
355
|
|
|
223
|
-
const
|
|
356
|
+
const keyed: Array<{ key: string; row: ImprovementGroupRow }> = [];
|
|
224
357
|
for (const group of map.values()) {
|
|
225
358
|
const recurrence = group.total;
|
|
226
359
|
const meanCost = group.costs.length > 0
|
|
@@ -229,56 +362,83 @@ export function groupRoutingRecords(
|
|
|
229
362
|
const meanLatency = group.latencies.length > 0
|
|
230
363
|
? Math.round(group.latencies.reduce((a, b) => a + b, 0) / group.latencies.length)
|
|
231
364
|
: 0;
|
|
232
|
-
const verifyPassRate =
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
const isCandidate =
|
|
365
|
+
const verifyPassRate = group.decided > 0 ? roundThree(group.passed / group.decided) : 0;
|
|
366
|
+
const distinctRuns = group.decidedRuns.size;
|
|
367
|
+
const askRecurrence = Math.max(0, ...[...group.askRuns.values()].map((runs) => runs.size));
|
|
368
|
+
const passes = verifyPassRate >= minPassRate;
|
|
369
|
+
|
|
370
|
+
const isCandidate = askRecurrence >= minRecurrence && passes && !group.writes;
|
|
371
|
+
let excludedReason: ImprovementExcludedReason | undefined;
|
|
372
|
+
if (!isCandidate && passes) {
|
|
373
|
+
if (askRecurrence >= minRecurrence && group.writes) excludedReason = "writes-repository";
|
|
374
|
+
else if (distinctRuns >= minRecurrence && askRecurrence < minRecurrence) excludedReason = "ask-not-repeated";
|
|
375
|
+
}
|
|
238
376
|
const candidateKind = isCandidate ? classifyCandidateKind(group.stepId, group.agentRole) : undefined;
|
|
239
377
|
const safeSlug = group.stepId.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 16) || "step";
|
|
240
378
|
const keyHash = createHash("sha256").update(`${group.workflowHash}:${group.stepId}:${group.agentRole}:${group.promptHash}`).digest("hex").slice(0, 10);
|
|
241
379
|
const candidateId = isCandidate && candidateKind ? `cand_${candidateKind.replace(/-/g, "_")}_${safeSlug}_${keyHash}` : undefined;
|
|
242
380
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
381
|
+
keyed.push({
|
|
382
|
+
key: group.key,
|
|
383
|
+
row: {
|
|
384
|
+
workflowHash: group.workflowHash,
|
|
385
|
+
...(group.workflowId !== undefined ? { workflowId: group.workflowId } : {}),
|
|
386
|
+
stepId: group.stepId,
|
|
387
|
+
agentRole: group.agentRole,
|
|
388
|
+
promptHash: group.promptHash,
|
|
389
|
+
recurrence,
|
|
390
|
+
distinctRuns,
|
|
391
|
+
askRecurrence,
|
|
392
|
+
undecidedRecords: group.undecided,
|
|
393
|
+
meanCost,
|
|
394
|
+
meanLatency,
|
|
395
|
+
verifyPassRate,
|
|
396
|
+
rework: group.reworkCount,
|
|
397
|
+
weightedRecurrence: roundThree(group.weight),
|
|
398
|
+
undatedRecords: group.undated,
|
|
399
|
+
costSamples: group.costs.length,
|
|
400
|
+
writesRepository: group.writes,
|
|
401
|
+
evidenceRefs: [...group.evidenceRefs],
|
|
402
|
+
isCandidate,
|
|
403
|
+
...(excludedReason !== undefined ? { excludedReason } : {}),
|
|
404
|
+
...(candidateKind !== undefined ? { candidateKind } : {}),
|
|
405
|
+
...(candidateId !== undefined ? { candidateId } : {}),
|
|
406
|
+
},
|
|
257
407
|
});
|
|
258
408
|
}
|
|
259
409
|
|
|
260
|
-
return
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
410
|
+
return keyed
|
|
411
|
+
.sort((left, right) => {
|
|
412
|
+
const a = left.row;
|
|
413
|
+
const b = right.row;
|
|
414
|
+
if (a.isCandidate !== b.isCandidate) return a.isCandidate ? -1 : 1;
|
|
415
|
+
if (b.weightedRecurrence !== a.weightedRecurrence) return b.weightedRecurrence - a.weightedRecurrence;
|
|
416
|
+
if (b.recurrence !== a.recurrence) return b.recurrence - a.recurrence;
|
|
417
|
+
return compareCodeUnitIds(a.stepId, b.stepId) || compareCodeUnitIds(left.key, right.key);
|
|
418
|
+
})
|
|
419
|
+
.map((entry) => entry.row);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export interface BuildImprovementReportOptions extends GroupRoutingOptions {
|
|
423
|
+
candidatesDir?: string | undefined;
|
|
424
|
+
projectRoot?: string | undefined;
|
|
425
|
+
dryRun?: boolean | undefined;
|
|
426
|
+
/** improvement.promotionPolicy; reported as readiness only. */
|
|
427
|
+
promotionPolicy?: string | undefined;
|
|
428
|
+
autoThreshold?: PromotionAutoThreshold | undefined;
|
|
265
429
|
}
|
|
266
430
|
|
|
267
431
|
export function buildImprovementReport(
|
|
268
432
|
records: Array<RoutingRecord | RoutingRecordV2>,
|
|
269
|
-
options: {
|
|
270
|
-
minRecurrence?: number;
|
|
271
|
-
minPassRate?: number;
|
|
272
|
-
candidatesDir?: string;
|
|
273
|
-
projectRoot?: string;
|
|
274
|
-
dryRun?: boolean;
|
|
275
|
-
} = {},
|
|
433
|
+
options: BuildImprovementReportOptions = {},
|
|
276
434
|
): ImprovementReport {
|
|
277
435
|
const projectRoot = options.projectRoot ? resolve(options.projectRoot) : process.cwd();
|
|
278
436
|
const candidatesDir = options.candidatesDir ? resolve(options.candidatesDir) : join(projectRoot, ".kxm", "candidates");
|
|
437
|
+
const promotionPolicy = options.promotionPolicy ?? "manual_pr";
|
|
279
438
|
|
|
280
439
|
const groups = groupRoutingRecords(records, options);
|
|
281
440
|
const candidates: ImprovementCandidate[] = [];
|
|
441
|
+
const promotion: ImprovementPromotionEntry[] = [];
|
|
282
442
|
|
|
283
443
|
for (const group of groups) {
|
|
284
444
|
if (!group.isCandidate || !group.candidateKind || !group.candidateId) continue;
|
|
@@ -287,7 +447,7 @@ export function buildImprovementReport(
|
|
|
287
447
|
group.candidateKind,
|
|
288
448
|
group.stepId,
|
|
289
449
|
group.agentRole,
|
|
290
|
-
group.
|
|
450
|
+
group.workflowId,
|
|
291
451
|
);
|
|
292
452
|
|
|
293
453
|
const diffFileName = `${group.candidateId}.diff`;
|
|
@@ -326,6 +486,18 @@ export function buildImprovementReport(
|
|
|
326
486
|
candidates.push(candidate);
|
|
327
487
|
}
|
|
328
488
|
|
|
489
|
+
const rowsByCandidate = new Map(groups.flatMap((row) => row.candidateId !== undefined ? [[row.candidateId, row] as const] : []));
|
|
490
|
+
for (const candidate of candidates) {
|
|
491
|
+
const row = rowsByCandidate.get(candidate.id);
|
|
492
|
+
promotion.push({
|
|
493
|
+
candidateId: candidate.id,
|
|
494
|
+
...evaluatePromotionPolicy(candidate, promotionPolicy, {
|
|
495
|
+
autoThreshold: options.autoThreshold,
|
|
496
|
+
...(row ? { costSamples: row.costSamples, distinctRuns: row.distinctRuns } : {}),
|
|
497
|
+
}),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
329
501
|
return {
|
|
330
502
|
schema: IMPROVEMENT_REPORT_SCHEMA,
|
|
331
503
|
createdAt: nowIso(),
|
|
@@ -333,6 +505,8 @@ export function buildImprovementReport(
|
|
|
333
505
|
recordsCount: records.length,
|
|
334
506
|
groups,
|
|
335
507
|
candidates,
|
|
508
|
+
promotionPolicy,
|
|
509
|
+
promotion,
|
|
336
510
|
};
|
|
337
511
|
}
|
|
338
512
|
|
|
@@ -348,27 +522,33 @@ export function writeImprovementReport(improvementsDir: string, report: Improvem
|
|
|
348
522
|
|
|
349
523
|
export function formatImprovementReport(report: ImprovementReport): string {
|
|
350
524
|
const lines: string[] = [
|
|
351
|
-
`Improvement Report (${report.recordsCount} record(s), ${report.groups.length} group(s), ${report.candidates.length} candidate(s))`,
|
|
525
|
+
`Improvement Report (${report.recordsCount} record(s), ${report.groups.length} group(s), ${report.candidates.length} candidate(s); promotion policy ${report.promotionPolicy})`,
|
|
352
526
|
"",
|
|
353
|
-
"Workflow Step Role Prompt
|
|
354
|
-
"
|
|
527
|
+
"Workflow Step Role Prompt Records Runs Asks Weighted Cost ($) Latency (ms) Accepted Rework Candidate",
|
|
528
|
+
"------------------------------------------------------------------------------------------------------------------------------------------",
|
|
355
529
|
];
|
|
356
530
|
|
|
357
531
|
for (const g of report.groups) {
|
|
358
532
|
const wf = g.workflowHash.slice(0, 12).padEnd(14);
|
|
359
533
|
const step = g.stepId.slice(0, 11).padEnd(12);
|
|
360
534
|
const role = g.agentRole.slice(0, 11).padEnd(12);
|
|
361
|
-
const prompt = g.promptHash.slice(0, 10).padEnd(12);
|
|
362
|
-
const rec = String(g.recurrence).padStart(
|
|
535
|
+
const prompt = g.promptHash.replace(/^sha256:/, "").slice(0, 10).padEnd(12);
|
|
536
|
+
const rec = String(g.recurrence).padStart(7);
|
|
537
|
+
const runs = String(g.distinctRuns).padStart(5);
|
|
538
|
+
const asks = String(g.askRecurrence).padStart(5);
|
|
539
|
+
const weighted = g.weightedRecurrence.toFixed(3).padStart(9);
|
|
363
540
|
const cost = g.meanCost.toFixed(3).padStart(9);
|
|
364
541
|
const lat = String(g.meanLatency).padStart(13);
|
|
365
|
-
const
|
|
542
|
+
const accepted = `${(g.verifyPassRate * 100).toFixed(0)}%`.padStart(9);
|
|
366
543
|
const rework = String(g.rework).padStart(7);
|
|
367
|
-
const cand = g.isCandidate
|
|
368
|
-
|
|
544
|
+
const cand = g.isCandidate
|
|
545
|
+
? `yes (${g.candidateKind})`
|
|
546
|
+
: g.excludedReason !== undefined ? `no (${g.excludedReason})` : "no";
|
|
547
|
+
lines.push(`${wf} ${step} ${role} ${prompt} ${rec} ${runs} ${asks} ${weighted} ${cost} ${lat} ${accepted} ${rework} ${cand}`);
|
|
369
548
|
}
|
|
370
549
|
|
|
371
550
|
if (report.candidates.length > 0) {
|
|
551
|
+
const readiness = new Map(report.promotion.map((entry) => [entry.candidateId, entry]));
|
|
372
552
|
lines.push("");
|
|
373
553
|
lines.push("Emitted Coded-Repeat Candidates:");
|
|
374
554
|
for (const c of report.candidates) {
|
|
@@ -376,80 +556,85 @@ export function formatImprovementReport(report: ImprovementReport): string {
|
|
|
376
556
|
lines.push(` Outcome: ${c.declaredOutcome}`);
|
|
377
557
|
lines.push(` Measure: ${c.measure}`);
|
|
378
558
|
lines.push(` Proposed diff: ${c.proposedDiffPath}`);
|
|
559
|
+
const entry = readiness.get(c.id);
|
|
560
|
+
if (entry) {
|
|
561
|
+
lines.push(` Promotion (${entry.policy}): ${entry.readyForReview ? "ready for review" : "not ready for review"}; ${entry.reason}`);
|
|
562
|
+
}
|
|
379
563
|
}
|
|
564
|
+
lines.push("");
|
|
565
|
+
lines.push("Candidates are proposals: readiness never authorizes, and activation is a reviewed Git change.");
|
|
380
566
|
}
|
|
381
567
|
|
|
382
568
|
return lines.join("\n");
|
|
383
569
|
}
|
|
384
570
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
policy:
|
|
388
|
-
|
|
571
|
+
/** Review readiness of one candidate under improvement.promotionPolicy. */
|
|
572
|
+
export interface PromotionReadiness {
|
|
573
|
+
policy: string;
|
|
574
|
+
readyForReview: boolean;
|
|
389
575
|
reason: string;
|
|
390
576
|
}
|
|
391
577
|
|
|
578
|
+
export interface PromotionAutoThreshold {
|
|
579
|
+
minRuns?: number | undefined;
|
|
580
|
+
minPassRate?: number | undefined;
|
|
581
|
+
minCostSavings?: number | undefined;
|
|
582
|
+
}
|
|
583
|
+
|
|
392
584
|
/**
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* -
|
|
585
|
+
* Report whether a candidate is ready for operator review (Decision Q11).
|
|
586
|
+
*
|
|
587
|
+
* No policy authorizes anything. AGENTS.md:141 forbids auto-promoting skills
|
|
588
|
+
* or gates from telemetry, and ADR-001 (docs/contracts/architecture.md:177-180)
|
|
589
|
+
* rejects automatic learned-policy activation: activation is a reviewed Git
|
|
590
|
+
* change. Every policy therefore ends at an operator PR:
|
|
591
|
+
* - manual_pr: always ready; the operator reviews the proposed diff.
|
|
592
|
+
* - critic_quorum: ready once two distinct critic receipts are cited.
|
|
593
|
+
* - auto_threshold: ready once the candidate's distinct runs and accepted rate
|
|
594
|
+
* reach the thresholds and, when minCostSavings is set, recorded cost samples
|
|
595
|
+
* show at least that mean cost; a group without cost samples is never ready.
|
|
596
|
+
* Any other policy is not ready.
|
|
398
597
|
*/
|
|
399
598
|
export function evaluatePromotionPolicy(
|
|
400
599
|
candidate: ImprovementCandidate,
|
|
401
|
-
policy:
|
|
402
|
-
options
|
|
403
|
-
|
|
404
|
-
autoThreshold?:
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
},
|
|
410
|
-
): PromotionDecision {
|
|
600
|
+
policy: string = "manual_pr",
|
|
601
|
+
options: {
|
|
602
|
+
criticReceipts?: string[] | undefined;
|
|
603
|
+
autoThreshold?: PromotionAutoThreshold | undefined;
|
|
604
|
+
costSamples?: number | undefined;
|
|
605
|
+
distinctRuns?: number | undefined;
|
|
606
|
+
} = {},
|
|
607
|
+
): PromotionReadiness {
|
|
411
608
|
if (policy === "manual_pr") {
|
|
412
|
-
return {
|
|
413
|
-
eligible: true,
|
|
414
|
-
policy,
|
|
415
|
-
authorized: false,
|
|
416
|
-
reason: "Manual PR review and signoff required by policy (fail-closed anti-privilege-escalation)",
|
|
417
|
-
};
|
|
609
|
+
return { policy, readyForReview: true, reason: `operator PR applying ${candidate.proposedDiffPath} required` };
|
|
418
610
|
}
|
|
419
611
|
|
|
420
612
|
if (policy === "critic_quorum") {
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
policy,
|
|
426
|
-
authorized: hasQuorum,
|
|
427
|
-
reason: hasQuorum
|
|
428
|
-
? `Authorized by critic quorum (${approvals.join(", ")})`
|
|
429
|
-
: `Requires dual critic quorum; current approvals: ${approvals.length}/2`,
|
|
430
|
-
};
|
|
613
|
+
const receipts = new Set((options.criticReceipts ?? []).map((receipt) => receipt.trim()).filter((receipt) => receipt.length > 0));
|
|
614
|
+
return receipts.size >= 2
|
|
615
|
+
? { policy, readyForReview: true, reason: "two critic receipts cited; operator PR still required" }
|
|
616
|
+
: { policy, readyForReview: false, reason: `awaiting 2 distinct critic receipts (have ${receipts.size})` };
|
|
431
617
|
}
|
|
432
618
|
|
|
433
619
|
if (policy === "auto_threshold") {
|
|
434
|
-
const
|
|
435
|
-
const
|
|
620
|
+
const minRuns = options.autoThreshold?.minRuns ?? 10;
|
|
621
|
+
const minPassRate = options.autoThreshold?.minPassRate ?? 0.95;
|
|
622
|
+
const minCostSavings = options.autoThreshold?.minCostSavings;
|
|
623
|
+
const runs = options.distinctRuns ?? candidate.baselineMetrics.recurrence;
|
|
436
624
|
const passRate = candidate.baselineMetrics.verifyPassRate;
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
625
|
+
const costSamples = options.costSamples ?? 0;
|
|
626
|
+
const meanCost = candidate.baselineMetrics.meanCost;
|
|
627
|
+
const costMet = minCostSavings === undefined || (costSamples > 0 && meanCost >= minCostSavings);
|
|
628
|
+
const ready = runs >= minRuns && passRate >= minPassRate && costMet;
|
|
629
|
+
const detail = [
|
|
630
|
+
`runs=${runs}/${minRuns}`,
|
|
631
|
+
`passRate=${passRate}/${minPassRate}`,
|
|
632
|
+
...(minCostSavings === undefined ? [] : [`meanCost=${meanCost}/${minCostSavings} over ${costSamples} cost sample(s)`]),
|
|
633
|
+
].join(", ");
|
|
634
|
+
return ready
|
|
635
|
+
? { policy, readyForReview: true, reason: `auto-threshold met (${detail}); operator PR still required` }
|
|
636
|
+
: { policy, readyForReview: false, reason: `auto-threshold not met (${detail})` };
|
|
447
637
|
}
|
|
448
638
|
|
|
449
|
-
return {
|
|
450
|
-
eligible: false,
|
|
451
|
-
policy,
|
|
452
|
-
authorized: false,
|
|
453
|
-
reason: `Unknown promotion policy: ${String(policy)}`,
|
|
454
|
-
};
|
|
639
|
+
return { policy, readyForReview: false, reason: "unknown promotion policy" };
|
|
455
640
|
}
|