akm-cli 0.7.5 → 0.8.0-rc2
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/.github/CHANGELOG.md +1 -1
- package/dist/cli/parse-args.js +43 -0
- package/dist/cli.js +853 -479
- package/dist/commands/agent-dispatch.js +102 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +823 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +244 -52
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +2 -23
- package/dist/commands/graph.js +222 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +53 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +3 -30
- package/dist/commands/improve.js +1170 -0
- package/dist/commands/info.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/install-audit.js +5 -1
- package/dist/commands/installed-stashes.js +118 -138
- package/dist/commands/knowledge.js +133 -0
- package/dist/commands/lint/agent-linter.js +46 -0
- package/dist/commands/lint/base-linter.js +285 -0
- package/dist/commands/lint/command-linter.js +46 -0
- package/dist/commands/lint/default-linter.js +13 -0
- package/dist/commands/lint/index.js +107 -0
- package/dist/commands/lint/knowledge-linter.js +13 -0
- package/dist/commands/lint/memory-linter.js +58 -0
- package/dist/commands/lint/registry.js +33 -0
- package/dist/commands/lint/skill-linter.js +42 -0
- package/dist/commands/lint/task-linter.js +47 -0
- package/dist/commands/lint/types.js +1 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +78 -28
- package/dist/commands/reflect.js +143 -35
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +54 -0
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +121 -17
- package/dist/commands/source-add.js +10 -10
- package/dist/commands/source-manage.js +11 -19
- package/dist/commands/tasks.js +385 -0
- package/dist/commands/url-checker.js +39 -0
- package/dist/commands/vault.js +8 -26
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-ref.js +4 -0
- package/dist/core/asset-registry.js +4 -16
- package/dist/core/asset-spec.js +10 -0
- package/dist/core/common.js +94 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +222 -128
- package/dist/core/events.js +73 -126
- package/dist/core/frontmatter.js +3 -1
- package/dist/core/markdown.js +17 -0
- package/dist/core/memory-improve.js +678 -0
- package/dist/core/parse.js +155 -0
- package/dist/core/paths.js +101 -3
- package/dist/core/proposal-validators.js +61 -0
- package/dist/core/proposals.js +49 -38
- package/dist/core/state-db.js +775 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +52 -238
- package/dist/indexer/db.js +378 -1
- package/dist/indexer/ensure-index.js +61 -0
- package/dist/indexer/graph-boost.js +247 -94
- package/dist/indexer/graph-db.js +201 -0
- package/dist/indexer/graph-dedup.js +99 -0
- package/dist/indexer/graph-extraction.js +409 -76
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +442 -290
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/match-contributors.js +141 -0
- package/dist/indexer/matchers.js +24 -190
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +194 -175
- package/dist/indexer/path-resolver.js +89 -0
- package/dist/indexer/ranking-contributors.js +204 -0
- package/dist/indexer/ranking.js +74 -0
- package/dist/indexer/search-hit-enrichers.js +22 -0
- package/dist/indexer/search-source.js +24 -9
- package/dist/indexer/semantic-status.js +2 -16
- package/dist/indexer/walker.js +25 -0
- package/dist/integrations/agent/config.js +175 -3
- package/dist/integrations/agent/index.js +3 -1
- package/dist/integrations/agent/pipeline.js +39 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +77 -72
- package/dist/integrations/agent/runners.js +31 -0
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +71 -16
- package/dist/integrations/lockfile.js +10 -18
- package/dist/integrations/session-logs/index.js +65 -0
- package/dist/integrations/session-logs/providers/claude-code.js +56 -0
- package/dist/integrations/session-logs/providers/opencode.js +52 -0
- package/dist/integrations/session-logs/types.js +1 -0
- package/dist/llm/call-ai.js +74 -0
- package/dist/llm/client.js +61 -122
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -62
- package/dist/llm/memory-infer.js +49 -71
- package/dist/llm/metadata-enhance.js +39 -22
- package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
- package/dist/output/cli-hints-full.md +277 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +2 -318
- package/dist/output/renderers.js +190 -123
- package/dist/output/shapes.js +33 -0
- package/dist/output/text.js +239 -2
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/website-ingest.js +4 -0
- package/dist/tasks/backends/cron.js +200 -0
- package/dist/tasks/backends/exec-utils.js +25 -0
- package/dist/tasks/backends/index.js +32 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +184 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +212 -0
- package/dist/tasks/parser.js +198 -0
- package/dist/tasks/resolveAkmBin.js +84 -0
- package/dist/tasks/runner.js +432 -0
- package/dist/tasks/schedule.js +208 -0
- package/dist/tasks/schema.js +13 -0
- package/dist/tasks/validator.js +59 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +12 -0
- package/dist/wiki/wiki.js +10 -61
- package/dist/workflows/authoring.js +5 -25
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +59 -91
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +3 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +3 -2
- package/dist/templates/wiki-templates.js +0 -100
package/dist/commands/distill.js
CHANGED
|
@@ -46,17 +46,46 @@
|
|
|
46
46
|
* be invoked from CI / automation without spinning up an agent harness.
|
|
47
47
|
*/
|
|
48
48
|
import fs from "node:fs";
|
|
49
|
+
import path from "node:path";
|
|
49
50
|
import { parseAssetRef } from "../core/asset-ref";
|
|
50
|
-
import { resolveStashDir } from "../core/common";
|
|
51
|
+
import { resolveStashDir, timestampForFilename } from "../core/common";
|
|
51
52
|
import { loadConfig } from "../core/config";
|
|
52
53
|
import { ConfigError, UsageError } from "../core/errors";
|
|
53
54
|
import { appendEvent, readEvents } from "../core/events";
|
|
54
55
|
import { parseFrontmatter } from "../core/frontmatter";
|
|
55
56
|
import { lintLessonContent } from "../core/lesson-lint";
|
|
57
|
+
import { stripMarkdownFences } from "../core/markdown";
|
|
56
58
|
import { createProposal } from "../core/proposals";
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
59
|
+
import { warnVerbose } from "../core/warn";
|
|
60
|
+
import { resolveAssetPath } from "../indexer/path-resolver";
|
|
61
|
+
import { chatCompletion, parseEmbeddedJsonResponse } from "../llm/client";
|
|
62
|
+
import { isLlmFeatureEnabled, tryLlmFeature } from "../llm/feature-gate";
|
|
63
|
+
import { assessMemoryKnowledgePromotionCandidate, deriveKnowledgeRef } from "./distill-promotion-policy";
|
|
64
|
+
/**
|
|
65
|
+
* Build the `distill_invoked` metadata object. All optional fields are
|
|
66
|
+
* included only when they carry a meaningful value — `undefined` fields are
|
|
67
|
+
* omitted.
|
|
68
|
+
*/
|
|
69
|
+
function buildDistillEventMetadata(outcome, lessonRef, extras = {}) {
|
|
70
|
+
const meta = { outcome, lessonRef };
|
|
71
|
+
if (extras.proposalKind !== undefined)
|
|
72
|
+
meta.proposalKind = extras.proposalKind;
|
|
73
|
+
if (extras.proposalId !== undefined)
|
|
74
|
+
meta.proposalId = extras.proposalId;
|
|
75
|
+
if (extras.proposalRef !== undefined)
|
|
76
|
+
meta.proposalRef = extras.proposalRef;
|
|
77
|
+
if (extras.score !== undefined)
|
|
78
|
+
meta.score = extras.score;
|
|
79
|
+
if (extras.reason !== undefined)
|
|
80
|
+
meta.reason = extras.reason;
|
|
81
|
+
if (extras.findingKinds !== undefined)
|
|
82
|
+
meta.findingKinds = extras.findingKinds;
|
|
83
|
+
if (extras.filteredFeedbackCount !== undefined)
|
|
84
|
+
meta.filteredFeedbackCount = extras.filteredFeedbackCount;
|
|
85
|
+
if (extras.sourceRun !== undefined)
|
|
86
|
+
meta.sourceRun = extras.sourceRun;
|
|
87
|
+
return meta;
|
|
88
|
+
}
|
|
60
89
|
// ── Lesson-ref derivation ───────────────────────────────────────────────────
|
|
61
90
|
/** Derive the proposed lesson ref from the input ref. See module docblock. */
|
|
62
91
|
export function deriveLessonRef(inputRef) {
|
|
@@ -75,7 +104,7 @@ export function deriveLessonRef(inputRef) {
|
|
|
75
104
|
return `lesson:${safe}-lesson`;
|
|
76
105
|
}
|
|
77
106
|
// ── Prompt assembly ─────────────────────────────────────────────────────────
|
|
78
|
-
const
|
|
107
|
+
const LESSON_SYSTEM_PROMPT = [
|
|
79
108
|
"You are the akm `distill` distiller.",
|
|
80
109
|
"Given an asset and recent feedback events about it, produce a single",
|
|
81
110
|
"concise *lesson* an agent should remember next time it works on this",
|
|
@@ -92,6 +121,28 @@ const SYSTEM_PROMPT = [
|
|
|
92
121
|
"Both `description` and `when_to_use` MUST be non-empty single-line strings.",
|
|
93
122
|
"Output ONLY the lesson file contents — no prose, no fences, no preamble.",
|
|
94
123
|
].join("\n");
|
|
124
|
+
const KNOWLEDGE_SYSTEM_PROMPT = [
|
|
125
|
+
"You are the akm `distill` distiller.",
|
|
126
|
+
"Given an asset and recent feedback events about it, produce a concise",
|
|
127
|
+
"*knowledge* markdown document capturing the durable, reusable facts.",
|
|
128
|
+
"Prefer stable guidance over narrative recap.",
|
|
129
|
+
"If you include YAML frontmatter, keep it compatible with normal knowledge",
|
|
130
|
+
"assets (for example `description`, `tags`, `sources`, `observed_at`).",
|
|
131
|
+
"Include a meaningful markdown body.",
|
|
132
|
+
"Output ONLY the knowledge file contents — no prose, no fences, no preamble.",
|
|
133
|
+
].join("\n");
|
|
134
|
+
function validateKnowledgeContent(content, inputRef) {
|
|
135
|
+
const parsed = parseFrontmatter(content);
|
|
136
|
+
if (parsed.content.trim().length > 0)
|
|
137
|
+
return [];
|
|
138
|
+
return [
|
|
139
|
+
{
|
|
140
|
+
kind: "missing-body",
|
|
141
|
+
field: "body",
|
|
142
|
+
message: `Distilled knowledge for ${inputRef} must include a non-empty markdown body.`,
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
}
|
|
95
146
|
/** Pure: build the user-prompt body. Exported for tests. */
|
|
96
147
|
export function buildDistillPrompt(input) {
|
|
97
148
|
const lines = [];
|
|
@@ -99,8 +150,9 @@ export function buildDistillPrompt(input) {
|
|
|
99
150
|
lines.push("");
|
|
100
151
|
lines.push("Asset content:");
|
|
101
152
|
if (input.assetContent) {
|
|
153
|
+
const body = input.assetContent.trim().slice(0, 3000);
|
|
102
154
|
lines.push("```");
|
|
103
|
-
lines.push(
|
|
155
|
+
lines.push(body);
|
|
104
156
|
lines.push("```");
|
|
105
157
|
}
|
|
106
158
|
else {
|
|
@@ -118,9 +170,96 @@ export function buildDistillPrompt(input) {
|
|
|
118
170
|
}
|
|
119
171
|
}
|
|
120
172
|
lines.push("");
|
|
121
|
-
lines.push(
|
|
173
|
+
lines.push(`Produce the ${input.proposalKind === "knowledge" ? "knowledge" : "lesson"} markdown file now.`);
|
|
122
174
|
return lines.join("\n");
|
|
123
175
|
}
|
|
176
|
+
// ── LLM-as-judge quality gate (P2-B) ────────────────────────────────────────
|
|
177
|
+
function buildJudgePrompt(lessonContent, sourceContent) {
|
|
178
|
+
return [
|
|
179
|
+
"You are evaluating a proposed lesson asset for an akm knowledge base.",
|
|
180
|
+
"",
|
|
181
|
+
"Score this lesson on each criterion from 1 (poor) to 5 (excellent):",
|
|
182
|
+
"1. NOVELTY: Does the lesson add information not already present in the source asset?",
|
|
183
|
+
"2. ACTIONABILITY: Can an agent follow this lesson without additional context?",
|
|
184
|
+
"3. NON-REDUNDANCY: Is this lesson meaningfully different from what the source already says?",
|
|
185
|
+
"",
|
|
186
|
+
"Source asset content:",
|
|
187
|
+
"```",
|
|
188
|
+
sourceContent.slice(0, 2000),
|
|
189
|
+
"```",
|
|
190
|
+
"",
|
|
191
|
+
"Proposed lesson content:",
|
|
192
|
+
"```",
|
|
193
|
+
lessonContent.slice(0, 1000),
|
|
194
|
+
"```",
|
|
195
|
+
"",
|
|
196
|
+
'Return ONLY valid JSON, no prose: {"score": <average score 1-5 as float>, "reason": "<one sentence>"}',
|
|
197
|
+
].join("\n");
|
|
198
|
+
}
|
|
199
|
+
async function runLessonQualityJudge(config, lessonContent, sourceContent, chat) {
|
|
200
|
+
if (!config.llm) {
|
|
201
|
+
return { pass: true, score: -1, reason: "no LLM configured — passing through" };
|
|
202
|
+
}
|
|
203
|
+
const judgeLlmConfig = config.llm.judgeModel ? { ...config.llm, model: config.llm.judgeModel } : config.llm;
|
|
204
|
+
const JUDGE_TIMEOUT_MS = 8_000;
|
|
205
|
+
try {
|
|
206
|
+
const raw = await Promise.race([
|
|
207
|
+
chat(judgeLlmConfig, [
|
|
208
|
+
{ role: "system", content: "Return only valid JSON. No prose." },
|
|
209
|
+
{ role: "user", content: buildJudgePrompt(lessonContent, sourceContent) },
|
|
210
|
+
]),
|
|
211
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("judge timeout")), JUDGE_TIMEOUT_MS)),
|
|
212
|
+
]);
|
|
213
|
+
const parsed = parseEmbeddedJsonResponse(raw);
|
|
214
|
+
if (!parsed || typeof parsed.score !== "number") {
|
|
215
|
+
return { pass: true, score: -1, reason: "judge parse failed — passing through" };
|
|
216
|
+
}
|
|
217
|
+
return { pass: parsed.score >= 3, score: parsed.score, reason: parsed.reason ?? "" };
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return { pass: true, score: -1, reason: "judge failed — passing through" };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// ── Quality-rejection helper ─────────────────────────────────────────────────
|
|
224
|
+
/**
|
|
225
|
+
* Write a rejected lesson to `.akm/distill-rejected/`, append a `distill_invoked`
|
|
226
|
+
* quality-rejected event, and return the `quality_rejected` envelope.
|
|
227
|
+
*
|
|
228
|
+
* @param stash - Root stash directory.
|
|
229
|
+
* @param inputRef - The original input ref (for the event).
|
|
230
|
+
* @param lessonRef - The proposed lesson/knowledge ref.
|
|
231
|
+
* @param content - The raw content that failed the quality gate.
|
|
232
|
+
* @param score - Quality score from the judge.
|
|
233
|
+
* @param reason - Human-readable rejection reason.
|
|
234
|
+
* @param extraMeta - Optional additional metadata for the event.
|
|
235
|
+
*/
|
|
236
|
+
function writeQualityRejection(stash, inputRef, lessonRef, content, score, reason, extraMeta = {}) {
|
|
237
|
+
const rejectDir = path.join(stash, ".akm", "distill-rejected");
|
|
238
|
+
fs.mkdirSync(rejectDir, { recursive: true });
|
|
239
|
+
const ts = timestampForFilename();
|
|
240
|
+
fs.writeFileSync(path.join(rejectDir, `${ts}-${lessonRef}.md`), `---\nscore: ${score}\nreason: ${reason}\n---\n\n${content}`, "utf8");
|
|
241
|
+
appendEvent({
|
|
242
|
+
eventType: "distill_invoked",
|
|
243
|
+
ref: inputRef,
|
|
244
|
+
metadata: {
|
|
245
|
+
outcome: "quality_rejected",
|
|
246
|
+
lessonRef,
|
|
247
|
+
score,
|
|
248
|
+
reason,
|
|
249
|
+
...extraMeta,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
return {
|
|
253
|
+
schemaVersion: 1,
|
|
254
|
+
ok: true,
|
|
255
|
+
outcome: "quality_rejected",
|
|
256
|
+
inputRef,
|
|
257
|
+
lessonRef,
|
|
258
|
+
score,
|
|
259
|
+
reason,
|
|
260
|
+
...extraMeta,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
124
263
|
// ── Main entry point ────────────────────────────────────────────────────────
|
|
125
264
|
/**
|
|
126
265
|
* Run a single bounded distillation pass for `ref`. Always emits exactly one
|
|
@@ -134,7 +273,7 @@ export async function akmDistill(options) {
|
|
|
134
273
|
}
|
|
135
274
|
// Validate the ref shape up front so a typo never reaches the LLM.
|
|
136
275
|
parseAssetRef(inputRef);
|
|
137
|
-
const
|
|
276
|
+
const targetKind = options.proposalKind ?? "lesson";
|
|
138
277
|
const config = options.config ?? loadConfig();
|
|
139
278
|
const stash = options.stashDir ?? resolveStashDir();
|
|
140
279
|
const chat = options.chat ?? chatCompletion;
|
|
@@ -175,9 +314,62 @@ export async function akmDistill(options) {
|
|
|
175
314
|
eventType: e.eventType,
|
|
176
315
|
...(e.metadata !== undefined ? { metadata: e.metadata } : {}),
|
|
177
316
|
}));
|
|
178
|
-
const
|
|
317
|
+
const promotion = targetKind === "lesson"
|
|
318
|
+
? null
|
|
319
|
+
: assessMemoryKnowledgePromotionCandidate({
|
|
320
|
+
inputRef,
|
|
321
|
+
assetContent,
|
|
322
|
+
feedbackEvents: filteredEvents.map((event) => ({
|
|
323
|
+
...(event.metadata !== undefined ? { metadata: event.metadata } : {}),
|
|
324
|
+
})),
|
|
325
|
+
});
|
|
326
|
+
if (promotion?.promote && promotion.content && (targetKind === "knowledge" || targetKind === "auto")) {
|
|
327
|
+
// Apply quality gate to fast-path knowledge promotion (Risk 4 fix).
|
|
328
|
+
if (isLlmFeatureEnabled(config, "lesson_quality_gate")) {
|
|
329
|
+
const judgeResult = await runLessonQualityJudge(config, promotion.content, assetContent ?? "", chat);
|
|
330
|
+
if (!judgeResult.pass) {
|
|
331
|
+
return writeQualityRejection(stash, inputRef, promotion.knowledgeRef, promotion.content, judgeResult.score, judgeResult.reason);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const knowledgeParsed = parseFrontmatter(promotion.content);
|
|
335
|
+
const proposal = createProposal(stash, {
|
|
336
|
+
ref: promotion.knowledgeRef,
|
|
337
|
+
source: "distill",
|
|
338
|
+
...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
|
|
339
|
+
payload: {
|
|
340
|
+
content: promotion.content,
|
|
341
|
+
...(Object.keys(knowledgeParsed.data).length > 0 ? { frontmatter: knowledgeParsed.data } : {}),
|
|
342
|
+
},
|
|
343
|
+
}, options.ctx);
|
|
344
|
+
appendEvent({
|
|
345
|
+
eventType: "distill_invoked",
|
|
346
|
+
ref: inputRef,
|
|
347
|
+
metadata: buildDistillEventMetadata("queued", promotion.knowledgeRef, {
|
|
348
|
+
proposalRef: promotion.knowledgeRef,
|
|
349
|
+
proposalKind: "knowledge",
|
|
350
|
+
proposalId: proposal.id,
|
|
351
|
+
...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
|
|
352
|
+
...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
|
|
353
|
+
}),
|
|
354
|
+
});
|
|
355
|
+
return {
|
|
356
|
+
schemaVersion: 1,
|
|
357
|
+
ok: true,
|
|
358
|
+
outcome: "queued",
|
|
359
|
+
inputRef,
|
|
360
|
+
lessonRef: promotion.knowledgeRef,
|
|
361
|
+
proposalRef: promotion.knowledgeRef,
|
|
362
|
+
proposalKind: "knowledge",
|
|
363
|
+
proposalId: proposal.id,
|
|
364
|
+
proposal,
|
|
365
|
+
...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const effectiveProposalKind = targetKind === "knowledge" ? "knowledge" : "lesson";
|
|
369
|
+
const effectiveLessonRef = effectiveProposalKind === "knowledge" ? deriveKnowledgeRef(inputRef) : deriveLessonRef(inputRef);
|
|
370
|
+
const userPrompt = buildDistillPrompt({ inputRef, assetContent, feedback, proposalKind: effectiveProposalKind });
|
|
179
371
|
const messages = [
|
|
180
|
-
{ role: "system", content:
|
|
372
|
+
{ role: "system", content: effectiveProposalKind === "knowledge" ? KNOWLEDGE_SYSTEM_PROMPT : LESSON_SYSTEM_PROMPT },
|
|
181
373
|
{ role: "user", content: userPrompt },
|
|
182
374
|
];
|
|
183
375
|
// Single bounded LLM call. The wrapper handles the gate-check, 30 s
|
|
@@ -190,23 +382,30 @@ export async function akmDistill(options) {
|
|
|
190
382
|
throw new ConfigError("No LLM connection configured. Set `llm.endpoint` and `llm.model` in the akm config.", "LLM_NOT_CONFIGURED");
|
|
191
383
|
}
|
|
192
384
|
return chat(config.llm, messages);
|
|
193
|
-
}, null
|
|
385
|
+
}, null, {
|
|
386
|
+
onFallback: (evt) => {
|
|
387
|
+
// Log the fallback reason; the caller (raw === null path) handles
|
|
388
|
+
// emitting the distill_invoked event so we don't double-emit here.
|
|
389
|
+
warnVerbose(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
|
|
390
|
+
},
|
|
391
|
+
});
|
|
194
392
|
if (raw === null || raw.trim() === "") {
|
|
195
393
|
appendEvent({
|
|
196
394
|
eventType: "distill_invoked",
|
|
197
395
|
ref: inputRef,
|
|
198
|
-
metadata: {
|
|
199
|
-
|
|
200
|
-
lessonRef,
|
|
396
|
+
metadata: buildDistillEventMetadata("skipped", effectiveLessonRef, {
|
|
397
|
+
proposalKind: effectiveProposalKind,
|
|
201
398
|
...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
|
|
202
|
-
},
|
|
399
|
+
}),
|
|
203
400
|
});
|
|
204
401
|
return {
|
|
205
402
|
schemaVersion: 1,
|
|
206
403
|
ok: true,
|
|
207
404
|
outcome: "skipped",
|
|
208
405
|
inputRef,
|
|
209
|
-
lessonRef,
|
|
406
|
+
lessonRef: effectiveLessonRef,
|
|
407
|
+
proposalRef: effectiveLessonRef,
|
|
408
|
+
proposalKind: effectiveProposalKind,
|
|
210
409
|
message: "feedback distillation is disabled or the LLM call failed; no proposal created.",
|
|
211
410
|
...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
|
|
212
411
|
};
|
|
@@ -217,27 +416,38 @@ export async function akmDistill(options) {
|
|
|
217
416
|
// canonical gate for required frontmatter (v1 spec §13). On failure we
|
|
218
417
|
// surface a structured error and exit non-zero — but still emit
|
|
219
418
|
// `distill_invoked` so the failure is observable.
|
|
220
|
-
const
|
|
221
|
-
|
|
419
|
+
const findings = effectiveProposalKind === "knowledge"
|
|
420
|
+
? validateKnowledgeContent(content, inputRef)
|
|
421
|
+
: lintLessonContent(content, `distill:${inputRef}`).findings;
|
|
422
|
+
if (findings.length > 0) {
|
|
222
423
|
appendEvent({
|
|
223
424
|
eventType: "distill_invoked",
|
|
224
425
|
ref: inputRef,
|
|
225
|
-
metadata: {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
findingKinds: lintReport.findings.map((f) => f.kind),
|
|
426
|
+
metadata: buildDistillEventMetadata("validation_failed", effectiveLessonRef, {
|
|
427
|
+
proposalKind: effectiveProposalKind,
|
|
428
|
+
findingKinds: findings.map((f) => f.kind),
|
|
229
429
|
...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
|
|
230
|
-
},
|
|
430
|
+
}),
|
|
231
431
|
});
|
|
232
|
-
const message =
|
|
233
|
-
throw new UsageError(`Distilled
|
|
432
|
+
const message = findings.map((f) => f.message).join("\n");
|
|
433
|
+
throw new UsageError(`Distilled ${effectiveProposalKind} failed validation:\n${message}`, "MISSING_REQUIRED_ARGUMENT", effectiveProposalKind === "knowledge"
|
|
434
|
+
? "Knowledge proposals require a non-empty markdown body."
|
|
435
|
+
: "Lessons require non-empty `description` and `when_to_use` frontmatter fields. See v1 spec §13.");
|
|
436
|
+
}
|
|
437
|
+
// LLM-as-judge quality gate (P2-B). Only active when the feature flag is
|
|
438
|
+
// explicitly enabled. Fail-open: judge failures always pass through.
|
|
439
|
+
if (isLlmFeatureEnabled(config, "lesson_quality_gate")) {
|
|
440
|
+
const judgeResult = await runLessonQualityJudge(config, content, assetContent ?? "", chat);
|
|
441
|
+
if (!judgeResult.pass) {
|
|
442
|
+
return writeQualityRejection(stash, inputRef, effectiveLessonRef, content, judgeResult.score, judgeResult.reason, exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {});
|
|
443
|
+
}
|
|
234
444
|
}
|
|
235
445
|
// Round-trip the parsed frontmatter so the proposal carries it as a
|
|
236
446
|
// structured payload alongside the raw content (matches the shape used by
|
|
237
447
|
// other proposal sources).
|
|
238
448
|
const parsed = parseFrontmatter(content);
|
|
239
449
|
const proposal = createProposal(stash, {
|
|
240
|
-
ref:
|
|
450
|
+
ref: effectiveLessonRef,
|
|
241
451
|
source: "distill",
|
|
242
452
|
...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
|
|
243
453
|
payload: {
|
|
@@ -248,20 +458,22 @@ export async function akmDistill(options) {
|
|
|
248
458
|
appendEvent({
|
|
249
459
|
eventType: "distill_invoked",
|
|
250
460
|
ref: inputRef,
|
|
251
|
-
metadata: {
|
|
252
|
-
|
|
253
|
-
|
|
461
|
+
metadata: buildDistillEventMetadata("queued", effectiveLessonRef, {
|
|
462
|
+
proposalRef: effectiveLessonRef,
|
|
463
|
+
proposalKind: effectiveProposalKind,
|
|
254
464
|
proposalId: proposal.id,
|
|
255
465
|
...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
|
|
256
466
|
...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
|
|
257
|
-
},
|
|
467
|
+
}),
|
|
258
468
|
});
|
|
259
469
|
return {
|
|
260
470
|
schemaVersion: 1,
|
|
261
471
|
ok: true,
|
|
262
472
|
outcome: "queued",
|
|
263
473
|
inputRef,
|
|
264
|
-
lessonRef,
|
|
474
|
+
lessonRef: effectiveLessonRef,
|
|
475
|
+
proposalRef: effectiveLessonRef,
|
|
476
|
+
proposalKind: effectiveProposalKind,
|
|
265
477
|
proposalId: proposal.id,
|
|
266
478
|
proposal,
|
|
267
479
|
...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
|
|
@@ -269,25 +481,5 @@ export async function akmDistill(options) {
|
|
|
269
481
|
}
|
|
270
482
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
271
483
|
async function defaultLookup(ref) {
|
|
272
|
-
|
|
273
|
-
const entry = await indexerLookup(parseAssetRef(ref));
|
|
274
|
-
return entry?.filePath ?? null;
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
/** Best-effort fence stripping. Keeps the body intact when no fence is present. */
|
|
281
|
-
function stripMarkdownFences(raw) {
|
|
282
|
-
// Strip <think>…</think> reasoning blocks first — local LLMs (e.g. Qwen3)
|
|
283
|
-
// emit these before the content, which breaks YAML frontmatter detection.
|
|
284
|
-
const stripped = raw
|
|
285
|
-
.trim()
|
|
286
|
-
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
287
|
-
.trim();
|
|
288
|
-
// Only strip outer triple-fence pairs — leave inner code blocks alone.
|
|
289
|
-
const fence = stripped.match(/^```(?:markdown|md)?\s*\n([\s\S]*?)\n```\s*$/i);
|
|
290
|
-
if (fence)
|
|
291
|
-
return fence[1].trim();
|
|
292
|
-
return stripped;
|
|
484
|
+
return resolveAssetPath(ref, { mode: "index-only" });
|
|
293
485
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { writeFileAtomic } from "../core/common";
|
|
4
|
+
export function writeEvalCase(stashDir, evalCase) {
|
|
5
|
+
const evalDir = path.join(stashDir, ".akm", "eval-cases");
|
|
6
|
+
fs.mkdirSync(evalDir, { recursive: true });
|
|
7
|
+
const fileName = `${evalCase.slug}.md`;
|
|
8
|
+
const filePath = path.join(evalDir, fileName);
|
|
9
|
+
const content = `---
|
|
10
|
+
ref: ${evalCase.ref}
|
|
11
|
+
failureReason: ${evalCase.failureReason}
|
|
12
|
+
assetType: ${evalCase.assetType}
|
|
13
|
+
rejectedAt: ${evalCase.rejectedAt}
|
|
14
|
+
source: ${evalCase.source}
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Eval Case: ${evalCase.ref}
|
|
18
|
+
|
|
19
|
+
**Failure reason:** ${evalCase.failureReason}
|
|
20
|
+
**Source:** ${evalCase.source}
|
|
21
|
+
**Asset type:** ${evalCase.assetType}
|
|
22
|
+
|
|
23
|
+
This case was automatically captured when a distillation or proposal was rejected.
|
|
24
|
+
Use it as a regression test: future improve runs on this ref should not produce
|
|
25
|
+
output that would be rejected for the same reason.
|
|
26
|
+
`;
|
|
27
|
+
writeFileAtomic(filePath, content);
|
|
28
|
+
return filePath;
|
|
29
|
+
}
|
|
30
|
+
export function countEvalCases(stashDir) {
|
|
31
|
+
const evalDir = path.join(stashDir, ".akm", "eval-cases");
|
|
32
|
+
if (!fs.existsSync(evalDir))
|
|
33
|
+
return 0;
|
|
34
|
+
try {
|
|
35
|
+
return fs.readdirSync(evalDir).filter((f) => f.endsWith(".md")).length;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/commands/events.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { parseAssetRef } from "../core/asset-ref";
|
|
11
11
|
import { UsageError } from "../core/errors";
|
|
12
12
|
import { readEvents, tailEvents } from "../core/events";
|
|
13
|
+
import { parseSinceToIso } from "../core/time";
|
|
13
14
|
/**
|
|
14
15
|
* Parse `--since` accepting either a byte-offset cursor (`@offset:<int>`) for
|
|
15
16
|
* cross-process resumption, or a timestamp / epoch-ms (the existing form).
|
|
@@ -30,7 +31,7 @@ function parseSinceFlag(since) {
|
|
|
30
31
|
}
|
|
31
32
|
return { sinceOffset: value };
|
|
32
33
|
}
|
|
33
|
-
return { since:
|
|
34
|
+
return { since: parseSinceToIso(trimmed) };
|
|
34
35
|
}
|
|
35
36
|
function validateRef(ref) {
|
|
36
37
|
if (ref === undefined)
|
|
@@ -42,28 +43,6 @@ function validateRef(ref) {
|
|
|
42
43
|
parseAssetRef(trimmed);
|
|
43
44
|
return trimmed;
|
|
44
45
|
}
|
|
45
|
-
function normalizeSince(since) {
|
|
46
|
-
if (since === undefined)
|
|
47
|
-
return undefined;
|
|
48
|
-
const trimmed = since.trim();
|
|
49
|
-
if (!trimmed) {
|
|
50
|
-
throw new UsageError("--since cannot be empty.", "INVALID_FLAG_VALUE");
|
|
51
|
-
}
|
|
52
|
-
// Accept ISO timestamp (preferred), epoch ms, or plain date.
|
|
53
|
-
if (/^\d+$/.test(trimmed)) {
|
|
54
|
-
const ms = Number.parseInt(trimmed, 10);
|
|
55
|
-
const d = new Date(ms);
|
|
56
|
-
if (Number.isNaN(d.getTime())) {
|
|
57
|
-
throw new UsageError(`Invalid --since value: ${since}`, "INVALID_FLAG_VALUE");
|
|
58
|
-
}
|
|
59
|
-
return d.toISOString();
|
|
60
|
-
}
|
|
61
|
-
const parsed = new Date(trimmed);
|
|
62
|
-
if (Number.isNaN(parsed.getTime())) {
|
|
63
|
-
throw new UsageError(`Invalid --since value: ${since}. Expected ISO timestamp (e.g. 2026-04-01T00:00:00Z) or epoch ms.`, "INVALID_FLAG_VALUE");
|
|
64
|
-
}
|
|
65
|
-
return parsed.toISOString();
|
|
66
|
-
}
|
|
67
46
|
export function akmEventsList(options = {}) {
|
|
68
47
|
const ref = validateRef(options.ref);
|
|
69
48
|
const parsed = parseSinceFlag(options.since);
|