akm-cli 0.9.0-rc.3 → 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 +12 -4
- package/README.md +7 -8
- package/SECURITY.md +5 -3
- package/dist/akm +44 -33
- package/dist/akm-migrate-storage +44 -33
- package/dist/assets/backends/schtasks-template.xml +2 -1
- package/dist/assets/help/help-improve.md +3 -0
- package/dist/cli.js +25 -10
- package/dist/commands/health/html-report.js +23 -2
- package/dist/commands/health/improve-metrics.js +45 -6
- package/dist/commands/health/md-report.js +10 -1
- package/dist/commands/health/windows.js +1 -1
- package/dist/commands/health.js +1 -1
- 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/commands/tasks/default-tasks.js +5 -5
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +261 -68
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +87 -7
- package/dist/core/maintenance-barrier.js +16 -0
- package/dist/core/state-db.js +1 -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 +23 -4
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +91 -9
- package/dist/storage/repositories/task-history-repository.js +30 -3
- package/dist/tasks/backends/cron.js +71 -38
- package/dist/tasks/backends/exec-utils.js +55 -3
- package/dist/tasks/backends/launchd.js +241 -50
- package/dist/tasks/backends/schtasks.js +434 -59
- package/dist/tasks/command-executable.js +93 -0
- package/dist/tasks/parser.js +142 -6
- package/dist/tasks/resolve-akm-bin.js +19 -4
- package/dist/tasks/runner.js +258 -93
- package/dist/tasks/schedule.js +108 -19
- package/dist/tasks/scheduler-invocation.js +86 -0
- package/dist/tasks/task-id.js +37 -0
- package/docs/migration/release-notes/0.9.0.md +4 -2
- package/docs/migration/v0.8-to-v0.9.md +64 -9
- package/package.json +2 -3
- package/schemas/akm-task.json +2 -2
- package/schemas/akm-workflow.json +2 -2
|
@@ -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
|
};
|