akm-cli 0.9.0-beta.11 → 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.
Files changed (88) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/dist/assets/prompts/consolidate-system.md +23 -0
  3. package/dist/assets/prompts/contradiction-judge.md +33 -0
  4. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  5. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  6. package/dist/assets/prompts/extract-session.md +5 -1
  7. package/dist/assets/prompts/graph-extract-system.md +1 -0
  8. package/dist/assets/prompts/memory-infer-system.md +1 -0
  9. package/dist/assets/prompts/memory-infer-user.md +5 -0
  10. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  11. package/dist/assets/prompts/procedural-system.md +44 -0
  12. package/dist/assets/prompts/recombine-system.md +40 -0
  13. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  14. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  15. package/dist/assets/templates/html/health.html +25 -27
  16. package/dist/cli.js +2 -2
  17. package/dist/commands/agent/contribute-cli.js +16 -3
  18. package/dist/commands/feedback-cli.js +48 -44
  19. package/dist/commands/health/html-report.js +140 -16
  20. package/dist/commands/health.js +277 -1
  21. package/dist/commands/improve/calibration.js +161 -0
  22. package/dist/commands/improve/consolidate.js +595 -105
  23. package/dist/commands/improve/dedup.js +482 -0
  24. package/dist/commands/improve/distill.js +119 -64
  25. package/dist/commands/improve/encoding-salience.js +205 -0
  26. package/dist/commands/improve/extract-cli.js +115 -1
  27. package/dist/commands/improve/extract-prompt.js +32 -1
  28. package/dist/commands/improve/extract-watch.js +140 -0
  29. package/dist/commands/improve/extract.js +210 -30
  30. package/dist/commands/improve/feedback-valence.js +54 -0
  31. package/dist/commands/improve/homeostatic.js +467 -0
  32. package/dist/commands/improve/improve-auto-accept.js +80 -7
  33. package/dist/commands/improve/improve-profiles.js +8 -0
  34. package/dist/commands/improve/improve.js +991 -61
  35. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  36. package/dist/commands/improve/outcome-loop.js +256 -0
  37. package/dist/commands/improve/proactive-maintenance.js +9 -35
  38. package/dist/commands/improve/procedural.js +409 -0
  39. package/dist/commands/improve/recombine.js +488 -0
  40. package/dist/commands/improve/reflect.js +20 -1
  41. package/dist/commands/improve/related-sessions.js +120 -0
  42. package/dist/commands/improve/salience.js +386 -0
  43. package/dist/commands/improve/triage.js +95 -0
  44. package/dist/commands/lint/agent-linter.js +19 -24
  45. package/dist/commands/lint/base-linter.js +173 -60
  46. package/dist/commands/lint/command-linter.js +19 -24
  47. package/dist/commands/lint/env-key-rules.js +34 -1
  48. package/dist/commands/lint/index.js +30 -13
  49. package/dist/commands/lint/memory-linter.js +1 -1
  50. package/dist/commands/lint/registry.js +5 -2
  51. package/dist/commands/lint/task-linter.js +3 -3
  52. package/dist/commands/lint/workflow-linter.js +26 -1
  53. package/dist/commands/proposal/validators/proposals.js +4 -0
  54. package/dist/commands/read/curate.js +284 -86
  55. package/dist/commands/read/search-cli.js +7 -0
  56. package/dist/commands/read/search.js +1 -0
  57. package/dist/commands/sources/installed-stashes.js +5 -1
  58. package/dist/core/asset/frontmatter.js +166 -167
  59. package/dist/core/asset/markdown.js +8 -0
  60. package/dist/core/config/config-schema.js +211 -3
  61. package/dist/core/config/config.js +2 -2
  62. package/dist/core/logs-db.js +4 -3
  63. package/dist/core/state-db.js +555 -29
  64. package/dist/indexer/db/db.js +250 -27
  65. package/dist/indexer/db/graph-db.js +81 -86
  66. package/dist/indexer/graph/graph-boost.js +51 -41
  67. package/dist/indexer/passes/memory-inference.js +10 -3
  68. package/dist/indexer/passes/staleness-detect.js +2 -5
  69. package/dist/indexer/search/db-search.js +15 -4
  70. package/dist/indexer/search/ranking.js +4 -0
  71. package/dist/integrations/harnesses/claude/session-log.js +10 -0
  72. package/dist/integrations/harnesses/opencode/session-log.js +9 -0
  73. package/dist/integrations/session-logs/index.js +16 -0
  74. package/dist/llm/embedder.js +27 -3
  75. package/dist/llm/embedders/local.js +66 -2
  76. package/dist/llm/graph-extract.js +2 -1
  77. package/dist/llm/memory-infer.js +4 -8
  78. package/dist/llm/metadata-enhance.js +9 -1
  79. package/dist/output/shapes/curate.js +14 -2
  80. package/dist/output/text/helpers.js +9 -0
  81. package/dist/runtime.js +25 -1
  82. package/dist/scripts/migrate-storage.js +1025 -567
  83. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +435 -269
  84. package/dist/storage/sqlite-pragmas.js +146 -0
  85. package/dist/workflows/db.js +3 -4
  86. package/dist/workflows/validate-summary.js +2 -7
  87. package/docs/data-and-telemetry.md +1 -0
  88. package/package.json +5 -4
@@ -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
- * helpers stay private. Tests can drive the public API or call the smaller
13
- * pure helpers (`curateSearchResults`, `orderCuratedTypes`,
14
- * `deriveCurateFallbackQueries`) by importing them directly.
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 CURATED_TYPE_FALLBACK_ORDER = ["skill", "command", "script", "knowledge", "agent", "memory"];
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.
@@ -54,9 +61,6 @@ function logCurateEvent(query, result) {
54
61
  try {
55
62
  const db = openExistingDatabase();
56
63
  try {
57
- // Summary row (entry_ref = NULL): preserves the query → itemRefs audit
58
- // trail. Retrieval counting ignores NULL-ref rows, so this row is purely
59
- // informational.
60
64
  insertUsageEvent(db, {
61
65
  event_type: "curate",
62
66
  query,
@@ -66,10 +70,6 @@ function logCurateEvent(query, result) {
66
70
  }),
67
71
  source: "user",
68
72
  });
69
- // Per-item rows with entry_ref populated so curation registers as a real
70
- // retrieval signal in getRetrievalCounts (which counts 'curate' events).
71
- // Only stash items expose a canonical asset ref; registry hits
72
- // (`registry:<id>`) have no asset ref and are skipped here.
73
73
  for (const item of result.items) {
74
74
  if (!("ref" in item) || typeof item.ref !== "string")
75
75
  continue;
@@ -87,13 +87,8 @@ function logCurateEvent(query, result) {
87
87
  }
88
88
  catch (err) {
89
89
  rethrowIfTestIsolationError(err);
90
- /* ignore logging failures */
91
90
  }
92
91
  }
93
- /**
94
- * Public curate entry point. Performs the search itself when
95
- * `options.searchResponse` is not supplied.
96
- */
97
92
  export async function akmCurate(options) {
98
93
  const trimmedQuery = options.query.trim();
99
94
  if (!trimmedQuery) {
@@ -105,8 +100,6 @@ export async function akmCurate(options) {
105
100
  (await searchForCuration({
106
101
  query: options.query,
107
102
  type: options.type,
108
- // Search deeper than the final curated count so we can pick one strong
109
- // match per type and still have room for fallback retries.
110
103
  limit: Math.max(limit * CURATE_SEARCH_LIMIT_MULTIPLIER, MIN_CURATE_SEARCH_LIMIT),
111
104
  source,
112
105
  }));
@@ -118,23 +111,22 @@ export async function curateSearchResults(query, result, limit, selectedType) {
118
111
  const stashHits = result.hits.filter((hit) => hit.type !== "registry");
119
112
  const registryHits = result.registryHits ?? [];
120
113
  let selectedStashHits;
114
+ let supportRefsByRef = new Map();
121
115
  if (selectedType && selectedType !== "any") {
122
116
  selectedStashHits = stashHits.slice(0, limit);
123
117
  }
124
118
  else {
125
- const bestByType = new Map();
126
- for (const hit of stashHits) {
127
- if (!bestByType.has(hit.type))
128
- bestByType.set(hit.type, hit);
129
- }
130
- const orderedTypes = orderCuratedTypes(query, Array.from(bestByType.keys()));
131
- selectedStashHits = orderedTypes
132
- .map((type) => bestByType.get(type))
133
- .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;
134
123
  }
135
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));
136
126
  const items = [
137
- ...(await Promise.all(selectedStashHits.slice(0, limit).map((hit) => enrichCuratedStashHit(query, hit)))),
127
+ ...(await Promise.all(selectedStashHits
128
+ .slice(0, limit)
129
+ .map((hit) => enrichCuratedStashHit(query, hit, supportRefsByRef.get(hit.ref) ?? [], selectedRefs)))),
138
130
  ...selectedRegistryHits.map((hit) => buildCuratedRegistryItem(query, hit)),
139
131
  ].slice(0, limit);
140
132
  return {
@@ -145,37 +137,7 @@ export async function curateSearchResults(query, result, limit, selectedType) {
145
137
  ...(result.tip ? { tip: result.tip } : {}),
146
138
  };
147
139
  }
148
- export function orderCuratedTypes(query, types) {
149
- const lower = query.toLowerCase();
150
- const boosts = new Map();
151
- const addBoost = (type, amount) => boosts.set(type, (boosts.get(type) ?? 0) + amount);
152
- if (/(run|script|bash|shell|cli|execute|automation|deploy|build|test|lint)/.test(lower)) {
153
- addBoost("script", 6);
154
- addBoost("command", 4);
155
- }
156
- if (/(guide|docs?|readme|reference|how|explain|learn|why)/.test(lower)) {
157
- addBoost("knowledge", 6);
158
- addBoost("skill", 4);
159
- }
160
- if (/(agent|assistant|planner|review|analy[sz]e|architect|prompt)/.test(lower)) {
161
- addBoost("agent", 6);
162
- addBoost("skill", 3);
163
- }
164
- if (/(config|template|release|generate|command)/.test(lower)) {
165
- addBoost("command", 5);
166
- }
167
- if (/(memory|context|recall|remember)/.test(lower)) {
168
- addBoost("memory", 6);
169
- }
170
- return [...types].sort((a, b) => {
171
- const boostDiff = (boosts.get(b) ?? 0) - (boosts.get(a) ?? 0);
172
- if (boostDiff !== 0)
173
- return boostDiff;
174
- return ((CURATED_TYPE_FALLBACK_INDEX.get(a) ?? Number.MAX_SAFE_INTEGER) -
175
- (CURATED_TYPE_FALLBACK_INDEX.get(b) ?? Number.MAX_SAFE_INTEGER));
176
- });
177
- }
178
- async function enrichCuratedStashHit(query, hit) {
140
+ async function enrichCuratedStashHit(query, hit, supportRefs, selectedRefs) {
179
141
  let shown;
180
142
  try {
181
143
  shown = await akmShowUnified({ ref: hit.ref });
@@ -185,6 +147,7 @@ async function enrichCuratedStashHit(query, hit) {
185
147
  }
186
148
  const description = shown?.description ?? hit.description;
187
149
  const preview = buildCuratedPreview(shown, hit);
150
+ const mergedSupportRefs = mergeCurateSupportRefs(supportRefs, shown?.related?.hits, selectedRefs, hit.ref);
188
151
  return {
189
152
  source: "stash",
190
153
  type: shown?.type ?? hit.type,
@@ -195,6 +158,7 @@ async function enrichCuratedStashHit(query, hit) {
195
158
  ...(shown?.keys?.length ? { keys: shown.keys } : {}),
196
159
  ...(shown?.parameters?.length ? { parameters: shown.parameters } : {}),
197
160
  ...(shown?.run ? { run: shown.run } : {}),
161
+ ...(mergedSupportRefs.length > 0 ? { supportRefs: mergedSupportRefs } : {}),
198
162
  followUp: `akm show ${hit.ref}`,
199
163
  reason: buildCuratedReason(query, shown?.type ?? hit.type),
200
164
  ...(hit.score !== undefined ? { score: hit.score } : {}),
@@ -226,19 +190,19 @@ function buildCuratedPreview(shown, hit) {
226
190
  function buildCuratedReason(query, type) {
227
191
  switch (type) {
228
192
  case "script":
229
- return `Best runnable script match for "${query}".`;
193
+ return `Strong runnable script match for "${query}".`;
230
194
  case "command":
231
- return `Best reusable command/template match for "${query}".`;
195
+ return `Strong reusable command/template match for "${query}".`;
232
196
  case "knowledge":
233
- return `Best reference document match for "${query}".`;
197
+ return `Strong reference document match for "${query}".`;
234
198
  case "skill":
235
- return `Best instructions/workflow match for "${query}".`;
199
+ return `Strong instructions/workflow match for "${query}".`;
236
200
  case "agent":
237
- return `Best specialized agent prompt match for "${query}".`;
201
+ return `Strong specialized agent prompt match for "${query}".`;
238
202
  case "memory":
239
- return `Best saved context match for "${query}".`;
203
+ return `Strong saved context match for "${query}".`;
240
204
  default:
241
- return `Best ${type} match for "${query}".`;
205
+ return `Strong ${type} match for "${query}".`;
242
206
  }
243
207
  }
244
208
  function buildCurateSummary(query, items) {
@@ -246,27 +210,23 @@ function buildCurateSummary(query, items) {
246
210
  return `No curated assets were selected for "${query}".`;
247
211
  }
248
212
  const labels = items.map((item) => `${item.type}:${item.name}`);
249
- return `Selected ${items.length} high-signal result${items.length === 1 ? "" : "s"}: ${labels.join(", ")}.`;
250
- }
251
- function hasSearchResults(result) {
252
- return result.hits.length > 0 || (result.registryHits?.length ?? 0) > 0;
213
+ return `Selected ${items.length} curated result${items.length === 1 ? "" : "s"}: ${labels.join(", ")}.`;
253
214
  }
254
- /**
255
- * Extract a small set of fallback keywords when a prompt-style curate query
256
- * returns no hits as a whole phrase.
257
- *
258
- * We keep up to MAX_CURATE_FALLBACK_KEYWORDS distinct keywords and drop short
259
- * or common filler words so follow-up searches stay inexpensive while focusing
260
- * on higher-signal terms.
261
- */
262
215
  export function deriveCurateFallbackQueries(query) {
263
- return Array.from(new Set(query
216
+ const normalizedWhole = query
217
+ .toLowerCase()
218
+ .replace(/[^a-z0-9]+/g, " ")
219
+ .trim();
220
+ const tokens = Array.from(new Set(query
264
221
  .toLowerCase()
265
222
  .split(/[^a-z0-9]+/)
266
223
  .map((token) => token.trim())
267
- // Keep longer tokens so fallback stays focused on higher-signal terms
268
- // and avoids broad one- and two-letter matches that overwhelm curation.
269
- .filter((token) => token.length >= MIN_CURATE_FALLBACK_TOKEN_LENGTH && !CURATE_FALLBACK_FILTER_WORDS.has(token)))).slice(0, MAX_CURATE_FALLBACK_KEYWORDS);
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;
270
230
  }
271
231
  export function mergeCurateSearchResponses(base, extras) {
272
232
  const hitsByRef = new Map();
@@ -306,10 +266,10 @@ export function mergeCurateSearchResponses(base, extras) {
306
266
  }
307
267
  export async function searchForCuration(input) {
308
268
  const initial = await akmSearch({ ...input, skipLogging: true });
309
- if (hasSearchResults(initial))
269
+ if (!shouldRunCurateFallback(initial, input.limit))
310
270
  return initial;
311
271
  const fallbackQueries = deriveCurateFallbackQueries(input.query);
312
- if (fallbackQueries.length <= 1)
272
+ if (fallbackQueries.length === 0)
313
273
  return initial;
314
274
  const fallbackResults = await Promise.all(fallbackQueries.map((token) => akmSearch({
315
275
  query: token,
@@ -320,3 +280,241 @@ export async function searchForCuration(input) {
320
280
  })));
321
281
  return mergeCurateSearchResponses(initial, fallbackResults);
322
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") {
@@ -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: synced.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,