akm-cli 0.7.4 → 0.8.0-rc.3
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 → .github/CHANGELOG.md} +34 -1
- package/.github/LICENSE +374 -0
- package/dist/cli/parse-args.js +86 -0
- package/dist/cli.js +1223 -650
- package/dist/commands/agent-dispatch.js +107 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +812 -0
- package/dist/commands/curate.js +1 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +224 -39
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +12 -24
- 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 +1161 -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 +291 -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 +145 -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/vault-key-rules.js +67 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/migration-help.js +2 -2
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +106 -43
- package/dist/commands/reflect.js +167 -41
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +55 -1
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +135 -55
- 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 +173 -87
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-ref.js +4 -0
- package/dist/core/asset-registry.js +5 -17
- package/dist/core/asset-spec.js +11 -1
- package/dist/core/common.js +100 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +240 -127
- package/dist/core/events.js +87 -123
- package/dist/core/frontmatter.js +0 -6
- 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 +731 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +86 -472
- package/dist/indexer/db.js +418 -59
- package/dist/indexer/ensure-index.js +133 -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 +417 -74
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +480 -298
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/matchers.js +124 -160
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +196 -197
- 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/builders.js +109 -0
- package/dist/integrations/agent/config.js +203 -3
- package/dist/integrations/agent/index.js +5 -2
- package/dist/integrations/agent/model-aliases.js +63 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +114 -29
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +158 -34
- 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 +63 -86
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -64
- package/dist/llm/memory-infer.js +52 -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 -309
- package/dist/output/renderers.js +226 -257
- package/dist/output/shapes.js +109 -96
- package/dist/output/text.js +274 -36
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/registry/resolve.js +8 -16
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/filesystem.js +16 -23
- package/dist/sources/providers/git.js +45 -4
- package/dist/sources/providers/website.js +15 -22
- 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/db.js +9 -0
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +73 -88
- package/dist/workflows/scope-key.js +76 -0
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +5 -2
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.4.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +20 -0
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +4 -3
- package/dist/templates/wiki-templates.js +0 -100
package/dist/commands/reflect.js
CHANGED
|
@@ -19,16 +19,22 @@
|
|
|
19
19
|
* a committed asset, and the `accept` flow is the bridge.
|
|
20
20
|
*/
|
|
21
21
|
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
22
23
|
import { parseAssetRef } from "../core/asset-ref";
|
|
23
24
|
import { resolveStashDir } from "../core/common";
|
|
24
|
-
import { loadConfig } from "../core/config";
|
|
25
25
|
import { ConfigError, UsageError } from "../core/errors";
|
|
26
26
|
import { appendEvent, readEvents } from "../core/events";
|
|
27
|
+
import { parseFrontmatter } from "../core/frontmatter";
|
|
27
28
|
import { lintLessonContent } from "../core/lesson-lint";
|
|
29
|
+
import { stripMarkdownFences } from "../core/markdown";
|
|
28
30
|
import { createProposal } from "../core/proposals";
|
|
29
31
|
import { lookup } from "../indexer/indexer";
|
|
30
|
-
import {
|
|
32
|
+
import { runAgent, } from "../integrations/agent";
|
|
33
|
+
import { resolveProcessAgentProfile } from "../integrations/agent/config";
|
|
31
34
|
import { buildReflectPrompt, parseAgentProposalPayload } from "../integrations/agent/prompts";
|
|
35
|
+
import { runAgentSdk } from "../integrations/agent/sdk-runner";
|
|
36
|
+
import { baseFailureFields, enoentHintMessage, isEnoentFailure, loadAgentConfigFromDisk, resolveAgentProfile, } from "./agent-support";
|
|
37
|
+
import { deriveLessonRef } from "./distill";
|
|
32
38
|
const MAX_FEEDBACK_LINES = 10;
|
|
33
39
|
const MAX_GLOBAL_FEEDBACK_LINES = 20;
|
|
34
40
|
/**
|
|
@@ -45,7 +51,7 @@ function readRecentFeedback(ref) {
|
|
|
45
51
|
for (const event of result.events.slice(-limit)) {
|
|
46
52
|
const md = (event.metadata ?? {});
|
|
47
53
|
const signal = typeof md.signal === "string" ? md.signal : "?";
|
|
48
|
-
const note = typeof md.
|
|
54
|
+
const note = typeof md.reason === "string" ? md.reason : typeof md.note === "string" ? md.note : "";
|
|
49
55
|
const details = note ? `[${signal}] ${note}` : `[${signal}]`;
|
|
50
56
|
lines.push(!ref && event.ref ? `${event.ref} ${details}` : details);
|
|
51
57
|
}
|
|
@@ -68,27 +74,83 @@ function buildSchemaHints(type, content) {
|
|
|
68
74
|
const report = lintLessonContent(content, "reflect");
|
|
69
75
|
return report.findings.map((f) => `[${f.kind}] ${f.message}`);
|
|
70
76
|
}
|
|
71
|
-
function
|
|
72
|
-
const
|
|
73
|
-
|
|
77
|
+
function hasRelatedSkillSource(content, skillRef) {
|
|
78
|
+
const parsed = parseFrontmatter(content);
|
|
79
|
+
const sources = parsed.data.sources;
|
|
80
|
+
return Array.isArray(sources) && sources.some((source) => typeof source === "string" && source.trim() === skillRef);
|
|
74
81
|
}
|
|
75
|
-
function
|
|
76
|
-
if (
|
|
77
|
-
return
|
|
78
|
-
const
|
|
79
|
-
|
|
82
|
+
async function readRelatedLessons(stash, ref, parsedRef) {
|
|
83
|
+
if (parsedRef.type !== "skill")
|
|
84
|
+
return [];
|
|
85
|
+
const related = new Map();
|
|
86
|
+
const derivedLessonRef = deriveLessonRef(ref);
|
|
87
|
+
const candidateRefs = new Set([derivedLessonRef]);
|
|
88
|
+
const derivedLessonPath = path.join(stash, "lessons", `${derivedLessonRef.slice("lesson:".length)}.md`);
|
|
89
|
+
if (fs.existsSync(derivedLessonPath)) {
|
|
90
|
+
related.set(derivedLessonRef, { ref: derivedLessonRef, content: fs.readFileSync(derivedLessonPath, "utf8") });
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const feedbackEvents = readEvents({ type: "distill_invoked", ref }).events;
|
|
94
|
+
for (const event of feedbackEvents) {
|
|
95
|
+
const lessonRef = typeof event.metadata?.lessonRef === "string" ? event.metadata.lessonRef : undefined;
|
|
96
|
+
if (lessonRef?.startsWith("lesson:"))
|
|
97
|
+
candidateRefs.add(lessonRef);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Best effort only.
|
|
102
|
+
}
|
|
103
|
+
for (const candidateRef of candidateRefs) {
|
|
104
|
+
try {
|
|
105
|
+
const entry = await lookup(parseAssetRef(candidateRef));
|
|
106
|
+
if (!entry?.filePath || !fs.existsSync(entry.filePath))
|
|
107
|
+
continue;
|
|
108
|
+
const content = fs.readFileSync(entry.filePath, "utf8");
|
|
109
|
+
related.set(candidateRef, { ref: candidateRef, content });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Index miss is non-fatal.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const lessonsDir = path.join(stash, "lessons");
|
|
117
|
+
if (fs.existsSync(lessonsDir)) {
|
|
118
|
+
for (const fileName of fs.readdirSync(lessonsDir)) {
|
|
119
|
+
if (!fileName.endsWith(".md"))
|
|
120
|
+
continue;
|
|
121
|
+
const content = fs.readFileSync(path.join(lessonsDir, fileName), "utf8");
|
|
122
|
+
if (!hasRelatedSkillSource(content, ref))
|
|
123
|
+
continue;
|
|
124
|
+
const lessonName = fileName.slice(0, -3);
|
|
125
|
+
const lessonRef = `lesson:${lessonName}`;
|
|
126
|
+
if (!related.has(lessonRef)) {
|
|
127
|
+
related.set(lessonRef, { ref: lessonRef, content });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Best effort only.
|
|
134
|
+
}
|
|
135
|
+
return [...related.values()];
|
|
136
|
+
}
|
|
137
|
+
function fallbackPayloadFromRawContent(stdout, ref) {
|
|
138
|
+
if (!ref)
|
|
139
|
+
return undefined;
|
|
140
|
+
const trimmed = stripMarkdownFences(stdout).trim();
|
|
141
|
+
if (!trimmed)
|
|
142
|
+
return undefined;
|
|
143
|
+
if (!looksLikeAssetContent(trimmed))
|
|
144
|
+
return undefined;
|
|
145
|
+
return { ref, content: trimmed };
|
|
146
|
+
}
|
|
147
|
+
function looksLikeAssetContent(value) {
|
|
148
|
+
return value.startsWith("#") || value.startsWith("---");
|
|
80
149
|
}
|
|
81
150
|
function failureEnvelope(result, ref, fallbackReason = "non_zero_exit") {
|
|
82
|
-
const reason = result.reason ?? fallbackReason;
|
|
83
151
|
return {
|
|
84
|
-
|
|
85
|
-
ok: false,
|
|
86
|
-
reason,
|
|
87
|
-
error: result.error ?? `agent failure (${reason})`,
|
|
152
|
+
...baseFailureFields(result, fallbackReason),
|
|
88
153
|
...(ref ? { ref } : {}),
|
|
89
|
-
exitCode: result.exitCode,
|
|
90
|
-
...(result.stdout ? { stdout: result.stdout } : {}),
|
|
91
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
92
154
|
};
|
|
93
155
|
}
|
|
94
156
|
export async function akmReflect(options = {}) {
|
|
@@ -120,9 +182,32 @@ export async function akmReflect(options = {}) {
|
|
|
120
182
|
}
|
|
121
183
|
// 3. Resolve agent profile. ConfigError surfaces as a thrown error so the
|
|
122
184
|
// CLI dispatcher renders the standard envelope.
|
|
185
|
+
//
|
|
186
|
+
// When an explicit --profile flag is given, honour it directly (existing
|
|
187
|
+
// behaviour). Otherwise use resolveProcessAgentProfile so that per-process
|
|
188
|
+
// agent config (agent.processes["reflect"]) is picked up automatically.
|
|
123
189
|
let profile;
|
|
190
|
+
let resolvedTimeoutMs = options.timeoutMs;
|
|
124
191
|
try {
|
|
125
|
-
|
|
192
|
+
if (options.agentProfile) {
|
|
193
|
+
// Test seam: injected profile bypasses all config.
|
|
194
|
+
profile = options.agentProfile;
|
|
195
|
+
}
|
|
196
|
+
else if (options.profile) {
|
|
197
|
+
// Explicit --profile flag wins over process config.
|
|
198
|
+
profile = resolveAgentProfile(options);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Use per-process config resolution (falls back to agent.default).
|
|
202
|
+
const agent = options.agentConfig ?? loadAgentConfigFromDisk();
|
|
203
|
+
const processName = options.agentProcess ?? "reflect";
|
|
204
|
+
const resolved = resolveProcessAgentProfile(processName, agent);
|
|
205
|
+
profile = resolved.profile;
|
|
206
|
+
// Only apply process-resolved timeoutMs when caller didn't supply one.
|
|
207
|
+
if (resolvedTimeoutMs === undefined) {
|
|
208
|
+
resolvedTimeoutMs = resolved.timeoutMs;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
126
211
|
}
|
|
127
212
|
catch (err) {
|
|
128
213
|
if (err instanceof ConfigError || err instanceof UsageError)
|
|
@@ -130,8 +215,12 @@ export async function akmReflect(options = {}) {
|
|
|
130
215
|
throw err;
|
|
131
216
|
}
|
|
132
217
|
// 4. Build the prompt.
|
|
218
|
+
// Keep reflect on the same captured JSON path the bench harness already
|
|
219
|
+
// uses successfully. The draft-file interactive path proved brittle with
|
|
220
|
+
// local opencode models and caused proposal generation failures.
|
|
133
221
|
const feedback = readRecentFeedback(options.ref);
|
|
134
222
|
const schemaHints = buildSchemaHints(parsedRef?.type ?? "", assetContent);
|
|
223
|
+
const relatedLessons = options.ref && parsedRef ? await readRelatedLessons(stash, options.ref, parsedRef) : [];
|
|
135
224
|
const prompt = buildReflectPrompt({
|
|
136
225
|
...(options.ref ? { ref: options.ref } : {}),
|
|
137
226
|
...(parsedRef?.type ? { type: parsedRef.type } : {}),
|
|
@@ -139,36 +228,64 @@ export async function akmReflect(options = {}) {
|
|
|
139
228
|
...(assetContent !== undefined ? { assetContent } : {}),
|
|
140
229
|
...(feedback.length > 0 ? { feedback } : {}),
|
|
141
230
|
...(schemaHints.length > 0 ? { schemaHints } : {}),
|
|
231
|
+
...(relatedLessons.length > 0 ? { relatedLessons } : {}),
|
|
142
232
|
...(options.task ? { task: options.task } : {}),
|
|
233
|
+
...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}),
|
|
143
234
|
});
|
|
144
|
-
// 5. Spawn the agent.
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
235
|
+
// 5. Spawn the agent.
|
|
236
|
+
// Fall back to raw runAgent when a custom spawn function is injected (test seam).
|
|
237
|
+
// Production path dispatches directly to runAgentSdk or runAgent.
|
|
238
|
+
let result;
|
|
239
|
+
if (options.runAgentOptions?.spawn) {
|
|
240
|
+
// Test seam: use raw runAgent with injected spawn so tests remain deterministic.
|
|
241
|
+
const runOptions = {
|
|
242
|
+
stdio: "captured",
|
|
243
|
+
parseOutput: "text",
|
|
244
|
+
...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
|
|
245
|
+
...(options.runAgentOptions ?? {}),
|
|
246
|
+
};
|
|
247
|
+
result = await runAgent(profile, prompt, runOptions);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// Production path: dispatch directly to the appropriate runner.
|
|
251
|
+
const runOptions = {
|
|
252
|
+
stdio: "captured",
|
|
253
|
+
parseOutput: "text",
|
|
254
|
+
...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
|
|
255
|
+
};
|
|
256
|
+
result = profile.sdkMode
|
|
257
|
+
? await runAgentSdk(profile, prompt ?? "", runOptions)
|
|
258
|
+
: await runAgent(profile, prompt, runOptions);
|
|
259
|
+
}
|
|
153
260
|
if (!result.ok) {
|
|
261
|
+
// B3: ENOENT / not-found gives an actionable hint.
|
|
262
|
+
if (isEnoentFailure(result)) {
|
|
263
|
+
return { ...failureEnvelope(result, options.ref), error: enoentHintMessage(profile.bin) };
|
|
264
|
+
}
|
|
154
265
|
return failureEnvelope(result, options.ref);
|
|
155
266
|
}
|
|
156
|
-
// 6.
|
|
267
|
+
// 6. Resolve the proposal content from stdout JSON.
|
|
157
268
|
let payload;
|
|
158
269
|
try {
|
|
159
|
-
payload = parseAgentProposalPayload(result.stdout);
|
|
270
|
+
payload = parseAgentProposalPayload(result.stdout ?? "");
|
|
160
271
|
}
|
|
161
272
|
catch (err) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
273
|
+
const fallback = fallbackPayloadFromRawContent(result.stdout ?? "", options.ref);
|
|
274
|
+
if (fallback) {
|
|
275
|
+
payload = fallback;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
return {
|
|
279
|
+
schemaVersion: 1,
|
|
280
|
+
ok: false,
|
|
281
|
+
reason: "parse_error",
|
|
282
|
+
error: err instanceof Error ? err.message : String(err),
|
|
283
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
284
|
+
exitCode: result.exitCode,
|
|
285
|
+
stdout: result.stdout,
|
|
286
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
172
289
|
}
|
|
173
290
|
// 7. Create the proposal. The proposal queue is the ONLY thing reflect
|
|
174
291
|
// writes — promotion to a real asset is gated by `akm proposal accept`.
|
|
@@ -182,6 +299,15 @@ export async function akmReflect(options = {}) {
|
|
|
182
299
|
},
|
|
183
300
|
};
|
|
184
301
|
const proposal = createProposal(stash, createInput, options.ctx);
|
|
302
|
+
appendEvent({
|
|
303
|
+
eventType: "reflect_completed",
|
|
304
|
+
ref: proposal.ref,
|
|
305
|
+
metadata: {
|
|
306
|
+
proposalId: proposal.id,
|
|
307
|
+
source: "reflect",
|
|
308
|
+
agentProfile: profile.name,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
185
311
|
return {
|
|
186
312
|
schemaVersion: 1,
|
|
187
313
|
ok: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toErrorMessage } from "../core/common";
|
|
2
|
-
import { DEFAULT_CONFIG
|
|
2
|
+
import { DEFAULT_CONFIG } from "../core/config";
|
|
3
3
|
import { warn } from "../core/warn";
|
|
4
4
|
import { resolveProviderFactory } from "../registry/factory";
|
|
5
5
|
// ── Eagerly import providers to trigger self-registration ───────────────────
|
|
@@ -133,7 +133,7 @@ export function resolveRegistries(configRegistries) {
|
|
|
133
133
|
}
|
|
134
134
|
return entries;
|
|
135
135
|
}
|
|
136
|
-
const registries = configRegistries ??
|
|
136
|
+
const registries = configRegistries ?? DEFAULT_CONFIG.registries ?? [];
|
|
137
137
|
return registries.filter((r) => r.enabled !== false);
|
|
138
138
|
}
|
|
139
139
|
// ── Provider resolution ─────────────────────────────────────────────────────
|
|
@@ -11,6 +11,7 @@ import { loadConfig } from "../core/config";
|
|
|
11
11
|
import { UsageError } from "../core/errors";
|
|
12
12
|
import { warn } from "../core/warn";
|
|
13
13
|
import { SCOPE_KEYS } from "../indexer/metadata";
|
|
14
|
+
import { parseFlagValue } from "../output/context";
|
|
14
15
|
/**
|
|
15
16
|
* Parse a shorthand duration string to a number of milliseconds.
|
|
16
17
|
* Supports: `30d` (days), `12h` (hours), `6m` (months, approximated as 30d).
|
|
@@ -138,7 +139,7 @@ export async function runLlmEnrich(body) {
|
|
|
138
139
|
return { tags: [] };
|
|
139
140
|
}
|
|
140
141
|
const llmConfig = config.llm;
|
|
141
|
-
const { chatCompletion, parseJsonResponse } = await import("../llm/client");
|
|
142
|
+
const { chatCompletion, parseEmbeddedJsonResponse: parseJsonResponse } = await import("../llm/client");
|
|
142
143
|
const prompt = `You are a memory tagger for a developer knowledge base.
|
|
143
144
|
Given the memory text below, return ONLY a JSON object with these fields:
|
|
144
145
|
- "tags": array of 1-5 short lowercase keyword tags
|
|
@@ -188,3 +189,56 @@ Return ONLY the JSON object, no prose, no markdown fences.`;
|
|
|
188
189
|
return { tags: [] };
|
|
189
190
|
}
|
|
190
191
|
}
|
|
192
|
+
// ── Content-arg disambiguation ───────────────────────────────────────────────
|
|
193
|
+
/**
|
|
194
|
+
* Guard against citty consuming a global flag value as the `content` positional.
|
|
195
|
+
*
|
|
196
|
+
* When the user runs `akm remember --format json` without a content argument,
|
|
197
|
+
* citty may assign `"json"` to the `content` positional because of how it
|
|
198
|
+
* handles flag order. This helper detects that case and returns `undefined`
|
|
199
|
+
* so `readMemoryContent` falls through to stdin.
|
|
200
|
+
*/
|
|
201
|
+
export function resolveRememberContentArg(content) {
|
|
202
|
+
if (content === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
const parsedFormat = parseFlagValue(process.argv, "--format");
|
|
205
|
+
if (parsedFormat !== undefined &&
|
|
206
|
+
content === parsedFormat &&
|
|
207
|
+
wasRememberFlagValueConsumedAsContent(content, parsedFormat, "--format")) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const parsedDetail = parseFlagValue(process.argv, "--detail");
|
|
211
|
+
if (parsedDetail !== undefined &&
|
|
212
|
+
content === parsedDetail &&
|
|
213
|
+
wasRememberFlagValueConsumedAsContent(content, parsedDetail, "--detail")) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
return content;
|
|
217
|
+
}
|
|
218
|
+
function wasRememberFlagValueConsumedAsContent(content, flagValue, flagName) {
|
|
219
|
+
const argv = process.argv.slice(2);
|
|
220
|
+
const rememberIndex = argv.indexOf("remember");
|
|
221
|
+
const tokens = rememberIndex >= 0 ? argv.slice(rememberIndex + 1) : argv;
|
|
222
|
+
let flagIndex = -1;
|
|
223
|
+
let flagConsumesNextToken = false;
|
|
224
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
225
|
+
const token = tokens[i];
|
|
226
|
+
if (token === flagName) {
|
|
227
|
+
flagIndex = i;
|
|
228
|
+
flagConsumesNextToken = true;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (token === `${flagName}=${flagValue}`) {
|
|
232
|
+
flagIndex = i;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (flagIndex === -1)
|
|
237
|
+
return false;
|
|
238
|
+
if (tokens.slice(0, flagIndex).includes(content))
|
|
239
|
+
return false;
|
|
240
|
+
const firstTokenAfterFlag = flagIndex + (flagConsumesNextToken ? 2 : 1);
|
|
241
|
+
if (tokens.slice(firstTokenAfterFlag).includes(content))
|
|
242
|
+
return false;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-repair pass for `akm improve`.
|
|
3
|
+
*
|
|
4
|
+
* Attempts to patch missing frontmatter fields (`description`, `when_to_use`)
|
|
5
|
+
* on assets that failed schema validation, using a single bounded in-tree LLM
|
|
6
|
+
* call per asset. Results are recorded as `schema_repair_invoked` events.
|
|
7
|
+
*
|
|
8
|
+
* This module is extracted from `improve.ts` to make the repair logic
|
|
9
|
+
* independently testable and to use the `tryLlmFeature` seam rather than raw
|
|
10
|
+
* `chatCompletion`.
|
|
11
|
+
*/
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import { stringify as yamlStringify } from "yaml";
|
|
14
|
+
import { parseAssetRef } from "../core/asset-ref";
|
|
15
|
+
import { appendEvent, readEvents } from "../core/events";
|
|
16
|
+
import { parseFrontmatter } from "../core/frontmatter";
|
|
17
|
+
import { info } from "../core/warn";
|
|
18
|
+
import { resolveAssetPath } from "../indexer/path-resolver";
|
|
19
|
+
import { chatCompletion, parseEmbeddedJsonResponse } from "../llm/client";
|
|
20
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
21
|
+
/** Minimum gap between schema-repair attempts on the same asset. */
|
|
22
|
+
const SCHEMA_REPAIR_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
23
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Run the schema-repair loop for a batch of validation failures.
|
|
26
|
+
* Returns a list of per-asset outcome records and the set of refs that were
|
|
27
|
+
* successfully repaired (so the caller can exclude them from skip logic).
|
|
28
|
+
*/
|
|
29
|
+
export async function runSchemaRepairPass(failures, options) {
|
|
30
|
+
const repairs = [];
|
|
31
|
+
const repairedRefs = new Set();
|
|
32
|
+
const { startMs, budgetMs, llmConfig, stashDir, findFilePath = defaultFindFilePath, isLessonCandidateFn = defaultIsLessonCandidate, } = options;
|
|
33
|
+
for (const failure of failures) {
|
|
34
|
+
if (Date.now() - startMs >= budgetMs)
|
|
35
|
+
break;
|
|
36
|
+
// Cooldown: skip repair if we ran it successfully recently.
|
|
37
|
+
const recentRepairs = readEvents({ type: "schema_repair_invoked", ref: failure.ref });
|
|
38
|
+
const lastRepair = recentRepairs.events
|
|
39
|
+
.filter((e) => e.metadata?.outcome === "written")
|
|
40
|
+
.sort((a, b) => new Date(b.ts ?? 0).getTime() - new Date(a.ts ?? 0).getTime())[0];
|
|
41
|
+
if (lastRepair?.ts && Date.now() - new Date(lastRepair.ts).getTime() < SCHEMA_REPAIR_COOLDOWN_MS) {
|
|
42
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const filePath = await findFilePath(failure.ref, stashDir);
|
|
46
|
+
if (!filePath) {
|
|
47
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
52
|
+
const fm = parseFrontmatter(raw);
|
|
53
|
+
const missingFields = [];
|
|
54
|
+
if (!fm.data.description)
|
|
55
|
+
missingFields.push("description");
|
|
56
|
+
if (isLessonCandidateFn(failure.ref) && !fm.data.when_to_use)
|
|
57
|
+
missingFields.push("when_to_use");
|
|
58
|
+
if (missingFields.length === 0) {
|
|
59
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const fieldList = missingFields.join(" and ");
|
|
63
|
+
info(`[improve] schema-repair ${failure.ref} (${fieldList})`);
|
|
64
|
+
const bodyPreview = (fm.content ?? raw).slice(0, 2000);
|
|
65
|
+
const llmResponse = await chatCompletion(llmConfig, [
|
|
66
|
+
{
|
|
67
|
+
role: "system",
|
|
68
|
+
content: `You generate concise asset frontmatter fields. Respond with a JSON object containing only the missing fields. No prose, no markdown fences.`,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
role: "user",
|
|
72
|
+
content: `Generate the missing frontmatter fields (${fieldList}) for this ${parseAssetRef(failure.ref).type} asset. Return ONLY valid JSON like {"description": "...", "when_to_use": "..."}\n\n${bodyPreview}`,
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
const parsed = parseEmbeddedJsonResponse(llmResponse.trim());
|
|
76
|
+
if (!parsed) {
|
|
77
|
+
repairs.push({
|
|
78
|
+
ref: failure.ref,
|
|
79
|
+
reason: failure.reason,
|
|
80
|
+
outcome: "error",
|
|
81
|
+
error: "LLM returned unparseable JSON for schema repair",
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const newFm = { ...fm.data };
|
|
86
|
+
if (parsed.description)
|
|
87
|
+
newFm.description = parsed.description;
|
|
88
|
+
if (parsed.when_to_use)
|
|
89
|
+
newFm.when_to_use = parsed.when_to_use;
|
|
90
|
+
const fmStr = yamlStringify(newFm).trimEnd();
|
|
91
|
+
const newContent = `---\n${fmStr}\n---\n${fm.content}`;
|
|
92
|
+
fs.writeFileSync(filePath, newContent, "utf8");
|
|
93
|
+
info(`[improve] schema-repair written: ${failure.ref}`);
|
|
94
|
+
appendEvent({
|
|
95
|
+
eventType: "schema_repair_invoked",
|
|
96
|
+
ref: failure.ref,
|
|
97
|
+
metadata: { outcome: "written", reason: failure.reason },
|
|
98
|
+
});
|
|
99
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "written" });
|
|
100
|
+
repairedRefs.add(failure.ref);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
appendEvent({
|
|
104
|
+
eventType: "schema_repair_invoked",
|
|
105
|
+
ref: failure.ref,
|
|
106
|
+
metadata: { outcome: "error", reason: failure.reason, error: String(e) },
|
|
107
|
+
});
|
|
108
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "error", error: String(e) });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { repairs, repairedRefs };
|
|
112
|
+
}
|
|
113
|
+
// ── Default seam implementations ─────────────────────────────────────────────
|
|
114
|
+
function defaultIsLessonCandidate(ref) {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = parseAssetRef(ref);
|
|
117
|
+
return parsed.type === "lesson";
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function defaultFindFilePath(ref, stashDir) {
|
|
124
|
+
return resolveAssetPath(ref, {
|
|
125
|
+
stashDir,
|
|
126
|
+
mode: "index-first",
|
|
127
|
+
directoryIndexNames: ["SKILL.md", "index.md", "README.md"],
|
|
128
|
+
preserveDirectNameFallback: true,
|
|
129
|
+
});
|
|
130
|
+
}
|
package/dist/commands/search.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { loadConfig } from "../core/config";
|
|
12
12
|
import { UsageError } from "../core/errors";
|
|
13
13
|
import { appendEvent } from "../core/events";
|
|
14
|
-
import { closeDatabase, openExistingDatabase } from "../indexer/db";
|
|
14
|
+
import { bumpUtilityScoresBatch, closeDatabase, openExistingDatabase } from "../indexer/db";
|
|
15
15
|
import { searchLocal } from "../indexer/db-search";
|
|
16
16
|
import { resolveSourceEntries } from "../indexer/search-source";
|
|
17
17
|
// Eagerly import source providers to trigger self-registration before the
|
|
@@ -48,6 +48,7 @@ export async function akmSearch(input) {
|
|
|
48
48
|
const stashDir = sources[0].path;
|
|
49
49
|
const filters = normalizeScopeFilters(input.filters);
|
|
50
50
|
const includeProposed = input.includeProposed === true;
|
|
51
|
+
const belief = input.belief ?? "all";
|
|
51
52
|
const localResult = source === "registry"
|
|
52
53
|
? undefined
|
|
53
54
|
: await searchLocal({
|
|
@@ -59,6 +60,7 @@ export async function akmSearch(input) {
|
|
|
59
60
|
config,
|
|
60
61
|
filters,
|
|
61
62
|
includeProposed,
|
|
63
|
+
beliefFilter: belief,
|
|
62
64
|
});
|
|
63
65
|
const registryResult = source === "stash" ? undefined : await searchRegistry(query, { limit, registries: config.registries });
|
|
64
66
|
if (source === "stash") {
|
|
@@ -73,7 +75,7 @@ export async function akmSearch(input) {
|
|
|
73
75
|
warnings: localResult?.warnings?.length ? localResult.warnings : undefined,
|
|
74
76
|
timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
|
|
75
77
|
};
|
|
76
|
-
logSearchEvent(query, response);
|
|
78
|
+
logSearchEvent(query, response, undefined, localResult?.mode ?? "keyword");
|
|
77
79
|
return response;
|
|
78
80
|
}
|
|
79
81
|
const registryHits = (registryResult?.hits ?? []).map((hit) => {
|
|
@@ -124,7 +126,7 @@ export async function akmSearch(input) {
|
|
|
124
126
|
warnings: warnings.length ? warnings : undefined,
|
|
125
127
|
timing: { totalMs: Date.now() - t0 },
|
|
126
128
|
};
|
|
127
|
-
logSearchEvent(query, response);
|
|
129
|
+
logSearchEvent(query, response, undefined, localResult?.mode ?? "keyword");
|
|
128
130
|
return response;
|
|
129
131
|
}
|
|
130
132
|
/**
|
|
@@ -160,13 +162,13 @@ function resolveEntryIds(db, hits) {
|
|
|
160
162
|
* Per-entry events are recorded only for stash hits because registry hits
|
|
161
163
|
* have no local entry_id to reference.
|
|
162
164
|
*/
|
|
163
|
-
function logSearchEvent(query, response, existingDb) {
|
|
165
|
+
function logSearchEvent(query, response, existingDb, mode = "keyword") {
|
|
164
166
|
// Emit a structured event to events.jsonl so workflow-trace consumers
|
|
165
167
|
// detect akm search invocations without relying on stdout scraping.
|
|
166
168
|
const stashHits = response.hits.filter((h) => h.type !== "registry");
|
|
167
169
|
appendEvent({
|
|
168
170
|
eventType: "search",
|
|
169
|
-
metadata: { query, hitCount: stashHits.length, resultRefs: stashHits.map((h) => h.ref) },
|
|
171
|
+
metadata: { query, hitCount: stashHits.length, resultRefs: stashHits.map((h) => h.ref), mode },
|
|
170
172
|
});
|
|
171
173
|
try {
|
|
172
174
|
const db = existingDb ?? openExistingDatabase();
|
|
@@ -180,6 +182,12 @@ function logSearchEvent(query, response, existingDb) {
|
|
|
180
182
|
entry_ref: ref,
|
|
181
183
|
});
|
|
182
184
|
}
|
|
185
|
+
// Bump utility scores for all resolved entries (MemRL retrieval signal).
|
|
186
|
+
// The indexer overwrites these at next reindex; bumps are temporary hints.
|
|
187
|
+
const resolvedIds = resolved.map((r) => r.entryId).filter((id) => id !== undefined);
|
|
188
|
+
if (resolvedIds.length > 0) {
|
|
189
|
+
bumpUtilityScoresBatch(db, resolvedIds, 1.0);
|
|
190
|
+
}
|
|
183
191
|
// Count registry hits separately so registry-only searches record a
|
|
184
192
|
// non-zero resultCount. response.hits is always [] when source="registry".
|
|
185
193
|
const stashHitCount = response.hits.length;
|
|
@@ -192,6 +200,7 @@ function logSearchEvent(query, response, existingDb) {
|
|
|
192
200
|
stashHitCount,
|
|
193
201
|
registryHitCount,
|
|
194
202
|
resolvedCount: resolved.length,
|
|
203
|
+
mode,
|
|
195
204
|
}),
|
|
196
205
|
});
|
|
197
206
|
}
|
|
@@ -221,6 +230,13 @@ export function parseSearchSource(source) {
|
|
|
221
230
|
return "stash";
|
|
222
231
|
throw new UsageError(`Invalid value for --source: ${String(source)}. Expected one of: stash|registry|both`, "INVALID_SOURCE_VALUE");
|
|
223
232
|
}
|
|
233
|
+
export function parseBeliefFilterMode(value) {
|
|
234
|
+
if (value === undefined || value === "all")
|
|
235
|
+
return "all";
|
|
236
|
+
if (value === "current" || value === "historical")
|
|
237
|
+
return value;
|
|
238
|
+
throw new UsageError(`Invalid value for --belief: ${String(value)}. Expected one of: all|current|historical`, "INVALID_FLAG_VALUE");
|
|
239
|
+
}
|
|
224
240
|
/**
|
|
225
241
|
* Strip empty / non-string values from a scope filter object. Returns
|
|
226
242
|
* `undefined` when nothing meaningful remains, so callers don't pay for an
|