akm-cli 0.9.0-beta.31 → 0.9.0-beta.32
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/CHANGELOG.md +26 -0
- package/dist/commands/improve/extract.js +4 -1
- package/dist/commands/improve/improve.js +21 -2
- package/dist/commands/improve/proactive-maintenance.js +28 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +15 -3
- package/dist/commands/improve/triage.js +17 -19
- package/dist/commands/proposal/drain-policies.js +5 -0
- package/dist/commands/proposal/drain.js +10 -1
- package/dist/core/config/config-schema.js +10 -0
- package/dist/scripts/migrate-storage.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.0-beta.32] — 2026-06-21
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Recombine acceptance path — confirmed lessons now auto-accept.** Recombine
|
|
14
|
+
hypotheses that reach the confirmation threshold (promoted to `type: lesson`,
|
|
15
|
+
#625/#633) now flow to ACCEPTED by reusing the existing drain mechanism instead
|
|
16
|
+
of piling up pending forever: the `personal-stash` drain policy gains a
|
|
17
|
+
`{ generator: "recombine", requireType: "lesson", maxDiffLines: 200 }` rule, via
|
|
18
|
+
a new optional `requireType` frontmatter filter on `DrainAcceptRule`. Only
|
|
19
|
+
confirmed `type: lesson` proposals auto-accept; unconfirmed `type: hypothesis`
|
|
20
|
+
proposals stay pending; the existing proposal quality gate still applies.
|
|
21
|
+
- **`processes.reflect.lowValueFilter` (opt-in, default OFF)** — deterministic
|
|
22
|
+
semantic value-floor that defers trivial reflect rewrites (#639A).
|
|
23
|
+
- **`processes.extract.triage.proceduralAwareFloor` (opt-in, default OFF)** —
|
|
24
|
+
triage floor requiring markers/edits so real lessons always pass (#641).
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Select-time proactive cooldown leak.** `selectProactiveMaintenanceRefs` plans
|
|
29
|
+
the due set BEFORE acquiring `reflect-distill.lock`, so overlapping/back-to-back
|
|
30
|
+
improve runs reused stale due-state and re-reflected the same asset repeatedly
|
|
31
|
+
(observed up to ~16× in a day). The orchestrator now re-applies the dueDays gate
|
|
32
|
+
with freshly-read timestamp maps INSIDE the lock (`filterProactiveDue`), dropping
|
|
33
|
+
refs a concurrent run already reflected.
|
|
34
|
+
|
|
9
35
|
## [0.9.0-beta.31] — 2026-06-20
|
|
10
36
|
|
|
11
37
|
### Changed
|
|
@@ -173,6 +173,7 @@ export function hashSessionContent(data) {
|
|
|
173
173
|
async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars,
|
|
174
174
|
// #626 — pre-LLM heuristic triage gate. Default-off (enabled:false) takes the
|
|
175
175
|
// exact pre-change path (no scorer call, no new skipReason).
|
|
176
|
+
// #641 — proceduralAwareFloor is opt-in, DEFAULT OFF.
|
|
176
177
|
triage, sessionIndexing, schemaSimilarityCtx,
|
|
177
178
|
// #602 — already-extracted skip moved INSIDE processSession: the content hash
|
|
178
179
|
// can only be computed after readSession, so the skip decision lives here. The
|
|
@@ -251,7 +252,9 @@ prior, force, singleSession) {
|
|
|
251
252
|
// the configured threshold we triage it out: no chat() call, no session asset,
|
|
252
253
|
// no proposals. Pure-heuristic — zero added LLM cost. Default-off → skipped.
|
|
253
254
|
if (triage.enabled) {
|
|
254
|
-
const t = scoreSessionTriage(data, triage.minScore
|
|
255
|
+
const t = scoreSessionTriage(data, triage.minScore, {
|
|
256
|
+
proceduralAwareFloor: triage.proceduralAwareFloor,
|
|
257
|
+
});
|
|
255
258
|
if (!t.pass) {
|
|
256
259
|
return {
|
|
257
260
|
sessionId: sessionRef.sessionId,
|
|
@@ -50,7 +50,7 @@ import { isProfileFilteredForAllPasses, resolveImproveProfile, resolveProcessEna
|
|
|
50
50
|
import { detectAndWriteContradictions } from "./memory/memory-contradiction-detect.js";
|
|
51
51
|
import { analyzeMemoryCleanup, applyMemoryCleanup } from "./memory/memory-improve.js";
|
|
52
52
|
import { computeProxyAdequacy, getAllAssetOutcomes, getOutcomeScoresByRef, outcomeScoreToSalience, updateAssetOutcome, } from "./outcome-loop.js";
|
|
53
|
-
import { DEFAULT_DUE_DAYS, DEFAULT_MAX_PER_RUN, selectProactiveMaintenanceRefs } from "./proactive-maintenance.js";
|
|
53
|
+
import { DEFAULT_DUE_DAYS, DEFAULT_MAX_PER_RUN, filterProactiveDue, selectProactiveMaintenanceRefs, } from "./proactive-maintenance.js";
|
|
54
54
|
import { akmProcedural } from "./procedural.js";
|
|
55
55
|
import { akmRecombine } from "./recombine.js";
|
|
56
56
|
import { akmReflect } from "./reflect.js";
|
|
@@ -1105,13 +1105,32 @@ export async function akmImprove(options = {}) {
|
|
|
1105
1105
|
// both write proposals to state.db). Released immediately after.
|
|
1106
1106
|
const reflectDistillLPath = processLockPath(lockBaseDir, "reflectDistill");
|
|
1107
1107
|
const reflectDistillAcquired = tryAcquireProcessLock(reflectDistillLPath, PROCESS_LOCK_DEFS.reflectDistill.staleAfterMs, options.skipIfLocked, "reflect-distill") === "acquired";
|
|
1108
|
+
// Post-lock cooldown re-filter for proactive refs (#SELECT-TIME-LEAK).
|
|
1109
|
+
// Planning built `lastReflectProposalTs` BEFORE acquiring this lock, so a
|
|
1110
|
+
// concurrent run's `reflect_invoked` writes are invisible to it. Now that
|
|
1111
|
+
// we hold the lock, re-read fresh timestamp maps for the proactive subset
|
|
1112
|
+
// and drop any ref whose cooldown has been consumed by the concurrent run.
|
|
1113
|
+
const proactiveLoopRefs = preparation.loopRefs.filter((r) => r.eligibilitySource === "proactive");
|
|
1114
|
+
let postLockLoopRefs = preparation.loopRefs;
|
|
1115
|
+
if (proactiveLoopRefs.length > 0) {
|
|
1116
|
+
const proactiveRefStrs = proactiveLoopRefs.map((r) => r.ref);
|
|
1117
|
+
const freshReflectTs = buildLatestProposalTsMap(proactiveRefStrs, "reflect");
|
|
1118
|
+
const freshDistillTs = buildLatestProposalTsMap(proactiveRefStrs, "distill");
|
|
1119
|
+
const pmDueDays = improveProfile.processes?.proactiveMaintenance?.dueDays ?? DEFAULT_DUE_DAYS;
|
|
1120
|
+
const stillDue = new Set(filterProactiveDue(proactiveLoopRefs, freshReflectTs, freshDistillTs, pmDueDays, Date.now()).map((r) => r.ref));
|
|
1121
|
+
const dropped = proactiveLoopRefs.filter((r) => !stillDue.has(r.ref));
|
|
1122
|
+
if (dropped.length > 0) {
|
|
1123
|
+
info(`[improve] post-lock cooldown re-filter: dropped ${dropped.length} proactive ref(s) claimed by concurrent run (${dropped.map((r) => r.ref).join(", ")})`);
|
|
1124
|
+
postLockLoopRefs = preparation.loopRefs.filter((r) => r.eligibilitySource !== "proactive" || stillDue.has(r.ref));
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1108
1127
|
const loopResult = await runImproveLoopStageImpl({
|
|
1109
1128
|
scope,
|
|
1110
1129
|
options,
|
|
1111
1130
|
primaryStashDir,
|
|
1112
1131
|
reflectFn,
|
|
1113
1132
|
distillFn,
|
|
1114
|
-
loopRefs:
|
|
1133
|
+
loopRefs: postLockLoopRefs,
|
|
1115
1134
|
actions: preparation.actions,
|
|
1116
1135
|
signalBearingSet: preparation.signalBearingSet,
|
|
1117
1136
|
distillCooledRefs: preparation.distillCooledRefs,
|
|
@@ -85,3 +85,31 @@ export function selectProactiveMaintenanceRefs(params) {
|
|
|
85
85
|
const selected = ranked.slice(0, Math.max(0, maxPerRun)).map((s) => s.ref);
|
|
86
86
|
return { selected, dueTotal, neverReflected, scored };
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Post-lock re-filter for proactive refs.
|
|
90
|
+
*
|
|
91
|
+
* Called INSIDE the reflect-distill.lock window with freshly-read timestamp
|
|
92
|
+
* maps so that any `reflect_invoked` events committed by a concurrent run
|
|
93
|
+
* while this run was waiting for the lock are visible. Refs that became
|
|
94
|
+
* non-due (staleDays ≤ dueDays) since planning time are dropped before
|
|
95
|
+
* execution, closing the SELECT-time cooldown race.
|
|
96
|
+
*
|
|
97
|
+
* The logic mirrors the DUE gate in `selectProactiveMaintenanceRefs` — an
|
|
98
|
+
* asset is due only when never touched OR last touched more than `dueDays`
|
|
99
|
+
* ago. The selector and this filter must always agree on the gate predicate.
|
|
100
|
+
*/
|
|
101
|
+
export function filterProactiveDue(selected, lastReflectTs, lastDistillTs, dueDays, now) {
|
|
102
|
+
return selected.filter((candidate) => {
|
|
103
|
+
const ref = candidate.ref;
|
|
104
|
+
const reflectIso = lastReflectTs.get(ref);
|
|
105
|
+
const distillIso = lastDistillTs.get(ref);
|
|
106
|
+
let lastTouchMs = 0;
|
|
107
|
+
if (reflectIso)
|
|
108
|
+
lastTouchMs = Math.max(lastTouchMs, Date.parse(reflectIso) || 0);
|
|
109
|
+
if (distillIso)
|
|
110
|
+
lastTouchMs = Math.max(lastTouchMs, Date.parse(distillIso) || 0);
|
|
111
|
+
const neverReflected = lastTouchMs === 0;
|
|
112
|
+
const staleDays = neverReflected ? Number.POSITIVE_INFINITY : (now - lastTouchMs) / DAY_MS;
|
|
113
|
+
return neverReflected || staleDays > dueDays;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
Binary file
|
|
@@ -1170,8 +1170,18 @@ export async function akmReflect(options = {}) {
|
|
|
1170
1170
|
// (new-asset proposals have nothing to diff against).
|
|
1171
1171
|
if (assetContent !== undefined) {
|
|
1172
1172
|
const changeKind = classifyReflectChange(assetContent, payload.content);
|
|
1173
|
-
|
|
1174
|
-
|
|
1173
|
+
// 'low-value' is config-gated (#639): only defer when
|
|
1174
|
+
// profiles.improve.default.processes.reflect.lowValueFilter.enabled is
|
|
1175
|
+
// explicitly true. DEFAULT OFF — absent flag = byte-identical pre-#639
|
|
1176
|
+
// behaviour (low-value treated the same as substantive).
|
|
1177
|
+
const lowValueFilterEnabled = runtimeConfig?.profiles?.improve?.default?.processes?.reflect?.lowValueFilter?.enabled === true;
|
|
1178
|
+
const isDeferred = changeKind === "noop" || changeKind === "cosmetic" || (changeKind === "low-value" && lowValueFilterEnabled);
|
|
1179
|
+
if (isDeferred) {
|
|
1180
|
+
const subreason = changeKind === "noop"
|
|
1181
|
+
? "reflect_skipped_noop"
|
|
1182
|
+
: changeKind === "low-value"
|
|
1183
|
+
? "reflect_skipped_low_value"
|
|
1184
|
+
: "reflect_skipped_cosmetic";
|
|
1175
1185
|
emitReflectFailed("no_change", subreason, options.ref, { changeKind });
|
|
1176
1186
|
return {
|
|
1177
1187
|
schemaVersion: 1,
|
|
@@ -1179,7 +1189,9 @@ export async function akmReflect(options = {}) {
|
|
|
1179
1189
|
reason: "no_change",
|
|
1180
1190
|
error: changeKind === "noop"
|
|
1181
1191
|
? `Reflect skipped: proposed content for ${payload.ref} is identical to the current asset (empty diff); no proposal created.`
|
|
1182
|
-
:
|
|
1192
|
+
: changeKind === "low-value"
|
|
1193
|
+
? `Reflect skipped: proposed content for ${payload.ref} is a low-value prose micro-rewrite (few changed tokens, no structural changes); no proposal created.`
|
|
1194
|
+
: `Reflect skipped: proposed content for ${payload.ref} is a cosmetic-only reformat of the current asset (whitespace/fence/YAML-folding changes); no proposal created.`,
|
|
1183
1195
|
...(options.ref ? { ref: options.ref } : {}),
|
|
1184
1196
|
exitCode: result.exitCode,
|
|
1185
1197
|
};
|
|
@@ -14,23 +14,7 @@ const MARKER_RE = /\b(error|failed|fix(?:ed)?|root cause|turns out|because|decid
|
|
|
14
14
|
const EDIT_COMMIT_RE = /\b(Edit|Write|MultiEdit|git commit|diff)\b/i;
|
|
15
15
|
// A substantive turn is an assistant or tool turn whose text is non-trivial.
|
|
16
16
|
const SUBSTANTIVE_MIN_CHARS = 40;
|
|
17
|
-
|
|
18
|
-
* Pure heuristic scorer. Operates only on `data.events`. Each sub-signal is
|
|
19
|
-
* normalized to a small bounded contribution; `score` is their sum and
|
|
20
|
-
* `pass = score >= minScore`.
|
|
21
|
-
*
|
|
22
|
-
* Sub-signals:
|
|
23
|
-
* - markers: presence of decision/outcome/error keywords across event text
|
|
24
|
-
* (capped, narrative-lesson signal).
|
|
25
|
-
* - toolDensity: bounded contribution from tool-use events (procedural).
|
|
26
|
-
* - editCommit: bounded contribution from edit/write/commit events (procedural).
|
|
27
|
-
* - substantiveRatio: scaled fraction of non-trivial assistant+tool turns,
|
|
28
|
-
* filtering pure short Q&A.
|
|
29
|
-
*
|
|
30
|
-
* The procedural sub-signals (toolDensity + editCommit) alone can clear
|
|
31
|
-
* DEFAULT_TRIAGE_MIN_SCORE so high-action / no-narrative sessions are KEPT (#615).
|
|
32
|
-
*/
|
|
33
|
-
export function scoreSessionTriage(data, minScore) {
|
|
17
|
+
export function scoreSessionTriage(data, minScore, options) {
|
|
34
18
|
const events = data.events;
|
|
35
19
|
const total = events.length;
|
|
36
20
|
// (a) markers — count word-bounded marker hits across all event text, capped.
|
|
@@ -72,7 +56,19 @@ export function scoreSessionTriage(data, minScore) {
|
|
|
72
56
|
const substantiveRatio = Math.min(ratio, 1);
|
|
73
57
|
const subscores = { markers, toolDensity, editCommit, substantiveRatio };
|
|
74
58
|
const score = markers + toolDensity + editCommit + substantiveRatio;
|
|
75
|
-
|
|
59
|
+
// Base gate: score must clear the floor.
|
|
60
|
+
let pass = score >= minScore;
|
|
61
|
+
// #641 procedural-aware floor (opt-in, DEFAULT OFF).
|
|
62
|
+
// When enabled, a session that clears the score gate must ALSO have at least
|
|
63
|
+
// one narrative marker (markers >= 1) OR meaningful edit/commit signal
|
|
64
|
+
// (editCommit >= 0.5). Pure read-only Q&A sessions that pass only via
|
|
65
|
+
// toolDensity + substantiveRatio are rejected as low-signal.
|
|
66
|
+
if (pass && options?.proceduralAwareFloor === true) {
|
|
67
|
+
const hasProceduralSignal = markers >= 1 || editCommit >= 0.5;
|
|
68
|
+
if (!hasProceduralSignal) {
|
|
69
|
+
pass = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
76
72
|
return {
|
|
77
73
|
pass,
|
|
78
74
|
score,
|
|
@@ -91,5 +87,7 @@ export function resolveTriageConfig(extractProcess) {
|
|
|
91
87
|
const triage = extractProcess?.triage;
|
|
92
88
|
const enabled = triage?.enabled === true;
|
|
93
89
|
const minScore = typeof triage?.minScore === "number" ? triage.minScore : DEFAULT_TRIAGE_MIN_SCORE;
|
|
94
|
-
|
|
90
|
+
// #641: default-off — only true when explicitly set to true.
|
|
91
|
+
const proceduralAwareFloor = triage?.proceduralAwareFloor === true;
|
|
92
|
+
return { enabled, minScore, proceduralAwareFloor };
|
|
95
93
|
}
|
|
@@ -45,6 +45,10 @@ export const PERSONAL_STASH = {
|
|
|
45
45
|
{ generator: "reflect", maxDiffLines: 80 },
|
|
46
46
|
// Consolidate within the diff band; mid-band lands in `defer` below.
|
|
47
47
|
{ generator: "consolidate", maxDiffLines: 200 },
|
|
48
|
+
// Recombine: accept only confirmed type:lesson proposals (promoted by the
|
|
49
|
+
// recombine confidence gate) within the diff band. type:hypothesis proposals
|
|
50
|
+
// have no matching rule here and stay pending for manual review.
|
|
51
|
+
{ generator: "recombine", requireType: "lesson", maxDiffLines: 200 },
|
|
48
52
|
],
|
|
49
53
|
rejectEmpty: true,
|
|
50
54
|
// Mid-band consolidate, distill duplicates, and contradiction escalations are
|
|
@@ -83,6 +87,7 @@ const DrainAcceptRuleSchema = z
|
|
|
83
87
|
generator: GeneratorSchema,
|
|
84
88
|
maxDiffLines: z.number().int().positive().optional(),
|
|
85
89
|
minContentLines: z.number().int().nonnegative().optional(),
|
|
90
|
+
requireType: z.string().optional(),
|
|
86
91
|
})
|
|
87
92
|
.strict();
|
|
88
93
|
const DrainPolicySchema = z
|
|
@@ -80,7 +80,16 @@ export function classifyProposal(proposal, policy, maxDiffLines) {
|
|
|
80
80
|
if (policy.rejectEmpty && isEmptyDiff(proposal)) {
|
|
81
81
|
return { verdict: "reject", reason: "empty diff", gate: { reason: "empty-diff" } };
|
|
82
82
|
}
|
|
83
|
-
const rule = policy.accept.find((r) =>
|
|
83
|
+
const rule = policy.accept.find((r) => {
|
|
84
|
+
if (r.generator !== proposal.source)
|
|
85
|
+
return false;
|
|
86
|
+
if (r.requireType !== undefined) {
|
|
87
|
+
const fm = parseFrontmatter(proposal.payload.content ?? "").data;
|
|
88
|
+
if (typeof fm.type !== "string" || fm.type !== r.requireType)
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
84
93
|
if (rule) {
|
|
85
94
|
const lines = contentLineCount(content);
|
|
86
95
|
const body = contentBodyLineCount(content);
|
|
@@ -329,6 +329,16 @@ export const ImproveProcessConfigSchema = z
|
|
|
329
329
|
// #615 — procedural process: asset type a compiled sequence is emitted as.
|
|
330
330
|
// Reserved; v1 always emits "workflow". Only meaningful on `procedural`.
|
|
331
331
|
emitAs: z.enum(["workflow", "skill"]).optional(),
|
|
332
|
+
// #639 — semantic value-floor filter for the `reflect` process. When
|
|
333
|
+
// enabled, proposals classified as "low-value" by the deterministic noise
|
|
334
|
+
// gate are deferred. DEFAULT OFF (absent / { enabled: false } = pre-#639
|
|
335
|
+
// byte-identical behaviour). Only meaningful on the `reflect` process.
|
|
336
|
+
lowValueFilter: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
337
|
+
// #641 — procedural-aware floor for the `extract` process triage gate.
|
|
338
|
+
// When true, a session must have markers>=1 OR editCommit>=0.5 to pass, even
|
|
339
|
+
// if score>=minScore. DEFAULT OFF (absent/false = pre-#641 byte-identical).
|
|
340
|
+
// Only meaningful on the `extract` process when triage is also enabled.
|
|
341
|
+
proceduralAwareFloor: z.boolean().optional(),
|
|
332
342
|
// Triage process config (only meaningful for the `triage` process)
|
|
333
343
|
applyMode: z.enum(["queue", "promote"]).optional(),
|
|
334
344
|
policy: z.string().min(1).optional(),
|
|
@@ -15920,6 +15920,8 @@ var init_config_schema = __esm(() => {
|
|
|
15920
15920
|
minRecurrence: exports_external.number().int().min(2).optional(),
|
|
15921
15921
|
maxProposalsPerRun: positiveInt.optional(),
|
|
15922
15922
|
emitAs: exports_external.enum(["workflow", "skill"]).optional(),
|
|
15923
|
+
lowValueFilter: exports_external.object({ enabled: exports_external.boolean().optional() }).strict().optional(),
|
|
15924
|
+
proceduralAwareFloor: exports_external.boolean().optional(),
|
|
15923
15925
|
applyMode: exports_external.enum(["queue", "promote"]).optional(),
|
|
15924
15926
|
policy: exports_external.string().min(1).optional(),
|
|
15925
15927
|
maxAcceptsPerRun: positiveInt.optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|