akm-cli 0.9.0-beta.2 → 0.9.0-beta.26
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 +614 -0
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +5 -1
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/templates/html/default.html +78 -0
- package/dist/assets/templates/html/health.html +730 -0
- package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
- package/dist/cli/shared.js +21 -5
- package/dist/cli.js +47 -5
- package/dist/commands/agent/contribute-cli.js +16 -3
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph.js +75 -71
- package/dist/commands/health/checks.js +48 -0
- package/dist/commands/health/html-report.js +790 -0
- package/dist/commands/health.js +478 -15
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate.js +634 -111
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +145 -69
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +115 -1
- package/dist/commands/improve/extract-prompt.js +33 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +280 -35
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +139 -6
- package/dist/commands/improve/improve-profiles.js +12 -0
- package/dist/commands/improve/improve.js +1851 -515
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/proactive-maintenance.js +87 -0
- package/dist/commands/improve/procedural.js +409 -0
- package/dist/commands/improve/recombine.js +488 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +51 -1
- package/dist/commands/improve/related-sessions.js +120 -0
- package/dist/commands/improve/salience.js +386 -0
- package/dist/commands/improve/triage.js +95 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +34 -1
- package/dist/commands/lint/index.js +30 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +5 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain.js +73 -6
- package/dist/commands/proposal/proposal-cli.js +22 -10
- package/dist/commands/proposal/proposal.js +17 -1
- package/dist/commands/proposal/validators/proposals.js +369 -329
- package/dist/commands/read/curate.js +294 -79
- package/dist/commands/read/search-cli.js +7 -0
- package/dist/commands/read/search.js +1 -0
- package/dist/commands/remember.js +6 -2
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/stash-cli.js +10 -2
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/config/config-schema.js +241 -0
- package/dist/core/config/config.js +2 -2
- package/dist/core/logs-db.js +305 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/state-db.js +706 -42
- package/dist/indexer/db/db.js +347 -38
- package/dist/indexer/db/graph-db.js +81 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +114 -111
- package/dist/indexer/passes/memory-inference.js +71 -25
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +15 -4
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/integrations/harnesses/claude/session-log.js +27 -5
- package/dist/integrations/harnesses/opencode/session-log.js +9 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +38 -4
- package/dist/llm/embedder.js +27 -3
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/graph-extract.js +2 -1
- package/dist/llm/memory-infer.js +4 -8
- package/dist/llm/metadata-enhance.js +9 -1
- package/dist/llm/usage-persist.js +77 -0
- package/dist/llm/usage-telemetry.js +103 -0
- package/dist/output/context.js +3 -2
- package/dist/output/html-render.js +73 -0
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +17 -1
- package/dist/output/text/helpers.js +78 -1
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +1194 -607
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +455 -270
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/runner.js +99 -16
- package/dist/workflows/db.js +5 -2
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +1 -0
- package/package.json +7 -5
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* high-signal set of stash + registry hits and enrich each with the data
|
|
9
9
|
* needed to act (ref, run, parameters, follow-up command).
|
|
10
10
|
*
|
|
11
|
-
* The exported `akmCurate()` API is the single entry point. Internal
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
11
|
+
* The exported `akmCurate()` API is the single entry point. Internal helpers
|
|
12
|
+
* stay private. Tests can drive the public API or call the smaller pure
|
|
13
|
+
* helpers (`curateSearchResults`, `deriveCurateFallbackQueries`,
|
|
14
|
+
* `mergeCurateSearchResponses`) by importing them directly.
|
|
15
15
|
*/
|
|
16
|
+
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
16
17
|
import { rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
|
|
17
18
|
import { appendEvent } from "../../core/events.js";
|
|
18
19
|
import { closeDatabase, openExistingDatabase } from "../../indexer/db/db.js";
|
|
@@ -34,13 +35,19 @@ const CURATE_FALLBACK_FILTER_WORDS = new Set([
|
|
|
34
35
|
"to",
|
|
35
36
|
"with",
|
|
36
37
|
]);
|
|
37
|
-
const
|
|
38
|
-
const CURATED_TYPE_FALLBACK_INDEX = new Map(CURATED_TYPE_FALLBACK_ORDER.map((type, index) => [type, index]));
|
|
38
|
+
const CURATE_SHORT_FALLBACK_TOKENS = new Set(["ai", "ci", "cd", "go", "js", "ts"]);
|
|
39
39
|
const MIN_CURATE_FALLBACK_TOKEN_LENGTH = 3;
|
|
40
40
|
const MAX_CURATE_FALLBACK_KEYWORDS = 6;
|
|
41
41
|
export const CURATE_SEARCH_LIMIT_MULTIPLIER = 4;
|
|
42
42
|
export const MIN_CURATE_SEARCH_LIMIT = 12;
|
|
43
43
|
const DEFAULT_CURATE_LIMIT = 4;
|
|
44
|
+
const CURATE_CLOSE_SCORE_BAND = 0.12;
|
|
45
|
+
const CURATE_TAIL_SCORE_FLOOR = 0.35;
|
|
46
|
+
const CURATE_RELATIVE_SCORE_FLOOR = 0.7;
|
|
47
|
+
const CURATE_FALLBACK_TOP_SCORE_THRESHOLD = 0.8;
|
|
48
|
+
const CURATE_FALLBACK_STRONG_SCORE_FLOOR = 0.35;
|
|
49
|
+
const MAX_CURATE_SUPPORT_REFS = 2;
|
|
50
|
+
const CURATE_REFERENCE_QUERY_RE = /\b(?:reference|docs?|guide|how|explain|learn|readme|why)\b/;
|
|
44
51
|
/**
|
|
45
52
|
* Fire-and-forget: log a curate event to the usage_events table and events.jsonl.
|
|
46
53
|
* Never blocks the caller; errors are silently ignored.
|
|
@@ -63,6 +70,16 @@ function logCurateEvent(query, result) {
|
|
|
63
70
|
}),
|
|
64
71
|
source: "user",
|
|
65
72
|
});
|
|
73
|
+
for (const item of result.items) {
|
|
74
|
+
if (!("ref" in item) || typeof item.ref !== "string")
|
|
75
|
+
continue;
|
|
76
|
+
insertUsageEvent(db, {
|
|
77
|
+
event_type: "curate",
|
|
78
|
+
query,
|
|
79
|
+
entry_ref: item.ref,
|
|
80
|
+
source: "user",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
66
83
|
}
|
|
67
84
|
finally {
|
|
68
85
|
closeDatabase(db);
|
|
@@ -70,13 +87,8 @@ function logCurateEvent(query, result) {
|
|
|
70
87
|
}
|
|
71
88
|
catch (err) {
|
|
72
89
|
rethrowIfTestIsolationError(err);
|
|
73
|
-
/* ignore logging failures */
|
|
74
90
|
}
|
|
75
91
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Public curate entry point. Performs the search itself when
|
|
78
|
-
* `options.searchResponse` is not supplied.
|
|
79
|
-
*/
|
|
80
92
|
export async function akmCurate(options) {
|
|
81
93
|
const trimmedQuery = options.query.trim();
|
|
82
94
|
if (!trimmedQuery) {
|
|
@@ -88,8 +100,6 @@ export async function akmCurate(options) {
|
|
|
88
100
|
(await searchForCuration({
|
|
89
101
|
query: options.query,
|
|
90
102
|
type: options.type,
|
|
91
|
-
// Search deeper than the final curated count so we can pick one strong
|
|
92
|
-
// match per type and still have room for fallback retries.
|
|
93
103
|
limit: Math.max(limit * CURATE_SEARCH_LIMIT_MULTIPLIER, MIN_CURATE_SEARCH_LIMIT),
|
|
94
104
|
source,
|
|
95
105
|
}));
|
|
@@ -101,23 +111,22 @@ export async function curateSearchResults(query, result, limit, selectedType) {
|
|
|
101
111
|
const stashHits = result.hits.filter((hit) => hit.type !== "registry");
|
|
102
112
|
const registryHits = result.registryHits ?? [];
|
|
103
113
|
let selectedStashHits;
|
|
114
|
+
let supportRefsByRef = new Map();
|
|
104
115
|
if (selectedType && selectedType !== "any") {
|
|
105
116
|
selectedStashHits = stashHits.slice(0, limit);
|
|
106
117
|
}
|
|
107
118
|
else {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
const orderedTypes = orderCuratedTypes(query, Array.from(bestByType.keys()));
|
|
114
|
-
selectedStashHits = orderedTypes
|
|
115
|
-
.map((type) => bestByType.get(type))
|
|
116
|
-
.filter((hit) => Boolean(hit));
|
|
119
|
+
const selected = selectCuratedStashHits(query, stashHits, limit);
|
|
120
|
+
const preferred = preferBroadRootRepresentative(query, selected.selected, stashHits, selected.supportRefsByRef);
|
|
121
|
+
selectedStashHits = preferred.selected;
|
|
122
|
+
supportRefsByRef = preferred.supportRefsByRef;
|
|
117
123
|
}
|
|
118
124
|
const selectedRegistryHits = selectedStashHits.length >= limit ? [] : registryHits.slice(0, Math.min(2, limit - selectedStashHits.length));
|
|
125
|
+
const selectedRefs = new Set(selectedStashHits.map((hit) => hit.ref));
|
|
119
126
|
const items = [
|
|
120
|
-
...(await Promise.all(selectedStashHits
|
|
127
|
+
...(await Promise.all(selectedStashHits
|
|
128
|
+
.slice(0, limit)
|
|
129
|
+
.map((hit) => enrichCuratedStashHit(query, hit, supportRefsByRef.get(hit.ref) ?? [], selectedRefs)))),
|
|
121
130
|
...selectedRegistryHits.map((hit) => buildCuratedRegistryItem(query, hit)),
|
|
122
131
|
].slice(0, limit);
|
|
123
132
|
return {
|
|
@@ -128,37 +137,7 @@ export async function curateSearchResults(query, result, limit, selectedType) {
|
|
|
128
137
|
...(result.tip ? { tip: result.tip } : {}),
|
|
129
138
|
};
|
|
130
139
|
}
|
|
131
|
-
|
|
132
|
-
const lower = query.toLowerCase();
|
|
133
|
-
const boosts = new Map();
|
|
134
|
-
const addBoost = (type, amount) => boosts.set(type, (boosts.get(type) ?? 0) + amount);
|
|
135
|
-
if (/(run|script|bash|shell|cli|execute|automation|deploy|build|test|lint)/.test(lower)) {
|
|
136
|
-
addBoost("script", 6);
|
|
137
|
-
addBoost("command", 4);
|
|
138
|
-
}
|
|
139
|
-
if (/(guide|docs?|readme|reference|how|explain|learn|why)/.test(lower)) {
|
|
140
|
-
addBoost("knowledge", 6);
|
|
141
|
-
addBoost("skill", 4);
|
|
142
|
-
}
|
|
143
|
-
if (/(agent|assistant|planner|review|analy[sz]e|architect|prompt)/.test(lower)) {
|
|
144
|
-
addBoost("agent", 6);
|
|
145
|
-
addBoost("skill", 3);
|
|
146
|
-
}
|
|
147
|
-
if (/(config|template|release|generate|command)/.test(lower)) {
|
|
148
|
-
addBoost("command", 5);
|
|
149
|
-
}
|
|
150
|
-
if (/(memory|context|recall|remember)/.test(lower)) {
|
|
151
|
-
addBoost("memory", 6);
|
|
152
|
-
}
|
|
153
|
-
return [...types].sort((a, b) => {
|
|
154
|
-
const boostDiff = (boosts.get(b) ?? 0) - (boosts.get(a) ?? 0);
|
|
155
|
-
if (boostDiff !== 0)
|
|
156
|
-
return boostDiff;
|
|
157
|
-
return ((CURATED_TYPE_FALLBACK_INDEX.get(a) ?? Number.MAX_SAFE_INTEGER) -
|
|
158
|
-
(CURATED_TYPE_FALLBACK_INDEX.get(b) ?? Number.MAX_SAFE_INTEGER));
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
async function enrichCuratedStashHit(query, hit) {
|
|
140
|
+
async function enrichCuratedStashHit(query, hit, supportRefs, selectedRefs) {
|
|
162
141
|
let shown;
|
|
163
142
|
try {
|
|
164
143
|
shown = await akmShowUnified({ ref: hit.ref });
|
|
@@ -168,6 +147,7 @@ async function enrichCuratedStashHit(query, hit) {
|
|
|
168
147
|
}
|
|
169
148
|
const description = shown?.description ?? hit.description;
|
|
170
149
|
const preview = buildCuratedPreview(shown, hit);
|
|
150
|
+
const mergedSupportRefs = mergeCurateSupportRefs(supportRefs, shown?.related?.hits, selectedRefs, hit.ref);
|
|
171
151
|
return {
|
|
172
152
|
source: "stash",
|
|
173
153
|
type: shown?.type ?? hit.type,
|
|
@@ -178,6 +158,7 @@ async function enrichCuratedStashHit(query, hit) {
|
|
|
178
158
|
...(shown?.keys?.length ? { keys: shown.keys } : {}),
|
|
179
159
|
...(shown?.parameters?.length ? { parameters: shown.parameters } : {}),
|
|
180
160
|
...(shown?.run ? { run: shown.run } : {}),
|
|
161
|
+
...(mergedSupportRefs.length > 0 ? { supportRefs: mergedSupportRefs } : {}),
|
|
181
162
|
followUp: `akm show ${hit.ref}`,
|
|
182
163
|
reason: buildCuratedReason(query, shown?.type ?? hit.type),
|
|
183
164
|
...(hit.score !== undefined ? { score: hit.score } : {}),
|
|
@@ -209,19 +190,19 @@ function buildCuratedPreview(shown, hit) {
|
|
|
209
190
|
function buildCuratedReason(query, type) {
|
|
210
191
|
switch (type) {
|
|
211
192
|
case "script":
|
|
212
|
-
return `
|
|
193
|
+
return `Strong runnable script match for "${query}".`;
|
|
213
194
|
case "command":
|
|
214
|
-
return `
|
|
195
|
+
return `Strong reusable command/template match for "${query}".`;
|
|
215
196
|
case "knowledge":
|
|
216
|
-
return `
|
|
197
|
+
return `Strong reference document match for "${query}".`;
|
|
217
198
|
case "skill":
|
|
218
|
-
return `
|
|
199
|
+
return `Strong instructions/workflow match for "${query}".`;
|
|
219
200
|
case "agent":
|
|
220
|
-
return `
|
|
201
|
+
return `Strong specialized agent prompt match for "${query}".`;
|
|
221
202
|
case "memory":
|
|
222
|
-
return `
|
|
203
|
+
return `Strong saved context match for "${query}".`;
|
|
223
204
|
default:
|
|
224
|
-
return `
|
|
205
|
+
return `Strong ${type} match for "${query}".`;
|
|
225
206
|
}
|
|
226
207
|
}
|
|
227
208
|
function buildCurateSummary(query, items) {
|
|
@@ -229,27 +210,23 @@ function buildCurateSummary(query, items) {
|
|
|
229
210
|
return `No curated assets were selected for "${query}".`;
|
|
230
211
|
}
|
|
231
212
|
const labels = items.map((item) => `${item.type}:${item.name}`);
|
|
232
|
-
return `Selected ${items.length}
|
|
213
|
+
return `Selected ${items.length} curated result${items.length === 1 ? "" : "s"}: ${labels.join(", ")}.`;
|
|
233
214
|
}
|
|
234
|
-
function hasSearchResults(result) {
|
|
235
|
-
return result.hits.length > 0 || (result.registryHits?.length ?? 0) > 0;
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Extract a small set of fallback keywords when a prompt-style curate query
|
|
239
|
-
* returns no hits as a whole phrase.
|
|
240
|
-
*
|
|
241
|
-
* We keep up to MAX_CURATE_FALLBACK_KEYWORDS distinct keywords and drop short
|
|
242
|
-
* or common filler words so follow-up searches stay inexpensive while focusing
|
|
243
|
-
* on higher-signal terms.
|
|
244
|
-
*/
|
|
245
215
|
export function deriveCurateFallbackQueries(query) {
|
|
246
|
-
|
|
216
|
+
const normalizedWhole = query
|
|
217
|
+
.toLowerCase()
|
|
218
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
219
|
+
.trim();
|
|
220
|
+
const tokens = Array.from(new Set(query
|
|
247
221
|
.toLowerCase()
|
|
248
222
|
.split(/[^a-z0-9]+/)
|
|
249
223
|
.map((token) => token.trim())
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
224
|
+
.filter((token) => token.length > 0 &&
|
|
225
|
+
!CURATE_FALLBACK_FILTER_WORDS.has(token) &&
|
|
226
|
+
(token.length >= MIN_CURATE_FALLBACK_TOKEN_LENGTH || CURATE_SHORT_FALLBACK_TOKENS.has(token))))).slice(0, MAX_CURATE_FALLBACK_KEYWORDS);
|
|
227
|
+
if (tokens.length === 1 && tokens[0] === normalizedWhole)
|
|
228
|
+
return [];
|
|
229
|
+
return tokens;
|
|
253
230
|
}
|
|
254
231
|
export function mergeCurateSearchResponses(base, extras) {
|
|
255
232
|
const hitsByRef = new Map();
|
|
@@ -289,10 +266,10 @@ export function mergeCurateSearchResponses(base, extras) {
|
|
|
289
266
|
}
|
|
290
267
|
export async function searchForCuration(input) {
|
|
291
268
|
const initial = await akmSearch({ ...input, skipLogging: true });
|
|
292
|
-
if (
|
|
269
|
+
if (!shouldRunCurateFallback(initial, input.limit))
|
|
293
270
|
return initial;
|
|
294
271
|
const fallbackQueries = deriveCurateFallbackQueries(input.query);
|
|
295
|
-
if (fallbackQueries.length
|
|
272
|
+
if (fallbackQueries.length === 0)
|
|
296
273
|
return initial;
|
|
297
274
|
const fallbackResults = await Promise.all(fallbackQueries.map((token) => akmSearch({
|
|
298
275
|
query: token,
|
|
@@ -303,3 +280,241 @@ export async function searchForCuration(input) {
|
|
|
303
280
|
})));
|
|
304
281
|
return mergeCurateSearchResponses(initial, fallbackResults);
|
|
305
282
|
}
|
|
283
|
+
function parseCurateIntent(query) {
|
|
284
|
+
const lower = query.toLowerCase();
|
|
285
|
+
return {
|
|
286
|
+
executionHeavy: /(run|script|bash|shell|cli|execute|automation|deploy|build|test|lint)/.test(lower),
|
|
287
|
+
multiStep: /(plan|workflow|steps?|procedure|rollout|review|migration|release|checklist)/.test(lower),
|
|
288
|
+
delegation: /(agent|assistant|planner|reviewer|architect|prompt)/.test(lower),
|
|
289
|
+
recall: /(memory|context|recall|remember)/.test(lower),
|
|
290
|
+
reference: CURATE_REFERENCE_QUERY_RE.test(lower),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function computeCurateTypeNudge(type, intent) {
|
|
294
|
+
let nudge = 0;
|
|
295
|
+
if (intent.executionHeavy) {
|
|
296
|
+
if (type === "script")
|
|
297
|
+
nudge += 0.06;
|
|
298
|
+
else if (type === "command")
|
|
299
|
+
nudge += 0.04;
|
|
300
|
+
else if (type === "memory")
|
|
301
|
+
nudge -= 0.04;
|
|
302
|
+
}
|
|
303
|
+
if (intent.multiStep) {
|
|
304
|
+
if (type === "workflow")
|
|
305
|
+
nudge += 0.06;
|
|
306
|
+
else if (type === "skill")
|
|
307
|
+
nudge += 0.04;
|
|
308
|
+
else if (type === "knowledge")
|
|
309
|
+
nudge -= 0.02;
|
|
310
|
+
}
|
|
311
|
+
if (intent.delegation && type === "agent")
|
|
312
|
+
nudge += 0.06;
|
|
313
|
+
if (intent.recall && type === "memory")
|
|
314
|
+
nudge += 0.08;
|
|
315
|
+
if (intent.reference) {
|
|
316
|
+
if (type === "knowledge")
|
|
317
|
+
nudge += 0.05;
|
|
318
|
+
else if (type === "skill")
|
|
319
|
+
nudge += 0.02;
|
|
320
|
+
}
|
|
321
|
+
return nudge;
|
|
322
|
+
}
|
|
323
|
+
function getCurateFamily(ref) {
|
|
324
|
+
try {
|
|
325
|
+
const parsed = parseAssetRef(ref);
|
|
326
|
+
if (parsed.type === "skill") {
|
|
327
|
+
return { key: parsed.name, role: "root" };
|
|
328
|
+
}
|
|
329
|
+
if (parsed.type !== "knowledge")
|
|
330
|
+
return undefined;
|
|
331
|
+
const match = /^skills\/(.+?)\/references\/(.+)$/.exec(parsed.name);
|
|
332
|
+
if (!match)
|
|
333
|
+
return undefined;
|
|
334
|
+
return {
|
|
335
|
+
key: match[1],
|
|
336
|
+
role: "reference",
|
|
337
|
+
topicTokens: match[2]
|
|
338
|
+
.split(/[^a-z0-9]+/i)
|
|
339
|
+
.map((token) => token.trim().toLowerCase())
|
|
340
|
+
.filter(Boolean),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function annotateCurateHit(query, hit, index, intent) {
|
|
348
|
+
const rawScore = hit.score ?? 0;
|
|
349
|
+
const family = getCurateFamily(hit.ref);
|
|
350
|
+
let adjustedScore = rawScore + computeCurateTypeNudge(hit.type, intent);
|
|
351
|
+
if (family?.role === "root" && !isNarrowReferenceFamilyQuery(query, family))
|
|
352
|
+
adjustedScore += 0.07;
|
|
353
|
+
if (family?.role === "reference" && isNarrowReferenceFamilyQuery(query, family))
|
|
354
|
+
adjustedScore += 0.07;
|
|
355
|
+
return {
|
|
356
|
+
hit,
|
|
357
|
+
rawScore,
|
|
358
|
+
adjustedScore,
|
|
359
|
+
originalIndex: index,
|
|
360
|
+
family,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function compareCurateHits(a, b) {
|
|
364
|
+
const rawDiff = b.rawScore - a.rawScore;
|
|
365
|
+
if (Math.abs(rawDiff) > CURATE_CLOSE_SCORE_BAND)
|
|
366
|
+
return rawDiff;
|
|
367
|
+
const adjustedDiff = b.adjustedScore - a.adjustedScore;
|
|
368
|
+
if (adjustedDiff !== 0)
|
|
369
|
+
return adjustedDiff;
|
|
370
|
+
if (rawDiff !== 0)
|
|
371
|
+
return rawDiff;
|
|
372
|
+
return a.originalIndex - b.originalIndex;
|
|
373
|
+
}
|
|
374
|
+
function passesCurateScoreFloor(hit, leaderScore) {
|
|
375
|
+
if (leaderScore === undefined)
|
|
376
|
+
return true;
|
|
377
|
+
return hit.rawScore >= Math.max(CURATE_TAIL_SCORE_FLOOR, leaderScore * CURATE_RELATIVE_SCORE_FLOOR);
|
|
378
|
+
}
|
|
379
|
+
function isNarrowReferenceFamilyQuery(query, family) {
|
|
380
|
+
if (!family || family.role !== "reference")
|
|
381
|
+
return false;
|
|
382
|
+
const lower = query.toLowerCase();
|
|
383
|
+
if (CURATE_REFERENCE_QUERY_RE.test(lower))
|
|
384
|
+
return true;
|
|
385
|
+
return family.topicTokens.some((token) => token.length >= 3 && lower.includes(token));
|
|
386
|
+
}
|
|
387
|
+
function appendCurateSupportRef(supportRefsByRef, ownerRef, supportRef) {
|
|
388
|
+
const existing = supportRefsByRef.get(ownerRef) ?? [];
|
|
389
|
+
if (existing.some((entry) => entry.ref === supportRef.ref))
|
|
390
|
+
return;
|
|
391
|
+
supportRefsByRef.set(ownerRef, [...existing, supportRef]);
|
|
392
|
+
}
|
|
393
|
+
function selectCuratedStashHits(query, hits, limit) {
|
|
394
|
+
const intent = parseCurateIntent(query);
|
|
395
|
+
const collapsed = collapseCurateFamilies(query, hits);
|
|
396
|
+
const ranked = collapsed.hits
|
|
397
|
+
.map(({ hit, originalIndex }) => annotateCurateHit(query, hit, originalIndex, intent))
|
|
398
|
+
.sort(compareCurateHits);
|
|
399
|
+
const selected = [];
|
|
400
|
+
const supportRefsByRef = collapsed.supportRefsByRef;
|
|
401
|
+
let leaderScore;
|
|
402
|
+
for (const candidate of ranked) {
|
|
403
|
+
if (!passesCurateScoreFloor(candidate, leaderScore))
|
|
404
|
+
continue;
|
|
405
|
+
selected.push(candidate);
|
|
406
|
+
if (leaderScore === undefined)
|
|
407
|
+
leaderScore = candidate.rawScore;
|
|
408
|
+
if (selected.length >= limit)
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
return { selected: selected.map((entry) => entry.hit), supportRefsByRef };
|
|
412
|
+
}
|
|
413
|
+
function collapseCurateFamilies(query, hits) {
|
|
414
|
+
const passthrough = [];
|
|
415
|
+
const supportRefsByRef = new Map();
|
|
416
|
+
const groups = new Map();
|
|
417
|
+
for (const [index, hit] of hits.entries()) {
|
|
418
|
+
const family = getCurateFamily(hit.ref);
|
|
419
|
+
if (!family) {
|
|
420
|
+
passthrough.push({ hit, originalIndex: index });
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const group = groups.get(family.key) ?? { references: [] };
|
|
424
|
+
if (family.role === "root") {
|
|
425
|
+
if (!group.root)
|
|
426
|
+
group.root = { hit, originalIndex: index };
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
group.references.push({ hit, originalIndex: index });
|
|
430
|
+
}
|
|
431
|
+
groups.set(family.key, group);
|
|
432
|
+
}
|
|
433
|
+
const collapsedFamilies = [];
|
|
434
|
+
for (const group of groups.values()) {
|
|
435
|
+
const bestReference = group.references[0];
|
|
436
|
+
const representative = group.root && !isNarrowReferenceFamilyQuery(query, getCurateFamily(bestReference?.hit.ref ?? group.root.hit.ref))
|
|
437
|
+
? group.root
|
|
438
|
+
: (bestReference ?? group.root);
|
|
439
|
+
if (!representative)
|
|
440
|
+
continue;
|
|
441
|
+
collapsedFamilies.push(representative);
|
|
442
|
+
const supportCandidates = [group.root, ...group.references].filter((entry) => {
|
|
443
|
+
return entry !== undefined && entry.hit.ref !== representative.hit.ref;
|
|
444
|
+
});
|
|
445
|
+
for (const support of supportCandidates) {
|
|
446
|
+
appendCurateSupportRef(supportRefsByRef, representative.hit.ref, {
|
|
447
|
+
ref: support.hit.ref,
|
|
448
|
+
type: support.hit.type,
|
|
449
|
+
reason: "Related family asset to inspect next.",
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
hits: [...passthrough, ...collapsedFamilies].sort((a, b) => a.originalIndex - b.originalIndex),
|
|
455
|
+
supportRefsByRef,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function preferBroadRootRepresentative(query, selected, allHits, supportRefsByRef) {
|
|
459
|
+
const first = selected[0];
|
|
460
|
+
if (!first)
|
|
461
|
+
return { selected, supportRefsByRef };
|
|
462
|
+
const match = /^knowledge:skills\/(.+?)\/references\/(.+)$/.exec(first.ref);
|
|
463
|
+
if (!match)
|
|
464
|
+
return { selected, supportRefsByRef };
|
|
465
|
+
const lower = query.toLowerCase();
|
|
466
|
+
const topicTokens = match[2].split(/[^a-z0-9]+/i).filter(Boolean);
|
|
467
|
+
const wantsReference = CURATE_REFERENCE_QUERY_RE.test(lower) ||
|
|
468
|
+
topicTokens.some((token) => token.length >= 3 && lower.includes(token.toLowerCase()));
|
|
469
|
+
if (wantsReference)
|
|
470
|
+
return { selected, supportRefsByRef };
|
|
471
|
+
const rootRef = `skill:${match[1]}`;
|
|
472
|
+
const rootHit = allHits.find((hit) => hit.ref === rootRef);
|
|
473
|
+
if (!rootHit)
|
|
474
|
+
return { selected, supportRefsByRef };
|
|
475
|
+
const next = [rootHit, ...selected.filter((hit) => hit.ref !== first.ref && hit.ref !== rootRef)];
|
|
476
|
+
const merged = new Map(supportRefsByRef);
|
|
477
|
+
const priorSupport = merged.get(first.ref) ?? [];
|
|
478
|
+
for (const entry of priorSupport)
|
|
479
|
+
appendCurateSupportRef(merged, rootRef, entry);
|
|
480
|
+
appendCurateSupportRef(merged, rootRef, {
|
|
481
|
+
ref: first.ref,
|
|
482
|
+
type: first.type,
|
|
483
|
+
reason: "Related family asset to inspect next.",
|
|
484
|
+
});
|
|
485
|
+
merged.delete(first.ref);
|
|
486
|
+
return { selected: next, supportRefsByRef: merged };
|
|
487
|
+
}
|
|
488
|
+
function mergeCurateSupportRefs(seeded, relatedHits, selectedRefs, ownerRef) {
|
|
489
|
+
const merged = [];
|
|
490
|
+
for (const entry of seeded) {
|
|
491
|
+
if (entry.ref === ownerRef || selectedRefs.has(entry.ref))
|
|
492
|
+
continue;
|
|
493
|
+
if (merged.some((existing) => existing.ref === entry.ref))
|
|
494
|
+
continue;
|
|
495
|
+
merged.push(entry);
|
|
496
|
+
if (merged.length >= MAX_CURATE_SUPPORT_REFS)
|
|
497
|
+
return merged;
|
|
498
|
+
}
|
|
499
|
+
if (!Array.isArray(relatedHits))
|
|
500
|
+
return merged;
|
|
501
|
+
for (const hit of relatedHits) {
|
|
502
|
+
if (!hit.ref || hit.ref === ownerRef || selectedRefs.has(hit.ref))
|
|
503
|
+
continue;
|
|
504
|
+
if (merged.some((existing) => existing.ref === hit.ref))
|
|
505
|
+
continue;
|
|
506
|
+
merged.push({ ref: hit.ref, type: hit.type, reason: "Related asset via shared entities." });
|
|
507
|
+
if (merged.length >= MAX_CURATE_SUPPORT_REFS)
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
return merged;
|
|
511
|
+
}
|
|
512
|
+
function shouldRunCurateFallback(initial, desiredCount) {
|
|
513
|
+
const stashHits = initial.hits.filter((hit) => hit.type !== "registry");
|
|
514
|
+
if (stashHits.length === 0)
|
|
515
|
+
return true;
|
|
516
|
+
const topScore = stashHits[0]?.score ?? 0;
|
|
517
|
+
const strongFloor = Math.max(CURATE_FALLBACK_STRONG_SCORE_FLOOR, topScore * CURATE_RELATIVE_SCORE_FLOOR);
|
|
518
|
+
const strongCount = stashHits.filter((hit) => (hit.score ?? 0) >= strongFloor).length;
|
|
519
|
+
return !(topScore >= CURATE_FALLBACK_TOP_SCORE_THRESHOLD && strongCount >= Math.min(2, desiredCount));
|
|
520
|
+
}
|
|
@@ -63,6 +63,11 @@ export const searchCommand = defineJsonCommand({
|
|
|
63
63
|
description: "Disable the automatic project-context ranking boost (also disabled by AKM_DISABLE_PROJECT_CONTEXT=1).",
|
|
64
64
|
default: false,
|
|
65
65
|
},
|
|
66
|
+
"include-sessions": {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
description: "Include session assets (excluded from default search results via config.search.defaultExcludeTypes).",
|
|
69
|
+
default: false,
|
|
70
|
+
},
|
|
66
71
|
},
|
|
67
72
|
async run({ args }) {
|
|
68
73
|
const query = (args.query ?? "").trim();
|
|
@@ -79,6 +84,7 @@ export const searchCommand = defineJsonCommand({
|
|
|
79
84
|
const includeProposed = args["include-proposed"] === true;
|
|
80
85
|
const belief = parseBeliefFilterMode(typeof args.belief === "string" ? args.belief : undefined);
|
|
81
86
|
const noProjectContext = getHyphenatedBoolean(args, "no-project-context");
|
|
87
|
+
const includeSessions = getHyphenatedBoolean(args, "include-sessions");
|
|
82
88
|
// --no-project-context sets env so searchDatabase picks it up without
|
|
83
89
|
// threading the flag through the entire call stack.
|
|
84
90
|
if (noProjectContext)
|
|
@@ -91,6 +97,7 @@ export const searchCommand = defineJsonCommand({
|
|
|
91
97
|
filters,
|
|
92
98
|
includeProposed,
|
|
93
99
|
belief,
|
|
100
|
+
includeSessions,
|
|
94
101
|
eventSource: resolveEventSource(),
|
|
95
102
|
});
|
|
96
103
|
output("search", result);
|
|
@@ -108,6 +108,7 @@ export async function akmSearch(input) {
|
|
|
108
108
|
// Without this, the index (which spans every configured source)
|
|
109
109
|
// would leak hits from sources the caller did not request.
|
|
110
110
|
restrictToSources: namedSourceName !== undefined,
|
|
111
|
+
includeExcludedTypes: input.includeSessions === true,
|
|
111
112
|
});
|
|
112
113
|
const registryResult = source === "stash" ? undefined : await searchRegistry(query, { limit, registries: config.registries });
|
|
113
114
|
if (source === "stash") {
|
|
@@ -149,6 +149,10 @@ export async function runLlmEnrich(body) {
|
|
|
149
149
|
return { tags: [] };
|
|
150
150
|
}
|
|
151
151
|
const { chatCompletion, parseEmbeddedJsonResponse: parseJsonResponse } = await import("../llm/client.js");
|
|
152
|
+
// #576: attribute this entry point's LLM call to the `remember` stage. The
|
|
153
|
+
// wrapper is ambient — if a usage sink is active it tags the record; if not,
|
|
154
|
+
// it is a no-op.
|
|
155
|
+
const { withLlmStage } = await import("../llm/usage-telemetry.js");
|
|
152
156
|
const prompt = `You are a memory tagger for a developer knowledge base.
|
|
153
157
|
Given the memory text below, return ONLY a JSON object with these fields:
|
|
154
158
|
- "tags": array of 1-5 short lowercase keyword tags
|
|
@@ -164,10 +168,10 @@ Return ONLY the JSON object, no prose, no markdown fences.`;
|
|
|
164
168
|
const result = await (async () => {
|
|
165
169
|
try {
|
|
166
170
|
return await Promise.race([
|
|
167
|
-
chatCompletion(llmConfig, [
|
|
171
|
+
withLlmStage("remember", () => chatCompletion(llmConfig, [
|
|
168
172
|
{ role: "system", content: "Return only valid JSON. No prose." },
|
|
169
173
|
{ role: "user", content: prompt },
|
|
170
|
-
], { maxTokens: 256, temperature: 0.1 }),
|
|
174
|
+
], { maxTokens: 256, temperature: 0.1 })),
|
|
171
175
|
new Promise((_, reject) => {
|
|
172
176
|
timeoutHandle = setTimeout(() => reject(new Error("LLM enrichment timed out")), LLM_ENRICH_TIMEOUT_MS);
|
|
173
177
|
}),
|
|
@@ -205,7 +205,11 @@ async function updateRegistryEntry(entry, force) {
|
|
|
205
205
|
const synced = await syncFromRef(entry.ref, { force });
|
|
206
206
|
const installedEntry = {
|
|
207
207
|
id: synced.id,
|
|
208
|
-
source
|
|
208
|
+
// Preserve the original source classification. syncFromRef() re-derives the
|
|
209
|
+
// source type from the ref scheme (e.g. "github:" → source: "github"), but
|
|
210
|
+
// an update should not reclassify an existing entry. A writable entry stored
|
|
211
|
+
// as source: "git" would fail config validation if rewritten to "github".
|
|
212
|
+
source: entry.source,
|
|
209
213
|
ref: synced.ref,
|
|
210
214
|
artifactUrl: synced.artifactUrl,
|
|
211
215
|
resolvedVersion: synced.resolvedVersion,
|
|
@@ -71,6 +71,11 @@ export const indexCommand = defineCommand({
|
|
|
71
71
|
description: "When combined with --clean, report stale entries without deleting them.",
|
|
72
72
|
default: false,
|
|
73
73
|
},
|
|
74
|
+
background: {
|
|
75
|
+
type: "boolean",
|
|
76
|
+
description: "Run as a background process (suppresses interactive output, manages PID file).",
|
|
77
|
+
default: false,
|
|
78
|
+
},
|
|
74
79
|
},
|
|
75
80
|
async run({ args }) {
|
|
76
81
|
await runWithJsonErrors(async () => {
|
|
@@ -80,6 +85,7 @@ export const indexCommand = defineCommand({
|
|
|
80
85
|
if (getHyphenatedBoolean(args, "re-enrich") || parseFlagValue(process.argv, "--re-enrich") !== undefined) {
|
|
81
86
|
throw new UsageError("`akm index --re-enrich` has been removed. Re-enrichment of index-time LLM passes is not exposed in this slice.");
|
|
82
87
|
}
|
|
88
|
+
const isBackground = args.background === true;
|
|
83
89
|
const outputMode = getOutputMode();
|
|
84
90
|
const controller = new AbortController();
|
|
85
91
|
const abort = () => controller.abort(new Error("index interrupted"));
|
|
@@ -88,7 +94,7 @@ export const indexCommand = defineCommand({
|
|
|
88
94
|
const indexLogFile = path.join(getCacheDir(), "logs", "index", `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
|
|
89
95
|
setLogFile(indexLogFile);
|
|
90
96
|
const verbose = isVerbose();
|
|
91
|
-
const spin = !verbose && outputMode.format === "text" ? p.spinner() : null;
|
|
97
|
+
const spin = !verbose && !isBackground && outputMode.format === "text" ? p.spinner() : null;
|
|
92
98
|
if (spin) {
|
|
93
99
|
spin.start(`Building search index${args.full ? " (full rebuild)" : ""}...`);
|
|
94
100
|
}
|
|
@@ -114,7 +120,9 @@ export const indexCommand = defineCommand({
|
|
|
114
120
|
if (spin) {
|
|
115
121
|
spin.stop(`Indexed ${result.totalEntries} assets.`);
|
|
116
122
|
}
|
|
117
|
-
|
|
123
|
+
if (!isBackground) {
|
|
124
|
+
output("index", result);
|
|
125
|
+
}
|
|
118
126
|
}
|
|
119
127
|
catch (error) {
|
|
120
128
|
if (spin) {
|