akm-cli 0.9.0-beta.9 → 0.9.0-rc.1

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 (381) hide show
  1. package/CHANGELOG.md +715 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  39. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  40. package/dist/assets/templates/html/health.html +281 -111
  41. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  42. package/dist/assets/wiki/schema-template.md +4 -4
  43. package/dist/cli/clack.js +56 -0
  44. package/dist/cli/config-migrate.js +7 -1
  45. package/dist/cli/confirm.js +1 -1
  46. package/dist/cli/parse-args.js +46 -1
  47. package/dist/cli/shared.js +28 -0
  48. package/dist/cli.js +25 -21
  49. package/dist/commands/agent/agent-dispatch.js +3 -2
  50. package/dist/commands/agent/agent-support.js +0 -7
  51. package/dist/commands/agent/contribute-cli.js +26 -7
  52. package/dist/commands/config-cli.js +26 -13
  53. package/dist/commands/env/child-env.js +47 -0
  54. package/dist/commands/env/env-binding.js +95 -0
  55. package/dist/commands/env/env-cli.js +228 -292
  56. package/dist/commands/env/env.js +14 -67
  57. package/dist/commands/env/secret-cli.js +140 -138
  58. package/dist/commands/feedback-cli.js +156 -155
  59. package/dist/commands/graph/graph-cli.js +5 -13
  60. package/dist/commands/graph/graph.js +3 -3
  61. package/dist/commands/health/advisories.js +151 -0
  62. package/dist/commands/health/checks.js +103 -16
  63. package/dist/commands/health/html-report.js +447 -81
  64. package/dist/commands/health/improve-metrics.js +771 -0
  65. package/dist/commands/health/llm-usage.js +65 -0
  66. package/dist/commands/health/md-report.js +103 -0
  67. package/dist/commands/health/metrics.js +278 -0
  68. package/dist/commands/health/stash-exposure.js +46 -0
  69. package/dist/commands/health/surfaces.js +216 -0
  70. package/dist/commands/health/task-runs.js +135 -0
  71. package/dist/commands/health/types.js +26 -0
  72. package/dist/commands/health/windows.js +195 -0
  73. package/dist/commands/health.js +91 -1091
  74. package/dist/commands/improve/anti-collapse.js +170 -0
  75. package/dist/commands/improve/calibration.js +161 -0
  76. package/dist/commands/improve/collapse-detector.js +421 -0
  77. package/dist/commands/improve/consolidate/chunking.js +141 -0
  78. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  79. package/dist/commands/improve/consolidate/merge.js +145 -0
  80. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  81. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  82. package/dist/commands/improve/consolidate.js +1295 -1277
  83. package/dist/commands/improve/dedup.js +482 -0
  84. package/dist/commands/improve/distill/content-repair.js +202 -0
  85. package/dist/commands/improve/distill/promote-memory.js +229 -0
  86. package/dist/commands/improve/distill/quality-gate.js +236 -0
  87. package/dist/commands/improve/distill-guards.js +127 -0
  88. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  89. package/dist/commands/improve/distill.js +228 -605
  90. package/dist/commands/improve/eligibility.js +434 -0
  91. package/dist/commands/improve/encoding-salience.js +205 -0
  92. package/dist/commands/improve/extract-cli.js +179 -59
  93. package/dist/commands/improve/extract-prompt.js +54 -3
  94. package/dist/commands/improve/extract-watch.js +140 -0
  95. package/dist/commands/improve/extract.js +409 -43
  96. package/dist/commands/improve/feedback-valence.js +54 -0
  97. package/dist/commands/improve/hot-probation.js +45 -0
  98. package/dist/commands/improve/improve-auto-accept.js +157 -10
  99. package/dist/commands/improve/improve-cli.js +115 -73
  100. package/dist/commands/improve/improve-profiles.js +28 -8
  101. package/dist/commands/improve/improve-result-file.js +15 -25
  102. package/dist/commands/improve/improve-session.js +58 -0
  103. package/dist/commands/improve/improve.js +485 -2764
  104. package/dist/commands/improve/locks.js +154 -0
  105. package/dist/commands/improve/loop-stages.js +1100 -0
  106. package/dist/commands/improve/memory/memory-belief.js +14 -15
  107. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  108. package/dist/commands/improve/memory/memory-improve.js +27 -27
  109. package/dist/commands/improve/outcome-loop.js +270 -0
  110. package/dist/commands/improve/preparation.js +2002 -0
  111. package/dist/commands/improve/proactive-maintenance.js +37 -35
  112. package/dist/commands/improve/procedural.js +398 -0
  113. package/dist/commands/improve/recombine.js +818 -0
  114. package/dist/commands/improve/reflect-noise.js +0 -0
  115. package/dist/commands/improve/reflect.js +206 -45
  116. package/dist/commands/improve/salience.js +455 -0
  117. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  118. package/dist/commands/improve/shared.js +51 -0
  119. package/dist/commands/improve/triage.js +93 -0
  120. package/dist/commands/lint/agent-linter.js +19 -24
  121. package/dist/commands/lint/base-linter.js +173 -60
  122. package/dist/commands/lint/command-linter.js +19 -24
  123. package/dist/commands/lint/env-key-rules.js +38 -1
  124. package/dist/commands/lint/fact-linter.js +39 -0
  125. package/dist/commands/lint/index.js +31 -13
  126. package/dist/commands/lint/memory-linter.js +1 -1
  127. package/dist/commands/lint/registry.js +7 -2
  128. package/dist/commands/lint/task-linter.js +3 -3
  129. package/dist/commands/lint/workflow-linter.js +26 -1
  130. package/dist/commands/observability-cli.js +4 -4
  131. package/dist/commands/proposal/drain-policies.js +13 -4
  132. package/dist/commands/proposal/drain.js +45 -51
  133. package/dist/commands/proposal/legacy-import.js +115 -0
  134. package/dist/commands/proposal/proposal-cli.js +24 -34
  135. package/dist/commands/proposal/proposal.js +2 -1
  136. package/dist/commands/proposal/propose.js +8 -3
  137. package/dist/commands/proposal/repository.js +829 -0
  138. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  139. package/dist/commands/proposal/validators/proposals.js +93 -895
  140. package/dist/commands/read/curate.js +410 -111
  141. package/dist/commands/read/knowledge.js +10 -3
  142. package/dist/commands/read/remember-cli.js +133 -138
  143. package/dist/commands/read/search-cli.js +15 -8
  144. package/dist/commands/read/search.js +22 -11
  145. package/dist/commands/read/show.js +106 -14
  146. package/dist/commands/registry-cli.js +76 -87
  147. package/dist/commands/remember.js +11 -12
  148. package/dist/commands/sources/add-cli.js +91 -95
  149. package/dist/commands/sources/history.js +1 -1
  150. package/dist/commands/sources/init.js +66 -18
  151. package/dist/commands/sources/installed-stashes.js +11 -3
  152. package/dist/commands/sources/migration-help.js +7 -4
  153. package/dist/commands/sources/schema-repair.js +44 -46
  154. package/dist/commands/sources/self-update.js +2 -2
  155. package/dist/commands/sources/source-add.js +7 -3
  156. package/dist/commands/sources/sources-cli.js +3 -3
  157. package/dist/commands/sources/stash-cli.js +19 -39
  158. package/dist/commands/sources/stash-skeleton.js +57 -8
  159. package/dist/commands/tasks/default-tasks.js +15 -2
  160. package/dist/commands/tasks/tasks-cli.js +20 -29
  161. package/dist/commands/tasks/tasks.js +39 -11
  162. package/dist/commands/wiki-cli.js +23 -38
  163. package/dist/commands/workflow-cli.js +291 -13
  164. package/dist/core/asset/asset-registry.js +3 -1
  165. package/dist/core/asset/asset-spec.js +79 -5
  166. package/dist/core/asset/frontmatter.js +188 -167
  167. package/dist/core/asset/markdown.js +8 -0
  168. package/dist/core/authoring-rules.js +92 -0
  169. package/dist/core/common.js +4 -23
  170. package/dist/core/concurrent.js +10 -1
  171. package/dist/core/config/config-io.js +10 -1
  172. package/dist/core/config/config-migration.js +18 -40
  173. package/dist/core/config/config-schema.js +403 -62
  174. package/dist/core/config/config-types.js +3 -3
  175. package/dist/core/config/config.js +67 -22
  176. package/dist/core/deep-merge.js +38 -0
  177. package/dist/core/errors.js +1 -0
  178. package/dist/core/eval/rank-metrics.js +113 -0
  179. package/dist/core/events.js +4 -7
  180. package/dist/core/improve-types.js +47 -8
  181. package/dist/core/json-schema.js +142 -0
  182. package/dist/core/logs-db.js +14 -75
  183. package/dist/core/parse.js +36 -16
  184. package/dist/core/paths.js +18 -18
  185. package/dist/core/standards/resolve-standards-context.js +87 -0
  186. package/dist/core/standards/resolve-stash-standards.js +99 -0
  187. package/dist/core/standards/resolve-type-conventions.js +66 -0
  188. package/dist/core/state/migrations.js +770 -0
  189. package/dist/core/state-db.js +132 -1126
  190. package/dist/core/structured.js +69 -0
  191. package/dist/core/time.js +53 -0
  192. package/dist/core/warn.js +21 -0
  193. package/dist/core/write-source.js +37 -0
  194. package/dist/indexer/db/db.js +261 -770
  195. package/dist/indexer/db/entry-mapper.js +41 -0
  196. package/dist/indexer/db/graph-db.js +129 -86
  197. package/dist/indexer/db/llm-cache.js +2 -2
  198. package/dist/indexer/db/schema.js +516 -0
  199. package/dist/indexer/ensure-index.js +36 -92
  200. package/dist/indexer/feedback/utility-policy.js +75 -0
  201. package/dist/indexer/graph/graph-boost.js +51 -41
  202. package/dist/indexer/graph/graph-extraction.js +207 -4
  203. package/dist/indexer/index-writer-lock.js +18 -11
  204. package/dist/indexer/index-written-assets.js +105 -0
  205. package/dist/indexer/indexer.js +182 -204
  206. package/dist/indexer/passes/dir-staleness.js +114 -0
  207. package/dist/indexer/passes/memory-inference.js +13 -5
  208. package/dist/indexer/passes/metadata.js +20 -0
  209. package/dist/indexer/read-preflight.js +23 -0
  210. package/dist/indexer/search/db-search.js +89 -13
  211. package/dist/indexer/search/fts-query.js +51 -0
  212. package/dist/indexer/search/ranking-contributors.js +95 -9
  213. package/dist/indexer/search/ranking.js +79 -3
  214. package/dist/indexer/search/search-fields.js +6 -0
  215. package/dist/indexer/search/search-source.js +32 -21
  216. package/dist/indexer/search/semantic-status.js +4 -0
  217. package/dist/indexer/walk/matchers.js +48 -0
  218. package/dist/indexer/walk/walker.js +21 -13
  219. package/dist/integrations/agent/builders.js +41 -13
  220. package/dist/integrations/agent/config.js +20 -59
  221. package/dist/integrations/agent/detect.js +9 -0
  222. package/dist/integrations/agent/index.js +3 -19
  223. package/dist/integrations/agent/model-aliases.js +16 -2
  224. package/dist/integrations/agent/profiles.js +79 -6
  225. package/dist/integrations/agent/prompts.js +75 -9
  226. package/dist/integrations/agent/runner-dispatch.js +83 -0
  227. package/dist/integrations/agent/runner.js +13 -9
  228. package/dist/integrations/agent/spawn.js +206 -81
  229. package/dist/integrations/harnesses/aider/agent-builder.js +113 -0
  230. package/dist/integrations/harnesses/aider/index.js +58 -0
  231. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  232. package/dist/integrations/harnesses/amazonq/agent-builder.js +153 -0
  233. package/dist/integrations/harnesses/amazonq/index.js +59 -0
  234. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  235. package/dist/integrations/harnesses/claude/agent-builder.js +46 -7
  236. package/dist/integrations/harnesses/claude/index.js +27 -23
  237. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  238. package/dist/integrations/harnesses/claude/session-log.js +10 -0
  239. package/dist/integrations/harnesses/codex/agent-builder.js +137 -0
  240. package/dist/integrations/harnesses/codex/index.js +63 -0
  241. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  242. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  243. package/dist/integrations/harnesses/copilot/index.js +60 -0
  244. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  245. package/dist/integrations/harnesses/gemini/agent-builder.js +121 -0
  246. package/dist/integrations/harnesses/gemini/index.js +60 -0
  247. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  248. package/dist/integrations/harnesses/index.js +28 -7
  249. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  250. package/dist/integrations/harnesses/opencode/index.js +17 -16
  251. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  252. package/dist/integrations/harnesses/opencode-sdk/harness.js +65 -0
  253. package/dist/integrations/harnesses/opencode-sdk/index.js +10 -34
  254. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +642 -71
  255. package/dist/integrations/harnesses/openhands/agent-builder.js +126 -0
  256. package/dist/integrations/harnesses/openhands/index.js +58 -0
  257. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  258. package/dist/integrations/harnesses/pi/agent-builder.js +104 -0
  259. package/dist/integrations/harnesses/pi/index.js +58 -0
  260. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  261. package/dist/integrations/harnesses/types.js +8 -0
  262. package/dist/integrations/session-logs/index.js +40 -11
  263. package/dist/llm/call-ai.js +2 -2
  264. package/dist/llm/client.js +34 -11
  265. package/dist/llm/embedder.js +67 -4
  266. package/dist/llm/embedders/cache.js +3 -1
  267. package/dist/llm/embedders/deterministic.js +66 -0
  268. package/dist/llm/embedders/local.js +73 -3
  269. package/dist/llm/feature-gate.js +16 -15
  270. package/dist/llm/graph-extract.js +67 -44
  271. package/dist/llm/memory-infer-impl.js +138 -0
  272. package/dist/llm/memory-infer.js +1 -127
  273. package/dist/llm/metadata-enhance.js +44 -31
  274. package/dist/llm/structured-call.js +49 -0
  275. package/dist/migrate-storage-node.mjs +8 -0
  276. package/dist/output/context.js +5 -5
  277. package/dist/output/renderers.js +87 -15
  278. package/dist/output/shapes/curate.js +14 -2
  279. package/dist/output/shapes/helpers.js +0 -3
  280. package/dist/output/shapes/passthrough.js +6 -1
  281. package/dist/output/text/helpers.js +241 -2
  282. package/dist/output/text/workflow.js +4 -1
  283. package/dist/registry/providers/skills-sh.js +21 -147
  284. package/dist/registry/providers/static-index.js +15 -157
  285. package/dist/registry/resolve.js +27 -9
  286. package/dist/runtime.js +25 -1
  287. package/dist/schemas/akm-config.json +14225 -0
  288. package/dist/schemas/akm-workflow.json +328 -0
  289. package/dist/scripts/migrate-storage.js +2743 -8390
  290. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +1652 -607
  291. package/dist/setup/detect.js +9 -0
  292. package/dist/setup/legacy-config.js +106 -0
  293. package/dist/setup/prompt.js +57 -0
  294. package/dist/setup/providers.js +14 -0
  295. package/dist/setup/registry-stash-loader.js +12 -0
  296. package/dist/setup/semantic-assets.js +124 -0
  297. package/dist/setup/setup.js +52 -1614
  298. package/dist/setup/steps/connection.js +734 -0
  299. package/dist/setup/steps/output.js +31 -0
  300. package/dist/setup/steps/platforms.js +124 -0
  301. package/dist/setup/steps/semantic.js +27 -0
  302. package/dist/setup/steps/sources.js +222 -0
  303. package/dist/setup/steps/stashdir.js +42 -0
  304. package/dist/setup/steps/tasks.js +152 -0
  305. package/dist/sources/include.js +6 -2
  306. package/dist/sources/providers/filesystem.js +0 -1
  307. package/dist/sources/providers/git-install.js +210 -0
  308. package/dist/sources/providers/git-provider.js +234 -0
  309. package/dist/sources/providers/git-stash.js +248 -0
  310. package/dist/sources/providers/git.js +10 -661
  311. package/dist/sources/providers/npm.js +2 -6
  312. package/dist/sources/providers/provider-utils.js +13 -7
  313. package/dist/sources/providers/sync-from-ref.js +9 -1
  314. package/dist/sources/providers/website.js +9 -5
  315. package/dist/sources/website-ingest.js +187 -29
  316. package/dist/sources/wiki-fetchers/registry.js +53 -0
  317. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  318. package/dist/storage/database.js +45 -10
  319. package/dist/storage/managed-db.js +82 -0
  320. package/dist/storage/repositories/canaries-repository.js +107 -0
  321. package/dist/storage/repositories/consolidation-repository.js +38 -0
  322. package/dist/storage/repositories/embeddings-repository.js +72 -0
  323. package/dist/storage/repositories/events-repository.js +187 -0
  324. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  325. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  326. package/dist/storage/repositories/index-db.js +14 -8
  327. package/dist/storage/repositories/proposals-repository.js +220 -0
  328. package/dist/storage/repositories/recombine-repository.js +213 -0
  329. package/dist/storage/repositories/registry-cache.js +93 -0
  330. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  331. package/dist/storage/repositories/task-history-repository.js +93 -0
  332. package/dist/storage/repositories/workflow-runs-repository.js +189 -1
  333. package/dist/storage/sqlite-pragmas.js +146 -0
  334. package/dist/tasks/backends/cron.js +1 -1
  335. package/dist/tasks/backends/index.js +9 -0
  336. package/dist/tasks/backends/launchd.js +1 -1
  337. package/dist/tasks/backends/schtasks.js +1 -1
  338. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  339. package/dist/tasks/runner.js +15 -13
  340. package/dist/text-import-hook.mjs +1 -1
  341. package/dist/wiki/wiki.js +52 -11
  342. package/dist/workflows/authoring/authoring.js +123 -10
  343. package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
  344. package/dist/workflows/cli.js +5 -0
  345. package/dist/workflows/db.js +138 -4
  346. package/dist/workflows/exec/brief.js +484 -0
  347. package/dist/workflows/exec/native-executor.js +975 -0
  348. package/dist/workflows/exec/param-secrets.js +115 -0
  349. package/dist/workflows/exec/report.js +1295 -0
  350. package/dist/workflows/exec/run-workflow.js +596 -0
  351. package/dist/workflows/exec/scheduler.js +100 -0
  352. package/dist/workflows/exec/step-work.js +1156 -0
  353. package/dist/workflows/exec/unit-writer.js +23 -0
  354. package/dist/workflows/exec/watch.js +116 -0
  355. package/dist/workflows/exec/worktree.js +171 -0
  356. package/dist/workflows/ir/compile.js +388 -0
  357. package/dist/workflows/ir/params.js +54 -0
  358. package/dist/workflows/ir/plan-hash.js +33 -0
  359. package/dist/workflows/ir/schema.js +4 -0
  360. package/dist/workflows/parser.js +3 -1
  361. package/dist/workflows/program/expressions.js +369 -0
  362. package/dist/workflows/program/parser.js +760 -0
  363. package/dist/workflows/program/project.js +105 -0
  364. package/dist/workflows/program/schema.js +54 -0
  365. package/dist/workflows/renderer.js +82 -5
  366. package/dist/workflows/runtime/agent-identity.js +59 -14
  367. package/dist/workflows/runtime/runs.js +248 -153
  368. package/dist/workflows/runtime/unit-checkin.js +45 -0
  369. package/dist/workflows/runtime/workflow-asset-loader.js +188 -0
  370. package/dist/workflows/validate-summary.js +26 -10
  371. package/dist/workflows/validator.js +1 -1
  372. package/docs/README.md +69 -18
  373. package/docs/data-and-telemetry.md +7 -5
  374. package/docs/migration/release-notes/0.7.0.md +1 -1
  375. package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
  376. package/docs/migration/release-notes/0.9.0.md +39 -0
  377. package/package.json +10 -10
  378. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  379. package/dist/commands/db-cli.js +0 -23
  380. package/dist/indexer/db/db-backup.js +0 -376
  381. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -16,8 +16,10 @@
16
16
  * Log lines are high-volume, append-only, and freely purgeable; state.db rows
17
17
  * (events, proposals, task_history) are durable records. Separating them keeps
18
18
  * state.db small and lets log retention be aggressive without touching durable
19
- * state. Cross-db queries (e.g. "failed task_history row its log lines") use
20
- * SQLite ATTACH see {@link attachStateDatabase}.
19
+ * state. Callers that need to correlate a task_history row with its log lines do
20
+ * an application-side join on the {@link buildTaskRunId} key (e.g. `health` via
21
+ * {@link getLoggedRunIds}) — no SQLite ATTACH, so the split survives a future
22
+ * provider change.
21
23
  *
22
24
  * ## run_id
23
25
  *
@@ -35,12 +37,10 @@
35
37
  *
36
38
  * @module logs-db
37
39
  */
38
- import fs from "node:fs";
39
40
  import path from "node:path";
40
- import { openDatabase } from "../storage/database.js";
41
41
  import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
42
+ import { openManagedDatabase } from "../storage/managed-db.js";
42
43
  import { getDataDir } from "./paths.js";
43
- import { getStateDbPath } from "./state-db.js";
44
44
  // ── Path helper ──────────────────────────────────────────────────────────────
45
45
  /**
46
46
  * Default path: `<dataDir>/logs.db` — alongside state.db so cooperating
@@ -70,16 +70,13 @@ export function getLogsDbPath() {
70
70
  */
71
71
  export function openLogsDatabase(dbPath) {
72
72
  const resolvedPath = dbPath ?? getLogsDbPath();
73
- const dir = path.dirname(resolvedPath);
74
- if (!fs.existsSync(dir)) {
75
- fs.mkdirSync(dir, { recursive: true });
76
- }
77
- const db = openDatabase(resolvedPath);
78
- // PRAGMAs must run before any DDL or DML.
79
- db.exec("PRAGMA journal_mode = WAL");
80
- db.exec("PRAGMA busy_timeout = 30000");
81
- runMigrations(db);
82
- return db;
73
+ // foreignKeys:false preserves this opener's historical behaviour — logs.db
74
+ // has never enforced foreign keys.
75
+ return openManagedDatabase({
76
+ path: resolvedPath,
77
+ pragmas: { dataDir: path.dirname(resolvedPath), foreignKeys: false },
78
+ init: runMigrations,
79
+ });
83
80
  }
84
81
  // ── Migrations ───────────────────────────────────────────────────────────────
85
82
  /**
@@ -145,8 +142,8 @@ export function runMigrations(db) {
145
142
  * Encode a task run's identity — the unique `(task_id, started_at)` pair from
146
143
  * state.db `task_history` — as a single run_id string.
147
144
  *
148
- * The format MUST stay in sync with the SQL expression
149
- * `task_id || '@' || started_at` used by {@link queryFailedRunLogLines}.
145
+ * The format MUST stay in sync with the application-side join key that callers
146
+ * build from a `task_history` row's `task_id` and `started_at`.
150
147
  */
151
148
  export function buildTaskRunId(taskId, startedAtIso) {
152
149
  return `${taskId}@${startedAtIso}`;
@@ -229,64 +226,6 @@ export function getLoggedRunIds(db, runIds) {
229
226
  }
230
227
  return out;
231
228
  }
232
- // ── Cross-db: ATTACH state.db ────────────────────────────────────────────────
233
- /**
234
- * ATTACH state.db to an open logs.db handle under the schema name `state`,
235
- * enabling cross-db joins like task_history × task_logs.
236
- *
237
- * The state.db file must already exist (callers always open state.db first in
238
- * practice); attaching a non-existent path would silently create an empty,
239
- * unmigrated database file, so this throws instead.
240
- */
241
- export function attachStateDatabase(db, stateDbPath) {
242
- const resolved = stateDbPath ?? getStateDbPath();
243
- if (!fs.existsSync(resolved)) {
244
- throw new Error(`Cannot ATTACH state.db: file does not exist at ${resolved}`);
245
- }
246
- // prepare().run() rather than db.run(): both drivers support parameterised
247
- // ATTACH through a prepared statement, and no other call site uses db.run().
248
- db.prepare("ATTACH DATABASE ? AS state").run(resolved);
249
- }
250
- /**
251
- * Convenience: open logs.db with state.db attached as `state`. The returned
252
- * handle supports cross-db queries such as {@link queryFailedRunLogLines}.
253
- * Close it like any other handle (DETACH is implicit on close).
254
- */
255
- export function openLogsDatabaseWithState(logsDbPath, stateDbPath) {
256
- const db = openLogsDatabase(logsDbPath);
257
- try {
258
- attachStateDatabase(db, stateDbPath);
259
- }
260
- catch (err) {
261
- db.close();
262
- throw err;
263
- }
264
- return db;
265
- }
266
- /**
267
- * Cross-db join: every log line belonging to a FAILED task_history run whose
268
- * `started_at` is `>= since` (all failed runs when omitted). Requires a handle
269
- * opened via {@link openLogsDatabaseWithState}.
270
- *
271
- * The join key is the run_id encoding documented on {@link buildTaskRunId}:
272
- * `task_logs.run_id = task_history.task_id || '@' || task_history.started_at`.
273
- */
274
- export function queryFailedRunLogLines(db, options = {}) {
275
- const conditions = ["th.status = 'failed'"];
276
- const params = [];
277
- if (options.since) {
278
- conditions.push("th.started_at >= ?");
279
- params.push(options.since);
280
- }
281
- const limit = options.limit !== undefined && options.limit >= 0 ? ` LIMIT ${Math.floor(options.limit)}` : "";
282
- return db
283
- .prepare(`SELECT th.task_id, l.run_id, th.started_at, th.status, l.ts, l.stream, l.level, l.line
284
- FROM state.task_history th
285
- JOIN task_logs l ON l.run_id = th.task_id || '@' || th.started_at
286
- WHERE ${conditions.join(" AND ")}
287
- ORDER BY th.started_at DESC, l.id ASC${limit}`)
288
- .all(...params);
289
- }
290
229
  // ── Retention ────────────────────────────────────────────────────────────────
291
230
  /**
292
231
  * Delete task_logs rows older than `retentionDays` (default: 90). Mirrors
@@ -98,16 +98,24 @@ export function parseJsonResponse(raw) {
98
98
  * balanced `{ }` or `[ ]` structure in the text and attempts to parse that
99
99
  * substring. Returns `undefined` if no valid JSON structure is found.
100
100
  *
101
- * Non-array results are preferred: if a `{…}` object is found first, it is
102
- * returned immediately. Arrays (`[…]`) are captured as a fallback and
103
- * returned only when no object was found.
101
+ * Shape preference is controlled by {@link ParseEmbeddedJsonOptions.expect}:
102
+ * - `"any"` (default): non-array results are preferred a `{…}` object found
103
+ * first is returned immediately; arrays (`[…]`) are a fallback.
104
+ * - `"array"`: only top-level arrays are returned. The direct parse is
105
+ * accepted only if it is an array, and the scanner returns the first
106
+ * balanced `[…]` while skipping `{…}` openers entirely.
104
107
  */
105
- export function parseEmbeddedJsonResponse(raw) {
108
+ export function parseEmbeddedJsonResponse(raw, options) {
109
+ const expectArray = options?.expect === "array";
106
110
  const direct = parseJsonResponse(raw);
107
- if (direct !== undefined)
111
+ if (direct !== undefined && (!expectArray || Array.isArray(direct)))
108
112
  return direct;
109
113
  const text = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw)));
110
114
  let arrayFallback;
115
+ // Scan only *top-level* balanced structures: once a `{…}`/`[…]` is matched we
116
+ // jump `start` past its closing bracket rather than re-scanning its interior.
117
+ // This keeps array mode from salvaging an array *nested inside* a leading
118
+ // object (e.g. the `entities` array of a bare `{entities,relations}` object).
111
119
  for (let start = 0; start < text.length; start++) {
112
120
  const opener = text[start];
113
121
  if (opener !== "{" && opener !== "[")
@@ -116,6 +124,7 @@ export function parseEmbeddedJsonResponse(raw) {
116
124
  let depth = 0;
117
125
  let inString = false;
118
126
  let escaped = false;
127
+ let end = -1;
119
128
  for (let i = start; i < text.length; i++) {
120
129
  const ch = text[i];
121
130
  if (inString) {
@@ -139,20 +148,31 @@ export function parseEmbeddedJsonResponse(raw) {
139
148
  if (ch === closer) {
140
149
  depth -= 1;
141
150
  if (depth === 0) {
142
- try {
143
- const parsed = JSON.parse(text.slice(start, i + 1));
144
- if (!Array.isArray(parsed)) {
145
- return parsed;
146
- }
147
- arrayFallback ??= parsed;
148
- break;
149
- }
150
- catch {
151
- break;
152
- }
151
+ end = i;
152
+ break;
153
153
  }
154
154
  }
155
155
  }
156
+ if (end === -1)
157
+ continue; // never balanced — let the next opener try
158
+ try {
159
+ const parsed = JSON.parse(text.slice(start, end + 1));
160
+ if (Array.isArray(parsed)) {
161
+ // First valid array wins in array mode; in "any" mode it is the
162
+ // fallback returned only if no object is found.
163
+ if (expectArray)
164
+ return parsed;
165
+ arrayFallback ??= parsed;
166
+ }
167
+ else if (!expectArray) {
168
+ return parsed;
169
+ }
170
+ // Skip past this balanced structure so we don't descend into it.
171
+ start = end;
172
+ }
173
+ catch {
174
+ // Malformed candidate — advance one char and try the next opener.
175
+ }
156
176
  }
157
177
  return arrayFallback;
158
178
  }
@@ -111,12 +111,12 @@ export function getConfigDir(env = process.env, platform = process.platform) {
111
111
  }
112
112
  return path.join(home, ".config", "akm");
113
113
  }
114
- export function getConfigPath() {
115
- return path.join(getConfigDir(), "config.json");
114
+ export function getConfigPath(env = process.env) {
115
+ return path.join(getConfigDir(env), "config.json");
116
116
  }
117
117
  // ── Cache directory ──────────────────────────────────────────────────────────
118
- export function getCacheDir() {
119
- const override = process.env.AKM_CACHE_DIR?.trim();
118
+ export function getCacheDir(env = process.env) {
119
+ const override = env.AKM_CACHE_DIR?.trim();
120
120
  if (override)
121
121
  return override;
122
122
  // Explicit XDG/platform overrides win before the transient-stash isolation
@@ -125,13 +125,13 @@ export function getCacheDir() {
125
125
  // as set, so the AKM_STASH_DIR transient rule does not silently move cache
126
126
  // writes away from where they pointed them.
127
127
  if (IS_WINDOWS) {
128
- const localAppData = process.env.LOCALAPPDATA?.trim();
128
+ const localAppData = env.LOCALAPPDATA?.trim();
129
129
  if (localAppData)
130
130
  return path.join(localAppData, "akm");
131
- const userProfile = process.env.USERPROFILE?.trim();
131
+ const userProfile = env.USERPROFILE?.trim();
132
132
  if (userProfile)
133
133
  return path.join(userProfile, "AppData", "Local", "akm");
134
- const appData = process.env.APPDATA?.trim();
134
+ const appData = env.APPDATA?.trim();
135
135
  if (appData) {
136
136
  // Heuristic fallback: APPDATA points to %APPDATA% (Roaming), so
137
137
  // navigate to the sibling "Local" directory. This is typically
@@ -141,7 +141,7 @@ export function getCacheDir() {
141
141
  }
142
142
  }
143
143
  else {
144
- const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
144
+ const xdgCacheHome = env.XDG_CACHE_HOME?.trim();
145
145
  if (xdgCacheHome)
146
146
  return path.join(xdgCacheHome, "akm");
147
147
  }
@@ -150,7 +150,7 @@ export function getCacheDir() {
150
150
  // into `${AKM_STASH_DIR}/.akm/cache` so that config backups, registry-index
151
151
  // cache, and other regenerable artifacts do not pollute the user's host
152
152
  // ~/.cache/akm directory.
153
- const stashOverride = process.env.AKM_STASH_DIR?.trim();
153
+ const stashOverride = env.AKM_STASH_DIR?.trim();
154
154
  if (stashOverride && isTransientStashPath(stashOverride)) {
155
155
  return path.join(stashOverride, ".akm", "cache");
156
156
  }
@@ -158,7 +158,7 @@ export function getCacheDir() {
158
158
  // None of LOCALAPPDATA / USERPROFILE / APPDATA were set above.
159
159
  throw new ConfigError("Unable to determine cache directory. Set LOCALAPPDATA, USERPROFILE, or APPDATA.", "CONFIG_DIR_UNRESOLVABLE");
160
160
  }
161
- const home = process.env.HOME?.trim();
161
+ const home = env.HOME?.trim();
162
162
  if (!home)
163
163
  return path.join("/tmp", "akm-cache");
164
164
  return path.join(home, ".cache", "akm");
@@ -212,8 +212,8 @@ export function getDataDir(env = process.env, platform = process.platform) {
212
212
  return path.join("/tmp", "akm-data");
213
213
  return path.join(home, ".local", "share", "akm");
214
214
  }
215
- export function getDbPath() {
216
- return path.join(getDataDir(), "index.db");
215
+ export function getDbPath(env = process.env) {
216
+ return path.join(getDataDir(env), "index.db");
217
217
  }
218
218
  export function getIndexWriterLockPath() {
219
219
  return path.join(getDataDir(), "index.db.write.lock");
@@ -257,17 +257,17 @@ export function getTaskHistoryDir() {
257
257
  return path.join(getCacheDir(), "tasks", "history");
258
258
  }
259
259
  // ── Default stash directory ──────────────────────────────────────────────────
260
- export function getDefaultStashDir() {
261
- const override = process.env.AKM_STASH_DIR?.trim();
260
+ export function getDefaultStashDir(env = process.env) {
261
+ const override = env.AKM_STASH_DIR?.trim();
262
262
  if (override)
263
263
  return override;
264
264
  if (IS_WINDOWS) {
265
- const userProfile = process.env.USERPROFILE?.trim();
265
+ const userProfile = env.USERPROFILE?.trim();
266
266
  if (userProfile)
267
267
  return path.join(userProfile, "Documents", "akm");
268
268
  return path.join("C:\\", "akm");
269
269
  }
270
- const home = process.env.HOME?.trim();
270
+ const home = env.HOME?.trim();
271
271
  if (!home) {
272
272
  throw new ConfigError("Unable to determine default stash directory. Set HOME.", "STASH_DIR_NOT_FOUND");
273
273
  }
@@ -294,7 +294,7 @@ export function getDefaultStashDir() {
294
294
  * is fine even though `~/.local` is refused). This catches fat-finger
295
295
  * `--dir /` or `--dir ~` without preventing legitimate nested use.
296
296
  */
297
- export function assertSafeStashDir(stashDir) {
297
+ export function assertSafeStashDir(stashDir, env = process.env) {
298
298
  const resolved = path.resolve(stashDir);
299
299
  // Filesystem root — POSIX and Windows drive roots.
300
300
  if (resolved === "/" || /^[A-Za-z]:[\\/]?$/.test(resolved)) {
@@ -334,7 +334,7 @@ export function assertSafeStashDir(stashDir) {
334
334
  // under bun test (which isolates HOME to a tempdir while os.homedir()
335
335
  // still returns the real user's home).
336
336
  const candidateHomes = new Set();
337
- const envHome = (process.env.HOME ?? process.env.USERPROFILE)?.trim();
337
+ const envHome = (env.HOME ?? env.USERPROFILE)?.trim();
338
338
  if (envHome)
339
339
  candidateHomes.add(path.resolve(envHome));
340
340
  try {
@@ -0,0 +1,87 @@
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
+ * Dispatch resolver for the standards prompt seam — selects which of the two
6
+ * standards features fires for a given write target, mutually exclusively:
7
+ *
8
+ * - **Feature A — wiki schema**: the target is a wiki page (a ref/path under
9
+ * `wikis/<name>/`, NOT a `raw/` file and NOT a wiki infra file
10
+ * `schema.md`/`index.md`/`log.md`). Returns that wiki's `schema.md` body.
11
+ * - **Feature B — stash standards**: the target is any non-wiki asset.
12
+ * Returns the concatenated `category: convention`/`meta` fact bodies.
13
+ * - **Neither fires**: a wiki `raw/` file or a wiki infra file. Returns `""`.
14
+ *
15
+ * The two NEVER both fire. Both underlying readers degrade to `""` on
16
+ * missing/malformed input and never throw, so this resolver never throws.
17
+ */
18
+ import { extractWikiNameFromRef, INDEX_MD, LOG_MD, loadWikiSchema, SCHEMA_MD } from "../../wiki/wiki.js";
19
+ import { resolveStashStandards } from "./resolve-stash-standards.js";
20
+ import { resolveTypeConventions, typeConventionRef } from "./resolve-type-conventions.js";
21
+ /** Wiki infra files that are not authored pages (relative to the wiki root). */
22
+ const WIKI_INFRA_BASENAMES = new Set([SCHEMA_MD, INDEX_MD, LOG_MD]);
23
+ /**
24
+ * Extract the asset type from a canonical ref (`[origin//]type:name`) without
25
+ * throwing. Returns `undefined` for refs that have no `type:` prefix. Kept local
26
+ * and lenient — the per-type resolver validates the result against
27
+ * `getAssetTypes()`, so a bogus prefix here simply yields no convention.
28
+ */
29
+ function refType(ref) {
30
+ if (!ref)
31
+ return undefined;
32
+ const body = ref.includes("//") ? ref.slice(ref.indexOf("//") + 2) : ref;
33
+ const colon = body.indexOf(":");
34
+ if (colon <= 0)
35
+ return undefined;
36
+ return body.slice(0, colon).trim() || undefined;
37
+ }
38
+ /**
39
+ * Resolve the standards context for a write target identified by its asset ref.
40
+ *
41
+ * @param ref Canonical asset ref of the write target (e.g. `skill:foo`,
42
+ * `wiki:research/topics/x`). When undefined, the target is a
43
+ * non-wiki authoring flow → stash standards.
44
+ * @param stashRoot Stash root directory.
45
+ */
46
+ export function resolveStandardsContext(ref, stashRoot) {
47
+ const wikiName = ref ? extractWikiNameFromRef(ref) : undefined;
48
+ if (!wikiName) {
49
+ // Non-wiki asset target → Feature B (general stash standards) plus the
50
+ // per-type SOFT conventions layer (#646), type-scoped to the write target.
51
+ const general = resolveStashStandards(stashRoot);
52
+ const type = refType(ref);
53
+ // A non-empty body here guarantees `type` is a `getAssetTypes()`-validated
54
+ // string (the resolver returns "" otherwise).
55
+ const typeConventions = type ? resolveTypeConventions(stashRoot, type) : "";
56
+ if (!typeConventions || !type)
57
+ return general;
58
+ // Soft, type-scoped guidance — clearly labeled and kept separate from the
59
+ // HARD (validator-enforced) rules that `authoringRulesForType` injects
60
+ // downstream. These facts are advice only; they never weaken the gate.
61
+ const softSection = [
62
+ `# ${typeConventionRef(type)} (soft per-type conventions — guidance, not enforced)`,
63
+ typeConventions,
64
+ ].join("\n");
65
+ return general ? `${general}\n\n${softSection}` : softSection;
66
+ }
67
+ // Wiki target. Extract the page path after `wiki:<name>/`.
68
+ const prefix = `wiki:${wikiName}/`;
69
+ const pagePath = ref?.startsWith(prefix) ? ref.slice(prefix.length) : "";
70
+ // `wiki:<name>` with no page, a `raw/` file, or a wiki infra file → neither
71
+ // feature fires.
72
+ if (!pagePath)
73
+ return "";
74
+ if (pagePath === "raw" || pagePath.startsWith("raw/"))
75
+ return "";
76
+ // Infra files (`schema`/`index`/`log`) are only special at the WIKI ROOT.
77
+ // A nested page like `wiki:research/analysis/schema` is a genuine page and
78
+ // must NOT be suppressed, so only check when the page is at root depth.
79
+ if (!pagePath.includes("/")) {
80
+ // Refs drop the `.md` extension; compare against both forms defensively.
81
+ if (WIKI_INFRA_BASENAMES.has(pagePath) || WIKI_INFRA_BASENAMES.has(`${pagePath}.md`)) {
82
+ return "";
83
+ }
84
+ }
85
+ // A genuine wiki page → Feature A (that wiki's schema body).
86
+ return loadWikiSchema(stashRoot, wikiName).body;
87
+ }
@@ -0,0 +1,99 @@
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
+ * Resolve the stash-authoring "standards" context (Feature B of the standards
6
+ * plan): gather the bodies of `fact` assets whose `category` frontmatter is
7
+ * `convention` or `meta` so naming / tag / frontmatter conventions are surfaced
8
+ * to the agent when it creates or edits a non-wiki asset.
9
+ *
10
+ * Selection is by **frontmatter `category`**, never by path — flat
11
+ * (`facts/x.md`) and nested (`facts/conventions/x.md`) layouts resolve
12
+ * identically. The MVP does no parsing of fenced blocks, no rule objects, and
13
+ * no warnings: it concatenates the selected facts' bodies in stable enumeration
14
+ * order, each preceded by a one-line `# <ref>` provenance header. Returns `""`
15
+ * when no matching facts exist.
16
+ */
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { parseFrontmatter } from "../asset/frontmatter.js";
20
+ /** `category` values that mark a fact as an authoring standard. */
21
+ const STANDARD_CATEGORIES = new Set(["convention", "meta"]);
22
+ /** Directory (under the stash root) where `fact` assets live. */
23
+ const FACTS_SUBDIR = "facts";
24
+ /**
25
+ * Per-type SOFT convention facts (`facts/conventions/assets/<type>.md`, #646)
26
+ * are surfaced **type-scoped** through `resolveTypeConventions`, so they must
27
+ * NOT leak into this un-type-scoped general layer (authoring a `command` must
28
+ * not pull the `skill` convention). Excluded by relative path (POSIX form).
29
+ */
30
+ const TYPE_CONVENTIONS_REL = "conventions/assets/";
31
+ /**
32
+ * Recursively collect `.md` files under `dir` in stable (sorted) enumeration
33
+ * order. Returns absolute paths. Missing dir → `[]`.
34
+ */
35
+ function collectMarkdownFiles(dir) {
36
+ let entries;
37
+ try {
38
+ entries = fs.readdirSync(dir, { withFileTypes: true });
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ const results = [];
44
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
45
+ const full = path.join(dir, entry.name);
46
+ if (entry.isDirectory()) {
47
+ results.push(...collectMarkdownFiles(full));
48
+ }
49
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
50
+ results.push(full);
51
+ }
52
+ }
53
+ return results;
54
+ }
55
+ /**
56
+ * Derive a fact ref (`fact:conventions/naming`) from an absolute markdown path
57
+ * relative to the facts root. Mirrors the canonical-name derivation in
58
+ * `asset-spec.ts` (POSIX separators, `.md` stripped).
59
+ */
60
+ function toFactRef(factsRoot, absPath) {
61
+ const rel = path.relative(factsRoot, absPath).split(path.sep).join("/");
62
+ const name = rel.endsWith(".md") ? rel.slice(0, -3) : rel;
63
+ return `fact:${name}`;
64
+ }
65
+ export function resolveStashStandards(stashRoot) {
66
+ const factsRoot = path.join(stashRoot, FACTS_SUBDIR);
67
+ const sections = [];
68
+ for (const absPath of collectMarkdownFiles(factsRoot)) {
69
+ // Per-type SOFT conventions are delivered type-scoped (#646); skip them
70
+ // here so they never leak un-type-scoped into every authoring flow.
71
+ const relPosix = path.relative(factsRoot, absPath).split(path.sep).join("/");
72
+ if (relPosix.startsWith(TYPE_CONVENTIONS_REL))
73
+ continue;
74
+ let raw;
75
+ try {
76
+ raw = fs.readFileSync(absPath, "utf8");
77
+ }
78
+ catch {
79
+ continue;
80
+ }
81
+ let category = "";
82
+ let body = "";
83
+ try {
84
+ const parsed = parseFrontmatter(raw);
85
+ category = typeof parsed.data.category === "string" ? parsed.data.category.trim() : "";
86
+ body = parsed.content;
87
+ }
88
+ catch {
89
+ continue;
90
+ }
91
+ if (!STANDARD_CATEGORIES.has(category))
92
+ continue;
93
+ const trimmed = body.trim();
94
+ if (!trimmed)
95
+ continue; // skip stub facts with frontmatter but no body
96
+ sections.push(`# ${toFactRef(factsRoot, absPath)}\n${trimmed}`);
97
+ }
98
+ return sections.join("\n\n");
99
+ }
@@ -0,0 +1,66 @@
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
+ * Resolve **per-type SOFT authoring conventions** (#646) — the third and final
6
+ * authoring-guidance layer:
7
+ *
8
+ * 1. HARD rules (validator-rejecting, code-sourced) → `authoringRulesForType()`
9
+ * (`src/core/authoring-rules.ts`, #645). Never editable; the gate enforces them.
10
+ * 2. General stash standards (cross-type naming/tag conventions) →
11
+ * `resolveStashStandards()` `category: convention|meta` facts (#642).
12
+ * 3. **Per-type SOFT conventions** (voice, structure, length *preference* for
13
+ * *this* asset type) → user-editable `facts/conventions/assets/<type>.md`
14
+ * (THIS module). Augments the built-in `TYPE_HINTS` fallback for display.
15
+ *
16
+ * These facts are **soft only** — advice, not contract. They MUST NOT carry
17
+ * hard, validator-rejecting rules: a user editing or deleting one must never be
18
+ * able to weaken the authoring contract the gate enforces (#645 boundary).
19
+ *
20
+ * Selection is by a `getAssetTypes()`-validated basename: only
21
+ * `facts/conventions/assets/<known-type>.md` resolves. Read directly from disk
22
+ * (no index rebuild); any missing dir/file, unknown type, or read error degrades
23
+ * to `""` and never throws.
24
+ */
25
+ import fs from "node:fs";
26
+ import path from "node:path";
27
+ import { getAssetTypes } from "../asset/asset-spec.js";
28
+ import { parseFrontmatter } from "../asset/frontmatter.js";
29
+ /** Sub-path (under the stash root) for per-type SOFT convention facts. */
30
+ export const TYPE_CONVENTIONS_SUBDIR = path.join("facts", "conventions", "assets");
31
+ /** The `fact:` ref prefix for a per-type convention, e.g. `fact:conventions/assets/skill`. */
32
+ export function typeConventionRef(type) {
33
+ return `fact:conventions/assets/${type}`;
34
+ }
35
+ /**
36
+ * Read the SOFT authoring-convention body for asset type `type`, if a stash
37
+ * owner has authored `facts/conventions/assets/<type>.md`.
38
+ *
39
+ * @returns the trimmed markdown body (frontmatter stripped), or `""` when the
40
+ * type is unknown, the file is absent, or anything goes wrong.
41
+ */
42
+ export function resolveTypeConventions(stashRoot, type) {
43
+ if (!stashRoot || !type)
44
+ return "";
45
+ // Basename MUST be a known asset type — never resolve an arbitrary file.
46
+ if (!getAssetTypes().includes(type))
47
+ return "";
48
+ const abs = path.join(stashRoot, TYPE_CONVENTIONS_SUBDIR, `${type}.md`);
49
+ let raw;
50
+ try {
51
+ raw = fs.readFileSync(abs, "utf8");
52
+ }
53
+ catch {
54
+ return ""; // missing dir/file or read error → degrade to empty
55
+ }
56
+ let body = "";
57
+ try {
58
+ body = parseFrontmatter(raw).content;
59
+ }
60
+ catch {
61
+ // Malformed frontmatter: fall back to the whole file (parseFrontmatter
62
+ // normally returns whole content as body, but guard defensively).
63
+ body = raw;
64
+ }
65
+ return body.trim();
66
+ }