akm-cli 0.9.0-rc.4 → 0.9.0-rc.5
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 +6 -0
- package/dist/assets/help/help-improve.md +3 -0
- package/dist/commands/improve/distill/promote-memory.js +6 -2
- package/dist/commands/improve/distill/quality-gate.js +94 -31
- package/dist/commands/improve/distill.js +7 -2
- package/dist/commands/improve/extract.js +5 -2
- package/dist/commands/improve/improve-auto-accept.js +2 -2
- package/dist/commands/improve/improve.js +158 -176
- package/dist/commands/improve/locks.js +22 -94
- package/dist/commands/improve/loop-stages.js +15 -10
- package/dist/commands/improve/preparation.js +21 -17
- package/dist/commands/improve/proactive-maintenance.js +4 -6
- package/dist/commands/improve/reflect.js +40 -67
- package/dist/core/extra-params.js +1 -0
- package/dist/core/maintenance-barrier.js +16 -0
- package/dist/core/write-source.js +1 -0
- package/dist/indexer/ensure-index.js +6 -4
- package/dist/llm/client.js +7 -5
- package/dist/schemas/akm-task.json +2 -2
- package/dist/schemas/akm-workflow.json +2 -2
- package/dist/scripts/migrate-storage.js +2 -1
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +2 -1
- package/package.json +1 -1
- package/schemas/akm-task.json +2 -2
- package/schemas/akm-workflow.json +2 -2
- package/docs/README.md +0 -103
package/CHANGELOG.md
CHANGED
|
@@ -350,6 +350,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
350
350
|
|
|
351
351
|
### Fixed
|
|
352
352
|
|
|
353
|
+
- **Improve RC stabilization.** Restored one ownership-safe whole-run lock from
|
|
354
|
+
triage through final sync; `--skip-if-locked` is a true no-op; the run deadline
|
|
355
|
+
now starts before indexing and reaches index waits, generation, reindexing, and
|
|
356
|
+
quality judges; reflect judges the sanitized final candidate with bounded
|
|
357
|
+
changed-region context; write-target selectors no longer replace durable source
|
|
358
|
+
identity; and vLLM thinking controls cannot be overridden through `extraParams`.
|
|
353
359
|
- **Proposal promotion, reversion, and rejection are durable and recoverable.**
|
|
354
360
|
Acceptance and reversion persist target ownership and content fingerprints,
|
|
355
361
|
publish atomically across filesystem layouts, index immediately, commit exact
|
|
@@ -67,6 +67,9 @@ Options:
|
|
|
67
67
|
Only process refs with recent feedback signal events
|
|
68
68
|
--min-retrieval-count <n>
|
|
69
69
|
Retrieval fallback threshold when no recent feedback exists (default: 5)
|
|
70
|
+
--timeout-ms <n> Wall-clock budget for the entire live run (default: 7200000)
|
|
71
|
+
--skip-if-locked Exit 0 without doing work when another improve run owns
|
|
72
|
+
the whole-run lock; no triage, indexing, events, or sync
|
|
70
73
|
--json-to-stdout Emit the full JSON result on stdout (legacy behaviour).
|
|
71
74
|
(0.8.0+: full result is recorded in the improve_runs table of
|
|
72
75
|
state.db and stdout is empty; use --json-to-stdout for the prior
|
|
@@ -83,7 +83,7 @@ export async function promoteMemoryToKnowledge(ctx) {
|
|
|
83
83
|
const mergeResponse = await chat(ctx.llmConfig, [
|
|
84
84
|
{ role: "system", content: "Return only valid JSON. No prose." },
|
|
85
85
|
{ role: "user", content: mergePrompt },
|
|
86
|
-
]);
|
|
86
|
+
], ctx.signal ? { signal: ctx.signal } : undefined);
|
|
87
87
|
const mergeResult = parseEmbeddedJsonResponse(mergeResponse);
|
|
88
88
|
if (mergeResult?.action === "NOOP") {
|
|
89
89
|
// Existing content is authoritative — no update needed.
|
|
@@ -139,7 +139,11 @@ export async function promoteMemoryToKnowledge(ctx) {
|
|
|
139
139
|
if (ctx.strategy?.processes?.distill?.qualityGate?.enabled ?? true) {
|
|
140
140
|
// D-4 / #390: retrieve top-3 similar lessons for dedup check in judge.
|
|
141
141
|
const similarLessons = await fetchSimilarLessonsFn(resolvedPromotionContent.slice(0, 500), 3);
|
|
142
|
-
const judgeResult = await runLessonQualityJudge(config, resolvedPromotionContent, assetContent ?? "", chat,
|
|
142
|
+
const judgeResult = await runLessonQualityJudge(config, resolvedPromotionContent, assetContent ?? "", chat, {
|
|
143
|
+
...(similarLessons.length > 0 ? { similarLessons } : {}),
|
|
144
|
+
...(ctx.llmConfig ? { llmConfig: ctx.llmConfig } : {}),
|
|
145
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
146
|
+
});
|
|
143
147
|
if (!judgeResult.pass) {
|
|
144
148
|
if (judgeResult.reviewNeeded) {
|
|
145
149
|
// Uncertainty band (2.5–3.5): queue as review_needed instead of rejecting.
|
|
@@ -98,37 +98,84 @@ export function buildJudgePrompt(lessonContent, sourceContent, similarLessons) {
|
|
|
98
98
|
lines.push('Return ONLY valid JSON, no prose: {"score": <average score 1-5 as float>, "reason": "<one sentence>"}');
|
|
99
99
|
return lines.join("\n");
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
101
|
+
function boundedDocument(content, maxChars = 6000) {
|
|
102
|
+
if (content.length <= maxChars)
|
|
103
|
+
return content;
|
|
104
|
+
const half = Math.floor((maxChars - 80) / 2);
|
|
105
|
+
return `${content.slice(0, half)}\n\n[... middle omitted for bounded judge context ...]\n\n${content.slice(-half)}`;
|
|
106
|
+
}
|
|
107
|
+
function buildChangedRegion(sourceContent, candidateContent) {
|
|
108
|
+
const source = sourceContent.split("\n");
|
|
109
|
+
const candidate = candidateContent.split("\n");
|
|
110
|
+
let prefix = 0;
|
|
111
|
+
while (prefix < source.length && prefix < candidate.length && source[prefix] === candidate[prefix])
|
|
112
|
+
prefix++;
|
|
113
|
+
let suffix = 0;
|
|
114
|
+
while (suffix < source.length - prefix &&
|
|
115
|
+
suffix < candidate.length - prefix &&
|
|
116
|
+
source[source.length - 1 - suffix] === candidate[candidate.length - 1 - suffix]) {
|
|
117
|
+
suffix++;
|
|
118
|
+
}
|
|
119
|
+
const removed = source.slice(prefix, source.length - suffix).join("\n");
|
|
120
|
+
const added = candidate.slice(prefix, candidate.length - suffix).join("\n");
|
|
121
|
+
return boundedDocument(`Removed or replaced:\n${removed || "(none)"}\n\nAdded or replacement:\n${added || "(none)"}`);
|
|
122
|
+
}
|
|
123
|
+
/** Build quality criteria for revising an existing asset in place. */
|
|
124
|
+
export function buildReflectJudgePrompt(candidateContent, sourceContent, feedback) {
|
|
125
|
+
return [
|
|
126
|
+
"You are evaluating a proposed revision to an existing akm asset.",
|
|
127
|
+
"",
|
|
128
|
+
"Score this revision on each criterion from 1 (poor) to 5 (excellent):",
|
|
129
|
+
"1. FEEDBACK ALIGNMENT: Does the revision address the supplied feedback or improve retrieval and clarity?",
|
|
130
|
+
"2. PRESERVATION: Does it retain the source's concrete facts, code, commands, examples, and structure without truncation?",
|
|
131
|
+
"3. QUALITY: Is the revision coherent, actionable, complete, and free of unsupported claims?",
|
|
132
|
+
"",
|
|
133
|
+
"Overlap with the source is expected and must not lower the score by itself; this is an in-place revision, not a new lesson.",
|
|
134
|
+
"",
|
|
135
|
+
"Feedback:",
|
|
136
|
+
"```",
|
|
137
|
+
(feedback.length > 0 ? feedback.join("\n") : "No explicit feedback supplied.").slice(0, 1000),
|
|
138
|
+
"```",
|
|
139
|
+
"",
|
|
140
|
+
"Source asset content:",
|
|
141
|
+
"```",
|
|
142
|
+
boundedDocument(sourceContent),
|
|
143
|
+
"```",
|
|
144
|
+
"",
|
|
145
|
+
"Proposed revision:",
|
|
146
|
+
"```",
|
|
147
|
+
boundedDocument(candidateContent),
|
|
148
|
+
"```",
|
|
149
|
+
"",
|
|
150
|
+
"Changed region:",
|
|
151
|
+
"```",
|
|
152
|
+
buildChangedRegion(sourceContent, candidateContent),
|
|
153
|
+
"```",
|
|
154
|
+
"",
|
|
155
|
+
'Return ONLY valid JSON, no prose: {"score": <average score 1-5 as float>, "reason": "<one sentence>"}',
|
|
156
|
+
].join("\n");
|
|
157
|
+
}
|
|
158
|
+
async function runQualityJudge(config, prompt, chat, options = {}) {
|
|
159
|
+
const llmConfig = options.llmConfig ?? getDefaultLlmConfig(config);
|
|
117
160
|
if (!llmConfig) {
|
|
118
161
|
return { pass: false, score: -1, reason: "no LLM configured — cannot judge, failing closed" };
|
|
119
162
|
}
|
|
120
|
-
const judgeLlmConfig = llmConfig;
|
|
121
|
-
const JUDGE_TIMEOUT_MS = 8_000;
|
|
122
163
|
try {
|
|
123
|
-
const raw = await
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
164
|
+
const raw = await chat(llmConfig, [
|
|
165
|
+
{ role: "system", content: "Return only valid JSON. No prose." },
|
|
166
|
+
{ role: "user", content: prompt },
|
|
167
|
+
], {
|
|
168
|
+
enableThinking: false,
|
|
169
|
+
...(Object.hasOwn(options, "timeoutMs") ? { timeoutMs: options.timeoutMs } : {}),
|
|
170
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
171
|
+
});
|
|
130
172
|
const parsed = parseEmbeddedJsonResponse(raw);
|
|
131
|
-
if (!parsed ||
|
|
173
|
+
if (!parsed ||
|
|
174
|
+
typeof parsed.score !== "number" ||
|
|
175
|
+
!Number.isFinite(parsed.score) ||
|
|
176
|
+
parsed.score < 1 ||
|
|
177
|
+
parsed.score > 5 ||
|
|
178
|
+
typeof parsed.reason !== "string") {
|
|
132
179
|
return { pass: false, score: -1, reason: "judge parse failed — cannot judge, failing closed" };
|
|
133
180
|
}
|
|
134
181
|
// D-5 / #388: Three-band system (MT-Bench arXiv:2306.05685 — ~±0.5 judge variance).
|
|
@@ -137,19 +184,35 @@ similarLessons, llmConfigOverride) {
|
|
|
137
184
|
// < 2.5: auto-reject (pass: false)
|
|
138
185
|
const score = parsed.score;
|
|
139
186
|
const reason = parsed.reason ?? "";
|
|
140
|
-
if (score >= 3.5)
|
|
187
|
+
if (score >= 3.5)
|
|
141
188
|
return { pass: true, score, reason };
|
|
142
|
-
|
|
143
|
-
if (score >= 2.5) {
|
|
144
|
-
// Uncertainty band: treat as failed for auto-queuing but flag for review.
|
|
189
|
+
if (score >= 2.5)
|
|
145
190
|
return { pass: false, score, reason, reviewNeeded: true };
|
|
146
|
-
}
|
|
147
191
|
return { pass: false, score, reason };
|
|
148
192
|
}
|
|
149
193
|
catch {
|
|
150
194
|
return { pass: false, score: -1, reason: "judge timeout/error — cannot judge, failing closed" };
|
|
151
195
|
}
|
|
152
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Run the LLM-as-judge quality gate on a proposal's content.
|
|
199
|
+
*
|
|
200
|
+
* Exported so reflect.ts can apply the same gate to reflect proposals (R-5 / #374).
|
|
201
|
+
* The selected strategy's distill/reflect quality-gate setting is resolved by
|
|
202
|
+
* the caller before this function runs.
|
|
203
|
+
*
|
|
204
|
+
* Fail-CLOSED (07 P0-2): returns `pass: false` (score -1) on timeout, parse
|
|
205
|
+
* failure, or missing LLM. Minted content that cannot be judged is rejected,
|
|
206
|
+
* not passed through — an unverifiable judge must never wave content into the
|
|
207
|
+
* stash. The rejection is `quality_rejected`, not `review_needed`.
|
|
208
|
+
*/
|
|
209
|
+
export async function runLessonQualityJudge(config, lessonContent, sourceContent, chat, options = {}) {
|
|
210
|
+
return runQualityJudge(config, buildJudgePrompt(lessonContent, sourceContent, options.similarLessons), chat, options);
|
|
211
|
+
}
|
|
212
|
+
/** Judge an in-place reflect revision without applying new-lesson novelty criteria. */
|
|
213
|
+
export async function runReflectQualityJudge(config, candidateContent, sourceContent, feedback, chat, options = {}) {
|
|
214
|
+
return runQualityJudge(config, buildReflectJudgePrompt(candidateContent, sourceContent, feedback), chat, options);
|
|
215
|
+
}
|
|
153
216
|
// ── Quality-rejection helper ─────────────────────────────────────────────────
|
|
154
217
|
/**
|
|
155
218
|
* Write a rejected lesson to `.akm/distill-rejected/`, append a `distill_invoked`
|
|
@@ -623,6 +623,7 @@ export async function akmDistill(options) {
|
|
|
623
623
|
config,
|
|
624
624
|
strategy: options.improveProfile,
|
|
625
625
|
llmConfig: distillLlm,
|
|
626
|
+
signal: options.signal,
|
|
626
627
|
chat,
|
|
627
628
|
stash,
|
|
628
629
|
lookup,
|
|
@@ -719,7 +720,7 @@ export async function akmDistill(options) {
|
|
|
719
720
|
}
|
|
720
721
|
// Test seam: preserve the two-arg signature so existing fake `chat`
|
|
721
722
|
// functions (which return markdown strings) continue to work.
|
|
722
|
-
return chat(distillLlm, messages);
|
|
723
|
+
return chat(distillLlm, messages, options.signal ? { signal: options.signal } : undefined);
|
|
723
724
|
}, null, {
|
|
724
725
|
enabled: resolveProcessEnabled("distill", options.improveProfile ?? resolveImproveStrategy(undefined, config).config),
|
|
725
726
|
onFallback: (evt) => {
|
|
@@ -846,7 +847,11 @@ export async function akmDistill(options) {
|
|
|
846
847
|
if (options.improveProfile?.processes?.distill?.qualityGate?.enabled ?? true) {
|
|
847
848
|
// D-4 / #390: retrieve top-3 similar lessons for dedup check in judge.
|
|
848
849
|
const similarLessons = await fetchSimilarLessonsFn(content.slice(0, 500), 3);
|
|
849
|
-
const judgeResult = await runLessonQualityJudge(config, content, assetContent ?? "", chat,
|
|
850
|
+
const judgeResult = await runLessonQualityJudge(config, content, assetContent ?? "", chat, {
|
|
851
|
+
...(similarLessons.length > 0 ? { similarLessons } : {}),
|
|
852
|
+
...(distillLlm ? { llmConfig: distillLlm } : {}),
|
|
853
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
854
|
+
});
|
|
850
855
|
if (!judgeResult.pass) {
|
|
851
856
|
if (judgeResult.reviewNeeded) {
|
|
852
857
|
return writeQualityRejection(stash, inputRef, effectiveLessonRef, content, judgeResult.score, judgeResult.reason, {
|
|
@@ -343,7 +343,7 @@ triage, sessionIndexing, schemaSimilarityCtx, hotProbationEnabled,
|
|
|
343
343
|
// prior row + bypass flags are threaded in from the caller. Skipping here still
|
|
344
344
|
// costs ZERO LLM calls (the expensive resource #602 protects); only the cheap
|
|
345
345
|
// file read is incurred.
|
|
346
|
-
prior, force,
|
|
346
|
+
prior, force, signal,
|
|
347
347
|
// Stash authoring standards (convention/meta fact bodies) for non-wiki
|
|
348
348
|
// output. Resolved ONCE per run by the caller and threaded in so facts are
|
|
349
349
|
// not re-read per session. Empty string when none exist.
|
|
@@ -481,6 +481,7 @@ standardsContext) {
|
|
|
481
481
|
llmRaw = await chat(getLlmConfig(), [{ role: "user", content: prompt }], {
|
|
482
482
|
timeoutMs,
|
|
483
483
|
responseSchema: EXTRACT_JSON_SCHEMA,
|
|
484
|
+
...(signal ? { signal } : {}),
|
|
484
485
|
});
|
|
485
486
|
return llmRaw;
|
|
486
487
|
}, "", { timeoutMs });
|
|
@@ -861,6 +862,8 @@ export async function akmExtract(options) {
|
|
|
861
862
|
// prompt so facts are not re-read per session.
|
|
862
863
|
const extractStandardsContext = resolveExtractStandards(stashDir);
|
|
863
864
|
for (const summary of candidates) {
|
|
865
|
+
if (options.signal?.aborted)
|
|
866
|
+
break;
|
|
864
867
|
// #602 — the already-extracted skip moved INTO processSession (the content
|
|
865
868
|
// hash needs the session body, only available after readSession). The prior
|
|
866
869
|
// row + bypass flags are threaded through; an unchanged session returns
|
|
@@ -901,7 +904,7 @@ export async function akmExtract(options) {
|
|
|
901
904
|
sessionLockOwnership = sessionLock.ownership;
|
|
902
905
|
}
|
|
903
906
|
try {
|
|
904
|
-
const result = await processSession(harness, summary, stashDir, config, getLlmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, triage, sessionIndexing, schemaSimilarityCtx, hotProbationEnabled, prior, options.force === true, extractStandardsContext);
|
|
907
|
+
const result = await processSession(harness, summary, stashDir, config, getLlmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, triage, sessionIndexing, schemaSimilarityCtx, hotProbationEnabled, prior, options.force === true, options.signal, extractStandardsContext);
|
|
905
908
|
sessions.push(result);
|
|
906
909
|
// #626 — triage aggregation. A session reached the triage gate only when it
|
|
907
910
|
// was NOT already preempted by an earlier skip (read_failed / too_short /
|
|
@@ -118,12 +118,11 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
|
|
|
118
118
|
continue;
|
|
119
119
|
}
|
|
120
120
|
try {
|
|
121
|
-
const target = resolvedConfig.defaultWriteTarget;
|
|
122
121
|
const resolvedEligibilitySource = isExploration
|
|
123
122
|
? "exploration"
|
|
124
123
|
: currentProposal?.eligibilitySource;
|
|
125
124
|
const promotion = await promoteFn(cfg.stashDir, resolvedConfig, proposalId, {
|
|
126
|
-
...(
|
|
125
|
+
...(cfg.targetSelector ? { target: cfg.targetSelector } : {}),
|
|
127
126
|
eventMetadata: {
|
|
128
127
|
autoAccept: true,
|
|
129
128
|
confidence,
|
|
@@ -255,6 +254,7 @@ export function makeGateConfig(phase, shared, overrides = {}) {
|
|
|
255
254
|
stashDir: shared.stashDir,
|
|
256
255
|
config: shared.config,
|
|
257
256
|
eventsCtx: shared.eventsCtx,
|
|
257
|
+
...(shared.targetSelector ? { targetSelector: shared.targetSelector } : {}),
|
|
258
258
|
...(explorationBudgetCount !== undefined && explorationBudgetCount > 0 ? { explorationBudgetCount } : {}),
|
|
259
259
|
...overrides,
|
|
260
260
|
};
|