akm-cli 0.9.0-beta.5 → 0.9.0-beta.50

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 (207) hide show
  1. package/CHANGELOG.md +709 -0
  2. package/dist/assets/profiles/default.json +9 -4
  3. package/dist/assets/profiles/frequent.json +1 -1
  4. package/dist/assets/profiles/memory-focus.json +1 -1
  5. package/dist/assets/profiles/quick.json +1 -1
  6. package/dist/assets/profiles/synthesize.json +15 -0
  7. package/dist/assets/profiles/thorough.json +1 -1
  8. package/dist/assets/prompts/consolidate-system.md +23 -0
  9. package/dist/assets/prompts/contradiction-judge.md +33 -0
  10. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  11. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  12. package/dist/assets/prompts/extract-session.md +6 -2
  13. package/dist/assets/prompts/graph-extract-system.md +1 -0
  14. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  15. package/dist/assets/prompts/memory-infer-system.md +1 -0
  16. package/dist/assets/prompts/memory-infer-user.md +5 -0
  17. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  18. package/dist/assets/prompts/procedural-system.md +44 -0
  19. package/dist/assets/prompts/recombine-system.md +40 -0
  20. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  21. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  24. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  25. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  31. package/dist/assets/templates/html/health.html +281 -111
  32. package/dist/assets/wiki/ingest-workflow-template.md +17 -10
  33. package/dist/cli/shared.js +28 -0
  34. package/dist/cli.js +15 -5
  35. package/dist/commands/agent/agent-dispatch.js +2 -2
  36. package/dist/commands/agent/agent-support.js +0 -7
  37. package/dist/commands/agent/contribute-cli.js +17 -4
  38. package/dist/commands/env/env-cli.js +16 -24
  39. package/dist/commands/env/secret-cli.js +12 -20
  40. package/dist/commands/feedback-cli.js +15 -6
  41. package/dist/commands/graph/graph-cli.js +5 -13
  42. package/dist/commands/graph/graph.js +76 -72
  43. package/dist/commands/health/checks.js +48 -0
  44. package/dist/commands/health/html-report.js +422 -80
  45. package/dist/commands/health.js +386 -9
  46. package/dist/commands/improve/calibration.js +161 -0
  47. package/dist/commands/improve/consolidate/chunking.js +141 -0
  48. package/dist/commands/improve/consolidate/eligibility.js +81 -0
  49. package/dist/commands/improve/consolidate/merge.js +145 -0
  50. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  51. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  52. package/dist/commands/improve/consolidate.js +635 -660
  53. package/dist/commands/improve/dedup.js +482 -0
  54. package/dist/commands/improve/distill.js +159 -69
  55. package/dist/commands/improve/eligibility.js +434 -0
  56. package/dist/commands/improve/encoding-salience.js +205 -0
  57. package/dist/commands/improve/extract-cli.js +124 -2
  58. package/dist/commands/improve/extract-prompt.js +39 -2
  59. package/dist/commands/improve/extract-watch.js +140 -0
  60. package/dist/commands/improve/extract.js +389 -40
  61. package/dist/commands/improve/feedback-valence.js +54 -0
  62. package/dist/commands/improve/homeostatic.js +467 -0
  63. package/dist/commands/improve/improve-auto-accept.js +109 -6
  64. package/dist/commands/improve/improve-cli.js +35 -60
  65. package/dist/commands/improve/improve-profiles.js +14 -0
  66. package/dist/commands/improve/improve-result-file.js +5 -23
  67. package/dist/commands/improve/improve-session.js +58 -0
  68. package/dist/commands/improve/improve.js +485 -2498
  69. package/dist/commands/improve/locks.js +154 -0
  70. package/dist/commands/improve/loop-stages.js +1083 -0
  71. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  72. package/dist/commands/improve/outcome-loop.js +256 -0
  73. package/dist/commands/improve/preparation.js +1966 -0
  74. package/dist/commands/improve/proactive-maintenance.js +115 -0
  75. package/dist/commands/improve/procedural.js +418 -0
  76. package/dist/commands/improve/recombine.js +813 -0
  77. package/dist/commands/improve/reflect-noise.js +0 -0
  78. package/dist/commands/improve/reflect.js +183 -40
  79. package/dist/commands/improve/salience.js +438 -0
  80. package/dist/commands/improve/triage.js +93 -0
  81. package/dist/commands/lint/agent-linter.js +19 -24
  82. package/dist/commands/lint/base-linter.js +173 -60
  83. package/dist/commands/lint/command-linter.js +19 -24
  84. package/dist/commands/lint/env-key-rules.js +34 -1
  85. package/dist/commands/lint/fact-linter.js +39 -0
  86. package/dist/commands/lint/index.js +31 -13
  87. package/dist/commands/lint/memory-linter.js +1 -1
  88. package/dist/commands/lint/registry.js +7 -2
  89. package/dist/commands/lint/task-linter.js +3 -3
  90. package/dist/commands/lint/workflow-linter.js +26 -1
  91. package/dist/commands/proposal/drain-policies.js +5 -0
  92. package/dist/commands/proposal/drain.js +43 -50
  93. package/dist/commands/proposal/proposal-cli.js +21 -31
  94. package/dist/commands/proposal/proposal.js +5 -0
  95. package/dist/commands/proposal/propose.js +7 -2
  96. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  97. package/dist/commands/proposal/validators/proposals.js +189 -63
  98. package/dist/commands/read/curate.js +414 -94
  99. package/dist/commands/read/knowledge.js +2 -2
  100. package/dist/commands/read/search-cli.js +7 -0
  101. package/dist/commands/read/search.js +1 -0
  102. package/dist/commands/read/show.js +67 -2
  103. package/dist/commands/sources/init.js +36 -9
  104. package/dist/commands/sources/installed-stashes.js +5 -1
  105. package/dist/commands/sources/schema-repair.js +13 -1
  106. package/dist/commands/sources/self-update.js +2 -2
  107. package/dist/commands/sources/stash-cli.js +28 -40
  108. package/dist/commands/sources/stash-skeleton.js +23 -8
  109. package/dist/commands/tasks/tasks-cli.js +19 -27
  110. package/dist/commands/tasks/tasks.js +1 -1
  111. package/dist/commands/wiki-cli.js +21 -35
  112. package/dist/core/asset/asset-registry.js +2 -0
  113. package/dist/core/asset/asset-spec.js +14 -0
  114. package/dist/core/asset/frontmatter.js +166 -167
  115. package/dist/core/asset/markdown.js +8 -0
  116. package/dist/core/authoring-rules.js +92 -0
  117. package/dist/core/common.js +0 -5
  118. package/dist/core/config/config-schema.js +340 -56
  119. package/dist/core/config/config-types.js +3 -3
  120. package/dist/core/config/config.js +28 -7
  121. package/dist/core/events.js +3 -7
  122. package/dist/core/improve-types.js +11 -8
  123. package/dist/core/logs-db.js +10 -66
  124. package/dist/core/parse.js +36 -16
  125. package/dist/core/paths.js +3 -0
  126. package/dist/core/standards/resolve-standards-context.js +87 -0
  127. package/dist/core/standards/resolve-stash-standards.js +99 -0
  128. package/dist/core/standards/resolve-type-conventions.js +66 -0
  129. package/dist/core/state/migrations.js +714 -0
  130. package/dist/core/state-db.js +525 -474
  131. package/dist/indexer/db/db.js +439 -247
  132. package/dist/indexer/db/graph-db.js +129 -86
  133. package/dist/indexer/ensure-index.js +152 -17
  134. package/dist/indexer/graph/graph-boost.js +51 -41
  135. package/dist/indexer/graph/graph-extraction.js +218 -4
  136. package/dist/indexer/index-writer-lock.js +99 -0
  137. package/dist/indexer/indexer.js +123 -221
  138. package/dist/indexer/passes/dir-staleness.js +114 -0
  139. package/dist/indexer/passes/memory-inference.js +10 -3
  140. package/dist/indexer/passes/staleness-detect.js +2 -5
  141. package/dist/indexer/search/db-search.js +15 -4
  142. package/dist/indexer/search/ranking-contributors.js +22 -0
  143. package/dist/indexer/search/ranking.js +4 -0
  144. package/dist/indexer/search/search-source.js +10 -24
  145. package/dist/indexer/search/semantic-status.js +4 -0
  146. package/dist/indexer/walk/matchers.js +9 -0
  147. package/dist/integrations/agent/config.js +6 -53
  148. package/dist/integrations/agent/index.js +2 -18
  149. package/dist/integrations/agent/prompts.js +74 -8
  150. package/dist/integrations/agent/runner-dispatch.js +59 -0
  151. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  152. package/dist/integrations/harnesses/index.js +2 -3
  153. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  154. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  155. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
  156. package/dist/integrations/session-logs/index.js +16 -0
  157. package/dist/llm/client.js +45 -15
  158. package/dist/llm/embedder.js +42 -3
  159. package/dist/llm/embedders/deterministic.js +66 -0
  160. package/dist/llm/embedders/local.js +66 -2
  161. package/dist/llm/feature-gate.js +8 -4
  162. package/dist/llm/graph-extract.js +67 -44
  163. package/dist/llm/memory-infer.js +38 -30
  164. package/dist/llm/metadata-enhance.js +44 -31
  165. package/dist/llm/structured-call.js +49 -0
  166. package/dist/output/context.js +5 -5
  167. package/dist/output/renderers.js +73 -1
  168. package/dist/output/shapes/curate.js +14 -2
  169. package/dist/output/shapes/passthrough.js +0 -1
  170. package/dist/output/text/helpers.js +16 -1
  171. package/dist/registry/providers/skills-sh.js +21 -147
  172. package/dist/registry/providers/static-index.js +15 -157
  173. package/dist/registry/resolve.js +22 -9
  174. package/dist/runtime.js +25 -1
  175. package/dist/scripts/migrate-storage.js +2136 -1596
  176. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +682 -433
  177. package/dist/setup/setup.js +29 -8
  178. package/dist/sources/providers/filesystem.js +0 -1
  179. package/dist/sources/providers/git-install.js +206 -0
  180. package/dist/sources/providers/git-provider.js +234 -0
  181. package/dist/sources/providers/git-stash.js +248 -0
  182. package/dist/sources/providers/git.js +10 -661
  183. package/dist/sources/providers/npm.js +2 -6
  184. package/dist/sources/providers/sync-from-ref.js +9 -1
  185. package/dist/sources/providers/tar-utils.js +16 -8
  186. package/dist/sources/providers/website.js +2 -3
  187. package/dist/sources/website-ingest.js +51 -9
  188. package/dist/sources/wiki-fetchers/registry.js +53 -0
  189. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  190. package/dist/storage/database.js +45 -10
  191. package/dist/storage/managed-db.js +82 -0
  192. package/dist/storage/repositories/registry-cache.js +92 -0
  193. package/dist/storage/sqlite-pragmas.js +146 -0
  194. package/dist/tasks/backends/cron.js +1 -1
  195. package/dist/tasks/backends/launchd.js +1 -1
  196. package/dist/tasks/backends/schtasks.js +1 -1
  197. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  198. package/dist/tasks/runner.js +5 -13
  199. package/dist/wiki/wiki.js +37 -0
  200. package/dist/workflows/db.js +3 -4
  201. package/dist/workflows/runtime/runs.js +1 -117
  202. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  203. package/dist/workflows/validate-summary.js +2 -7
  204. package/docs/data-and-telemetry.md +1 -0
  205. package/package.json +9 -7
  206. package/dist/commands/db-cli.js +0 -23
  207. package/dist/indexer/db/db-backup.js +0 -376
@@ -18,20 +18,16 @@
18
18
  * the connection via `resolveIndexPassLLM("memory", config)` and pass it
19
19
  * straight through.
20
20
  */
21
+ import memoryInferSystemPrompt from "../assets/prompts/memory-infer-system.md" with { type: "text" };
22
+ import memoryInferUserPrompt from "../assets/prompts/memory-infer-user.md" with { type: "text" };
21
23
  import { toErrorMessage } from "../core/common.js";
22
24
  import { warn } from "../core/warn.js";
23
- import { chatCompletion, LlmCallError, parseEmbeddedJsonResponse } from "./client.js";
24
- import { tryLlmFeature } from "./feature-gate.js";
25
+ import { parseEmbeddedJsonResponse } from "./client.js";
26
+ import { callStructured } from "./structured-call.js";
25
27
  /** Hard cap on body chars sent to the model — pragmatic and matches `runLlmEnrich`. */
26
28
  const MAX_BODY_CHARS = 4000;
27
- const SYSTEM_PROMPT = "You compress a developer memory into one high-signal derived memory for later retrieval. " +
28
- "Return only valid JSON. No prose outside the JSON object. No markdown fences.";
29
- const USER_PROMPT_PREFIX = `Compress the memory below into one derived memory. Output ONLY JSON:
30
- {"title":"short title string","description":"one sentence summary string","tags":["tag1","tag2"],"searchHints":["search phrase 1","search phrase 2"],"content":"2-3 sentence compressed body preserving key facts verbatim"}
31
- Rules: be specific, no vague generalizations, preserve key facts (names/versions/paths/config keys verbatim), merge related points, 3-8 tags, 3-6 searchHints. The content field must be a plain string with 2-3 sentences.
32
-
33
- Memory:
34
- `;
29
+ const SYSTEM_PROMPT = memoryInferSystemPrompt;
30
+ const USER_PROMPT_PREFIX = memoryInferUserPrompt;
35
31
  /**
36
32
  * Strict JSON Schema for the derived-memory payload. Sent to providers that
37
33
  * opt in via `LlmConnectionConfig.supportsJsonSchema = true`; the client
@@ -63,26 +59,39 @@ const DERIVED_MEMORY_JSON_SCHEMA = {
63
59
  * Errors are logged via `warn()` but never thrown — a failed split for one memory
64
60
  * must not abort the rest of the index pass.
65
61
  *
66
- * Routes through `tryLlmFeature("memory_inference", ...)` so the feature gate
67
- * and onFallback hook are honoured uniformly (Fix C5).
62
+ * Routes through `callStructured({ feature: "memory_inference", ... })` so the
63
+ * feature gate, error classification, and onFallback hook are honoured uniformly
64
+ * (Fix C5).
68
65
  */
69
66
  export async function compressMemoryToDerivedMemory(llmConfig, body, signal, akmConfig, onFallback, telemetry, onRetryAttempt) {
70
67
  const trimmedBody = body.trim();
71
68
  if (!trimmedBody)
72
69
  return undefined;
73
70
  const userPrompt = `${USER_PROMPT_PREFIX}${trimmedBody.slice(0, MAX_BODY_CHARS)}`;
74
- return tryLlmFeature("memory_inference", akmConfig, async () => {
75
- try {
76
- const raw = await chatCompletion(llmConfig, [
77
- { role: "system", content: SYSTEM_PROMPT },
78
- { role: "user", content: userPrompt },
79
- ], {
80
- temperature: 0.1,
81
- timeoutMs: llmConfig.timeoutMs,
82
- signal,
83
- responseSchema: DERIVED_MEMORY_JSON_SCHEMA,
84
- onRetryAttempt,
85
- });
71
+ // Memory-inference is ALWAYS gated: no `akmConfig` gate closed (no chat,
72
+ // `disabled` fallback), never the seam's ungated/propagate path (which is for
73
+ // direct callers like `enhanceMetadata`). This is the gate-closed branch
74
+ // `tryLlmFeature(_, undefined, _)` took before the migration.
75
+ if (!akmConfig) {
76
+ onFallback?.({ feature: "memory_inference", reason: "disabled" });
77
+ return undefined;
78
+ }
79
+ return callStructured({
80
+ feature: "memory_inference",
81
+ akmConfig,
82
+ config: llmConfig,
83
+ messages: [
84
+ { role: "system", content: SYSTEM_PROMPT },
85
+ { role: "user", content: userPrompt },
86
+ ],
87
+ request: {
88
+ temperature: 0.1,
89
+ timeoutMs: llmConfig.timeoutMs,
90
+ signal,
91
+ responseSchema: DERIVED_MEMORY_JSON_SCHEMA,
92
+ onRetryAttempt,
93
+ },
94
+ parse: (raw) => {
86
95
  if (!raw)
87
96
  return undefined;
88
97
  const parsed = parseEmbeddedJsonResponse(raw);
@@ -112,9 +121,9 @@ export async function compressMemoryToDerivedMemory(llmConfig, body, signal, akm
112
121
  return undefined;
113
122
  }
114
123
  return { title, description, tags, searchHints, content };
115
- }
116
- catch (err) {
117
- if (err instanceof LlmCallError && err.code === "provider_html_error") {
124
+ },
125
+ onError: (cls, err) => {
126
+ if (cls === "html") {
118
127
  if (telemetry)
119
128
  telemetry.htmlErrorCount = (telemetry.htmlErrorCount ?? 0) + 1;
120
129
  warn(`memory inference: provider returned HTML instead of JSON; skipping memory: ${toErrorMessage(err)}`);
@@ -122,9 +131,8 @@ export async function compressMemoryToDerivedMemory(llmConfig, body, signal, akm
122
131
  }
123
132
  warn(`memory inference failed: ${toErrorMessage(err)}`);
124
133
  return undefined;
125
- }
126
- }, undefined, {
127
- timeoutMs: llmConfig.timeoutMs,
134
+ },
135
+ fallback: undefined,
128
136
  onFallback,
129
137
  });
130
138
  }
@@ -1,9 +1,17 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { chatCompletion, parseJsonResponse } from "./client.js";
5
- import { tryLlmFeature } from "./feature-gate.js";
6
- const SYSTEM_PROMPT = `You are a metadata generator for a developer asset registry. Given a script/skill/command/agent entry, generate improved metadata. Respond with ONLY valid JSON, no markdown fencing.`;
4
+ /**
5
+ * LLM-driven metadata enhancement for stash entries.
6
+ *
7
+ * Split out of `llm.ts` so the higher-level workflow (prompting the LLM to
8
+ * improve descriptions/tags/searchHints) lives separately from the low-level
9
+ * transport client in `client.ts`.
10
+ */
11
+ import metadataEnhanceSystemPrompt from "../assets/prompts/metadata-enhance-system.md" with { type: "text" };
12
+ import { parseJsonResponse } from "./client.js";
13
+ import { callStructured } from "./structured-call.js";
14
+ const SYSTEM_PROMPT = metadataEnhanceSystemPrompt;
7
15
  /**
8
16
  * Use an LLM to enhance a stash entry's metadata: improve description,
9
17
  * generate searchHints, and suggest tags.
@@ -33,34 +41,39 @@ Generate improved metadata for this ${entry.type}. Return JSON with these fields
33
41
  - "tags": an array of 3-8 relevant keyword tags
34
42
 
35
43
  Return ONLY the JSON object, no explanation.`;
36
- const runLlm = async () => {
37
- const raw = await chatCompletion(config, [
44
+ // `parse` owns the raw response: the `!raw`/unparseable case ⇒ `{}`, plus the
45
+ // description/searchHints/tags shaping. `enhanceMetadata` never warns and
46
+ // never bumps telemetry, so `onError` (gated path only) just swallows to `{}`
47
+ // — identical to the surrounding control flow's pre-migration behaviour. The
48
+ // ungated path (akmConfig === undefined) propagates errors via callStructured.
49
+ return callStructured({
50
+ feature: "metadata_enhance",
51
+ akmConfig,
52
+ config,
53
+ messages: [
38
54
  { role: "system", content: SYSTEM_PROMPT },
39
55
  { role: "user", content: userPrompt },
40
- ], { signal });
41
- const parsed = parseJsonResponse(raw);
42
- if (!parsed)
43
- return {};
44
- const result = {};
45
- if (typeof parsed.description === "string" && parsed.description) {
46
- result.description = parsed.description;
47
- }
48
- if (Array.isArray(parsed.searchHints)) {
49
- result.searchHints = parsed.searchHints
50
- .filter((s) => typeof s === "string" && s.trim().length > 0)
51
- .slice(0, 8);
52
- }
53
- if (Array.isArray(parsed.tags)) {
54
- result.tags = parsed.tags.filter((s) => typeof s === "string" && s.trim().length > 0).slice(0, 10);
55
- }
56
- return result;
57
- };
58
- // When no akmConfig is provided, bypass the feature gate entirely: run the
59
- // LLM call directly and let errors propagate to the caller (pre-gate
60
- // behaviour). When akmConfig is present, honour the feature flag and swallow
61
- // errors to {} via tryLlmFeature.
62
- if (akmConfig === undefined) {
63
- return runLlm();
64
- }
65
- return tryLlmFeature("metadata_enhance", akmConfig, runLlm, {}, { timeoutMs: config.timeoutMs });
56
+ ],
57
+ request: { signal, timeoutMs: config.timeoutMs },
58
+ parse: (raw) => {
59
+ const parsed = raw ? parseJsonResponse(raw) : undefined;
60
+ if (!parsed)
61
+ return {};
62
+ const result = {};
63
+ if (typeof parsed.description === "string" && parsed.description) {
64
+ result.description = parsed.description;
65
+ }
66
+ if (Array.isArray(parsed.searchHints)) {
67
+ result.searchHints = parsed.searchHints
68
+ .filter((s) => typeof s === "string" && s.trim().length > 0)
69
+ .slice(0, 8);
70
+ }
71
+ if (Array.isArray(parsed.tags)) {
72
+ result.tags = parsed.tags.filter((s) => typeof s === "string" && s.trim().length > 0).slice(0, 10);
73
+ }
74
+ return result;
75
+ },
76
+ onError: () => ({}),
77
+ fallback: {},
78
+ });
66
79
  }
@@ -0,0 +1,49 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { chatCompletion, isContextSizeError, LlmCallError } from "./client.js";
5
+ import { tryLlmFeature } from "./feature-gate.js";
6
+ /**
7
+ * Classify a thrown LLM error into one of the three buckets. This is the single
8
+ * home for the `isContextSizeError -> html -> other` ladder that was previously
9
+ * inlined at every call site.
10
+ */
11
+ export function classifyLlmError(err) {
12
+ const message = err instanceof Error ? err.message : String(err);
13
+ if (isContextSizeError(message))
14
+ return "context_limit";
15
+ if (err instanceof LlmCallError && err.code === "provider_html_error")
16
+ return "html";
17
+ return "other";
18
+ }
19
+ export async function callStructured(opts) {
20
+ const { feature, akmConfig, config, messages, request, parse, onError, fallback, onFallback } = opts;
21
+ const chat = request?.chat ?? chatCompletion;
22
+ const chatOptions = {
23
+ temperature: request?.temperature,
24
+ timeoutMs: request?.timeoutMs,
25
+ signal: request?.signal,
26
+ responseSchema: request?.responseSchema,
27
+ onRetryAttempt: request?.onRetryAttempt,
28
+ };
29
+ // UNGATED: run the chat+parse directly. Errors propagate — no `onError`
30
+ // funnel — matching the pre-gate behaviour of direct callers.
31
+ if (akmConfig === undefined) {
32
+ const raw = await chat(config, messages, chatOptions);
33
+ return parse(raw);
34
+ }
35
+ // GATED: run through `tryLlmFeature`. A throw inside is classified ONCE and
36
+ // routed to `onError`; `tryLlmFeature` returns `fallback` on disablement/timeout.
37
+ return tryLlmFeature(feature, akmConfig, async () => {
38
+ try {
39
+ const raw = await chat(config, messages, chatOptions);
40
+ return parse(raw);
41
+ }
42
+ catch (err) {
43
+ return onError(classifyLlmError(err), err);
44
+ }
45
+ }, fallback, {
46
+ timeoutMs: request?.timeoutMs,
47
+ onFallback,
48
+ });
49
+ }
@@ -12,10 +12,10 @@
12
12
  * Initialized from `cli.ts` before `runMain`.
13
13
  */
14
14
  import { UsageError } from "../core/errors.js";
15
- export const OUTPUT_FORMATS = ["json", "yaml", "text", "jsonl", "md", "html"];
16
- export const DETAIL_LEVELS = ["brief", "normal", "full"];
17
- export const SHAPE_MODES = ["human", "agent", "summary"];
18
- export function parseOutputFormat(value) {
15
+ const OUTPUT_FORMATS = ["json", "yaml", "text", "jsonl", "md", "html"];
16
+ const DETAIL_LEVELS = ["brief", "normal", "full"];
17
+ const SHAPE_MODES = ["human", "agent", "summary"];
18
+ function parseOutputFormat(value) {
19
19
  if (!value)
20
20
  return undefined;
21
21
  if (OUTPUT_FORMATS.includes(value))
@@ -29,7 +29,7 @@ export function parseDetailLevel(value) {
29
29
  return value;
30
30
  throw new UsageError(`Invalid value for --detail: ${value}. Expected one of: ${DETAIL_LEVELS.join("|")}`, "INVALID_DETAIL_VALUE");
31
31
  }
32
- export function parseShapeMode(value) {
32
+ function parseShapeMode(value) {
33
33
  if (!value)
34
34
  return undefined;
35
35
  if (SHAPE_MODES.includes(value))
@@ -468,6 +468,44 @@ const sessionMdRenderer = {
468
468
  };
469
469
  },
470
470
  };
471
+ // ── 9. fact-md ───────────────────────────────────────────────────────────────
472
+ /**
473
+ * Renderer for the `fact` asset type. A fact is durable stash-level semantic
474
+ * knowledge (personal/team/project details, coding conventions, stash-meta).
475
+ * It carries `category` (personal|team|project|convention|meta) and an
476
+ * optional `pinned` flag marking it as part of the always-injected core. The
477
+ * renderer surfaces a one-liner (category + pinned marker) so an agent can tell
478
+ * at a glance what kind of fact it is and whether it is core context.
479
+ */
480
+ const factMdRenderer = {
481
+ name: "fact-md",
482
+ buildShowResponse(ctx) {
483
+ const name = deriveName(ctx);
484
+ const parsed = parseFrontmatter(ctx.content());
485
+ const fm = parsed.data;
486
+ const category = asNonEmptyString(fm.category);
487
+ const description = asNonEmptyString(fm.description);
488
+ const pinned = fm.pinned === true;
489
+ const headerParts = [
490
+ category ? `category: ${category}` : undefined,
491
+ pinned ? "pinned (core context)" : undefined,
492
+ ].filter((p) => !!p);
493
+ const action = [
494
+ "Durable stash fact — apply it as background context.",
495
+ headerParts.length > 0 ? headerParts.join(" ") : undefined,
496
+ ]
497
+ .filter((p) => !!p)
498
+ .join("\n");
499
+ return {
500
+ type: "fact",
501
+ name,
502
+ path: ctx.absPath,
503
+ action,
504
+ description,
505
+ content: parsed.content,
506
+ };
507
+ },
508
+ };
471
509
  function applySessionMetadata(entry, ctx) {
472
510
  try {
473
511
  const fm = applyFrontmatterDescriptionAndTags(entry, ctx);
@@ -501,6 +539,34 @@ function applyTocMetadata(entry, ctx) {
501
539
  // Non-fatal: skip TOC if file can't be read
502
540
  }
503
541
  }
542
+ /**
543
+ * Fact metadata: surface `category` and the `pinned` core marker as tags +
544
+ * search hints (no dedicated DB columns — same encoding pattern as session /
545
+ * task). `pinned` is mirrored to both a `pinned` tag and a `pinned` search
546
+ * hint so the ranking contributor can detect it and queries can target it.
547
+ */
548
+ function applyFactMetadata(entry, ctx) {
549
+ try {
550
+ const fm = applyFrontmatterDescriptionAndTags(entry, ctx);
551
+ const tags = new Set([...(entry.tags ?? []), "fact"]);
552
+ const hints = new Set(entry.searchHints ?? []);
553
+ const category = asNonEmptyString(fm.category);
554
+ if (category) {
555
+ tags.add(category);
556
+ hints.add(`category:${category}`);
557
+ }
558
+ if (fm.pinned === true) {
559
+ tags.add("pinned");
560
+ hints.add("pinned");
561
+ }
562
+ entry.tags = Array.from(tags).filter(Boolean);
563
+ if (hints.size > 0)
564
+ entry.searchHints = Array.from(hints).filter(Boolean);
565
+ }
566
+ catch {
567
+ // Non-fatal: skip metadata extraction on parse error
568
+ }
569
+ }
504
570
  /**
505
571
  * Parse frontmatter, apply description (if not already set) and merge tags
506
572
  * into `entry`. Returns the raw frontmatter data object so callers can access
@@ -660,6 +726,11 @@ registerMetadataContributor({
660
726
  appliesTo: ({ rendererName }) => rendererName === "session-md",
661
727
  contribute: (entry, ctx) => applySessionMetadata(entry, ctx.renderContext),
662
728
  });
729
+ registerMetadataContributor({
730
+ name: "fact-md-metadata",
731
+ appliesTo: ({ rendererName }) => rendererName === "fact-md",
732
+ contribute: (entry, ctx) => applyFactMetadata(entry, ctx.renderContext),
733
+ });
663
734
  // ── Registration ─────────────────────────────────────────────────────────────
664
735
  /** All built-in renderers. */
665
736
  const builtinRenderers = [
@@ -676,6 +747,7 @@ const builtinRenderers = [
676
747
  secretFileRenderer,
677
748
  taskMdRenderer,
678
749
  sessionMdRenderer,
750
+ factMdRenderer,
679
751
  ];
680
752
  /**
681
753
  * Register all built-in renderers with the file-context registry.
@@ -687,4 +759,4 @@ export function registerBuiltinRenderers() {
687
759
  }
688
760
  }
689
761
  // ── Named exports for testing ────────────────────────────────────────────────
690
- export { agentMdRenderer, commandMdRenderer, envFileRenderer, INTERPRETER_MAP, knowledgeMdRenderer, lessonMdRenderer, memoryMdRenderer, SETUP_SIGNALS, scriptSourceRenderer, secretFileRenderer, skillMdRenderer, wikiMdRenderer, workflowMdRenderer, };
762
+ export { agentMdRenderer, commandMdRenderer, envFileRenderer, factMdRenderer, INTERPRETER_MAP, knowledgeMdRenderer, lessonMdRenderer, memoryMdRenderer, SETUP_SIGNALS, scriptSourceRenderer, secretFileRenderer, skillMdRenderer, wikiMdRenderer, workflowMdRenderer, };
@@ -5,7 +5,7 @@ import { capDescription, NORMAL_DESCRIPTION_LIMIT, pickFields } from "./helpers.
5
5
  // Curation is a small, high-signal top-N. Even at `brief` we keep `followUp`
6
6
  // (the actionable `akm show <ref>` command) and `reason` (why this asset was
7
7
  // selected) — these are the point of curate, unlike a bulk search listing.
8
- const BRIEF_FIELDS = ["source", "type", "name", "ref", "id", "followUp", "reason"];
8
+ const BRIEF_FIELDS = ["source", "type", "name", "ref", "id", "supportRefs", "followUp", "reason"];
9
9
  const NORMAL_FIELDS = [
10
10
  "source",
11
11
  "type",
@@ -17,12 +17,24 @@ const NORMAL_FIELDS = [
17
17
  "keys",
18
18
  "parameters",
19
19
  "run",
20
+ "supportRefs",
20
21
  "followUp",
21
22
  "reason",
22
23
  "score",
23
24
  ];
24
25
  // Agent shape: the minimal field set an LLM needs to decide and act.
25
- const AGENT_FIELDS = ["source", "type", "name", "ref", "id", "description", "followUp", "reason", "score"];
26
+ const AGENT_FIELDS = [
27
+ "source",
28
+ "type",
29
+ "name",
30
+ "ref",
31
+ "id",
32
+ "description",
33
+ "supportRefs",
34
+ "followUp",
35
+ "reason",
36
+ "score",
37
+ ];
26
38
  function shapeCurateItem(item, detail, shape) {
27
39
  if (shape === "agent") {
28
40
  return capDescription(pickFields(item, AGENT_FIELDS), NORMAL_DESCRIPTION_LIMIT);
@@ -23,7 +23,6 @@ const PASSTHROUGH_COMMANDS = [
23
23
  "agent-result",
24
24
  "clone",
25
25
  "config",
26
- "db-backups",
27
26
  "disable",
28
27
  "enable",
29
28
  "env-create",
@@ -1005,6 +1005,15 @@ export function formatCuratePlain(r, detail) {
1005
1005
  lines.push(` run: ${String(item.run)}`);
1006
1006
  if (item.followUp)
1007
1007
  lines.push(` show: ${String(item.followUp)}`);
1008
+ if (Array.isArray(item.supportRefs) && item.supportRefs.length > 0) {
1009
+ for (const support of item.supportRefs) {
1010
+ if (!support.ref)
1011
+ continue;
1012
+ const label = typeof support.type === "string" ? `[${support.type}] ` : "";
1013
+ const why = typeof support.reason === "string" ? ` — ${support.reason}` : "";
1014
+ lines.push(` support: ${label}${String(support.ref)}${why}`);
1015
+ }
1016
+ }
1008
1017
  if (detail !== "brief" && item.reason)
1009
1018
  lines.push(` why: ${String(item.reason)}`);
1010
1019
  }
@@ -1026,8 +1035,14 @@ export function formatCuratePlain(r, detail) {
1026
1035
  }
1027
1036
  export function formatInitPlain(r) {
1028
1037
  let out = `Stash initialized at ${r.stashDir ?? "unknown"}`;
1029
- if (r.configPath)
1038
+ // When --dir scaffolded a secondary stash but the default was deliberately
1039
+ // left untouched, tell the user instead of silently repointing their default.
1040
+ if (r.defaultStashUpdated === false && typeof r.previousStashDir === "string" && r.previousStashDir) {
1041
+ out += `\nYour default stash is unchanged (${r.previousStashDir}). Re-run with --set-default to make ${r.stashDir} the default.`;
1042
+ }
1043
+ else if (r.configPath) {
1030
1044
  out += `\nConfig saved to ${r.configPath}`;
1045
+ }
1031
1046
  return out;
1032
1047
  }
1033
1048
  export function formatIndexPlain(r) {
@@ -2,46 +2,12 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { fetchWithRetry } from "../../core/common.js";
5
- import { rethrowIfTestIsolationError } from "../../core/errors.js";
6
- import { closeDatabase, getRegistryIndexCache, openDatabase, upsertRegistryIndexCache } from "../../indexer/db/db.js";
7
5
  import { md5Hex } from "../../runtime.js";
6
+ import { fetchCachedJson } from "../../storage/repositories/registry-cache.js";
8
7
  import { registerProvider } from "../factory.js";
9
8
  // ── Constants ───────────────────────────────────────────────────────────────
10
9
  /** Per-query cache TTL in milliseconds (15 minutes). */
11
10
  const QUERY_CACHE_TTL_MS = 15 * 60 * 1000;
12
- // ── Cache DB lifecycle ────────────────────────────────────────────────────────
13
- /**
14
- * RAII-style lifecycle helper for the registry cache DB. Opens the DB (treating
15
- * a failed open exactly like the legacy fall-through: the bun-test isolation
16
- * guard is re-thrown, any other failure yields `db = undefined`), runs `fn`,
17
- * and guarantees the DB is closed in a `finally` after `fn` has fully settled
18
- * (the await is required: the callbacks are async, and closing before they
19
- * settle would tear the DB down mid-write).
20
- */
21
- async function withRegistryCacheDb(fn) {
22
- let db;
23
- try {
24
- db = openDatabase();
25
- }
26
- catch (err) {
27
- // Never mask the bun-test isolation guard as "DB unavailable".
28
- rethrowIfTestIsolationError(err);
29
- db = undefined;
30
- }
31
- try {
32
- return await fn(db);
33
- }
34
- finally {
35
- if (db) {
36
- try {
37
- closeDatabase(db);
38
- }
39
- catch {
40
- /* ignore */
41
- }
42
- }
43
- }
44
- }
45
11
  // ── Provider class ──────────────────────────────────────────────────────────
46
12
  class SkillsShProvider {
47
13
  type = "skills-sh";
@@ -66,131 +32,39 @@ class SkillsShProvider {
66
32
  return { hits: [], warnings: [`Registry ${label}: ${message}`] };
67
33
  }
68
34
  }
69
- // ── v1-spec §3.1 surface ────────────────────────────────────────────────
70
- async searchKits(q) {
71
- const result = await this.search({
72
- query: q.text,
73
- limit: q.limit ?? 20,
74
- includeAssets: false,
75
- });
76
- return result.hits.map((hit) => ({
77
- id: hit.id,
78
- title: hit.title,
79
- summary: hit.description,
80
- installRef: hit.installRef,
81
- score: hit.score,
82
- }));
83
- }
84
- async searchAssets(q) {
85
- const result = await this.search({
86
- query: q.text,
87
- limit: q.limit ?? 20,
88
- includeAssets: true,
89
- });
90
- return (result.assetHits ?? []).map((hit) => ({
91
- kitId: hit.stash.id,
92
- type: hit.assetType,
93
- name: hit.assetName,
94
- summary: hit.description,
95
- cloneRef: hit.action.replace(/^akm add\s+/, ""),
96
- }));
97
- }
98
- /**
99
- * skills.sh has no `getKit` API — every entry corresponds to a GitHub
100
- * repository whose metadata we already include in the search result. We
101
- * synthesize a manifest from the search hit when the caller knows the stash
102
- * id; if not present in the most recent results, return null.
103
- */
104
- async getKit(id) {
105
- if (!id.startsWith("skills-sh:"))
106
- return null;
107
- const slug = id.slice("skills-sh:".length);
108
- // Best-effort: the API gives us search-by-name; extract the leaf segment.
109
- const segments = slug.split("/").filter(Boolean);
110
- const leaf = segments[segments.length - 1] ?? slug;
111
- const result = await this.search({ query: leaf, limit: 50, includeAssets: false });
112
- const match = result.hits.find((hit) => hit.id === id);
113
- if (!match)
114
- return null;
115
- return { id: match.id, installRef: match.installRef };
116
- }
117
- /**
118
- * skills.sh entries are always GitHub repositories. Claim only refs whose
119
- * parsed source is `github`; defer everything else (npm tarballs, local
120
- * paths, raw git URLs) to other registries.
121
- */
122
- canHandle(ref) {
123
- return ref.source === "github";
124
- }
125
35
  async fetchSkills(query, limit) {
126
36
  // Build a stable DB cache key for this query
127
37
  const dbCacheKey = this.queryDbCacheKey(query, limit);
128
- return withRegistryCacheDb(async (db) => {
129
- // ── Step 1: Try DB cache (index.db) ───────────────────────────────────
130
- let dbCacheResult;
131
- try {
132
- if (db) {
133
- dbCacheResult = getRegistryIndexCache(db, dbCacheKey, QUERY_CACHE_TTL_MS);
134
- }
135
- }
136
- catch (err) {
137
- // Never mask the bun-test isolation guard as "DB unavailable" — see
138
- // rethrowIfTestIsolationError in src/core/errors.ts. Without this,
139
- // a leaky test silently gets a cold cache + fresh fetch instead of
140
- // the loud TEST_ISOLATION_MISSING failure the guard intends.
141
- rethrowIfTestIsolationError(err);
142
- // index.db not available yet (pre-migration install or test env) — fall through
143
- }
144
- if (dbCacheResult) {
38
+ const baseUrl = this.config.url.replace(/\/+$/, "");
39
+ const url = `${baseUrl}/api/search?q=${encodeURIComponent(query)}&limit=${limit}`;
40
+ return fetchCachedJson({
41
+ cacheKey: dbCacheKey,
42
+ ttlMs: QUERY_CACHE_TTL_MS,
43
+ // A fresh hit returns even an empty array; a stale fallback only when
44
+ // non-empty. Corrupt cache JSON is swallowed and treated as a miss.
45
+ parseCache: (json, { stale }) => {
145
46
  try {
146
- const parsed = JSON.parse(dbCacheResult.indexJson);
147
- if (Array.isArray(parsed)) {
148
- const entries = parsed.filter(isValidSkillsEntry);
149
- return entries;
150
- }
47
+ const parsed = JSON.parse(json);
48
+ if (!Array.isArray(parsed))
49
+ return undefined;
50
+ const entries = parsed.filter(isValidSkillsEntry);
51
+ if (stale && entries.length === 0)
52
+ return undefined;
53
+ return entries;
151
54
  }
152
55
  catch {
153
- /* corrupt DB entry — fall through */
56
+ return undefined;
154
57
  }
155
- }
156
- // ── Step 2: Fetch from API ─────────────────────────────────────────────
157
- const baseUrl = this.config.url.replace(/\/+$/, "");
158
- const url = `${baseUrl}/api/search?q=${encodeURIComponent(query)}&limit=${limit}`;
159
- try {
58
+ },
59
+ fetchFresh: async () => {
160
60
  const response = await fetchWithRetry(url, undefined, { timeout: 10_000, retries: 1 });
161
61
  if (!response.ok) {
162
62
  throw new Error(`HTTP ${response.status}`);
163
63
  }
164
64
  const data = (await response.json());
165
65
  const entries = parseSkillsResponse(data);
166
- // Write to DB cache (primary)
167
- if (db) {
168
- try {
169
- upsertRegistryIndexCache(db, dbCacheKey, JSON.stringify(entries));
170
- }
171
- catch {
172
- /* best-effort */
173
- }
174
- }
175
- return entries;
176
- }
177
- catch (err) {
178
- // Fetch failed — use stale DB cache if available
179
- if (dbCacheResult) {
180
- try {
181
- const parsed = JSON.parse(dbCacheResult.indexJson);
182
- if (Array.isArray(parsed)) {
183
- const entries = parsed.filter(isValidSkillsEntry);
184
- if (entries.length > 0)
185
- return entries;
186
- }
187
- }
188
- catch {
189
- /* ignore */
190
- }
191
- }
192
- throw err;
193
- }
66
+ return { value: entries, cacheJson: JSON.stringify(entries) };
67
+ },
194
68
  });
195
69
  }
196
70
  mapToHits(entries) {