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

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 (221) hide show
  1. package/CHANGELOG.md +711 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/profiles/default.json +9 -4
  6. package/dist/assets/profiles/frequent.json +1 -1
  7. package/dist/assets/profiles/memory-focus.json +1 -1
  8. package/dist/assets/profiles/quick.json +1 -1
  9. package/dist/assets/profiles/synthesize.json +15 -0
  10. package/dist/assets/profiles/thorough.json +1 -1
  11. package/dist/assets/prompts/consolidate-system.md +23 -0
  12. package/dist/assets/prompts/contradiction-judge.md +33 -0
  13. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  14. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  15. package/dist/assets/prompts/extract-session.md +6 -2
  16. package/dist/assets/prompts/graph-extract-system.md +1 -0
  17. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  18. package/dist/assets/prompts/memory-infer-system.md +1 -0
  19. package/dist/assets/prompts/memory-infer-user.md +5 -0
  20. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  21. package/dist/assets/prompts/procedural-system.md +44 -0
  22. package/dist/assets/prompts/recombine-system.md +40 -0
  23. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  24. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  25. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  34. package/dist/assets/templates/html/health.html +281 -111
  35. package/dist/assets/wiki/ingest-workflow-template.md +38 -10
  36. package/dist/cli/parse-args.js +46 -1
  37. package/dist/cli/shared.js +28 -0
  38. package/dist/cli.js +27 -11
  39. package/dist/commands/agent/agent-dispatch.js +2 -2
  40. package/dist/commands/agent/agent-support.js +0 -7
  41. package/dist/commands/agent/contribute-cli.js +17 -4
  42. package/dist/commands/config-cli.js +18 -2
  43. package/dist/commands/env/child-env.js +47 -0
  44. package/dist/commands/env/env-cli.js +33 -26
  45. package/dist/commands/env/secret-cli.js +36 -22
  46. package/dist/commands/feedback-cli.js +15 -6
  47. package/dist/commands/graph/graph-cli.js +5 -13
  48. package/dist/commands/graph/graph.js +76 -72
  49. package/dist/commands/health/checks.js +49 -1
  50. package/dist/commands/health/html-report.js +422 -80
  51. package/dist/commands/health.js +386 -9
  52. package/dist/commands/improve/calibration.js +161 -0
  53. package/dist/commands/improve/consolidate/chunking.js +141 -0
  54. package/dist/commands/improve/consolidate/eligibility.js +81 -0
  55. package/dist/commands/improve/consolidate/merge.js +145 -0
  56. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  57. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  58. package/dist/commands/improve/consolidate.js +635 -660
  59. package/dist/commands/improve/dedup.js +482 -0
  60. package/dist/commands/improve/distill.js +159 -69
  61. package/dist/commands/improve/eligibility.js +434 -0
  62. package/dist/commands/improve/encoding-salience.js +205 -0
  63. package/dist/commands/improve/extract-cli.js +124 -2
  64. package/dist/commands/improve/extract-prompt.js +39 -2
  65. package/dist/commands/improve/extract-watch.js +140 -0
  66. package/dist/commands/improve/extract.js +389 -40
  67. package/dist/commands/improve/feedback-valence.js +54 -0
  68. package/dist/commands/improve/homeostatic.js +467 -0
  69. package/dist/commands/improve/improve-auto-accept.js +138 -7
  70. package/dist/commands/improve/improve-cli.js +36 -61
  71. package/dist/commands/improve/improve-profiles.js +14 -0
  72. package/dist/commands/improve/improve-result-file.js +14 -25
  73. package/dist/commands/improve/improve-session.js +58 -0
  74. package/dist/commands/improve/improve.js +485 -2498
  75. package/dist/commands/improve/locks.js +154 -0
  76. package/dist/commands/improve/loop-stages.js +1083 -0
  77. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  78. package/dist/commands/improve/outcome-loop.js +256 -0
  79. package/dist/commands/improve/preparation.js +1966 -0
  80. package/dist/commands/improve/proactive-maintenance.js +115 -0
  81. package/dist/commands/improve/procedural.js +418 -0
  82. package/dist/commands/improve/recombine.js +850 -0
  83. package/dist/commands/improve/reflect-noise.js +0 -0
  84. package/dist/commands/improve/reflect.js +183 -40
  85. package/dist/commands/improve/salience.js +438 -0
  86. package/dist/commands/improve/triage.js +93 -0
  87. package/dist/commands/lint/agent-linter.js +19 -24
  88. package/dist/commands/lint/base-linter.js +173 -60
  89. package/dist/commands/lint/command-linter.js +19 -24
  90. package/dist/commands/lint/env-key-rules.js +38 -1
  91. package/dist/commands/lint/fact-linter.js +39 -0
  92. package/dist/commands/lint/index.js +31 -13
  93. package/dist/commands/lint/memory-linter.js +1 -1
  94. package/dist/commands/lint/registry.js +7 -2
  95. package/dist/commands/lint/task-linter.js +3 -3
  96. package/dist/commands/lint/workflow-linter.js +26 -1
  97. package/dist/commands/proposal/drain-policies.js +5 -0
  98. package/dist/commands/proposal/drain.js +43 -50
  99. package/dist/commands/proposal/proposal-cli.js +21 -31
  100. package/dist/commands/proposal/proposal.js +5 -0
  101. package/dist/commands/proposal/propose.js +7 -2
  102. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  103. package/dist/commands/proposal/validators/proposals.js +189 -63
  104. package/dist/commands/read/curate.js +414 -94
  105. package/dist/commands/read/knowledge.js +6 -3
  106. package/dist/commands/read/search-cli.js +9 -4
  107. package/dist/commands/read/search.js +10 -6
  108. package/dist/commands/read/show.js +86 -7
  109. package/dist/commands/sources/init.js +49 -17
  110. package/dist/commands/sources/installed-stashes.js +11 -3
  111. package/dist/commands/sources/schema-repair.js +43 -45
  112. package/dist/commands/sources/self-update.js +2 -2
  113. package/dist/commands/sources/source-add.js +7 -3
  114. package/dist/commands/sources/stash-cli.js +28 -40
  115. package/dist/commands/sources/stash-skeleton.js +23 -8
  116. package/dist/commands/tasks/tasks-cli.js +19 -27
  117. package/dist/commands/tasks/tasks.js +39 -11
  118. package/dist/commands/wiki-cli.js +21 -35
  119. package/dist/core/asset/asset-registry.js +3 -1
  120. package/dist/core/asset/asset-spec.js +18 -2
  121. package/dist/core/asset/frontmatter.js +166 -167
  122. package/dist/core/asset/markdown.js +8 -0
  123. package/dist/core/authoring-rules.js +92 -0
  124. package/dist/core/common.js +0 -5
  125. package/dist/core/config/config-migration.js +12 -11
  126. package/dist/core/config/config-schema.js +340 -56
  127. package/dist/core/config/config-types.js +3 -3
  128. package/dist/core/config/config.js +28 -7
  129. package/dist/core/events.js +3 -7
  130. package/dist/core/improve-types.js +11 -8
  131. package/dist/core/logs-db.js +10 -66
  132. package/dist/core/parse.js +36 -16
  133. package/dist/core/paths.js +3 -0
  134. package/dist/core/standards/resolve-standards-context.js +87 -0
  135. package/dist/core/standards/resolve-stash-standards.js +99 -0
  136. package/dist/core/standards/resolve-type-conventions.js +66 -0
  137. package/dist/core/state/migrations.js +714 -0
  138. package/dist/core/state-db.js +525 -474
  139. package/dist/indexer/db/db.js +439 -247
  140. package/dist/indexer/db/graph-db.js +129 -86
  141. package/dist/indexer/ensure-index.js +152 -17
  142. package/dist/indexer/graph/graph-boost.js +51 -41
  143. package/dist/indexer/graph/graph-extraction.js +218 -4
  144. package/dist/indexer/index-writer-lock.js +99 -0
  145. package/dist/indexer/indexer.js +123 -221
  146. package/dist/indexer/passes/dir-staleness.js +114 -0
  147. package/dist/indexer/passes/memory-inference.js +13 -5
  148. package/dist/indexer/passes/staleness-detect.js +2 -5
  149. package/dist/indexer/search/db-search.js +19 -6
  150. package/dist/indexer/search/ranking-contributors.js +22 -0
  151. package/dist/indexer/search/ranking.js +4 -0
  152. package/dist/indexer/search/search-source.js +17 -18
  153. package/dist/indexer/search/semantic-status.js +4 -0
  154. package/dist/indexer/walk/matchers.js +9 -0
  155. package/dist/integrations/agent/config.js +6 -53
  156. package/dist/integrations/agent/index.js +2 -18
  157. package/dist/integrations/agent/prompts.js +75 -9
  158. package/dist/integrations/agent/runner-dispatch.js +59 -0
  159. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  160. package/dist/integrations/harnesses/index.js +2 -3
  161. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  162. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  163. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
  164. package/dist/integrations/session-logs/index.js +16 -0
  165. package/dist/llm/client.js +45 -15
  166. package/dist/llm/embedder.js +42 -3
  167. package/dist/llm/embedders/deterministic.js +66 -0
  168. package/dist/llm/embedders/local.js +66 -2
  169. package/dist/llm/feature-gate.js +8 -4
  170. package/dist/llm/graph-extract.js +67 -44
  171. package/dist/llm/memory-infer-impl.js +138 -0
  172. package/dist/llm/memory-infer.js +1 -127
  173. package/dist/llm/metadata-enhance.js +44 -31
  174. package/dist/llm/structured-call.js +49 -0
  175. package/dist/migrate-storage-node.mjs +8 -0
  176. package/dist/output/context.js +5 -5
  177. package/dist/output/renderers.js +74 -2
  178. package/dist/output/shapes/curate.js +14 -2
  179. package/dist/output/shapes/passthrough.js +0 -1
  180. package/dist/output/text/helpers.js +16 -1
  181. package/dist/registry/providers/skills-sh.js +21 -147
  182. package/dist/registry/providers/static-index.js +15 -157
  183. package/dist/registry/resolve.js +22 -9
  184. package/dist/runtime.js +25 -1
  185. package/dist/scripts/migrate-storage.js +2617 -1961
  186. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
  187. package/dist/setup/setup.js +29 -8
  188. package/dist/sources/include.js +6 -2
  189. package/dist/sources/providers/filesystem.js +0 -1
  190. package/dist/sources/providers/git-install.js +210 -0
  191. package/dist/sources/providers/git-provider.js +234 -0
  192. package/dist/sources/providers/git-stash.js +248 -0
  193. package/dist/sources/providers/git.js +10 -661
  194. package/dist/sources/providers/npm.js +2 -6
  195. package/dist/sources/providers/provider-utils.js +13 -7
  196. package/dist/sources/providers/sync-from-ref.js +9 -1
  197. package/dist/sources/providers/tar-utils.js +16 -8
  198. package/dist/sources/providers/website.js +9 -5
  199. package/dist/sources/website-ingest.js +187 -29
  200. package/dist/sources/wiki-fetchers/registry.js +53 -0
  201. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  202. package/dist/storage/database.js +45 -10
  203. package/dist/storage/managed-db.js +82 -0
  204. package/dist/storage/repositories/registry-cache.js +92 -0
  205. package/dist/storage/sqlite-pragmas.js +146 -0
  206. package/dist/tasks/backends/cron.js +1 -1
  207. package/dist/tasks/backends/launchd.js +1 -1
  208. package/dist/tasks/backends/schtasks.js +1 -1
  209. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  210. package/dist/tasks/runner.js +5 -13
  211. package/dist/text-import-hook.mjs +0 -0
  212. package/dist/wiki/wiki.js +37 -0
  213. package/dist/workflows/db.js +3 -4
  214. package/dist/workflows/runtime/runs.js +1 -117
  215. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  216. package/dist/workflows/validate-summary.js +2 -7
  217. package/docs/data-and-telemetry.md +3 -2
  218. package/docs/migration/release-notes/0.9.0.md +39 -0
  219. package/package.json +13 -11
  220. package/dist/commands/db-cli.js +0 -23
  221. package/dist/indexer/db/db-backup.js +0 -376
@@ -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,10 +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");
82
- // --no-project-context sets env so searchDatabase picks it up without
83
- // threading the flag through the entire call stack.
84
- if (noProjectContext)
85
- process.env.AKM_DISABLE_PROJECT_CONTEXT = "1";
87
+ const includeSessions = getHyphenatedBoolean(args, "include-sessions");
86
88
  const result = await akmSearch({
87
89
  query,
88
90
  type,
@@ -91,6 +93,9 @@ export const searchCommand = defineJsonCommand({
91
93
  filters,
92
94
  includeProposed,
93
95
  belief,
96
+ includeSessions,
97
+ disableProjectContext: noProjectContext,
98
+ disableScopedUtility: noProjectContext,
94
99
  eventSource: resolveEventSource(),
95
100
  });
96
101
  output("search", result);
@@ -108,6 +108,9 @@ 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,
112
+ disableProjectContext: input.disableProjectContext === true,
113
+ disableScopedUtility: input.disableScopedUtility === true,
111
114
  });
112
115
  const registryResult = source === "stash" ? undefined : await searchRegistry(query, { limit, registries: config.registries });
113
116
  if (source === "stash") {
@@ -122,8 +125,9 @@ export async function akmSearch(input) {
122
125
  warnings: localResult?.warnings?.length ? localResult.warnings : undefined,
123
126
  timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
124
127
  };
125
- if (!input.skipLogging)
126
- logSearchEvent(query, response, localResult?.mode ?? "keyword", input.eventSource);
128
+ if (!input.skipLogging) {
129
+ logSearchEvent(query, response, localResult?.mode ?? "keyword", input.eventSource, input.disableScopedUtility === true);
130
+ }
127
131
  return response;
128
132
  }
129
133
  const registryHits = (registryResult?.hits ?? []).map((hit) => {
@@ -158,7 +162,7 @@ export async function akmSearch(input) {
158
162
  timing: { totalMs: Date.now() - t0 },
159
163
  };
160
164
  if (!input.skipLogging)
161
- logSearchEvent(query, response, undefined, input.eventSource);
165
+ logSearchEvent(query, response, undefined, input.eventSource, input.disableScopedUtility === true);
162
166
  return response;
163
167
  }
164
168
  // source === "both"
@@ -176,7 +180,7 @@ export async function akmSearch(input) {
176
180
  timing: { totalMs: Date.now() - t0 },
177
181
  };
178
182
  if (!input.skipLogging)
179
- logSearchEvent(query, response, undefined, input.eventSource);
183
+ logSearchEvent(query, response, undefined, input.eventSource, input.disableScopedUtility === true);
180
184
  return response;
181
185
  }
182
186
  /**
@@ -211,7 +215,7 @@ function resolveEntryIds(db, hits) {
211
215
  * Per-entry events are recorded only for stash hits because registry hits
212
216
  * have no local entry_id to reference.
213
217
  */
214
- function logSearchEvent(query, response, mode = "keyword", eventSource = "user") {
218
+ function logSearchEvent(query, response, mode = "keyword", eventSource = "user", disableScopedUtility = false) {
215
219
  // Emit a structured event to events.jsonl so workflow-trace consumers
216
220
  // detect akm search invocations without relying on stdout scraping.
217
221
  const stashHits = response.hits.filter((h) => h.type !== "registry");
@@ -241,7 +245,7 @@ function logSearchEvent(query, response, mode = "keyword", eventSource = "user")
241
245
  let scopeKey;
242
246
  try {
243
247
  const stashPath = response.stashDir;
244
- const disabled = process.env.AKM_DISABLE_SCOPED_UTILITY === "1" || (stashPath && isTransientStashPath(stashPath));
248
+ const disabled = disableScopedUtility || (stashPath && isTransientStashPath(stashPath));
245
249
  scopeKey = disabled ? undefined : getCurrentWorkflowScopeKey();
246
250
  }
247
251
  catch {
@@ -18,22 +18,27 @@
18
18
  */
19
19
  import fs from "node:fs";
20
20
  import path from "node:path";
21
+ import { findCittyTopLevelCommandIndex } from "../../cli/parse-args.js";
21
22
  import { parseAssetRef } from "../../core/asset/asset-ref.js";
22
23
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
23
24
  import { META_DIR, parseMetaRef, resolveMetaFilePath } from "../../core/asset/stash-meta.js";
24
25
  import { asNonEmptyString } from "../../core/common.js";
25
- import { loadConfig } from "../../core/config/config.js";
26
+ import { getIndexPassConfig, loadConfig } from "../../core/config/config.js";
26
27
  import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
27
28
  import { appendEvent, readEvents } from "../../core/events.js";
28
- import { findEntryIdByRef } from "../../indexer/db/db.js";
29
+ import { closeDatabase, computeBodyHash, findEntryIdByRef, openExistingDatabase } from "../../indexer/db/db.js";
30
+ import { hasGraphData } from "../../indexer/db/graph-db.js";
29
31
  import { ensureIndex } from "../../indexer/ensure-index.js";
30
32
  import { listRelatedPathsForFile } from "../../indexer/graph/graph-boost.js";
33
+ import { extractGraphForSingleFile } from "../../indexer/graph/graph-extraction.js";
31
34
  import { lookup } from "../../indexer/indexer.js";
32
35
  import { buildEditHint, findSourceForPath, isEditable, resolveSourceEntries } from "../../indexer/search/search-source.js";
33
36
  import { insertUsageEvent } from "../../indexer/usage/usage-events.js";
34
37
  import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "../../indexer/walk/file-context.js";
35
38
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
39
+ import { resolveIndexPassLLM } from "../../llm/index-passes.js";
36
40
  import { resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
41
+ import { resolveStorageLocations } from "../../storage/locations.js";
37
42
  import { withIndexDb } from "../../storage/repositories/index-db.js";
38
43
  // Eagerly import source providers to trigger self-registration.
39
44
  import "../../sources/providers/index.js";
@@ -383,6 +388,15 @@ export async function showLocal(input) {
383
388
  if (activeRun) {
384
389
  fullResponse.activeRun = activeRun;
385
390
  }
391
+ // #624-P3: opt-in inline graph extraction. Default OFF — when the flag is
392
+ // unset this whole block is skipped (no hasGraphData check, no LLM call), so
393
+ // behavior is byte-identical to today. When ON, it extracts graph data for an
394
+ // ungraphed asset, but ONLY when a model is configured (model-available
395
+ // guard) and ALWAYS bounded by a 30s timeout so `show` can never hang. Any
396
+ // timeout/model-unavailable/error path returns the response unchanged.
397
+ if (getIndexPassConfig(config.index, "graph")?.lazyGraphExtraction === true) {
398
+ await maybeExtractGraphInline(config, sourceStashDir, assetPath);
399
+ }
386
400
  if (input.detail === "brief") {
387
401
  return buildBriefResponse(fullResponse, assetPath);
388
402
  }
@@ -391,6 +405,58 @@ export async function showLocal(input) {
391
405
  }
392
406
  return fullResponse;
393
407
  }
408
+ /**
409
+ * #624-P3 — opt-in inline graph extraction for `akm show`. Best-effort and
410
+ * timeout-bounded: never throws, never hangs, never mutates the response.
411
+ *
412
+ * Preconditions (caller already checked the flag): a model must be configured
413
+ * (model-available guard via {@link resolveIndexPassLLM}) and the asset must be
414
+ * ungraphed ({@link hasGraphData}). Extraction races a 30s timeout so `show`
415
+ * cannot block on a slow provider; any timeout/error/missing-model path is
416
+ * swallowed and `show` returns its already-assembled response unchanged.
417
+ */
418
+ async function maybeExtractGraphInline(config, sourceStashDir, assetPath) {
419
+ try {
420
+ // Model-available guard — no provider configured ⇒ silent skip, no LLM call.
421
+ if (!resolveIndexPassLLM("graph", config))
422
+ return;
423
+ let alreadyGraphed = false;
424
+ let bodyHash;
425
+ try {
426
+ const raw = fs.readFileSync(assetPath, "utf8");
427
+ bodyHash = computeBodyHash(parseFrontmatter(raw).content.trim());
428
+ }
429
+ catch {
430
+ return; // file gone/unreadable ⇒ nothing to extract
431
+ }
432
+ withIndexDb((db) => {
433
+ alreadyGraphed = hasGraphData(db, sourceStashDir, assetPath);
434
+ });
435
+ if (alreadyGraphed)
436
+ return;
437
+ // Open the db for the async extraction ourselves: `withIndexDb` is
438
+ // synchronous and would close the connection the instant the async fn
439
+ // returns its Promise (before extraction completes). Close it explicitly
440
+ // after the race settles instead.
441
+ const db = openExistingDatabase(resolveStorageLocations().indexDb);
442
+ let timer;
443
+ const timeout = new Promise((resolve) => {
444
+ timer = setTimeout(resolve, 30_000);
445
+ });
446
+ try {
447
+ await Promise.race([extractGraphForSingleFile(db, sourceStashDir, assetPath, bodyHash, { config }), timeout]);
448
+ }
449
+ finally {
450
+ if (timer)
451
+ clearTimeout(timer);
452
+ closeDatabase(db);
453
+ }
454
+ }
455
+ catch (err) {
456
+ rethrowIfTestIsolationError(err);
457
+ // Any other failure: silently return the unchanged show response.
458
+ }
459
+ }
394
460
  /**
395
461
  * Minimal `show`: ref → indexer lookup → file contents. Used by callers that
396
462
  * just need the raw file (e.g. clone, write-source) and don't want the full
@@ -458,6 +524,14 @@ function buildSummaryResponse(full, assetPath) {
458
524
  }
459
525
  // ── argv normalisation ───────────────────────────────────────────────────────
460
526
  const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
527
+ const SHOW_ARGV_TOP_LEVEL_ARGS = {
528
+ format: { type: "string" },
529
+ output: { type: "string" },
530
+ detail: { type: "string" },
531
+ shape: { type: "string" },
532
+ quiet: { type: "boolean", alias: "q" },
533
+ verbose: { type: "boolean" },
534
+ };
461
535
  /**
462
536
  * Normalize argv so positional view-mode arguments after the asset ref
463
537
  * are rewritten into internal flags that citty can parse.
@@ -471,15 +545,20 @@ const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines
471
545
  * Returns a new array; the input is never modified.
472
546
  */
473
547
  export function normalizeShowArgv(argv) {
474
- // argv[0]=bun argv[1]=script argv[2]=subcommand argv[3]=ref argv[4..]=rest
475
- if (argv[2] !== "show")
548
+ const rawArgs = argv.slice(2);
549
+ const commandIndex = findCittyTopLevelCommandIndex(rawArgs, SHOW_ARGV_TOP_LEVEL_ARGS);
550
+ if (commandIndex < 0 || rawArgs[commandIndex] !== "show")
476
551
  return argv;
477
- if (argv.includes("--view") || argv.includes("--heading") || argv.includes("--start") || argv.includes("--end")) {
552
+ const commandArgs = rawArgs.slice(commandIndex + 1);
553
+ if (commandArgs.includes("--view") ||
554
+ commandArgs.includes("--heading") ||
555
+ commandArgs.includes("--start") ||
556
+ commandArgs.includes("--end")) {
478
557
  throw new UsageError('Legacy show flags are no longer supported. Use positional syntax like `akm show knowledge:guide toc` or `akm show knowledge:guide section "Auth"`.');
479
558
  }
480
559
  // Separate global flags from positional/show-specific args
481
- const prefix = argv.slice(0, 3); // [bun, script, show]
482
- const rest = argv.slice(3);
560
+ const prefix = [...argv.slice(0, 2), ...rawArgs.slice(0, commandIndex + 1)];
561
+ const rest = commandArgs;
483
562
  const globalFlags = [];
484
563
  const showArgs = [];
485
564
  for (let i = 0; i < rest.length; i++) {
@@ -37,7 +37,7 @@ import { copyStashSkeleton, scaffoldStashMeta } from "./stash-skeleton.js";
37
37
  function assertInitSandbox(stashDir, dirExplicitlyProvided) {
38
38
  if (!dirExplicitlyProvided)
39
39
  return; // Only guard explicit --dir, not default HOME resolution.
40
- const isUnderTest = process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
40
+ const isUnderTest = isUnderTestRunner();
41
41
  if (!isUnderTest)
42
42
  return;
43
43
  if (process.env.AKM_FORCE_INIT_TMP_STASH === "1")
@@ -52,7 +52,12 @@ function assertInitSandbox(stashDir, dirExplicitlyProvided) {
52
52
  return;
53
53
  throw new ConfigError(`refusing to persist --dir stashDir to a temporary path while under test runner; set AKM_FORCE_INIT_TMP_STASH=1 if you really mean it (stashDir=${stashDir})`, "INIT_TMP_STASH_REFUSED");
54
54
  }
55
+ function isUnderTestRunner() {
56
+ return process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
57
+ }
55
58
  export async function akmInit(options) {
59
+ const dirExplicitlyProvided = options?.dir != null;
60
+ const setDefault = options?.setDefault === true;
56
61
  const stashDir = options?.dir ? path.resolve(options.dir) : getDefaultStashDir();
57
62
  // Safety check (#473): refuse stashDir at /, $HOME, /etc, ~/.config, etc.
58
63
  // Runs BEFORE any disk write — a fat-fingered `akm init --dir /` or
@@ -61,7 +66,7 @@ export async function akmInit(options) {
61
66
  assertSafeStashDir(stashDir);
62
67
  // Defense-in-depth: refuse to persist an explicit --dir /tmp/... stashDir
63
68
  // to config under a test runner. Default HOME-resolved paths are exempt.
64
- assertInitSandbox(stashDir, options?.dir != null);
69
+ assertInitSandbox(stashDir, dirExplicitlyProvided);
65
70
  let created = false;
66
71
  if (!fs.existsSync(stashDir)) {
67
72
  fs.mkdirSync(stashDir, { recursive: true });
@@ -75,27 +80,54 @@ export async function akmInit(options) {
75
80
  }
76
81
  // Ensure the default stash is a local git repo (no remote required)
77
82
  ensureGitRepo(stashDir);
78
- if (created) {
79
- copyStashSkeleton(stashDir);
80
- scaffoldStashMeta(stashDir);
81
- }
82
- // Persist stashDir in config.json
83
+ // Run seeding UNCONDITIONALLY (not just when the stash was newly created) so
84
+ // re-running `akm init` on an existing stash backfills any missing skeleton
85
+ // files — the README, the per-type SOFT convention templates under
86
+ // facts/conventions/assets/, and the `.meta/index.md` orientation doc. Both
87
+ // helpers are absent-only: they never overwrite a file a user has edited.
88
+ copyStashSkeleton(stashDir);
89
+ scaffoldStashMeta(stashDir);
90
+ // Persist stashDir in config.json — but ONLY when the user is actually
91
+ // setting up / opting into a default. A bare `akm init --dir <secondary>`
92
+ // must NOT silently repoint the user's real default stash (the footgun
93
+ // documented in memory:akm-init-persists-stashdir-warning).
94
+ //
95
+ // Decision matrix — persist when ANY of:
96
+ // (a) no --dir provided → default HOME-resolved setup flow
97
+ // (b) --dir AND no existing stashDir in config → first-time bootstrap
98
+ // (c) --dir AND --set-default → explicit opt-in
99
+ // Otherwise (--dir + existing default + no --set-default) leave the default
100
+ // pointer alone; the target dir is still scaffolded above.
83
101
  const configPath = getConfigPath();
84
102
  const existing = loadUserConfig();
85
- if (!existing.stashDir || existing.stashDir !== stashDir) {
86
- saveConfig({ ...existing, stashDir });
103
+ const existingStashDir = existing.stashDir;
104
+ const shouldPersist = !dirExplicitlyProvided || !existingStashDir || setDefault;
105
+ let defaultStashUpdated = false;
106
+ let previousStashDir;
107
+ if (shouldPersist) {
108
+ if (!existingStashDir || existingStashDir !== stashDir) {
109
+ saveConfig({ ...existing, stashDir });
110
+ defaultStashUpdated = true;
111
+ }
112
+ // else: already pointed here — no-op, no spurious rewrite.
113
+ }
114
+ else {
115
+ // Default left untouched; surface it so the CLI can inform the user.
116
+ previousStashDir = existingStashDir;
87
117
  }
88
118
  // Ensure ripgrep is available (install to cache/bin if needed)
89
119
  let ripgrep;
90
- try {
91
- const binDir = getBinDir();
92
- const rgResult = ensureRg(binDir);
93
- ripgrep = rgResult;
94
- }
95
- catch {
96
- // Non-fatal: ripgrep is optional, search works without it
120
+ if (!isUnderTestRunner()) {
121
+ try {
122
+ const binDir = getBinDir();
123
+ const rgResult = ensureRg(binDir);
124
+ ripgrep = rgResult;
125
+ }
126
+ catch {
127
+ // Non-fatal: ripgrep is optional, search works without it
128
+ }
97
129
  }
98
- return { stashDir, created, configPath, ripgrep };
130
+ return { stashDir, created, configPath, defaultStashUpdated, previousStashDir, ripgrep };
99
131
  }
100
132
  /** Initialise `dir` as a git repository if it is not already one. */
101
133
  function ensureGitRepo(dir) {
@@ -17,7 +17,7 @@ import { removeLockEntry, upsertLockEntry } from "../../integrations/lockfile.js
17
17
  import { parseRegistryRef } from "../../registry/resolve.js";
18
18
  import { parseGitRepoUrl, syncMirroredRepo } from "../../sources/providers/git.js";
19
19
  import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
20
- import { ensureWebsiteMirror } from "../../sources/website-ingest.js";
20
+ import { ensureWebsiteMirror, shouldAllowPrivateWebsiteUrlForTests } from "../../sources/website-ingest.js";
21
21
  import { listWikis, resolveWikisRoot } from "../../wiki/wiki.js";
22
22
  import { removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./source-add.js";
23
23
  import { removeStash } from "./source-manage.js";
@@ -194,7 +194,11 @@ async function updateGitSource(stashDir, target, all, gitSource) {
194
194
  /** Re-crawl a website source and return an UpdateResponse. */
195
195
  async function updateWebsiteSource(stashDir, target, all, websiteSource) {
196
196
  // TODO: full incremental re-crawl with delta tracking (#19)
197
- await ensureWebsiteMirror(websiteSource, { requireStashDir: true, force: true });
197
+ await ensureWebsiteMirror(websiteSource, {
198
+ requireStashDir: true,
199
+ force: true,
200
+ ...(shouldAllowPrivateWebsiteUrlForTests(websiteSource.url ?? "") ? { allowPrivateHosts: true } : {}),
201
+ });
198
202
  return buildUpdateResponse(stashDir, target, all, []);
199
203
  }
200
204
  /** Sync a single installed registry entry and return the processed record. */
@@ -205,7 +209,11 @@ async function updateRegistryEntry(entry, force) {
205
209
  const synced = await syncFromRef(entry.ref, { force });
206
210
  const installedEntry = {
207
211
  id: synced.id,
208
- source: synced.source,
212
+ // Preserve the original source classification. syncFromRef() re-derives the
213
+ // source type from the ref scheme (e.g. "github:" → source: "github"), but
214
+ // an update should not reclassify an existing entry. A writable entry stored
215
+ // as source: "git" would fail config validation if rewritten to "github".
216
+ source: entry.source,
209
217
  ref: synced.ref,
210
218
  artifactUrl: synced.artifactUrl,
211
219
  resolvedVersion: synced.resolvedVersion,
@@ -17,8 +17,10 @@ import path from "node:path";
17
17
  import { parseAssetRef } from "../../core/asset/asset-ref.js";
18
18
  import { assembleAsset } from "../../core/asset/asset-serialize.js";
19
19
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
20
+ import { authoringRulesForType } from "../../core/authoring-rules.js";
20
21
  import { appendEvent, readEvents } from "../../core/events.js";
21
- import { info, warn } from "../../core/warn.js";
22
+ import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
23
+ import { info } from "../../core/warn.js";
22
24
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
23
25
  import { chatCompletion, parseEmbeddedJsonResponse } from "../../llm/client.js";
24
26
  import { createProposal, isProposalSkipped } from "../proposal/validators/proposals.js";
@@ -44,6 +46,9 @@ export async function runSchemaRepairPass(failures, options) {
44
46
  const repairs = [];
45
47
  const repairedRefs = new Set();
46
48
  const { startMs, budgetMs, llmConfig, stashDir, findFilePath = defaultFindFilePath, isLessonCandidateFn = defaultIsLessonCandidate, chatFn = chatCompletion, } = options;
49
+ if (!stashDir) {
50
+ throw new Error("runSchemaRepairPass requires stashDir so repairs route through the proposal queue");
51
+ }
47
52
  for (const failure of failures) {
48
53
  if (Date.now() - startMs >= budgetMs)
49
54
  break;
@@ -94,6 +99,16 @@ export async function runSchemaRepairPass(failures, options) {
94
99
  const fieldList = missingFields.join(" and ");
95
100
  info(`[improve] schema-repair ${failure.ref} (${fieldList})`);
96
101
  const bodyPreview = (fm.content ?? raw).slice(0, 2000);
102
+ // Standards "rulebook" for this target — wiki schema (wiki page) or stash
103
+ // convention/meta facts (non-wiki asset). `resolveStandardsContext`
104
+ // dispatches on the ref.
105
+ const standardsContext = resolveStandardsContext(failure.ref, stashDir);
106
+ const standardsSection = standardsContext.trim()
107
+ ? `\n\nStandards to follow (the rulebook for this target):\n${standardsContext.trim()}`
108
+ : "";
109
+ const assetType = parseAssetRef(failure.ref).type;
110
+ const authoringRules = authoringRulesForType(assetType);
111
+ const authoringRulesSection = authoringRules ? `\n\n${authoringRules}` : "";
97
112
  const llmResponse = await chatFn(llmConfig, [
98
113
  {
99
114
  role: "system",
@@ -101,7 +116,7 @@ export async function runSchemaRepairPass(failures, options) {
101
116
  },
102
117
  {
103
118
  role: "user",
104
- content: `Generate the missing frontmatter fields (${fieldList}) for this ${parseAssetRef(failure.ref).type} asset. Return ONLY valid JSON like {"description": "...", "when_to_use": "..."}\n\n${bodyPreview}`,
119
+ content: `Generate the missing frontmatter fields (${fieldList}) for this ${assetType} asset. Return ONLY valid JSON like {"description": "...", "when_to_use": "..."}${standardsSection}${authoringRulesSection}\n\n${bodyPreview}`,
105
120
  },
106
121
  ]);
107
122
  const parsed = parseEmbeddedJsonResponse(llmResponse.trim());
@@ -127,50 +142,33 @@ export async function runSchemaRepairPass(failures, options) {
127
142
  // them human-reviewable before they affect search ranking and curate hints.
128
143
  // mem0 open gaps (arXiv:2504.19413) — any LLM write to a memory field
129
144
  // should be human-reviewable.
130
- if (stashDir) {
131
- const proposalResult = createProposal(stashDir, {
132
- ref: failure.ref,
133
- source: "schema-repair",
134
- payload: {
135
- content: newContent,
136
- ...(Object.keys(newFm).length > 0 ? { frontmatter: newFm } : {}),
137
- },
138
- });
139
- if (isProposalSkipped(proposalResult)) {
140
- info(`[improve] schema-repair proposal skipped for ${failure.ref}: ${proposalResult.message}`);
141
- repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
142
- continue;
143
- }
144
- info(`[improve] schema-repair queued: ${failure.ref} (proposal id: ${proposalResult.id})`);
145
- appendEvent({
146
- eventType: "schema_repair_invoked",
147
- ref: failure.ref,
148
- metadata: { outcome: "queued", reason: failure.reason, proposalId: proposalResult.id },
149
- });
150
- repairs.push({
151
- ref: failure.ref,
152
- reason: failure.reason,
153
- outcome: "queued",
154
- proposalId: proposalResult.id,
155
- });
156
- // Mark as repaired so the caller removes it from the validation-failure set.
157
- repairedRefs.add(failure.ref);
158
- }
159
- else {
160
- // Fallback: no stash dir available — write directly (legacy path).
161
- // This should not occur in production; stashDir is always provided by
162
- // `runSchemaRepairPass` callers in improve.ts.
163
- warn(`[improve] schema-repair: no stashDir available for ${failure.ref}, falling back to direct write`);
164
- fs.writeFileSync(filePath, newContent, "utf8");
165
- info(`[improve] schema-repair written: ${failure.ref}`);
166
- appendEvent({
167
- eventType: "schema_repair_invoked",
168
- ref: failure.ref,
169
- metadata: { outcome: "written", reason: failure.reason },
170
- });
171
- repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "written" });
172
- repairedRefs.add(failure.ref);
145
+ const proposalResult = createProposal(stashDir, {
146
+ ref: failure.ref,
147
+ source: "schema-repair",
148
+ payload: {
149
+ content: newContent,
150
+ ...(Object.keys(newFm).length > 0 ? { frontmatter: newFm } : {}),
151
+ },
152
+ });
153
+ if (isProposalSkipped(proposalResult)) {
154
+ info(`[improve] schema-repair proposal skipped for ${failure.ref}: ${proposalResult.message}`);
155
+ repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
156
+ continue;
173
157
  }
158
+ info(`[improve] schema-repair queued: ${failure.ref} (proposal id: ${proposalResult.id})`);
159
+ appendEvent({
160
+ eventType: "schema_repair_invoked",
161
+ ref: failure.ref,
162
+ metadata: { outcome: "queued", reason: failure.reason, proposalId: proposalResult.id },
163
+ });
164
+ repairs.push({
165
+ ref: failure.ref,
166
+ reason: failure.reason,
167
+ outcome: "queued",
168
+ proposalId: proposalResult.id,
169
+ });
170
+ // Mark as repaired so the caller removes it from the validation-failure set.
171
+ repairedRefs.add(failure.ref);
174
172
  }
175
173
  catch (e) {
176
174
  appendEvent({
@@ -306,8 +306,8 @@ export async function performUpgrade(check, opts) {
306
306
  * The new binary's `akm index` does the work for us:
307
307
  * 1. loadConfig() runs at startup — auto-migrates legacy `stashes` →
308
308
  * `sources` if the on-disk config still uses the old key.
309
- * 2. ensureSchema() detects DB_VERSION mismatch and rebuilds index.db
310
- * tables (preserving usage_events).
309
+ * 2. ensureSchema() converges index.db forward via its idempotent baseline
310
+ * schema + additive migrations (no destructive rebuild).
311
311
  * 3. The full reindex repopulates entries + workflow_documents + FTS.
312
312
  */
313
313
  function runPostUpgradeTasks(akmBin, opts) {
@@ -11,7 +11,7 @@ import { upsertLockEntry } from "../../integrations/lockfile.js";
11
11
  import { parseRegistryRef } from "../../registry/resolve.js";
12
12
  import { detectStashRoot } from "../../sources/providers/provider-utils.js";
13
13
  import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
14
- import { ensureWebsiteMirror, validateWebsiteInputUrl } from "../../sources/website-ingest.js";
14
+ import { ensureWebsiteMirror, shouldAllowPrivateWebsiteUrlForTests, validateWebsiteInputUrl, } from "../../sources/website-ingest.js";
15
15
  import { ensureWikiNameAvailable, validateWikiName } from "../../wiki/wiki.js";
16
16
  const VALID_OVERRIDE_TYPES = new Set(["wiki"]);
17
17
  export async function akmAdd(input) {
@@ -126,7 +126,8 @@ async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName)
126
126
  };
127
127
  }
128
128
  async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
129
- const normalizedUrl = validateWebsiteInputUrl(ref);
129
+ const allowPrivateHosts = shouldAllowPrivateWebsiteUrlForTests(ref);
130
+ const normalizedUrl = validateWebsiteInputUrl(ref, { allowPrivateHosts });
130
131
  const config = loadUserConfig();
131
132
  const sources = [...getSources(config)];
132
133
  let entry = sources.find((stash) => stash.type === "website" && stash.url === normalizedUrl);
@@ -154,7 +155,10 @@ async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
154
155
  if (changed)
155
156
  saveConfig({ ...config, sources });
156
157
  }
157
- const cachePaths = await ensureWebsiteMirror(entry, { requireStashDir: true });
158
+ const cachePaths = await ensureWebsiteMirror(entry, {
159
+ requireStashDir: true,
160
+ ...(allowPrivateHosts ? { allowPrivateHosts: true } : {}),
161
+ });
158
162
  const index = await akmIndex({ stashDir });
159
163
  const updatedConfig = loadConfig();
160
164
  return {