akm-cli 0.9.0-beta.4 → 0.9.0-beta.41

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 (132) hide show
  1. package/CHANGELOG.md +646 -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 +6 -2
  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/stash-skeleton/facts/conventions/assets/agent.md +22 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +22 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +24 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +22 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +25 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +21 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +21 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +23 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +22 -0
  24. package/dist/assets/templates/html/health.html +281 -111
  25. package/dist/cli.js +14 -3
  26. package/dist/commands/agent/contribute-cli.js +16 -3
  27. package/dist/commands/feedback-cli.js +15 -6
  28. package/dist/commands/graph/graph.js +75 -71
  29. package/dist/commands/health/checks.js +48 -0
  30. package/dist/commands/health/html-report.js +422 -80
  31. package/dist/commands/health.js +381 -9
  32. package/dist/commands/improve/calibration.js +161 -0
  33. package/dist/commands/improve/consolidate.js +631 -111
  34. package/dist/commands/improve/dedup.js +482 -0
  35. package/dist/commands/improve/distill.js +163 -69
  36. package/dist/commands/improve/encoding-salience.js +205 -0
  37. package/dist/commands/improve/extract-cli.js +115 -1
  38. package/dist/commands/improve/extract-prompt.js +39 -2
  39. package/dist/commands/improve/extract-watch.js +140 -0
  40. package/dist/commands/improve/extract.js +403 -40
  41. package/dist/commands/improve/feedback-valence.js +54 -0
  42. package/dist/commands/improve/homeostatic.js +467 -0
  43. package/dist/commands/improve/improve-auto-accept.js +113 -6
  44. package/dist/commands/improve/improve-profiles.js +12 -0
  45. package/dist/commands/improve/improve.js +2042 -612
  46. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  47. package/dist/commands/improve/outcome-loop.js +256 -0
  48. package/dist/commands/improve/proactive-maintenance.js +115 -0
  49. package/dist/commands/improve/procedural.js +418 -0
  50. package/dist/commands/improve/recombine.js +602 -0
  51. package/dist/commands/improve/reflect-noise.js +0 -0
  52. package/dist/commands/improve/reflect.js +46 -4
  53. package/dist/commands/improve/related-sessions.js +120 -0
  54. package/dist/commands/improve/salience.js +438 -0
  55. package/dist/commands/improve/triage.js +93 -0
  56. package/dist/commands/lint/agent-linter.js +19 -24
  57. package/dist/commands/lint/base-linter.js +173 -60
  58. package/dist/commands/lint/command-linter.js +19 -24
  59. package/dist/commands/lint/env-key-rules.js +34 -1
  60. package/dist/commands/lint/fact-linter.js +39 -0
  61. package/dist/commands/lint/index.js +31 -13
  62. package/dist/commands/lint/memory-linter.js +1 -1
  63. package/dist/commands/lint/registry.js +7 -2
  64. package/dist/commands/lint/task-linter.js +3 -3
  65. package/dist/commands/lint/workflow-linter.js +26 -1
  66. package/dist/commands/proposal/drain-policies.js +5 -0
  67. package/dist/commands/proposal/drain.js +17 -1
  68. package/dist/commands/proposal/proposal.js +5 -0
  69. package/dist/commands/proposal/propose.js +5 -0
  70. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  71. package/dist/commands/proposal/validators/proposals.js +187 -57
  72. package/dist/commands/read/curate.js +344 -80
  73. package/dist/commands/read/search-cli.js +7 -0
  74. package/dist/commands/read/search.js +1 -0
  75. package/dist/commands/read/show.js +67 -2
  76. package/dist/commands/sources/init.js +36 -9
  77. package/dist/commands/sources/installed-stashes.js +5 -1
  78. package/dist/commands/sources/schema-repair.js +13 -1
  79. package/dist/commands/sources/stash-cli.js +19 -3
  80. package/dist/commands/sources/stash-skeleton.js +23 -8
  81. package/dist/core/asset/asset-registry.js +2 -0
  82. package/dist/core/asset/asset-spec.js +14 -0
  83. package/dist/core/asset/frontmatter.js +166 -167
  84. package/dist/core/asset/markdown.js +8 -0
  85. package/dist/core/authoring-rules.js +83 -0
  86. package/dist/core/config/config-schema.js +274 -2
  87. package/dist/core/config/config.js +2 -2
  88. package/dist/core/logs-db.js +4 -3
  89. package/dist/core/paths.js +3 -0
  90. package/dist/core/standards/resolve-standards-context.js +87 -0
  91. package/dist/core/standards/resolve-stash-standards.js +99 -0
  92. package/dist/core/standards/resolve-type-conventions.js +66 -0
  93. package/dist/core/state-db.js +691 -30
  94. package/dist/indexer/db/db.js +364 -38
  95. package/dist/indexer/db/graph-db.js +129 -86
  96. package/dist/indexer/ensure-index.js +152 -17
  97. package/dist/indexer/graph/graph-boost.js +51 -41
  98. package/dist/indexer/graph/graph-extraction.js +203 -3
  99. package/dist/indexer/index-writer-lock.js +99 -0
  100. package/dist/indexer/indexer.js +114 -111
  101. package/dist/indexer/passes/memory-inference.js +10 -3
  102. package/dist/indexer/passes/staleness-detect.js +2 -5
  103. package/dist/indexer/search/db-search.js +15 -4
  104. package/dist/indexer/search/ranking-contributors.js +22 -0
  105. package/dist/indexer/search/ranking.js +4 -0
  106. package/dist/indexer/walk/matchers.js +9 -0
  107. package/dist/integrations/agent/prompts.js +33 -0
  108. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  109. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  110. package/dist/integrations/session-logs/index.js +16 -0
  111. package/dist/llm/client.js +23 -4
  112. package/dist/llm/embedder.js +27 -3
  113. package/dist/llm/embedders/local.js +66 -2
  114. package/dist/llm/feature-gate.js +8 -4
  115. package/dist/llm/graph-extract.js +2 -1
  116. package/dist/llm/memory-infer.js +4 -8
  117. package/dist/llm/metadata-enhance.js +9 -1
  118. package/dist/output/renderers.js +73 -1
  119. package/dist/output/shapes/curate.js +14 -2
  120. package/dist/output/text/helpers.js +16 -1
  121. package/dist/runtime.js +25 -1
  122. package/dist/scripts/migrate-storage.js +1378 -599
  123. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +479 -270
  124. package/dist/setup/setup.js +3 -3
  125. package/dist/sources/providers/git.js +71 -61
  126. package/dist/sources/providers/tar-utils.js +16 -8
  127. package/dist/storage/sqlite-pragmas.js +146 -0
  128. package/dist/wiki/wiki.js +37 -0
  129. package/dist/workflows/db.js +3 -4
  130. package/dist/workflows/validate-summary.js +2 -7
  131. package/docs/data-and-telemetry.md +1 -0
  132. package/package.json +8 -6
@@ -22,18 +22,22 @@ import { parseAssetRef } from "../../core/asset/asset-ref.js";
22
22
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
23
23
  import { META_DIR, parseMetaRef, resolveMetaFilePath } from "../../core/asset/stash-meta.js";
24
24
  import { asNonEmptyString } from "../../core/common.js";
25
- import { loadConfig } from "../../core/config/config.js";
25
+ import { getIndexPassConfig, loadConfig } from "../../core/config/config.js";
26
26
  import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
27
27
  import { appendEvent, readEvents } from "../../core/events.js";
28
- import { findEntryIdByRef } from "../../indexer/db/db.js";
28
+ import { closeDatabase, computeBodyHash, findEntryIdByRef, openExistingDatabase } from "../../indexer/db/db.js";
29
+ import { hasGraphData } from "../../indexer/db/graph-db.js";
29
30
  import { ensureIndex } from "../../indexer/ensure-index.js";
30
31
  import { listRelatedPathsForFile } from "../../indexer/graph/graph-boost.js";
32
+ import { extractGraphForSingleFile } from "../../indexer/graph/graph-extraction.js";
31
33
  import { lookup } from "../../indexer/indexer.js";
32
34
  import { buildEditHint, findSourceForPath, isEditable, resolveSourceEntries } from "../../indexer/search/search-source.js";
33
35
  import { insertUsageEvent } from "../../indexer/usage/usage-events.js";
34
36
  import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "../../indexer/walk/file-context.js";
35
37
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
38
+ import { resolveIndexPassLLM } from "../../llm/index-passes.js";
36
39
  import { resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
40
+ import { resolveStorageLocations } from "../../storage/locations.js";
37
41
  import { withIndexDb } from "../../storage/repositories/index-db.js";
38
42
  // Eagerly import source providers to trigger self-registration.
39
43
  import "../../sources/providers/index.js";
@@ -383,6 +387,15 @@ export async function showLocal(input) {
383
387
  if (activeRun) {
384
388
  fullResponse.activeRun = activeRun;
385
389
  }
390
+ // #624-P3: opt-in inline graph extraction. Default OFF — when the flag is
391
+ // unset this whole block is skipped (no hasGraphData check, no LLM call), so
392
+ // behavior is byte-identical to today. When ON, it extracts graph data for an
393
+ // ungraphed asset, but ONLY when a model is configured (model-available
394
+ // guard) and ALWAYS bounded by a 30s timeout so `show` can never hang. Any
395
+ // timeout/model-unavailable/error path returns the response unchanged.
396
+ if (getIndexPassConfig(config.index, "graph")?.lazyGraphExtraction === true) {
397
+ await maybeExtractGraphInline(config, sourceStashDir, assetPath);
398
+ }
386
399
  if (input.detail === "brief") {
387
400
  return buildBriefResponse(fullResponse, assetPath);
388
401
  }
@@ -391,6 +404,58 @@ export async function showLocal(input) {
391
404
  }
392
405
  return fullResponse;
393
406
  }
407
+ /**
408
+ * #624-P3 — opt-in inline graph extraction for `akm show`. Best-effort and
409
+ * timeout-bounded: never throws, never hangs, never mutates the response.
410
+ *
411
+ * Preconditions (caller already checked the flag): a model must be configured
412
+ * (model-available guard via {@link resolveIndexPassLLM}) and the asset must be
413
+ * ungraphed ({@link hasGraphData}). Extraction races a 30s timeout so `show`
414
+ * cannot block on a slow provider; any timeout/error/missing-model path is
415
+ * swallowed and `show` returns its already-assembled response unchanged.
416
+ */
417
+ async function maybeExtractGraphInline(config, sourceStashDir, assetPath) {
418
+ try {
419
+ // Model-available guard — no provider configured ⇒ silent skip, no LLM call.
420
+ if (!resolveIndexPassLLM("graph", config))
421
+ return;
422
+ let alreadyGraphed = false;
423
+ let bodyHash;
424
+ try {
425
+ const raw = fs.readFileSync(assetPath, "utf8");
426
+ bodyHash = computeBodyHash(parseFrontmatter(raw).content.trim());
427
+ }
428
+ catch {
429
+ return; // file gone/unreadable ⇒ nothing to extract
430
+ }
431
+ withIndexDb((db) => {
432
+ alreadyGraphed = hasGraphData(db, sourceStashDir, assetPath);
433
+ });
434
+ if (alreadyGraphed)
435
+ return;
436
+ // Open the db for the async extraction ourselves: `withIndexDb` is
437
+ // synchronous and would close the connection the instant the async fn
438
+ // returns its Promise (before extraction completes). Close it explicitly
439
+ // after the race settles instead.
440
+ const db = openExistingDatabase(resolveStorageLocations().indexDb);
441
+ let timer;
442
+ const timeout = new Promise((resolve) => {
443
+ timer = setTimeout(resolve, 30_000);
444
+ });
445
+ try {
446
+ await Promise.race([extractGraphForSingleFile(db, sourceStashDir, assetPath, bodyHash, { config }), timeout]);
447
+ }
448
+ finally {
449
+ if (timer)
450
+ clearTimeout(timer);
451
+ closeDatabase(db);
452
+ }
453
+ }
454
+ catch (err) {
455
+ rethrowIfTestIsolationError(err);
456
+ // Any other failure: silently return the unchanged show response.
457
+ }
458
+ }
394
459
  /**
395
460
  * Minimal `show`: ref → indexer lookup → file contents. Used by callers that
396
461
  * just need the raw file (e.g. clone, write-source) and don't want the full
@@ -53,6 +53,8 @@ function assertInitSandbox(stashDir, dirExplicitlyProvided) {
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
55
  export async function akmInit(options) {
56
+ const dirExplicitlyProvided = options?.dir != null;
57
+ const setDefault = options?.setDefault === true;
56
58
  const stashDir = options?.dir ? path.resolve(options.dir) : getDefaultStashDir();
57
59
  // Safety check (#473): refuse stashDir at /, $HOME, /etc, ~/.config, etc.
58
60
  // Runs BEFORE any disk write — a fat-fingered `akm init --dir /` or
@@ -61,7 +63,7 @@ export async function akmInit(options) {
61
63
  assertSafeStashDir(stashDir);
62
64
  // Defense-in-depth: refuse to persist an explicit --dir /tmp/... stashDir
63
65
  // to config under a test runner. Default HOME-resolved paths are exempt.
64
- assertInitSandbox(stashDir, options?.dir != null);
66
+ assertInitSandbox(stashDir, dirExplicitlyProvided);
65
67
  let created = false;
66
68
  if (!fs.existsSync(stashDir)) {
67
69
  fs.mkdirSync(stashDir, { recursive: true });
@@ -75,15 +77,40 @@ export async function akmInit(options) {
75
77
  }
76
78
  // Ensure the default stash is a local git repo (no remote required)
77
79
  ensureGitRepo(stashDir);
78
- if (created) {
79
- copyStashSkeleton(stashDir);
80
- scaffoldStashMeta(stashDir);
81
- }
82
- // Persist stashDir in config.json
80
+ // Run seeding UNCONDITIONALLY (not just when the stash was newly created) so
81
+ // re-running `akm init` on an existing stash backfills any missing skeleton
82
+ // files — the README, the per-type SOFT convention templates under
83
+ // facts/conventions/assets/, and the `.meta/index.md` orientation doc. Both
84
+ // helpers are absent-only: they never overwrite a file a user has edited.
85
+ copyStashSkeleton(stashDir);
86
+ scaffoldStashMeta(stashDir);
87
+ // Persist stashDir in config.json — but ONLY when the user is actually
88
+ // setting up / opting into a default. A bare `akm init --dir <secondary>`
89
+ // must NOT silently repoint the user's real default stash (the footgun
90
+ // documented in memory:akm-init-persists-stashdir-warning).
91
+ //
92
+ // Decision matrix — persist when ANY of:
93
+ // (a) no --dir provided → default HOME-resolved setup flow
94
+ // (b) --dir AND no existing stashDir in config → first-time bootstrap
95
+ // (c) --dir AND --set-default → explicit opt-in
96
+ // Otherwise (--dir + existing default + no --set-default) leave the default
97
+ // pointer alone; the target dir is still scaffolded above.
83
98
  const configPath = getConfigPath();
84
99
  const existing = loadUserConfig();
85
- if (!existing.stashDir || existing.stashDir !== stashDir) {
86
- saveConfig({ ...existing, stashDir });
100
+ const existingStashDir = existing.stashDir;
101
+ const shouldPersist = !dirExplicitlyProvided || !existingStashDir || setDefault;
102
+ let defaultStashUpdated = false;
103
+ let previousStashDir;
104
+ if (shouldPersist) {
105
+ if (!existingStashDir || existingStashDir !== stashDir) {
106
+ saveConfig({ ...existing, stashDir });
107
+ defaultStashUpdated = true;
108
+ }
109
+ // else: already pointed here — no-op, no spurious rewrite.
110
+ }
111
+ else {
112
+ // Default left untouched; surface it so the CLI can inform the user.
113
+ previousStashDir = existingStashDir;
87
114
  }
88
115
  // Ensure ripgrep is available (install to cache/bin if needed)
89
116
  let ripgrep;
@@ -95,7 +122,7 @@ export async function akmInit(options) {
95
122
  catch {
96
123
  // Non-fatal: ripgrep is optional, search works without it
97
124
  }
98
- return { stashDir, created, configPath, ripgrep };
125
+ return { stashDir, created, configPath, defaultStashUpdated, previousStashDir, ripgrep };
99
126
  }
100
127
  /** Initialise `dir` as a git repository if it is not already one. */
101
128
  function ensureGitRepo(dir) {
@@ -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,
@@ -17,7 +17,9 @@ 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";
22
+ import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
21
23
  import { info, warn } from "../../core/warn.js";
22
24
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
23
25
  import { chatCompletion, parseEmbeddedJsonResponse } from "../../llm/client.js";
@@ -94,6 +96,16 @@ export async function runSchemaRepairPass(failures, options) {
94
96
  const fieldList = missingFields.join(" and ");
95
97
  info(`[improve] schema-repair ${failure.ref} (${fieldList})`);
96
98
  const bodyPreview = (fm.content ?? raw).slice(0, 2000);
99
+ // Standards "rulebook" for this target — wiki schema (wiki page) or stash
100
+ // convention/meta facts (non-wiki asset); empty when neither fires or no
101
+ // stash dir is available. `resolveStandardsContext` dispatches on the ref.
102
+ const standardsContext = stashDir ? resolveStandardsContext(failure.ref, stashDir) : "";
103
+ const standardsSection = standardsContext.trim()
104
+ ? `\n\nStandards to follow (the rulebook for this target):\n${standardsContext.trim()}`
105
+ : "";
106
+ const assetType = parseAssetRef(failure.ref).type;
107
+ const authoringRules = authoringRulesForType(assetType);
108
+ const authoringRulesSection = authoringRules ? `\n\n${authoringRules}` : "";
97
109
  const llmResponse = await chatFn(llmConfig, [
98
110
  {
99
111
  role: "system",
@@ -101,7 +113,7 @@ export async function runSchemaRepairPass(failures, options) {
101
113
  },
102
114
  {
103
115
  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}`,
116
+ 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
117
  },
106
118
  ]);
107
119
  const parsed = parseEmbeddedJsonResponse(llmResponse.trim());
@@ -48,12 +48,20 @@ export const initCommand = defineJsonCommand({
48
48
  },
49
49
  args: {
50
50
  dir: { type: "string", description: "Custom stash directory path (default: ~/akm)" },
51
+ "set-default": {
52
+ type: "boolean",
53
+ description: "Make --dir the default stash (write stashDir to config.json). Without this, `akm init --dir X` scaffolds X but leaves your existing default stash unchanged.",
54
+ default: false,
55
+ },
51
56
  },
52
57
  async run({ args }) {
53
58
  // Accept both historical spellings for backwards compatibility with
54
59
  // older docs/scripts that used `--stashDir`.
55
60
  const legacyDir = parseFlagValue(process.argv, "--stashDir") ?? parseFlagValue(process.argv, "--stash-dir");
56
- const result = await akmInit({ dir: args.dir ?? legacyDir });
61
+ const result = await akmInit({
62
+ dir: args.dir ?? legacyDir,
63
+ setDefault: getHyphenatedBoolean(args, "set-default"),
64
+ });
57
65
  output("init", result);
58
66
  },
59
67
  });
@@ -71,6 +79,11 @@ export const indexCommand = defineCommand({
71
79
  description: "When combined with --clean, report stale entries without deleting them.",
72
80
  default: false,
73
81
  },
82
+ background: {
83
+ type: "boolean",
84
+ description: "Run as a background process (suppresses interactive output, manages PID file).",
85
+ default: false,
86
+ },
74
87
  },
75
88
  async run({ args }) {
76
89
  await runWithJsonErrors(async () => {
@@ -80,6 +93,7 @@ export const indexCommand = defineCommand({
80
93
  if (getHyphenatedBoolean(args, "re-enrich") || parseFlagValue(process.argv, "--re-enrich") !== undefined) {
81
94
  throw new UsageError("`akm index --re-enrich` has been removed. Re-enrichment of index-time LLM passes is not exposed in this slice.");
82
95
  }
96
+ const isBackground = args.background === true;
83
97
  const outputMode = getOutputMode();
84
98
  const controller = new AbortController();
85
99
  const abort = () => controller.abort(new Error("index interrupted"));
@@ -88,7 +102,7 @@ export const indexCommand = defineCommand({
88
102
  const indexLogFile = path.join(getCacheDir(), "logs", "index", `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
89
103
  setLogFile(indexLogFile);
90
104
  const verbose = isVerbose();
91
- const spin = !verbose && outputMode.format === "text" ? p.spinner() : null;
105
+ const spin = !verbose && !isBackground && outputMode.format === "text" ? p.spinner() : null;
92
106
  if (spin) {
93
107
  spin.start(`Building search index${args.full ? " (full rebuild)" : ""}...`);
94
108
  }
@@ -114,7 +128,9 @@ export const indexCommand = defineCommand({
114
128
  if (spin) {
115
129
  spin.stop(`Indexed ${result.totalEntries} assets.`);
116
130
  }
117
- output("index", result);
131
+ if (!isBackground) {
132
+ output("index", result);
133
+ }
118
134
  }
119
135
  catch (error) {
120
136
  if (spin) {
@@ -6,27 +6,42 @@ import path from "node:path";
6
6
  import { getDirname } from "../../runtime.js";
7
7
  const SKELETON_DIR = path.join(getDirname(import.meta.url), "../../assets/stash-skeleton");
8
8
  /**
9
- * Copy the default stash skeleton into a newly created stash directory.
9
+ * Copy the default stash skeleton into a stash directory.
10
10
  *
11
- * Each file in src/assets/stash-skeleton/ is written to the stash root only
12
- * if the destination does not already exist — existing files are never
13
- * overwritten. Non-fatal: if the skeleton directory is missing or a copy
14
- * fails the caller continues normally.
11
+ * The skeleton tree under src/assets/stash-skeleton/ is mirrored **recursively**
12
+ * into the stash root, preserving relative subpaths (e.g.
13
+ * `facts/conventions/assets/skill.md` lands at the matching stash subpath).
14
+ * Each file is written only if the destination does not already exist — existing
15
+ * (possibly user-edited) files are never overwritten. Intermediate directories
16
+ * are created as needed.
17
+ *
18
+ * Idempotent and absent-only: running it again on an existing stash backfills
19
+ * any skeleton files that are missing without clobbering present ones. Non-fatal:
20
+ * if the skeleton directory is missing or a copy fails the caller continues.
15
21
  */
16
22
  export function copyStashSkeleton(stashDir) {
23
+ copySkeletonDir(SKELETON_DIR, stashDir);
24
+ }
25
+ /** Recursively mirror `srcDir` into `destDir`, writing files only when absent. */
26
+ function copySkeletonDir(srcDir, destDir) {
17
27
  let entries;
18
28
  try {
19
- entries = fs.readdirSync(SKELETON_DIR);
29
+ entries = fs.readdirSync(srcDir, { withFileTypes: true });
20
30
  }
21
31
  catch {
22
32
  return;
23
33
  }
24
34
  for (const entry of entries) {
25
- const src = path.join(SKELETON_DIR, entry);
26
- const dest = path.join(stashDir, entry);
35
+ const src = path.join(srcDir, entry.name);
36
+ const dest = path.join(destDir, entry.name);
37
+ if (entry.isDirectory()) {
38
+ copySkeletonDir(src, dest);
39
+ continue;
40
+ }
27
41
  if (fs.existsSync(dest))
28
42
  continue;
29
43
  try {
44
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
30
45
  fs.copyFileSync(src, dest);
31
46
  }
32
47
  catch {
@@ -29,6 +29,7 @@ export const TYPE_TO_RENDERER = {
29
29
  wiki: "wiki-md",
30
30
  task: "task-yaml",
31
31
  session: "session-md",
32
+ fact: "fact-md",
32
33
  };
33
34
  /** Map asset types to action builder functions for search results. */
34
35
  export const ACTION_BUILDERS = {
@@ -45,6 +46,7 @@ export const ACTION_BUILDERS = {
45
46
  wiki: (ref) => `akm show ${ref} -> read the wiki page`,
46
47
  task: (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`,
47
48
  session: (ref) => `akm show ${ref} -> read the session summary; follow the \`access\` frontmatter to open the raw log at \`log_path\``,
49
+ fact: (ref) => `akm show ${ref} -> read the stash fact and apply it as durable context`,
48
50
  };
49
51
  /**
50
52
  * Register a type-to-renderer mapping.
@@ -154,6 +154,20 @@ const ASSET_SPECS_INTERNAL = {
154
154
  rendererName: "session-md",
155
155
  actionBuilder: (ref) => `akm show ${ref} -> read the session summary; follow the \`access\` frontmatter to open the raw log at \`log_path\``,
156
156
  },
157
+ // Durable stash-level semantic knowledge — facts about the user, team, or
158
+ // project (personal details, team tool stacks, coding conventions /
159
+ // "constitution", and stash-meta like naming conventions or the active
160
+ // projects list). Unlike `memory` (episodic, recency-decayed) these are
161
+ // mostly-static declarations meant to be reliably surfaced as context. A
162
+ // plain markdown spec; `category` frontmatter scopes the fact and
163
+ // `pinned: true` marks the small always-injected core. See
164
+ // docs/design/fact-asset-type.md.
165
+ fact: {
166
+ stashDir: "facts",
167
+ ...markdownSpec,
168
+ rendererName: "fact-md",
169
+ actionBuilder: (ref) => `akm show ${ref} -> read the stash fact and apply it as durable context`,
170
+ },
157
171
  };
158
172
  export const ASSET_SPECS = ASSET_SPECS_INTERNAL;
159
173
  /**