akm-cli 0.9.0-rc.1 → 0.9.0-rc.13

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 (587) hide show
  1. package/CHANGELOG.md +1190 -52
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/stash-skeleton/README.md +38 -10
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  38. package/dist/assets/tasks/core/extract.yml +3 -2
  39. package/dist/assets/tasks/core/improve.yml +2 -1
  40. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  41. package/dist/assets/tasks/core/sync.yml +1 -0
  42. package/dist/assets/tasks/core/version-check.yml +2 -1
  43. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  44. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  45. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  46. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  48. package/dist/assets/templates/html/health.html +5 -4
  49. package/dist/assets/workflows/workflow-template.md +31 -15
  50. package/dist/cli/invocation.js +279 -0
  51. package/dist/cli/parse-args.js +5 -90
  52. package/dist/cli/retired-commands.js +78 -0
  53. package/dist/cli/shared.js +158 -48
  54. package/dist/cli-node.mjs +2 -1
  55. package/dist/cli.js +747 -293
  56. package/dist/commands/agent/agent-dispatch.js +19 -18
  57. package/dist/commands/agent/agent-support.js +0 -24
  58. package/dist/commands/agent/contribute-cli.js +43 -97
  59. package/dist/commands/completions.js +80 -23
  60. package/dist/commands/config-cli.js +44 -281
  61. package/dist/commands/env/env-binding.js +13 -9
  62. package/dist/commands/env/env-cli.js +76 -159
  63. package/dist/commands/env/env.js +12 -163
  64. package/dist/commands/env/marker-path.js +6 -0
  65. package/dist/commands/env/secret-cli.js +45 -61
  66. package/dist/commands/env/secret.js +32 -62
  67. package/dist/commands/feedback-cli.js +179 -85
  68. package/dist/commands/health/accept-rate.js +58 -0
  69. package/dist/commands/health/advisories.js +7 -8
  70. package/dist/commands/health/checks.js +279 -94
  71. package/dist/commands/health/html-report.js +197 -578
  72. package/dist/commands/health/improve-metrics.js +277 -246
  73. package/dist/commands/health/llm-usage.js +19 -19
  74. package/dist/commands/health/md-report.js +16 -7
  75. package/dist/commands/health/metrics.js +67 -32
  76. package/dist/commands/health/renderers.js +47 -0
  77. package/dist/commands/health/report-view-model.js +508 -0
  78. package/dist/commands/health/stash-exposure.js +1 -1
  79. package/dist/commands/health/surfaces.js +16 -56
  80. package/dist/commands/health/task-runs.js +3 -67
  81. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  82. package/dist/commands/health/types-improve.js +29 -0
  83. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  84. package/dist/commands/health/types-result.js +7 -0
  85. package/dist/commands/health/types-runs.js +4 -0
  86. package/dist/commands/health/types-session-log.js +4 -0
  87. package/dist/commands/health/types-windows.js +4 -0
  88. package/dist/commands/health/types.js +26 -21
  89. package/dist/commands/health/windows.js +2 -3
  90. package/dist/commands/health.js +296 -167
  91. package/dist/commands/improve/anti-collapse.js +5 -5
  92. package/dist/commands/improve/autonomy-gate.js +68 -0
  93. package/dist/commands/improve/collapse-detector.js +65 -52
  94. package/dist/commands/improve/consolidate/chunking.js +9 -7
  95. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  96. package/dist/commands/improve/consolidate/merge.js +4 -0
  97. package/dist/commands/improve/consolidate.js +454 -1354
  98. package/dist/commands/improve/content-hash.js +39 -0
  99. package/dist/commands/improve/distill/content-repair.js +4 -10
  100. package/dist/commands/improve/distill/promote-memory.js +89 -64
  101. package/dist/commands/improve/distill/quality-gate.js +118 -42
  102. package/dist/commands/improve/distill-guards.js +1 -1
  103. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  104. package/dist/commands/improve/distill.js +607 -363
  105. package/dist/commands/improve/eligibility.js +165 -79
  106. package/dist/commands/improve/extract-cli.js +35 -126
  107. package/dist/commands/improve/extract-prompt.js +6 -35
  108. package/dist/commands/improve/extract.js +640 -391
  109. package/dist/commands/improve/feedback-valence.js +2 -12
  110. package/dist/commands/improve/improve-cli.js +134 -135
  111. package/dist/commands/improve/improve-result-file.js +30 -50
  112. package/dist/commands/improve/improve-run-types.js +4 -0
  113. package/dist/commands/improve/improve-strategies.js +135 -0
  114. package/dist/commands/improve/improve.js +904 -701
  115. package/dist/commands/improve/locks.js +64 -111
  116. package/dist/commands/improve/loop-stages.js +1110 -923
  117. package/dist/commands/improve/memory/derived-ref.js +124 -0
  118. package/dist/commands/improve/memory/memory-belief.js +79 -7
  119. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  120. package/dist/commands/improve/memory/memory-improve.js +25 -37
  121. package/dist/commands/improve/outcome-loop.js +25 -88
  122. package/dist/commands/improve/preparation.js +1034 -813
  123. package/dist/commands/improve/proactive-maintenance.js +34 -9
  124. package/dist/commands/improve/proposal-envelope.js +31 -0
  125. package/dist/commands/improve/reflect.js +983 -794
  126. package/dist/commands/improve/run-context.js +119 -0
  127. package/dist/commands/improve/salience.js +24 -127
  128. package/dist/commands/improve/session-asset.js +7 -3
  129. package/dist/commands/improve/shared.js +14 -34
  130. package/dist/commands/improve/source-identity.js +28 -0
  131. package/dist/commands/improve/triage.js +20 -17
  132. package/dist/commands/lint/base-linter.js +340 -313
  133. package/dist/commands/lint/env-key-rules.js +31 -47
  134. package/dist/commands/lint/index.js +185 -30
  135. package/dist/commands/{events.js → log.js} +28 -38
  136. package/dist/commands/migrate-cli.js +54 -0
  137. package/dist/commands/migration-tool.js +55 -0
  138. package/dist/commands/observability-cli.js +70 -208
  139. package/dist/commands/proposal/diff-format.js +50 -0
  140. package/dist/commands/proposal/drain-policies.js +0 -6
  141. package/dist/commands/proposal/drain.js +91 -40
  142. package/dist/commands/proposal/proposal-cli.js +134 -132
  143. package/dist/commands/proposal/proposal-types.js +56 -0
  144. package/dist/commands/proposal/proposal.js +83 -65
  145. package/dist/commands/proposal/propose-cli.js +88 -0
  146. package/dist/commands/proposal/propose.js +105 -88
  147. package/dist/commands/proposal/repository.js +1303 -278
  148. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  149. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  150. package/dist/commands/proposal/validators/proposals.js +6 -8
  151. package/dist/commands/read/curate.js +78 -73
  152. package/dist/commands/read/knowledge.js +510 -13
  153. package/dist/commands/read/registry-search.js +2 -2
  154. package/dist/commands/read/remember-cli.js +84 -15
  155. package/dist/commands/read/search-cli.js +203 -96
  156. package/dist/commands/read/search.js +126 -94
  157. package/dist/commands/read/show.js +226 -250
  158. package/dist/commands/registry-cli.js +34 -60
  159. package/dist/commands/remember.js +18 -57
  160. package/dist/commands/sources/add-cli.js +104 -49
  161. package/dist/commands/sources/bundle-cli.js +166 -0
  162. package/dist/commands/sources/bundle-config-ops.js +63 -0
  163. package/dist/commands/sources/info.js +27 -15
  164. package/dist/commands/sources/init.js +30 -40
  165. package/dist/commands/sources/installed-stashes.js +469 -172
  166. package/dist/commands/sources/schema-repair.js +10 -9
  167. package/dist/commands/sources/self-update.js +182 -121
  168. package/dist/commands/sources/source-add.js +169 -178
  169. package/dist/commands/sources/source-clone.js +144 -41
  170. package/dist/commands/sources/source-manage.js +94 -59
  171. package/dist/commands/sources/sources-cli.js +64 -205
  172. package/dist/commands/sources/stash-cli.js +91 -54
  173. package/dist/commands/sources/stash-skeleton.js +1 -1
  174. package/dist/commands/tasks/tasks-cli.js +106 -104
  175. package/dist/commands/tasks/tasks.js +445 -262
  176. package/dist/commands/workflow-cli.js +75 -228
  177. package/dist/core/action-contributors.js +1 -1
  178. package/dist/core/activation-policy.js +49 -0
  179. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  180. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  181. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  182. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  183. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  184. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  185. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  186. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  187. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  188. package/dist/core/adapter/adapters/index.js +80 -0
  189. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  190. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  191. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  192. package/dist/core/adapter/adapters/shared.js +286 -0
  193. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  194. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  195. package/dist/core/adapter/bundle-adapter.js +4 -0
  196. package/dist/core/adapter/detect-adapter.js +17 -0
  197. package/dist/core/adapter/recognize-match.js +44 -0
  198. package/dist/core/adapter/registry.js +56 -0
  199. package/dist/core/adapter/types.js +4 -0
  200. package/dist/core/asset/akm-markdown.js +30 -0
  201. package/dist/core/asset/asset-placement.js +243 -0
  202. package/dist/core/asset/asset-ref.js +110 -79
  203. package/dist/core/asset/asset-serialize.js +20 -0
  204. package/dist/core/asset/frontmatter.js +28 -12
  205. package/dist/core/asset/markdown.js +40 -51
  206. package/dist/core/asset/resolve-ref.js +274 -0
  207. package/dist/core/asset/stash-meta.js +2 -2
  208. package/dist/core/bundle-id.js +51 -0
  209. package/dist/core/common.js +281 -86
  210. package/dist/core/config/config-io.js +42 -128
  211. package/dist/core/config/config-schema.js +233 -855
  212. package/dist/core/config/config-sources.js +162 -39
  213. package/dist/core/config/config-types.js +16 -11
  214. package/dist/core/config/config-version.js +29 -0
  215. package/dist/core/config/config-walker.js +126 -37
  216. package/dist/core/config/config.js +154 -331
  217. package/dist/core/config/deep-merge.js +41 -0
  218. package/dist/core/config/engine-semantics.js +28 -0
  219. package/dist/core/config/experimental.js +21 -0
  220. package/dist/core/config/schema/embedding.js +38 -0
  221. package/dist/core/config/schema/engines.js +116 -0
  222. package/dist/core/config/schema/experimental.js +47 -0
  223. package/dist/core/config/schema/feedback.js +31 -0
  224. package/dist/core/config/schema/improve-processes.js +389 -0
  225. package/dist/core/config/schema/improve.js +94 -0
  226. package/dist/core/config/schema/index-config.js +176 -0
  227. package/dist/core/config/schema/output.js +18 -0
  228. package/dist/core/config/schema/primitives.js +94 -0
  229. package/dist/core/config/schema/search.js +30 -0
  230. package/dist/core/config/schema/setup.js +18 -0
  231. package/dist/core/config/schema/sources-bundles.js +169 -0
  232. package/dist/core/config/schema/workflow.js +29 -0
  233. package/dist/core/env-secret-ref.js +155 -20
  234. package/dist/core/errors.js +17 -15
  235. package/dist/core/events-types.js +4 -0
  236. package/dist/core/events.js +46 -128
  237. package/dist/core/extra-params.js +62 -0
  238. package/dist/core/file-change.js +17 -0
  239. package/dist/core/file-lock.js +202 -57
  240. package/dist/core/fs-txn.js +392 -0
  241. package/dist/core/git-message.js +59 -0
  242. package/dist/core/improve-result.js +167 -0
  243. package/dist/core/lesson-lint.js +1 -17
  244. package/dist/core/logs-db.js +1 -1
  245. package/dist/core/maintenance-barrier.js +135 -0
  246. package/dist/core/migration-operation.js +44 -0
  247. package/dist/core/mutation-target.js +78 -0
  248. package/dist/core/paths.js +22 -25
  249. package/dist/core/platform.js +10 -0
  250. package/dist/core/recognition-util.js +128 -0
  251. package/dist/core/redaction.js +392 -0
  252. package/dist/core/standards/resolve-standards-context.js +36 -65
  253. package/dist/core/standards/resolve-stash-standards.js +2 -2
  254. package/dist/core/standards/resolve-type-conventions.js +5 -5
  255. package/dist/core/state/migrations.js +242 -11
  256. package/dist/core/state-db.js +98 -10
  257. package/dist/core/structured.js +1 -1
  258. package/dist/core/subprocess.js +303 -0
  259. package/dist/core/text-truncation.js +9 -5
  260. package/dist/core/time.js +20 -0
  261. package/dist/core/type-presentation.js +130 -0
  262. package/dist/core/warn.js +0 -3
  263. package/dist/core/write-source.js +834 -118
  264. package/dist/indexer/bundle-identity-guard.js +92 -0
  265. package/dist/indexer/db/graph-db.js +1 -25
  266. package/dist/indexer/db/llm-cache.js +1 -1
  267. package/dist/indexer/ensure-index.js +30 -9
  268. package/dist/indexer/graph/graph-boost.js +9 -30
  269. package/dist/indexer/graph/graph-extraction.js +41 -27
  270. package/dist/indexer/graph/graph-types.js +4 -0
  271. package/dist/indexer/index-writer-lock.js +93 -49
  272. package/dist/indexer/index-written-assets.js +100 -53
  273. package/dist/indexer/indexer.js +746 -329
  274. package/dist/indexer/init.js +18 -25
  275. package/dist/indexer/installations.js +142 -0
  276. package/dist/indexer/passes/dir-staleness.js +18 -10
  277. package/dist/indexer/passes/memory-inference.js +25 -15
  278. package/dist/indexer/passes/metadata.js +412 -243
  279. package/dist/indexer/scan/doc-to-entry.js +160 -0
  280. package/dist/indexer/scan/drain-dir.js +134 -0
  281. package/dist/indexer/search/db-search.js +292 -108
  282. package/dist/indexer/search/fts-query.js +64 -0
  283. package/dist/indexer/search/ranking-contributors.js +145 -25
  284. package/dist/indexer/search/ranking-types.js +4 -0
  285. package/dist/indexer/search/ranking.js +28 -71
  286. package/dist/indexer/search/search-attribution.js +67 -0
  287. package/dist/indexer/search/search-fields.js +18 -3
  288. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  289. package/dist/indexer/search/search-source.js +157 -111
  290. package/dist/indexer/search/semantic-status.js +4 -1
  291. package/dist/indexer/usage/usage-events.js +10 -30
  292. package/dist/indexer/walk/file-context.js +3 -45
  293. package/dist/indexer/walk/matchers.js +42 -73
  294. package/dist/indexer/walk/path-resolver.js +11 -5
  295. package/dist/indexer/walk/walker.js +42 -14
  296. package/dist/integrations/agent/builder-shared.js +7 -0
  297. package/dist/integrations/agent/builders.js +5 -58
  298. package/dist/integrations/agent/config.js +3 -143
  299. package/dist/integrations/agent/detect.js +17 -2
  300. package/dist/integrations/agent/engine-resolution.js +231 -0
  301. package/dist/integrations/agent/index.js +1 -2
  302. package/dist/integrations/agent/model-aliases.js +8 -3
  303. package/dist/integrations/agent/profiles.js +6 -99
  304. package/dist/integrations/agent/prompts.js +46 -18
  305. package/dist/integrations/agent/runner-dispatch.js +78 -13
  306. package/dist/integrations/agent/runner.js +76 -208
  307. package/dist/integrations/agent/spawn.js +48 -279
  308. package/dist/integrations/harnesses/aider/agent-builder.js +9 -8
  309. package/dist/integrations/harnesses/aider/index.js +2 -12
  310. package/dist/integrations/harnesses/amazonq/agent-builder.js +10 -16
  311. package/dist/integrations/harnesses/amazonq/index.js +3 -17
  312. package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
  313. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  314. package/dist/integrations/harnesses/claude/index.js +1 -14
  315. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  316. package/dist/integrations/harnesses/codex/agent-builder.js +8 -7
  317. package/dist/integrations/harnesses/codex/index.js +2 -13
  318. package/dist/integrations/harnesses/copilot/agent-builder.js +9 -9
  319. package/dist/integrations/harnesses/copilot/index.js +1 -13
  320. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -9
  321. package/dist/integrations/harnesses/gemini/index.js +1 -13
  322. package/dist/integrations/harnesses/ids.js +24 -0
  323. package/dist/integrations/harnesses/index.js +31 -33
  324. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  325. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  326. package/dist/integrations/harnesses/opencode/index.js +1 -18
  327. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  328. package/dist/integrations/harnesses/opencode-sdk/harness.js +3 -17
  329. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
  330. package/dist/integrations/harnesses/openhands/agent-builder.js +12 -10
  331. package/dist/integrations/harnesses/openhands/index.js +2 -12
  332. package/dist/integrations/harnesses/pi/agent-builder.js +11 -18
  333. package/dist/integrations/harnesses/pi/index.js +3 -16
  334. package/dist/integrations/harnesses/shared.js +17 -0
  335. package/dist/integrations/harnesses/types.js +38 -34
  336. package/dist/integrations/lockfile.js +211 -24
  337. package/dist/integrations/session-logs/index.js +24 -40
  338. package/dist/integrations/session-logs/provider-base.js +113 -0
  339. package/dist/llm/client.js +182 -110
  340. package/dist/llm/embedders/deterministic.js +2 -2
  341. package/dist/llm/embedders/remote.js +21 -9
  342. package/dist/llm/feature-gate.js +17 -57
  343. package/dist/llm/graph-extract.js +12 -13
  344. package/dist/llm/index-passes.js +8 -42
  345. package/dist/llm/memory-infer.js +144 -1
  346. package/dist/llm/metadata-enhance.js +45 -30
  347. package/dist/llm/structured-call.js +16 -8
  348. package/dist/llm/usage-persist.js +30 -5
  349. package/dist/llm/usage-telemetry.js +59 -6
  350. package/dist/output/cli-hints.js +1 -2
  351. package/dist/output/command-registry.js +27 -0
  352. package/dist/output/context.js +22 -7
  353. package/dist/output/format-exempt.js +80 -0
  354. package/dist/output/generic-render.js +251 -0
  355. package/dist/output/html-render.js +11 -16
  356. package/dist/output/render-registry.js +57 -0
  357. package/dist/output/renderers.js +15 -281
  358. package/dist/output/shapes/curate.js +10 -1
  359. package/dist/output/shapes/events.js +12 -7
  360. package/dist/output/shapes/helpers.js +58 -84
  361. package/dist/output/shapes/passthrough.js +8 -40
  362. package/dist/output/shapes/proposal/producer.js +15 -7
  363. package/dist/output/shapes/registry.js +12 -6
  364. package/dist/output/shapes.js +0 -9
  365. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  366. package/dist/output/text/bundle-show.js +7 -0
  367. package/dist/output/text/command-format.js +562 -0
  368. package/dist/output/text/env.js +1 -3
  369. package/dist/output/text/events.js +8 -7
  370. package/dist/output/text/helpers.js +15 -1375
  371. package/dist/output/text/proposal/producer.js +4 -2
  372. package/dist/output/text/proposal-format.js +202 -0
  373. package/dist/output/text/registry-commands.js +1 -2
  374. package/dist/output/text/registry.js +12 -6
  375. package/dist/output/text/show-directives.js +117 -0
  376. package/dist/output/text/show-format.js +103 -0
  377. package/dist/output/text/sync.js +5 -0
  378. package/dist/output/text/workflow-format.js +332 -0
  379. package/dist/output/text/workflow.js +1 -2
  380. package/dist/output/text.js +10 -19
  381. package/dist/registry/factory.js +4 -6
  382. package/dist/registry/origin-resolve.js +16 -27
  383. package/dist/registry/providers/skills-sh.js +3 -3
  384. package/dist/registry/providers/static-index.js +15 -25
  385. package/dist/registry/resolve.js +43 -94
  386. package/dist/registry/semver.js +43 -0
  387. package/dist/runtime.js +81 -12
  388. package/dist/scripts/akm-migrate.js +35529 -0
  389. package/dist/setup/detect.js +5 -7
  390. package/dist/setup/detected-engines.js +136 -0
  391. package/dist/setup/engine-config.js +100 -0
  392. package/dist/setup/registry-stash-loader.js +3 -3
  393. package/dist/setup/semantic-assets.js +12 -9
  394. package/dist/setup/setup.js +444 -208
  395. package/dist/setup/steps/connection-shared.js +120 -0
  396. package/dist/setup/steps/connection.js +108 -305
  397. package/dist/setup/steps/platforms.js +13 -12
  398. package/dist/setup/steps/semantic.js +15 -3
  399. package/dist/setup/steps/sources.js +21 -15
  400. package/dist/setup/steps/stashdir.js +6 -4
  401. package/dist/setup/steps/tasks.js +236 -119
  402. package/dist/setup/steps.js +3 -2
  403. package/dist/sources/freshness.js +39 -0
  404. package/dist/sources/provider-factory.js +11 -17
  405. package/dist/sources/providers/filesystem.js +2 -3
  406. package/dist/sources/providers/git-install.js +278 -34
  407. package/dist/sources/providers/git-provider.js +54 -56
  408. package/dist/sources/providers/git-stash.js +420 -91
  409. package/dist/sources/providers/git.js +2 -2
  410. package/dist/sources/providers/npm.js +16 -19
  411. package/dist/sources/providers/provider-utils.js +47 -22
  412. package/dist/sources/providers/sync-from-ref.js +3 -9
  413. package/dist/sources/providers/website.js +2 -2
  414. package/dist/sources/resolve.js +11 -10
  415. package/dist/sources/snapshot-fetchers/types.js +4 -0
  416. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  417. package/dist/storage/database.js +60 -4
  418. package/dist/storage/engines/sqlite-migrations.js +156 -5
  419. package/dist/storage/locations.js +1 -2
  420. package/dist/storage/repositories/canaries-repository.js +1 -1
  421. package/dist/storage/repositories/events-repository.js +51 -11
  422. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  423. package/dist/storage/repositories/index-connection.js +79 -0
  424. package/dist/storage/repositories/index-db.js +4 -3
  425. package/dist/storage/repositories/index-entries-repository.js +863 -0
  426. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  427. package/dist/storage/repositories/index-entry-types.js +4 -0
  428. package/dist/storage/repositories/index-fts-repository.js +167 -0
  429. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  430. package/dist/storage/repositories/index-meta-repository.js +49 -0
  431. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  432. package/dist/storage/repositories/index-sql.js +12 -0
  433. package/dist/storage/repositories/index-utility-repository.js +356 -0
  434. package/dist/storage/repositories/index-vec-repository.js +250 -0
  435. package/dist/storage/repositories/outcome-repository.js +119 -0
  436. package/dist/storage/repositories/proposals-repository.js +317 -75
  437. package/dist/storage/repositories/registry-cache.js +1 -1
  438. package/dist/storage/repositories/salience-repository.js +172 -0
  439. package/dist/storage/repositories/task-history-repository.js +110 -3
  440. package/dist/storage/repositories/workflow-runs-repository.js +68 -35
  441. package/dist/tasks/backends/cron.js +169 -46
  442. package/dist/tasks/backends/exec-utils.js +76 -3
  443. package/dist/tasks/backends/index.js +6 -9
  444. package/dist/tasks/backends/launchd.js +292 -55
  445. package/dist/tasks/backends/schtasks.js +557 -70
  446. package/dist/tasks/backends/types.js +4 -0
  447. package/dist/tasks/command-executable.js +93 -0
  448. package/dist/tasks/embedded.js +56 -38
  449. package/dist/tasks/parser.js +156 -64
  450. package/dist/tasks/resolve-akm-bin.js +144 -51
  451. package/dist/tasks/runner.js +377 -209
  452. package/dist/tasks/schedule.js +108 -19
  453. package/dist/tasks/scheduler-invocation.js +296 -0
  454. package/dist/tasks/schema.js +1 -1
  455. package/dist/tasks/task-id.js +35 -0
  456. package/dist/tasks/validator.js +30 -16
  457. package/dist/workflows/authoring/authoring.js +96 -148
  458. package/dist/workflows/authoring/scope-key.js +1 -1
  459. package/dist/workflows/cli.js +0 -20
  460. package/dist/workflows/concurrency-policy.js +15 -0
  461. package/dist/workflows/exec/brief.js +25 -59
  462. package/dist/workflows/exec/frozen-judge.js +47 -0
  463. package/dist/workflows/exec/native-executor.js +157 -94
  464. package/dist/workflows/exec/report.js +365 -200
  465. package/dist/workflows/exec/run-workflow.js +46 -40
  466. package/dist/workflows/exec/scheduler.js +12 -41
  467. package/dist/workflows/exec/step-work.js +239 -205
  468. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  469. package/dist/workflows/ir/compile.js +141 -283
  470. package/dist/workflows/ir/freeze.js +233 -0
  471. package/dist/workflows/ir/plan-hash.js +40 -5
  472. package/dist/workflows/ir/schema.js +537 -1
  473. package/dist/workflows/parser.js +878 -306
  474. package/dist/workflows/program/expressions.js +20 -208
  475. package/dist/workflows/program/schema.js +7 -10
  476. package/dist/workflows/renderer.js +99 -121
  477. package/dist/workflows/resource-limits.js +22 -0
  478. package/dist/workflows/runtime/checkin.js +1 -1
  479. package/dist/workflows/runtime/plan-classifier.js +131 -0
  480. package/dist/workflows/runtime/runs.js +200 -113
  481. package/dist/workflows/runtime/unit-checkin.js +1 -1
  482. package/dist/workflows/runtime/unit-phases.js +20 -0
  483. package/dist/workflows/runtime/workflow-asset-loader.js +235 -97
  484. package/dist/workflows/schema.js +1 -11
  485. package/dist/workflows/validate-summary.js +4 -26
  486. package/dist/workflows/validator.js +52 -30
  487. package/docs/README.md +42 -78
  488. package/docs/migration/README.md +8 -0
  489. package/docs/migration/release-notes/0.6.0.md +1 -1
  490. package/docs/migration/release-notes/0.7.0.md +9 -8
  491. package/docs/migration/release-notes/0.9.0.md +158 -14
  492. package/docs/migration/v0.7-to-v0.8.md +46 -47
  493. package/docs/migration/v0.8-to-v0.9.md +844 -0
  494. package/docs/reference/README.md +12 -0
  495. package/docs/reference/data-and-telemetry.md +333 -0
  496. package/package.json +21 -17
  497. package/schemas/akm-asset-envelope.json +93 -0
  498. package/schemas/akm-config.json +4636 -0
  499. package/schemas/akm-task.json +87 -0
  500. package/{dist/schemas → schemas}/akm-workflow.json +127 -82
  501. package/dist/akm-migrate-storage +0 -38
  502. package/dist/assets/help/help-accept.md +0 -12
  503. package/dist/assets/help/help-improve.md +0 -84
  504. package/dist/assets/help/help-proposals.md +0 -17
  505. package/dist/assets/help/help-propose.md +0 -17
  506. package/dist/assets/help/help-reject.md +0 -11
  507. package/dist/assets/profiles/frequent.json +0 -13
  508. package/dist/assets/profiles/recombine-only.json +0 -21
  509. package/dist/assets/profiles/reflect-distill.json +0 -30
  510. package/dist/assets/profiles/synthesize.json +0 -15
  511. package/dist/assets/prompts/procedural-system.md +0 -44
  512. package/dist/assets/prompts/recombine-system.md +0 -40
  513. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  514. package/dist/assets/tasks/core/backup.yml +0 -4
  515. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  516. package/dist/assets/templates/html/default.html +0 -78
  517. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  518. package/dist/assets/wiki/index-template.md +0 -12
  519. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  520. package/dist/assets/wiki/log-template.md +0 -8
  521. package/dist/assets/wiki/schema-template.md +0 -61
  522. package/dist/cli/config-migrate.js +0 -150
  523. package/dist/cli/config-validate.js +0 -39
  524. package/dist/commands/graph/graph-cli.js +0 -124
  525. package/dist/commands/graph/graph.js +0 -487
  526. package/dist/commands/improve/calibration.js +0 -161
  527. package/dist/commands/improve/dedup.js +0 -482
  528. package/dist/commands/improve/extract-watch.js +0 -140
  529. package/dist/commands/improve/hot-probation.js +0 -45
  530. package/dist/commands/improve/improve-auto-accept.js +0 -276
  531. package/dist/commands/improve/improve-profiles.js +0 -168
  532. package/dist/commands/improve/procedural.js +0 -398
  533. package/dist/commands/improve/recombine.js +0 -818
  534. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  535. package/dist/commands/lint/agent-linter.js +0 -44
  536. package/dist/commands/lint/command-linter.js +0 -44
  537. package/dist/commands/lint/default-linter.js +0 -16
  538. package/dist/commands/lint/fact-linter.js +0 -39
  539. package/dist/commands/lint/knowledge-linter.js +0 -16
  540. package/dist/commands/lint/memory-linter.js +0 -61
  541. package/dist/commands/lint/registry.js +0 -41
  542. package/dist/commands/lint/skill-linter.js +0 -45
  543. package/dist/commands/lint/task-linter.js +0 -50
  544. package/dist/commands/lint/workflow-linter.js +0 -81
  545. package/dist/commands/proposal/legacy-import.js +0 -115
  546. package/dist/commands/sources/history.js +0 -196
  547. package/dist/commands/tasks/default-tasks.js +0 -186
  548. package/dist/commands/wiki-cli.js +0 -292
  549. package/dist/core/asset/asset-registry.js +0 -76
  550. package/dist/core/asset/asset-spec.js +0 -316
  551. package/dist/core/config/config-migration.js +0 -602
  552. package/dist/core/deep-merge.js +0 -38
  553. package/dist/core/eval/rank-metrics.js +0 -113
  554. package/dist/core/ripgrep/install.js +0 -163
  555. package/dist/core/ripgrep/resolve.js +0 -81
  556. package/dist/indexer/db/db.js +0 -1414
  557. package/dist/indexer/manifest.js +0 -170
  558. package/dist/indexer/passes/metadata-contributors.js +0 -31
  559. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  560. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -25
  561. package/dist/llm/call-ai.js +0 -62
  562. package/dist/llm/memory-infer-impl.js +0 -138
  563. package/dist/output/shapes/distill.js +0 -14
  564. package/dist/output/shapes/history.js +0 -11
  565. package/dist/output/text/distill.js +0 -6
  566. package/dist/output/text/enable-disable.js +0 -8
  567. package/dist/output/text/history.js +0 -6
  568. package/dist/output/text/wiki.js +0 -16
  569. package/dist/registry/build-index.js +0 -386
  570. package/dist/schemas/akm-config.json +0 -14225
  571. package/dist/scripts/migrate-storage.js +0 -13169
  572. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -10169
  573. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  574. package/dist/setup/legacy-config.js +0 -106
  575. package/dist/storage/repositories/consolidation-repository.js +0 -38
  576. package/dist/storage/repositories/recombine-repository.js +0 -213
  577. package/dist/wiki/wiki-templates.js +0 -15
  578. package/dist/wiki/wiki.js +0 -1012
  579. package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
  580. package/dist/workflows/db.js +0 -350
  581. package/dist/workflows/exec/watch.js +0 -116
  582. package/dist/workflows/program/parser.js +0 -760
  583. package/dist/workflows/program/project.js +0 -105
  584. package/docs/data-and-telemetry.md +0 -227
  585. package/docs/migration/release-notes/0.9.0-beta.60.md +0 -19
  586. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  587. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
package/dist/wiki/wiki.js DELETED
@@ -1,1012 +0,0 @@
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
- /**
5
- * Multi-wiki support for akm (issue #119).
6
- *
7
- * A wiki lives at `<stashDir>/wikis/<name>/` and contains:
8
- * - `schema.md` — the rulebook the agent reads first
9
- * - `index.md` — catalog, regenerable (rebuilt by `akm index`)
10
- * - `log.md` — append-only, agent-maintained
11
- * - `raw/<slug>.md` — immutable ingested sources
12
- * - `<page>.md` — wiki pages (optionally nested)
13
- *
14
- * Principle: "akm surfaces. The agent writes." akm owns lifecycle, raw-slug
15
- * generation, structural lint, and `index.md` regeneration. The agent uses
16
- * its native file tools for every other page operation.
17
- *
18
- * ## Canonical wiki content contract
19
- *
20
- * The three "infrastructure" files at the wiki root — `schema.md`, `index.md`,
21
- * and `log.md` — are excluded from all user-facing content surfaces:
22
- *
23
- * | Surface | schema/index/log | raw/<slug>.md | <page>.md |
24
- * | -------------------- | ---------------- | ------------- | --------- |
25
- * | `wiki pages` | excluded | included | included |
26
- * | scoped wiki search | excluded | included | included |
27
- * | stash-wide FTS index | excluded | included | included |
28
- * | `wiki lint` | excluded | tracked | tracked |
29
- *
30
- * `raw/` files are first-class addressable content (`wiki:<n>/raw/<slug>`),
31
- * searchable, and listed. They are NOT authored pages — they are source
32
- * material the agent turns into pages. `lint` tracks whether each raw file
33
- * has been cited by a page's `sources:` frontmatter field.
34
- *
35
- * ## Regeneration contract
36
- *
37
- * `regenerateWikiIndex` / `regenerateAllWikiIndexes` apply ONLY to
38
- * stash-owned wikis (directories under `<stashDir>/wikis/`). External wikis
39
- * registered via `akm wiki register` are read-only caches; mutating their
40
- * `index.md` would corrupt source-of-truth content that akm does not own.
41
- * The indexer therefore calls `regenerateAllWikiIndexes(stashDir)` — which
42
- * only iterates `<stashDir>/wikis/` — and never touches registered sources.
43
- */
44
- import fs from "node:fs";
45
- import path from "node:path";
46
- import { parse as yamlParse } from "yaml";
47
- import ingestWorkflowTemplate from "../assets/wiki/ingest-workflow-template.md" with { type: "text" };
48
- import { akmSearch } from "../commands/read/search.js";
49
- import { parseFrontmatter, parseFrontmatterBlock } from "../core/asset/frontmatter.js";
50
- import { isWithin, todayIso } from "../core/common.js";
51
- import { getSources, loadUserConfig, saveConfig } from "../core/config/config.js";
52
- import { NotFoundError, UsageError } from "../core/errors.js";
53
- import { resolveSourceEntries } from "../indexer/search/search-source.js";
54
- import { buildIndexMd, buildLogMd, buildSchemaMd } from "./wiki-templates.js";
55
- // ── Constants ───────────────────────────────────────────────────────────────
56
- export const WIKIS_SUBDIR = "wikis";
57
- export const SCHEMA_MD = "schema.md";
58
- export const INDEX_MD = "index.md";
59
- export const LOG_MD = "log.md";
60
- export const RAW_SUBDIR = "raw";
61
- /** Canonical location for agent-authored pages, mirroring RAW_SUBDIR. */
62
- export const PAGES_SUBDIR = "pages";
63
- /** Files at a wiki root that are not pages. */
64
- const WIKI_SPECIAL_FILES = new Set([SCHEMA_MD, INDEX_MD, LOG_MD]);
65
- const WIKI_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
66
- // ── Validation + resolution ─────────────────────────────────────────────────
67
- export function validateWikiName(name) {
68
- if (!name)
69
- throw new UsageError("Wiki name cannot be empty.");
70
- if (!WIKI_NAME_RE.test(name)) {
71
- throw new UsageError(`Invalid wiki name "${name}". Use lowercase letters, digits, and hyphens (must start with a lowercase letter or digit).`);
72
- }
73
- }
74
- export function resolveWikisRoot(stashDir) {
75
- return path.join(stashDir, WIKIS_SUBDIR);
76
- }
77
- /**
78
- * Resolve `<stashDir>/wikis/<name>/` with an isWithin guard so a malicious
79
- * or mistyped name can never escape the wikis root.
80
- */
81
- export function resolveWikiDir(stashDir, name) {
82
- validateWikiName(name);
83
- const wikisRoot = resolveWikisRoot(stashDir);
84
- const dir = path.join(wikisRoot, name);
85
- if (!isWithin(dir, wikisRoot)) {
86
- throw new UsageError(`Invalid wiki path for name "${name}".`);
87
- }
88
- return dir;
89
- }
90
- /** Parse a wiki name out of a `wiki:<name>/<...>` ref, or return undefined. */
91
- export function extractWikiNameFromRef(ref) {
92
- const match = ref.match(/^wiki:([a-z0-9][a-z0-9-]*)(?:\/|$)/);
93
- return match?.[1];
94
- }
95
- function wikiNotFoundMessage(name) {
96
- return `Wiki not found: ${name}. Run \`akm wiki create ${name}\` to create it or \`akm wiki register ${name} <path-or-repo>\` to register an external wiki.`;
97
- }
98
- function registeredWikiSources(stashDir) {
99
- return resolveSourceEntries(stashDir)
100
- .filter((source) => typeof source.wikiName === "string")
101
- .map((source) => ({
102
- name: source.wikiName,
103
- path: source.path,
104
- mode: "external",
105
- source,
106
- }));
107
- }
108
- export function resolveWikiSource(stashDir, name) {
109
- validateWikiName(name);
110
- const wikiDir = resolveWikiDir(stashDir, name);
111
- if (fs.existsSync(wikiDir) && isRecognizedStashWiki(wikiDir)) {
112
- return { name, path: wikiDir, mode: "stash" };
113
- }
114
- const external = registeredWikiSources(stashDir).find((source) => source.name === name);
115
- if (external)
116
- return external;
117
- throw new NotFoundError(wikiNotFoundMessage(name), "STASH_NOT_FOUND");
118
- }
119
- export function ensureWikiNameAvailable(stashDir, name) {
120
- validateWikiName(name);
121
- const wikiDir = resolveWikiDir(stashDir, name);
122
- if (fs.existsSync(wikiDir) && isRecognizedStashWiki(wikiDir)) {
123
- throw new UsageError(`Wiki already exists: ${name}.`, "RESOURCE_ALREADY_EXISTS");
124
- }
125
- const external = registeredWikiSources(stashDir).find((source) => source.name === name);
126
- if (external) {
127
- throw new UsageError(`Wiki already registered: ${name}.`, "RESOURCE_ALREADY_EXISTS");
128
- }
129
- }
130
- /**
131
- * Walk a wiki directory and bucket files into pages vs raws.
132
- *
133
- * "Pages" are any `.md` files under the wiki root EXCEPT `schema.md`,
134
- * `index.md`, or `log.md`. Raw sources are bucketed separately so callers can
135
- * distinguish authored pages from ingested source material while still
136
- * surfacing both.
137
- *
138
- * Returns two mtime signals:
139
- * - `lastModifiedMs` — newest across all .md files. Used for the `show` /
140
- * `list` "last activity" display, which should reflect any edit.
141
- * - `pagesLastModifiedMs` — newest page mtime only. Used by `lintWiki` to
142
- * decide `stale-index`: the index tracks pages, so stashing a raw or
143
- * editing log.md must NOT flag the index stale.
144
- */
145
- function scanWikiFiles(wikiDir) {
146
- const pages = [];
147
- const raws = [];
148
- let lastModifiedMs;
149
- let pagesLastModifiedMs;
150
- const stack = [{ abs: wikiDir, relDirSegs: [] }];
151
- while (stack.length > 0) {
152
- const current = stack.pop();
153
- if (!current)
154
- continue;
155
- let entries;
156
- try {
157
- entries = fs.readdirSync(current.abs, { withFileTypes: true });
158
- }
159
- catch {
160
- continue;
161
- }
162
- for (const entry of entries) {
163
- if (entry.name.startsWith("."))
164
- continue;
165
- const abs = path.join(current.abs, entry.name);
166
- if (entry.isSymbolicLink())
167
- continue;
168
- if (entry.isDirectory()) {
169
- stack.push({ abs, relDirSegs: [...current.relDirSegs, entry.name] });
170
- continue;
171
- }
172
- if (!entry.isFile() || !entry.name.endsWith(".md"))
173
- continue;
174
- let mtimeMs;
175
- try {
176
- mtimeMs = fs.statSync(abs).mtimeMs;
177
- }
178
- catch {
179
- /* best-effort */
180
- }
181
- if (mtimeMs !== undefined) {
182
- lastModifiedMs = lastModifiedMs === undefined ? mtimeMs : Math.max(lastModifiedMs, mtimeMs);
183
- }
184
- const atRoot = current.relDirSegs.length === 0;
185
- const firstDir = current.relDirSegs[0];
186
- if (firstDir === RAW_SUBDIR) {
187
- raws.push(abs);
188
- }
189
- else if (!(atRoot && WIKI_SPECIAL_FILES.has(entry.name))) {
190
- // schema.md / index.md / log.md at the wiki root are not pages
191
- pages.push(abs);
192
- if (mtimeMs !== undefined) {
193
- pagesLastModifiedMs = pagesLastModifiedMs === undefined ? mtimeMs : Math.max(pagesLastModifiedMs, mtimeMs);
194
- }
195
- }
196
- }
197
- }
198
- return { pages, raws, lastModifiedMs, pagesLastModifiedMs };
199
- }
200
- function hasWikiInfrastructure(wikiDir) {
201
- for (const file of WIKI_SPECIAL_FILES) {
202
- if (fs.existsSync(path.join(wikiDir, file)))
203
- return true;
204
- }
205
- return false;
206
- }
207
- function isRecognizedStashWiki(wikiDir, buckets) {
208
- const scanned = buckets ?? scanWikiFiles(wikiDir);
209
- return scanned.pages.length > 0 || hasWikiInfrastructure(wikiDir);
210
- }
211
- function readSchemaDescription(wikiDir) {
212
- const schemaPath = path.join(wikiDir, SCHEMA_MD);
213
- let raw;
214
- try {
215
- raw = fs.readFileSync(schemaPath, "utf8");
216
- }
217
- catch {
218
- return undefined;
219
- }
220
- try {
221
- const parsed = parseFrontmatter(raw);
222
- const desc = parsed.data.description;
223
- return typeof desc === "string" && desc.trim().length > 0 ? desc.trim() : undefined;
224
- }
225
- catch {
226
- return undefined;
227
- }
228
- }
229
- /**
230
- * Load a wiki's `schema.md` body and frontmatter.
231
- *
232
- * Unlike {@link readSchemaDescription} (which returns only the frontmatter
233
- * `description` for `listWikis` summaries), this returns the markdown **body**
234
- * — everything after the closing `---` of the frontmatter — which is where the
235
- * page contract, operations, and hard rules live. The body is the rulebook
236
- * injected into the write-time prompt for wiki-page edits.
237
- *
238
- * Swallow-and-degrade like the existing reader: a missing file, an unresolvable
239
- * wiki dir, or malformed content yields `{ body: "", frontmatter: {} }`. Never
240
- * throws.
241
- */
242
- export function loadWikiSchema(stashRoot, name) {
243
- const empty = { body: "", frontmatter: {} };
244
- let wikiDir;
245
- try {
246
- wikiDir = resolveWikiDir(stashRoot, name);
247
- }
248
- catch {
249
- return empty;
250
- }
251
- let raw;
252
- try {
253
- raw = fs.readFileSync(path.join(wikiDir, SCHEMA_MD), "utf8");
254
- }
255
- catch {
256
- return empty;
257
- }
258
- try {
259
- const parsed = parseFrontmatter(raw);
260
- return { body: parsed.content, frontmatter: parsed.data };
261
- }
262
- catch {
263
- return empty;
264
- }
265
- }
266
- function toIsoDate(ms) {
267
- return new Date(ms).toISOString();
268
- }
269
- // ── List ────────────────────────────────────────────────────────────────────
270
- /**
271
- * Return summaries for every wiki directly under `<stashDir>/wikis/`.
272
- *
273
- * A "wiki" is any directory whose name matches {@link WIKI_NAME_RE}. Anything
274
- * else (dot-directories, lone files, directories with odd names) is skipped
275
- * silently so `ls` noise doesn't crash listing.
276
- */
277
- export function listWikis(stashDir) {
278
- const wikisRoot = resolveWikisRoot(stashDir);
279
- const summaries = new Map();
280
- let entries = [];
281
- if (fs.existsSync(wikisRoot)) {
282
- try {
283
- entries = fs.readdirSync(wikisRoot, { withFileTypes: true });
284
- }
285
- catch {
286
- entries = [];
287
- }
288
- }
289
- const summarize = (name, dir) => {
290
- const buckets = scanWikiFiles(dir);
291
- if (!isRecognizedStashWiki(dir, buckets))
292
- return;
293
- const summary = {
294
- name,
295
- path: dir,
296
- pages: buckets.pages.length,
297
- raws: buckets.raws.length,
298
- };
299
- const description = readSchemaDescription(dir);
300
- if (description)
301
- summary.description = description;
302
- if (buckets.lastModifiedMs !== undefined)
303
- summary.lastModified = toIsoDate(buckets.lastModifiedMs);
304
- summaries.set(name, summary);
305
- };
306
- for (const entry of entries) {
307
- if (!entry.isDirectory())
308
- continue;
309
- if (!WIKI_NAME_RE.test(entry.name))
310
- continue;
311
- summarize(entry.name, path.join(wikisRoot, entry.name));
312
- }
313
- for (const source of registeredWikiSources(stashDir)) {
314
- if (summaries.has(source.name))
315
- continue;
316
- summarize(source.name, source.path);
317
- }
318
- return Array.from(summaries.values()).sort((a, b) => a.name.localeCompare(b.name));
319
- }
320
- // ── Show ────────────────────────────────────────────────────────────────────
321
- /**
322
- * Extract the top N `##` log entries from `log.md`.
323
- *
324
- * The log convention (defined by `schema.md` and enforced by nothing) is
325
- * newest-first: the most recent entry sits at the top of the file, so the
326
- * first `limit` `##` blocks encountered in file order are the most recent.
327
- * Agents that append to the bottom instead will have their entries appear
328
- * at the end of this list.
329
- *
330
- * `log.md` is agent-maintained and can be free-form, so `parseFrontmatter`
331
- * is called defensively: if the frontmatter is malformed we fall back to
332
- * treating the whole file as body.
333
- */
334
- function readRecentLog(wikiDir, limit = 3) {
335
- const logPath = path.join(wikiDir, LOG_MD);
336
- let raw;
337
- try {
338
- raw = fs.readFileSync(logPath, "utf8");
339
- }
340
- catch {
341
- return [];
342
- }
343
- let body;
344
- try {
345
- body = parseFrontmatter(raw).content ?? raw;
346
- }
347
- catch {
348
- body = raw;
349
- }
350
- const sections = [];
351
- let current;
352
- for (const line of body.split(/\r?\n/)) {
353
- if (/^##\s+/.test(line)) {
354
- if (current && current.length > 0)
355
- sections.push(current.join("\n").trim());
356
- current = [line];
357
- }
358
- else if (current) {
359
- current.push(line);
360
- }
361
- }
362
- if (current && current.length > 0)
363
- sections.push(current.join("\n").trim());
364
- // Newest-first convention: the top `limit` `##` blocks are the most recent.
365
- return sections.slice(0, limit);
366
- }
367
- export function showWikiAtPath(name, wikiDir) {
368
- const buckets = scanWikiFiles(wikiDir);
369
- const result = {
370
- name,
371
- ref: `wiki:${name}`,
372
- path: wikiDir,
373
- pages: buckets.pages.length,
374
- raws: buckets.raws.length,
375
- recentLog: readRecentLog(wikiDir),
376
- };
377
- const description = readSchemaDescription(wikiDir);
378
- if (description)
379
- result.description = description;
380
- if (buckets.lastModifiedMs !== undefined)
381
- result.lastModified = toIsoDate(buckets.lastModifiedMs);
382
- return result;
383
- }
384
- export function showWiki(stashDir, name) {
385
- return showWikiAtPath(name, resolveWikiSource(stashDir, name).path);
386
- }
387
- // ── Create ──────────────────────────────────────────────────────────────────
388
- export function createWiki(stashDir, name) {
389
- const existing = registeredWikiSources(stashDir).find((source) => source.name === name);
390
- if (existing) {
391
- throw new UsageError(`Wiki already registered: ${name}.`, "RESOURCE_ALREADY_EXISTS");
392
- }
393
- const wikiDir = resolveWikiDir(stashDir, name);
394
- fs.mkdirSync(wikiDir, { recursive: true });
395
- const files = [
396
- { relPath: SCHEMA_MD, content: buildSchemaMd(name) },
397
- { relPath: INDEX_MD, content: buildIndexMd(name) },
398
- { relPath: LOG_MD, content: buildLogMd(name) },
399
- ];
400
- const created = [];
401
- const skipped = [];
402
- for (const { relPath, content } of files) {
403
- const absPath = path.join(wikiDir, relPath);
404
- if (fs.existsSync(absPath)) {
405
- skipped.push(absPath);
406
- continue;
407
- }
408
- fs.writeFileSync(absPath, content, "utf8");
409
- created.push(absPath);
410
- }
411
- // Ensure raw/ and pages/ exist with a .gitkeep so empty wikis survive clean
412
- // clones. Handle the dir-exists-but-no-.gitkeep case too (partial scaffolds,
413
- // user-created directories) so the invariant always holds after `create`.
414
- for (const subdir of [RAW_SUBDIR, PAGES_SUBDIR]) {
415
- const dir = path.join(wikiDir, subdir);
416
- fs.mkdirSync(dir, { recursive: true });
417
- const gitkeepPath = path.join(dir, ".gitkeep");
418
- if (fs.existsSync(gitkeepPath)) {
419
- skipped.push(gitkeepPath);
420
- }
421
- else {
422
- fs.writeFileSync(gitkeepPath, "", "utf8");
423
- created.push(gitkeepPath);
424
- }
425
- }
426
- return { name, ref: `wiki:${name}`, path: wikiDir, created, skipped };
427
- }
428
- /**
429
- * Remove a wiki.
430
- *
431
- * Deletes pages + `schema.md` + `index.md` + `log.md` by default. The `raw/`
432
- * directory is preserved because raw sources are often hand-curated and
433
- * outlive the wiki pages built from them. Pass `withSources: true` to
434
- * delete everything including `raw/`.
435
- *
436
- * The guard: `wikiDir` must resolve under `<stashDir>/wikis/`. If the wiki
437
- * directory doesn't exist, throws `NotFoundError` — callers can decide to
438
- * ignore that (e.g. idempotent cleanup) by catching.
439
- */
440
- export function removeWiki(stashDir, name, options = {}) {
441
- validateWikiName(name);
442
- const wikiDir = resolveWikiDir(stashDir, name);
443
- const external = registeredWikiSources(stashDir).find((source) => source.name === name);
444
- const isStashWiki = fs.existsSync(wikiDir) && isRecognizedStashWiki(wikiDir);
445
- if (!isStashWiki && external) {
446
- const config = loadUserConfig();
447
- const filteredSources = getSources(config).filter((entry) => entry.wikiName !== name);
448
- const installed = (config.installed ?? []).filter((entry) => entry.wikiName !== name);
449
- saveConfig({
450
- ...config,
451
- sources: filteredSources.length > 0 ? filteredSources : undefined,
452
- installed: installed.length > 0 ? installed : undefined,
453
- });
454
- return {
455
- name,
456
- path: wikiDir,
457
- removed: [],
458
- preservedRaw: false,
459
- unregistered: true,
460
- };
461
- }
462
- if (!fs.existsSync(wikiDir) || (!isStashWiki && !options.withSources)) {
463
- throw new NotFoundError(wikiNotFoundMessage(name), "STASH_NOT_FOUND");
464
- }
465
- const wikisRoot = resolveWikisRoot(stashDir);
466
- if (!isWithin(wikiDir, wikisRoot)) {
467
- throw new UsageError(`Refusing to remove a path outside the wikis root: ${wikiDir}`, "PATH_ESCAPE_VIOLATION");
468
- }
469
- const removed = [];
470
- const rawDir = path.join(wikiDir, RAW_SUBDIR);
471
- const preserveRaw = !options.withSources && fs.existsSync(rawDir);
472
- let entries;
473
- try {
474
- entries = fs.readdirSync(wikiDir, { withFileTypes: true });
475
- }
476
- catch {
477
- entries = [];
478
- }
479
- for (const entry of entries) {
480
- const abs = path.join(wikiDir, entry.name);
481
- if (preserveRaw && entry.name === RAW_SUBDIR)
482
- continue;
483
- try {
484
- fs.rmSync(abs, { recursive: true, force: true });
485
- removed.push(abs);
486
- }
487
- catch {
488
- /* best-effort — entry may have been removed concurrently */
489
- }
490
- }
491
- if (!preserveRaw) {
492
- // Remove the now-empty wiki directory itself.
493
- try {
494
- fs.rmdirSync(wikiDir);
495
- }
496
- catch {
497
- /* dir may be non-empty (e.g. uncollected dotfiles); leave it */
498
- }
499
- }
500
- const result = {
501
- name,
502
- path: wikiDir,
503
- removed,
504
- preservedRaw: preserveRaw,
505
- };
506
- if (preserveRaw)
507
- result.rawPath = rawDir;
508
- return result;
509
- }
510
- function pageNameFromPath(wikiDir, absPath) {
511
- const rel = path.relative(wikiDir, absPath).split(path.sep).join("/");
512
- return rel.endsWith(".md") ? rel.slice(0, -3) : rel;
513
- }
514
- /**
515
- * Parse the raw frontmatter block with a real YAML parser so list-valued
516
- * keys (`xrefs:`, `sources:`) round-trip correctly. The project's hand-rolled
517
- * `parseFrontmatter` deliberately drops YAML lists; for lint + index work we
518
- * need them, and the `yaml` package is already a runtime dependency.
519
- */
520
- function parsePageFrontmatterYaml(raw) {
521
- const block = parseFrontmatterBlock(raw);
522
- if (!block)
523
- return {};
524
- try {
525
- const value = yamlParse(block.frontmatter);
526
- if (value && typeof value === "object" && !Array.isArray(value)) {
527
- return value;
528
- }
529
- }
530
- catch {
531
- /* malformed YAML — fall through to the lightweight parser */
532
- }
533
- try {
534
- return parseFrontmatter(raw).data;
535
- }
536
- catch {
537
- return {};
538
- }
539
- }
540
- function readPageFrontmatter(absPath) {
541
- let raw;
542
- try {
543
- raw = fs.readFileSync(absPath, "utf8");
544
- }
545
- catch {
546
- return {};
547
- }
548
- const data = parsePageFrontmatterYaml(raw);
549
- const out = {};
550
- if (typeof data.description === "string" && data.description.trim().length > 0) {
551
- out.description = data.description.trim();
552
- }
553
- if (typeof data.pageKind === "string" && data.pageKind.trim().length > 0) {
554
- out.pageKind = data.pageKind.trim();
555
- }
556
- if (Array.isArray(data.xrefs)) {
557
- const xrefs = data.xrefs.filter((x) => typeof x === "string" && x.trim().length > 0);
558
- if (xrefs.length > 0)
559
- out.xrefs = xrefs;
560
- }
561
- if (Array.isArray(data.sources)) {
562
- const sources = data.sources.filter((s) => typeof s === "string" && s.trim().length > 0);
563
- if (sources.length > 0)
564
- out.sources = sources;
565
- }
566
- return out;
567
- }
568
- /**
569
- * List all addressable wiki content entries.
570
- *
571
- * Per the canonical wiki contract: `schema.md`, `index.md`, and `log.md` at
572
- * the wiki root are infrastructure files and are excluded. Everything else —
573
- * authored pages AND `raw/<slug>.md` sources — is included and addressable as
574
- * `wiki:<name>/<rel-path-without-.md>`.
575
- *
576
- * Callers that need to distinguish authored pages from raw sources should
577
- * check whether the returned `name` starts with `"raw/"`.
578
- */
579
- export function listPages(stashDir, name) {
580
- const wikiDir = resolveWikiSource(stashDir, name).path;
581
- const { pages, raws } = scanWikiFiles(wikiDir);
582
- const result = [];
583
- for (const abs of [...pages, ...raws]) {
584
- const pageName = pageNameFromPath(wikiDir, abs);
585
- const ref = `wiki:${name}/${pageName}`;
586
- const fm = readPageFrontmatter(abs);
587
- const entry = { ref, name: pageName, path: abs, ...fm };
588
- result.push(entry);
589
- }
590
- result.sort((a, b) => a.name.localeCompare(b.name));
591
- return result;
592
- }
593
- /**
594
- * Thin wrapper over the stash-wide search that narrows to one wiki.
595
- *
596
- * Uses `akmSearch({ type: "wiki" })` to reuse the full FTS5+boost pipeline,
597
- * then drops hits that aren't inside `wikis/<name>/`. No parallel scorer.
598
- *
599
- * Per the canonical wiki contract: infrastructure files (`schema.md`,
600
- * `index.md`, `log.md`) at the wiki root are excluded. `raw/<slug>.md`
601
- * sources are included — they are first-class addressable content.
602
- *
603
- * When the index is absent (e.g. fresh stash), `akmSearch` falls back to its
604
- * substring walker; hits still come through path-filtered here.
605
- */
606
- export async function searchInWiki(input) {
607
- validateWikiName(input.wikiName);
608
- const response = await akmSearch({
609
- query: input.query,
610
- type: "wiki",
611
- limit: input.limit,
612
- source: "stash",
613
- });
614
- let wikiDir;
615
- try {
616
- wikiDir = resolveWikiSource(input.stashDir, input.wikiName).path;
617
- }
618
- catch (err) {
619
- if (err instanceof NotFoundError) {
620
- return { ...response, hits: [], registryHits: undefined };
621
- }
622
- throw err;
623
- }
624
- const filtered = [];
625
- for (const hit of response.hits) {
626
- // hits can be SourceSearchHit or RegistrySearchResultHit (union); filter
627
- // by path inclusion. Registry hits have no path and are dropped.
628
- if (hit.type === "registry")
629
- continue;
630
- const stashHit = hit;
631
- if (!stashHit.path)
632
- continue;
633
- if (!isWithin(stashHit.path, wikiDir))
634
- continue;
635
- // Exclude infrastructure files: schema.md, index.md, log.md at wiki root
636
- const basename = path.basename(stashHit.path);
637
- if (WIKI_SPECIAL_FILES.has(basename) && path.dirname(stashHit.path) === wikiDir)
638
- continue;
639
- filtered.push(stashHit);
640
- }
641
- return { ...response, hits: filtered, registryHits: undefined };
642
- }
643
- // ── Slug + raw stash ───────────────────────────────────────────────────────
644
- const SLUG_MAX_LENGTH = 64;
645
- /**
646
- * Turn an arbitrary string into a filesystem-safe wiki slug.
647
- *
648
- * - lowercased
649
- * - leading markdown noise (`#`, `>`, `-`, whitespace) stripped
650
- * - non-alphanumerics collapsed to `-`
651
- * - leading/trailing `-` trimmed
652
- * - capped at {@link SLUG_MAX_LENGTH}
653
- *
654
- * Falls back to `note-<base36-ms>` for empty inputs so raw files are never
655
- * written to a blank name.
656
- */
657
- export function slugifyForWiki(value) {
658
- const slug = value
659
- .toLowerCase()
660
- .replace(/^[#>\-\s]+/, "")
661
- .replace(/[^a-z0-9]+/g, "-")
662
- .replace(/^-+|-+$/g, "")
663
- .slice(0, SLUG_MAX_LENGTH);
664
- return slug || `note-${Date.now().toString(36)}`;
665
- }
666
- /**
667
- * Derive a slug hint from the first non-empty line of source content.
668
- *
669
- * Used when the caller didn't pass a preferredName. Skips frontmatter.
670
- * Caps words at 8 so the slug stays manageable.
671
- */
672
- function deriveQueryFromSource(content) {
673
- const lines = content.split(/\r?\n/);
674
- let inFrontmatter = false;
675
- let closed = false;
676
- for (let i = 0; i < lines.length; i++) {
677
- const trimmed = lines[i].trim();
678
- if (i === 0 && trimmed === "---") {
679
- inFrontmatter = true;
680
- continue;
681
- }
682
- if (inFrontmatter && !closed) {
683
- if (trimmed === "---")
684
- closed = true;
685
- continue;
686
- }
687
- if (!trimmed)
688
- continue;
689
- return trimmed
690
- .replace(/^#+\s*/, "")
691
- .split(/\s+/)
692
- .slice(0, 8)
693
- .join(" ");
694
- }
695
- return "";
696
- }
697
- function pickUniqueRawSlug(rawDir, baseSlug) {
698
- let candidate = baseSlug;
699
- let n = 0;
700
- while (fs.existsSync(path.join(rawDir, `${candidate}.md`))) {
701
- n += 1;
702
- candidate = `${baseSlug}-${n}`;
703
- }
704
- return candidate;
705
- }
706
- function withRawFrontmatter(content, slug) {
707
- // If the source already starts with a YAML frontmatter block, keep it — we
708
- // don't want to shadow user metadata. The raw location itself is enough to
709
- // tag the wikiRole for the indexer.
710
- if (content.startsWith("---"))
711
- return content;
712
- const date = todayIso();
713
- return `---\nwikiRole: raw\ningestedAt: ${date}\nslug: ${slug}\n---\n\n${content}`;
714
- }
715
- function ensureTrailingNewline(value) {
716
- return value.endsWith("\n") ? value : `${value}\n`;
717
- }
718
- /**
719
- * Copy raw content into `wikis/<name>/raw/<slug>.md`.
720
- *
721
- * Invariants this owns (which an agent could get wrong):
722
- * 1. Raw files never overwrite — collisions get `-1`, `-2`, … suffixes.
723
- * 2. Path is guaranteed to stay within the wiki's raw/ directory.
724
- * 3. If the content has no frontmatter, a `wikiRole: raw` block is added.
725
- *
726
- * Does not update the log, does not write any wiki pages. That's the agent's
727
- * job (see `akm wiki ingest <name>` for the workflow).
728
- */
729
- export function stashRaw(input) {
730
- const wikiDir = resolveWikiSource(input.stashDir, input.wikiName).path;
731
- const rawDir = path.join(wikiDir, RAW_SUBDIR);
732
- fs.mkdirSync(rawDir, { recursive: true });
733
- const baseSlug = slugifyForWiki(input.preferredName ?? deriveQueryFromSource(input.content) ?? "source");
734
- if (input.explicitSlug === true && fs.existsSync(path.join(rawDir, `${baseSlug}.md`))) {
735
- throw new UsageError(`Raw slug "${baseSlug}" already exists in wiki:${input.wikiName}. Pass a different --as or omit --as to auto-increment.`);
736
- }
737
- const slug = pickUniqueRawSlug(rawDir, baseSlug);
738
- const absPath = path.join(rawDir, `${slug}.md`);
739
- if (!isWithin(absPath, rawDir)) {
740
- throw new UsageError(`Invalid raw path for slug "${slug}".`);
741
- }
742
- fs.writeFileSync(absPath, ensureTrailingNewline(withRawFrontmatter(input.content, slug)), "utf8");
743
- return {
744
- slug,
745
- path: absPath,
746
- wrote: true,
747
- ref: `wiki:${input.wikiName}/raw/${slug}`,
748
- };
749
- }
750
- /**
751
- * Deterministic structural lint for a single wiki. No reasoning, no LLM.
752
- *
753
- * Checks:
754
- * - `orphan`: page has no incoming AND no outgoing xrefs
755
- * - `broken-xref`: page xref points at a nonexistent wiki page
756
- * - `missing-description`: page frontmatter `description` is empty/missing
757
- * - `uncited-raw`: `raw/<slug>.md` not listed in any page's `sources:`
758
- * - `stale-index`: `index.md` mtime is older than the newest page mtime
759
- */
760
- export function lintWiki(stashDir, name) {
761
- const wikiDir = resolveWikiSource(stashDir, name).path;
762
- const pages = listPages(stashDir, name);
763
- const { raws, pagesLastModifiedMs } = scanWikiFiles(wikiDir);
764
- const pageRefs = new Set(pages.map((p) => p.ref));
765
- const incomingXrefs = new Map();
766
- for (const page of pages) {
767
- for (const xref of page.xrefs ?? []) {
768
- incomingXrefs.set(xref, (incomingXrefs.get(xref) ?? 0) + 1);
769
- }
770
- }
771
- const findings = [];
772
- // orphans + missing-description + broken-xref
773
- for (const page of pages) {
774
- const outCount = page.xrefs?.length ?? 0;
775
- const inCount = incomingXrefs.get(page.ref) ?? 0;
776
- if (outCount === 0 && inCount === 0) {
777
- findings.push({
778
- kind: "orphan",
779
- refs: [page.ref],
780
- message: `Page ${page.ref} has no incoming or outgoing xrefs.`,
781
- });
782
- }
783
- if (!page.description) {
784
- findings.push({
785
- kind: "missing-description",
786
- refs: [page.ref],
787
- message: `Page ${page.ref} is missing a frontmatter \`description\`.`,
788
- });
789
- }
790
- for (const xref of page.xrefs ?? []) {
791
- // Only validate wiki:<this-wiki>/... refs. External refs (other wikis,
792
- // knowledge:, skill:, etc.) are left alone — a cross-wiki link is a
793
- // feature, not a defect.
794
- const target = extractWikiNameFromRef(xref);
795
- if (target !== name)
796
- continue;
797
- if (!pageRefs.has(xref)) {
798
- findings.push({
799
- kind: "broken-xref",
800
- refs: [page.ref, xref],
801
- message: `Page ${page.ref} has xref to nonexistent page ${xref}.`,
802
- });
803
- }
804
- }
805
- }
806
- // uncited-raw
807
- const citedRawSlugs = new Set();
808
- for (const page of pages) {
809
- for (const src of page.sources ?? []) {
810
- // accept "raw/<slug>.md" or "raw/<slug>"
811
- const match = src.match(/^raw\/([^/\s]+?)(?:\.md)?$/);
812
- if (match)
813
- citedRawSlugs.add(match[1]);
814
- }
815
- }
816
- for (const rawPath of raws) {
817
- const base = path.basename(rawPath, ".md");
818
- if (!citedRawSlugs.has(base)) {
819
- findings.push({
820
- kind: "uncited-raw",
821
- refs: [`wiki:${name}/raw/${base}`],
822
- message: `Raw source raw/${base}.md is not cited by any page's sources: frontmatter.`,
823
- });
824
- }
825
- }
826
- // broken-source: each page's sources: entries must resolve to an existing raw file.
827
- for (const page of pages) {
828
- for (const src of page.sources ?? []) {
829
- const match = src.match(/^raw\/([^/\s]+?)(?:\.md)?$/);
830
- if (!match)
831
- continue; // non-raw source entries are out of scope
832
- const slug = match[1];
833
- const rawFilePath = path.join(wikiDir, RAW_SUBDIR, `${slug}.md`);
834
- if (!fs.existsSync(rawFilePath)) {
835
- findings.push({
836
- kind: "broken-source",
837
- refs: [page.ref],
838
- message: `Page "${page.ref}" references missing raw source "raw/${slug}".`,
839
- });
840
- }
841
- }
842
- }
843
- // stale-index: compare index.md's mtime to the newest PAGE mtime only.
844
- // Stashing a raw source or appending to log.md must NOT flag the index as
845
- // stale — the index catalogs pages, not raws or meta files.
846
- const indexPath = path.join(wikiDir, INDEX_MD);
847
- try {
848
- const indexMtimeMs = fs.statSync(indexPath).mtimeMs;
849
- if (pagesLastModifiedMs !== undefined && pagesLastModifiedMs > indexMtimeMs + 1) {
850
- // +1 ms fudge factor: when index is regenerated in the same tick as a
851
- // page, the two stats can tie exactly; don't flag equality.
852
- findings.push({
853
- kind: "stale-index",
854
- refs: [`wiki:${name}/index`],
855
- message: `index.md is older than the newest page. Run \`akm index\` to regenerate.`,
856
- });
857
- }
858
- }
859
- catch {
860
- // No index.md — report it as a stale/missing index so `akm wiki lint`
861
- // still gives actionable output without erroring out.
862
- findings.push({
863
- kind: "stale-index",
864
- refs: [`wiki:${name}/index`],
865
- message: "index.md is missing. Run `akm index` to regenerate.",
866
- });
867
- }
868
- return {
869
- wiki: name,
870
- pagesScanned: pages.length,
871
- rawsScanned: raws.length,
872
- findings,
873
- };
874
- }
875
- // ── Index regeneration ─────────────────────────────────────────────────────
876
- /**
877
- * Rebuild a stash-owned wiki's `index.md` from its pages' frontmatter.
878
- *
879
- * This function uses `resolveWikiDir` (not `resolveWikiSource`) so it only
880
- * ever operates on the stash-owned path `<stashDir>/wikis/<name>/`. External
881
- * wikis registered via `akm wiki register` are never regenerated here — they
882
- * are read-only caches. See the canonical wiki contract at the top of this
883
- * file for the full regeneration rule.
884
- *
885
- * Pages are grouped by `pageKind` (falling back to `uncategorised`) and
886
- * listed alphabetically inside each group. If the wiki directory doesn't
887
- * exist or has no pages, a fresh empty template is written.
888
- *
889
- * The function is best-effort: it catches all filesystem errors and returns
890
- * a boolean so callers (the indexer) can keep going even if one wiki is
891
- * broken. Never throws.
892
- */
893
- export function regenerateWikiIndex(stashDir, name) {
894
- try {
895
- const wikiDir = resolveWikiDir(stashDir, name);
896
- if (!fs.existsSync(wikiDir))
897
- return false;
898
- const pages = listPages(stashDir, name);
899
- if (pages.length === 0) {
900
- fs.writeFileSync(path.join(wikiDir, INDEX_MD), buildIndexMd(name), "utf8");
901
- return true;
902
- }
903
- const byKind = new Map();
904
- for (const page of pages) {
905
- const kind = page.pageKind ?? "uncategorised";
906
- const group = byKind.get(kind);
907
- if (group)
908
- group.push(page);
909
- else
910
- byKind.set(kind, [page]);
911
- }
912
- const kindOrder = Array.from(byKind.keys()).sort((a, b) => {
913
- if (a === "uncategorised")
914
- return 1;
915
- if (b === "uncategorised")
916
- return -1;
917
- return a.localeCompare(b);
918
- });
919
- const lines = [
920
- "---",
921
- `description: Catalog of pages in the ${name} wiki. Regenerated by \`akm index\`.`,
922
- "wikiRole: index",
923
- "---",
924
- "",
925
- `# ${name} — index`,
926
- "",
927
- "_This file is regenerated on every `akm index` run. Manual edits are preserved until the next regeneration, then replaced._",
928
- "",
929
- ];
930
- for (const kind of kindOrder) {
931
- const group = (byKind.get(kind) ?? []).slice().sort((a, b) => a.name.localeCompare(b.name));
932
- const heading = kind.charAt(0).toUpperCase() + kind.slice(1);
933
- lines.push(`## ${heading}`);
934
- lines.push("");
935
- for (const page of group) {
936
- const desc = page.description ? ` — ${page.description}` : "";
937
- lines.push(`- \`${page.ref}\`${desc}`);
938
- }
939
- lines.push("");
940
- }
941
- const next = `${lines.join("\n")}\n`;
942
- const indexPath = path.join(wikiDir, INDEX_MD);
943
- try {
944
- const current = fs.readFileSync(indexPath, "utf8");
945
- if (current === next)
946
- return false;
947
- }
948
- catch {
949
- /* missing file -> write below */
950
- }
951
- fs.writeFileSync(indexPath, next, "utf8");
952
- return true;
953
- }
954
- catch {
955
- return false;
956
- }
957
- }
958
- /**
959
- * Regenerate `index.md` for every stash-owned wiki under `<stashDir>/wikis/`.
960
- *
961
- * Per the canonical wiki contract: regeneration applies ONLY to stash-owned
962
- * wikis. External wikis registered via `akm wiki register` are read-only
963
- * caches whose source-of-truth lives outside this stash; mutating their
964
- * `index.md` would corrupt content that akm does not own. Those wikis
965
- * therefore appear only in the FTS index (read), never in regeneration
966
- * (write).
967
- *
968
- * Called from `akmIndex()` as a side effect after the FTS rebuild. Never
969
- * throws; returns the list of wiki names that were regenerated.
970
- */
971
- export function regenerateAllWikiIndexes(stashDir) {
972
- const wikisRoot = resolveWikisRoot(stashDir);
973
- if (!fs.existsSync(wikisRoot))
974
- return [];
975
- let entries;
976
- try {
977
- entries = fs.readdirSync(wikisRoot, { withFileTypes: true });
978
- }
979
- catch {
980
- return [];
981
- }
982
- const regenerated = [];
983
- for (const entry of entries) {
984
- if (!entry.isDirectory())
985
- continue;
986
- if (!WIKI_NAME_RE.test(entry.name))
987
- continue;
988
- if (regenerateWikiIndex(stashDir, entry.name))
989
- regenerated.push(entry.name);
990
- }
991
- return regenerated;
992
- }
993
- /**
994
- * Build a markdown workflow string for ingesting a source into the named
995
- * wiki. Does NOT perform the ingest — it prints the recipe the agent
996
- * follows using the other eight verbs plus its native file tools.
997
- *
998
- * The workflow is parameterised with the wiki's resolved absolute path and
999
- * schema location so the agent can jump straight in without any additional
1000
- * lookup. Because the output references the CLI by name (`akm wiki stash`
1001
- * etc.), the printer never drifts from the actual command surface — changing
1002
- * a verb here and in the printer stays colocated.
1003
- */
1004
- export function buildIngestWorkflow(stashDir, name) {
1005
- const wikiDir = resolveWikiSource(stashDir, name).path;
1006
- const schemaPath = path.join(wikiDir, SCHEMA_MD);
1007
- const workflow = ingestWorkflowTemplate
1008
- .replaceAll("{{WIKI_NAME}}", name)
1009
- .replaceAll("{{WIKI_DIR}}", wikiDir)
1010
- .replaceAll("{{SCHEMA_PATH}}", schemaPath);
1011
- return { wiki: name, path: wikiDir, schemaPath, workflow };
1012
- }