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

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 (325) hide show
  1. package/CHANGELOG.md +592 -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/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  39. package/dist/assets/templates/html/health.html +281 -111
  40. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  41. package/dist/assets/wiki/schema-template.md +4 -4
  42. package/dist/cli/clack.js +56 -0
  43. package/dist/cli/config-migrate.js +7 -1
  44. package/dist/cli/confirm.js +1 -1
  45. package/dist/cli/parse-args.js +46 -1
  46. package/dist/cli/shared.js +28 -0
  47. package/dist/cli.js +25 -21
  48. package/dist/commands/agent/agent-dispatch.js +3 -2
  49. package/dist/commands/agent/agent-support.js +0 -7
  50. package/dist/commands/agent/contribute-cli.js +26 -7
  51. package/dist/commands/config-cli.js +26 -13
  52. package/dist/commands/env/child-env.js +47 -0
  53. package/dist/commands/env/env-cli.js +220 -227
  54. package/dist/commands/env/env.js +14 -67
  55. package/dist/commands/env/secret-cli.js +140 -138
  56. package/dist/commands/feedback-cli.js +156 -155
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +3 -3
  59. package/dist/commands/health/advisories.js +151 -0
  60. package/dist/commands/health/checks.js +103 -16
  61. package/dist/commands/health/html-report.js +447 -81
  62. package/dist/commands/health/improve-metrics.js +771 -0
  63. package/dist/commands/health/llm-usage.js +65 -0
  64. package/dist/commands/health/md-report.js +103 -0
  65. package/dist/commands/health/metrics.js +278 -0
  66. package/dist/commands/health/stash-exposure.js +46 -0
  67. package/dist/commands/health/surfaces.js +216 -0
  68. package/dist/commands/health/task-runs.js +135 -0
  69. package/dist/commands/health/types.js +26 -0
  70. package/dist/commands/health/windows.js +195 -0
  71. package/dist/commands/health.js +91 -1091
  72. package/dist/commands/improve/anti-collapse.js +170 -0
  73. package/dist/commands/improve/calibration.js +161 -0
  74. package/dist/commands/improve/collapse-detector.js +421 -0
  75. package/dist/commands/improve/consolidate/chunking.js +141 -0
  76. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  77. package/dist/commands/improve/consolidate/merge.js +145 -0
  78. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  79. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  80. package/dist/commands/improve/consolidate.js +1295 -1277
  81. package/dist/commands/improve/dedup.js +482 -0
  82. package/dist/commands/improve/distill/content-repair.js +202 -0
  83. package/dist/commands/improve/distill/promote-memory.js +229 -0
  84. package/dist/commands/improve/distill/quality-gate.js +236 -0
  85. package/dist/commands/improve/distill-guards.js +127 -0
  86. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  87. package/dist/commands/improve/distill.js +228 -605
  88. package/dist/commands/improve/eligibility.js +434 -0
  89. package/dist/commands/improve/encoding-salience.js +205 -0
  90. package/dist/commands/improve/extract-cli.js +179 -59
  91. package/dist/commands/improve/extract-prompt.js +54 -3
  92. package/dist/commands/improve/extract-watch.js +140 -0
  93. package/dist/commands/improve/extract.js +409 -43
  94. package/dist/commands/improve/feedback-valence.js +54 -0
  95. package/dist/commands/improve/hot-probation.js +45 -0
  96. package/dist/commands/improve/improve-auto-accept.js +157 -10
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +28 -8
  99. package/dist/commands/improve/improve-result-file.js +15 -25
  100. package/dist/commands/improve/improve-session.js +58 -0
  101. package/dist/commands/improve/improve.js +485 -2764
  102. package/dist/commands/improve/locks.js +154 -0
  103. package/dist/commands/improve/loop-stages.js +1100 -0
  104. package/dist/commands/improve/memory/memory-belief.js +14 -15
  105. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  106. package/dist/commands/improve/memory/memory-improve.js +27 -27
  107. package/dist/commands/improve/outcome-loop.js +270 -0
  108. package/dist/commands/improve/preparation.js +2002 -0
  109. package/dist/commands/improve/proactive-maintenance.js +37 -35
  110. package/dist/commands/improve/procedural.js +398 -0
  111. package/dist/commands/improve/recombine.js +818 -0
  112. package/dist/commands/improve/reflect-noise.js +0 -0
  113. package/dist/commands/improve/reflect.js +206 -45
  114. package/dist/commands/improve/salience.js +455 -0
  115. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  116. package/dist/commands/improve/shared.js +51 -0
  117. package/dist/commands/improve/triage.js +93 -0
  118. package/dist/commands/lint/agent-linter.js +19 -24
  119. package/dist/commands/lint/base-linter.js +173 -60
  120. package/dist/commands/lint/command-linter.js +19 -24
  121. package/dist/commands/lint/env-key-rules.js +38 -1
  122. package/dist/commands/lint/fact-linter.js +39 -0
  123. package/dist/commands/lint/index.js +31 -13
  124. package/dist/commands/lint/memory-linter.js +1 -1
  125. package/dist/commands/lint/registry.js +7 -2
  126. package/dist/commands/lint/task-linter.js +3 -3
  127. package/dist/commands/lint/workflow-linter.js +26 -1
  128. package/dist/commands/observability-cli.js +4 -4
  129. package/dist/commands/proposal/drain-policies.js +13 -4
  130. package/dist/commands/proposal/drain.js +45 -51
  131. package/dist/commands/proposal/legacy-import.js +115 -0
  132. package/dist/commands/proposal/proposal-cli.js +24 -34
  133. package/dist/commands/proposal/proposal.js +2 -1
  134. package/dist/commands/proposal/propose.js +8 -3
  135. package/dist/commands/proposal/repository.js +829 -0
  136. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  137. package/dist/commands/proposal/validators/proposals.js +93 -895
  138. package/dist/commands/read/curate.js +410 -111
  139. package/dist/commands/read/knowledge.js +10 -3
  140. package/dist/commands/read/remember-cli.js +133 -138
  141. package/dist/commands/read/search-cli.js +15 -8
  142. package/dist/commands/read/search.js +22 -11
  143. package/dist/commands/read/show.js +106 -14
  144. package/dist/commands/registry-cli.js +76 -87
  145. package/dist/commands/remember.js +11 -12
  146. package/dist/commands/sources/add-cli.js +91 -95
  147. package/dist/commands/sources/history.js +1 -1
  148. package/dist/commands/sources/init.js +66 -18
  149. package/dist/commands/sources/installed-stashes.js +11 -3
  150. package/dist/commands/sources/schema-repair.js +44 -46
  151. package/dist/commands/sources/self-update.js +2 -2
  152. package/dist/commands/sources/source-add.js +7 -3
  153. package/dist/commands/sources/sources-cli.js +3 -3
  154. package/dist/commands/sources/stash-cli.js +19 -39
  155. package/dist/commands/sources/stash-skeleton.js +57 -8
  156. package/dist/commands/tasks/default-tasks.js +15 -2
  157. package/dist/commands/tasks/tasks-cli.js +20 -29
  158. package/dist/commands/tasks/tasks.js +39 -11
  159. package/dist/commands/wiki-cli.js +23 -38
  160. package/dist/commands/workflow-cli.js +15 -1
  161. package/dist/core/asset/asset-registry.js +3 -1
  162. package/dist/core/asset/asset-spec.js +21 -4
  163. package/dist/core/asset/frontmatter.js +188 -167
  164. package/dist/core/asset/markdown.js +8 -0
  165. package/dist/core/authoring-rules.js +92 -0
  166. package/dist/core/common.js +4 -23
  167. package/dist/core/concurrent.js +10 -1
  168. package/dist/core/config/config-io.js +10 -1
  169. package/dist/core/config/config-migration.js +18 -40
  170. package/dist/core/config/config-schema.js +382 -62
  171. package/dist/core/config/config-types.js +3 -3
  172. package/dist/core/config/config.js +67 -22
  173. package/dist/core/deep-merge.js +38 -0
  174. package/dist/core/errors.js +1 -0
  175. package/dist/core/eval/rank-metrics.js +113 -0
  176. package/dist/core/events.js +4 -7
  177. package/dist/core/improve-types.js +47 -8
  178. package/dist/core/logs-db.js +14 -75
  179. package/dist/core/parse.js +36 -16
  180. package/dist/core/paths.js +18 -18
  181. package/dist/core/standards/resolve-standards-context.js +87 -0
  182. package/dist/core/standards/resolve-stash-standards.js +99 -0
  183. package/dist/core/standards/resolve-type-conventions.js +66 -0
  184. package/dist/core/state/migrations.js +770 -0
  185. package/dist/core/state-db.js +132 -1126
  186. package/dist/core/structured.js +69 -0
  187. package/dist/core/time.js +53 -0
  188. package/dist/core/warn.js +21 -0
  189. package/dist/core/write-source.js +37 -0
  190. package/dist/indexer/db/db.js +259 -769
  191. package/dist/indexer/db/entry-mapper.js +41 -0
  192. package/dist/indexer/db/graph-db.js +129 -86
  193. package/dist/indexer/db/llm-cache.js +2 -2
  194. package/dist/indexer/db/schema.js +516 -0
  195. package/dist/indexer/ensure-index.js +36 -92
  196. package/dist/indexer/feedback/utility-policy.js +75 -0
  197. package/dist/indexer/graph/graph-boost.js +51 -41
  198. package/dist/indexer/graph/graph-extraction.js +207 -4
  199. package/dist/indexer/index-writer-lock.js +18 -11
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +182 -204
  202. package/dist/indexer/passes/dir-staleness.js +114 -0
  203. package/dist/indexer/passes/memory-inference.js +13 -5
  204. package/dist/indexer/passes/metadata.js +20 -0
  205. package/dist/indexer/read-preflight.js +23 -0
  206. package/dist/indexer/search/db-search.js +89 -13
  207. package/dist/indexer/search/fts-query.js +51 -0
  208. package/dist/indexer/search/ranking-contributors.js +95 -9
  209. package/dist/indexer/search/ranking.js +79 -3
  210. package/dist/indexer/search/search-fields.js +6 -0
  211. package/dist/indexer/search/search-source.js +32 -21
  212. package/dist/indexer/search/semantic-status.js +4 -0
  213. package/dist/indexer/walk/matchers.js +9 -0
  214. package/dist/indexer/walk/walker.js +21 -13
  215. package/dist/integrations/agent/builders.js +39 -13
  216. package/dist/integrations/agent/config.js +20 -59
  217. package/dist/integrations/agent/detect.js +9 -0
  218. package/dist/integrations/agent/index.js +3 -19
  219. package/dist/integrations/agent/model-aliases.js +7 -2
  220. package/dist/integrations/agent/profiles.js +7 -1
  221. package/dist/integrations/agent/prompts.js +75 -9
  222. package/dist/integrations/agent/runner-dispatch.js +59 -0
  223. package/dist/integrations/agent/runner.js +13 -9
  224. package/dist/integrations/agent/spawn.js +69 -67
  225. package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
  226. package/dist/integrations/harnesses/claude/index.js +2 -0
  227. package/dist/integrations/harnesses/claude/session-log.js +10 -0
  228. package/dist/integrations/harnesses/index.js +2 -3
  229. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  230. package/dist/integrations/harnesses/opencode/index.js +2 -0
  231. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  232. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  233. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
  234. package/dist/integrations/harnesses/types.js +1 -0
  235. package/dist/integrations/session-logs/index.js +16 -0
  236. package/dist/llm/call-ai.js +2 -2
  237. package/dist/llm/client.js +34 -11
  238. package/dist/llm/embedder.js +67 -4
  239. package/dist/llm/embedders/cache.js +3 -1
  240. package/dist/llm/embedders/deterministic.js +66 -0
  241. package/dist/llm/embedders/local.js +73 -3
  242. package/dist/llm/feature-gate.js +16 -15
  243. package/dist/llm/graph-extract.js +67 -44
  244. package/dist/llm/memory-infer-impl.js +138 -0
  245. package/dist/llm/memory-infer.js +1 -127
  246. package/dist/llm/metadata-enhance.js +44 -31
  247. package/dist/llm/structured-call.js +49 -0
  248. package/dist/migrate-storage-node.mjs +8 -0
  249. package/dist/output/context.js +5 -5
  250. package/dist/output/renderers.js +85 -14
  251. package/dist/output/shapes/curate.js +14 -2
  252. package/dist/output/shapes/helpers.js +0 -3
  253. package/dist/output/shapes/passthrough.js +2 -1
  254. package/dist/output/text/helpers.js +29 -1
  255. package/dist/output/text/workflow.js +1 -0
  256. package/dist/registry/providers/skills-sh.js +21 -147
  257. package/dist/registry/providers/static-index.js +15 -157
  258. package/dist/registry/resolve.js +27 -9
  259. package/dist/runtime.js +25 -1
  260. package/dist/scripts/migrate-storage.js +2661 -2369
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
  262. package/dist/setup/detect.js +9 -0
  263. package/dist/setup/legacy-config.js +106 -0
  264. package/dist/setup/prompt.js +57 -0
  265. package/dist/setup/providers.js +14 -0
  266. package/dist/setup/registry-stash-loader.js +12 -0
  267. package/dist/setup/semantic-assets.js +124 -0
  268. package/dist/setup/setup.js +52 -1614
  269. package/dist/setup/steps/connection.js +734 -0
  270. package/dist/setup/steps/output.js +31 -0
  271. package/dist/setup/steps/platforms.js +124 -0
  272. package/dist/setup/steps/semantic.js +27 -0
  273. package/dist/setup/steps/sources.js +222 -0
  274. package/dist/setup/steps/stashdir.js +42 -0
  275. package/dist/setup/steps/tasks.js +152 -0
  276. package/dist/sources/include.js +6 -2
  277. package/dist/sources/providers/filesystem.js +0 -1
  278. package/dist/sources/providers/git-install.js +210 -0
  279. package/dist/sources/providers/git-provider.js +234 -0
  280. package/dist/sources/providers/git-stash.js +248 -0
  281. package/dist/sources/providers/git.js +10 -661
  282. package/dist/sources/providers/npm.js +2 -6
  283. package/dist/sources/providers/provider-utils.js +13 -7
  284. package/dist/sources/providers/sync-from-ref.js +9 -1
  285. package/dist/sources/providers/website.js +9 -5
  286. package/dist/sources/website-ingest.js +187 -29
  287. package/dist/sources/wiki-fetchers/registry.js +53 -0
  288. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  289. package/dist/storage/database.js +45 -10
  290. package/dist/storage/managed-db.js +82 -0
  291. package/dist/storage/repositories/canaries-repository.js +107 -0
  292. package/dist/storage/repositories/consolidation-repository.js +38 -0
  293. package/dist/storage/repositories/embeddings-repository.js +72 -0
  294. package/dist/storage/repositories/events-repository.js +187 -0
  295. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  296. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  297. package/dist/storage/repositories/index-db.js +14 -8
  298. package/dist/storage/repositories/proposals-repository.js +220 -0
  299. package/dist/storage/repositories/recombine-repository.js +213 -0
  300. package/dist/storage/repositories/registry-cache.js +93 -0
  301. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  302. package/dist/storage/repositories/task-history-repository.js +93 -0
  303. package/dist/storage/sqlite-pragmas.js +146 -0
  304. package/dist/tasks/backends/cron.js +1 -1
  305. package/dist/tasks/backends/index.js +9 -0
  306. package/dist/tasks/backends/launchd.js +1 -1
  307. package/dist/tasks/backends/schtasks.js +1 -1
  308. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  309. package/dist/tasks/runner.js +15 -13
  310. package/dist/text-import-hook.mjs +0 -0
  311. package/dist/wiki/wiki.js +52 -11
  312. package/dist/workflows/cli.js +1 -0
  313. package/dist/workflows/db.js +3 -4
  314. package/dist/workflows/runtime/runs.js +43 -118
  315. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  316. package/dist/workflows/validate-summary.js +2 -7
  317. package/docs/README.md +69 -18
  318. package/docs/data-and-telemetry.md +5 -4
  319. package/docs/migration/release-notes/0.7.0.md +1 -1
  320. package/docs/migration/release-notes/0.9.0.md +39 -0
  321. package/package.json +10 -10
  322. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  323. package/dist/commands/db-cli.js +0 -23
  324. package/dist/indexer/db/db-backup.js +0 -376
  325. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -1,32 +1,13 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { computeSalience } from "./salience.js";
4
5
  /** One day in milliseconds. */
5
6
  const DAY_MS = 86_400_000;
6
- /**
7
- * Importance multipliers by asset type. Higher = more worth maintaining. These
8
- * are the design defaults; callers may override any subset via config.
9
- */
10
- export const DEFAULT_IMPORTANCE_WEIGHTS = Object.freeze({
11
- skill: 1.5,
12
- agent: 1.5,
13
- command: 1.3,
14
- workflow: 1.3,
15
- lesson: 1.2,
16
- knowledge: 1.0,
17
- script: 0.9,
18
- memory: 0.7,
19
- });
20
7
  /** Default staleness gate: an asset is due when last reflected > this many days ago (or never). */
21
8
  export const DEFAULT_DUE_DAYS = 30;
22
9
  /** Default bound on how many assets the selector surfaces per run. */
23
10
  export const DEFAULT_MAX_PER_RUN = 25;
24
- /**
25
- * Half-life (days) for the recency-of-use decay term. An asset used today
26
- * contributes a full recency multiplier; one unused for one half-life
27
- * contributes half. Mirrors the validated prototype (21 days).
28
- */
29
- const RECENCY_HALFLIFE_DAYS = 21;
30
11
  /** Lower bound on size used in the cost denominator so tiny files don't divide by ~0. */
31
12
  const SIZE_FLOOR_BYTES = 200;
32
13
  /** Parse the bare asset type out of a `type:name` ref. Returns "" when unparseable. */
@@ -37,10 +18,9 @@ function refType(ref) {
37
18
  /**
38
19
  * Score and select due assets for proactive maintenance.
39
20
  *
40
- * Priority formula (mirrors the validated prototype):
41
- *
42
- * priority = (importance × log(1 + retrievalFreq) × (0.1 + 0.5^(useAgeDays/21)))
43
- * / log10(max(size, 200))
21
+ * Priority: delegates to `computeSalience(...).rankScore` (WS-1 unified salience
22
+ * vector). The ranking key is therefore identical to every other selector that
23
+ * uses salience a single formula governs attention across the whole improve loop.
44
24
  *
45
25
  * DUE gate: an asset is eligible only if it was never reflected OR last
46
26
  * reflected/distilled more than `dueDays` ago. The same gate doubles as the
@@ -52,7 +32,6 @@ export function selectProactiveMaintenanceRefs(params) {
52
32
  const now = params.now ?? Date.now();
53
33
  const dueDays = params.dueDays ?? DEFAULT_DUE_DAYS;
54
34
  const maxPerRun = params.maxPerRun ?? DEFAULT_MAX_PER_RUN;
55
- const weights = { ...DEFAULT_IMPORTANCE_WEIGHTS, ...(params.importanceWeights ?? {}) };
56
35
  const scored = [];
57
36
  for (const candidate of params.candidates) {
58
37
  const ref = candidate.ref;
@@ -70,28 +49,23 @@ export function selectProactiveMaintenanceRefs(params) {
70
49
  const staleDays = neverReflected ? Number.POSITIVE_INFINITY : (now - lastTouchMs) / DAY_MS;
71
50
  // DUE / rotation gate.
72
51
  const due = neverReflected || staleDays > dueDays;
73
- // Retrieval frequency + recency decay.
52
+ // Retrieval frequency (for salience inputs).
74
53
  const retrievalFreq = params.retrievalCounts.get(ref) ?? 0;
75
54
  const lastUse = params.lastUseMs?.get(ref) ?? 0;
76
- const useAgeDays = lastUse > 0 ? (now - lastUse) / DAY_MS : 9999;
77
- const recencyDecay = 0.1 + 0.5 ** (useAgeDays / RECENCY_HALFLIFE_DAYS);
78
- // Size proxy (cost): larger assets are slightly deprioritized, but only by
79
- // log10 so a big-but-hot asset is never starved.
55
+ // Size proxy (cost): kept for salience input computeSalience applies
56
+ // the log10 denominator internally.
80
57
  let sizeBytes = params.sizeBytesOf?.(candidate) ?? 0;
81
58
  if (!sizeBytes || sizeBytes < 0)
82
59
  sizeBytes = SIZE_FLOOR_BYTES;
83
- const sizeProxy = Math.max(SIZE_FLOOR_BYTES, sizeBytes);
84
- const importance = weights[type] ?? 1.0;
85
- const priority = (importance * Math.log(1 + retrievalFreq) * recencyDecay) / Math.log10(sizeProxy);
60
+ // Unified priority via WS-1 salience vector (replaces the old inline formula).
61
+ const priority = computeSalience({ ref, type, retrievalFreq, lastUseMs: lastUse, sizeBytes, now }).rankScore;
86
62
  scored.push({
87
63
  ref: candidate,
88
64
  type,
89
65
  staleDays,
90
66
  neverReflected,
91
67
  retrievalFreq,
92
- recencyDecay,
93
68
  sizeBytes,
94
- importance,
95
69
  priority,
96
70
  due,
97
71
  });
@@ -111,3 +85,31 @@ export function selectProactiveMaintenanceRefs(params) {
111
85
  const selected = ranked.slice(0, Math.max(0, maxPerRun)).map((s) => s.ref);
112
86
  return { selected, dueTotal, neverReflected, scored };
113
87
  }
88
+ /**
89
+ * Post-lock re-filter for proactive refs.
90
+ *
91
+ * Called INSIDE the reflect-distill.lock window with freshly-read timestamp
92
+ * maps so that any `reflect_invoked` events committed by a concurrent run
93
+ * while this run was waiting for the lock are visible. Refs that became
94
+ * non-due (staleDays ≤ dueDays) since planning time are dropped before
95
+ * execution, closing the SELECT-time cooldown race.
96
+ *
97
+ * The logic mirrors the DUE gate in `selectProactiveMaintenanceRefs` — an
98
+ * asset is due only when never touched OR last touched more than `dueDays`
99
+ * ago. The selector and this filter must always agree on the gate predicate.
100
+ */
101
+ export function filterProactiveDue(selected, lastReflectTs, lastDistillTs, dueDays, now) {
102
+ return selected.filter((candidate) => {
103
+ const ref = candidate.ref;
104
+ const reflectIso = lastReflectTs.get(ref);
105
+ const distillIso = lastDistillTs.get(ref);
106
+ let lastTouchMs = 0;
107
+ if (reflectIso)
108
+ lastTouchMs = Math.max(lastTouchMs, Date.parse(reflectIso) || 0);
109
+ if (distillIso)
110
+ lastTouchMs = Math.max(lastTouchMs, Date.parse(distillIso) || 0);
111
+ const neverReflected = lastTouchMs === 0;
112
+ const staleDays = neverReflected ? Number.POSITIVE_INFINITY : (now - lastTouchMs) / DAY_MS;
113
+ return neverReflected || staleDays > dueDays;
114
+ });
115
+ }
@@ -0,0 +1,398 @@
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
+ * #615 — procedural-compilation pass.
6
+ *
7
+ * An OPT-IN post-loop improve stage (default disabled via
8
+ * `IMPROVE_PROCESS_DEFAULTS.procedural`). It reads assets that carry an
9
+ * `orderedActions` frontmatter list (captured by #619), detects RECURRING
10
+ * successful action sequences across sessions (the SAME normalized ordered step
11
+ * list appearing >= `minRecurrence` times with a non-failure `outcomeData`), and
12
+ * emits ONE normal `type: workflow` proposal per recurring sequence through the
13
+ * existing proposal queue + quality gate.
14
+ *
15
+ * The ordered step list — NOT the LLM — is the source of truth. The bounded LLM
16
+ * call only NAMES the workflow and per-step titles/instructions; the parser
17
+ * rejects any output whose step count / order drifts from the deterministic
18
+ * sequence, and the assembled workflow markdown is re-parsed locally before it
19
+ * is ever queued. A justified null (the LLM determines the sequence is not a
20
+ * coherent procedure) is an acceptable outcome and produces no proposal.
21
+ */
22
+ import { createHash } from "node:crypto";
23
+ import fs from "node:fs";
24
+ import proceduralSystemPrompt from "../../assets/prompts/procedural-system.md" with { type: "text" };
25
+ import { parseFrontmatter } from "../../core/asset/frontmatter.js";
26
+ import { resolveStashDir } from "../../core/common.js";
27
+ import { loadConfig } from "../../core/config/config.js";
28
+ import { appendEvent } from "../../core/events.js";
29
+ import { parseEmbeddedJsonResponse } from "../../core/parse.js";
30
+ import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
31
+ import { closeDatabase, getAllEntries, openExistingDatabase } from "../../indexer/db/db.js";
32
+ import { parseWorkflow } from "../../workflows/parser.js";
33
+ import { createProposal, isProposalSkipped } from "../proposal/repository.js";
34
+ import { validateProposalFrontmatter } from "../proposal/validators/proposal-quality-validators.js";
35
+ import { resolveImproveLlmFn } from "./shared.js";
36
+ const PROCEDURAL_SYSTEM_PROMPT = proceduralSystemPrompt;
37
+ const DEFAULT_MIN_RECURRENCE = 3;
38
+ const DEFAULT_MAX_PROPOSALS_PER_RUN = 3;
39
+ /** Failure-signal heuristic: an outcome matching this is NOT counted as success. */
40
+ const FAILURE_SIGNAL = /\b(fail|failed|failure|error|errored|abort|aborted|rollback|reverted)\b/i;
41
+ // ── Normalization + recurrence model (deterministic, no LLM) ────────────────────
42
+ /** Normalize a single action string: trim, lowercase, collapse whitespace, strip trailing punctuation. */
43
+ function normalizeStep(s) {
44
+ return s
45
+ .trim()
46
+ .toLowerCase()
47
+ .replace(/\s+/g, " ")
48
+ .replace(/[.;,:]+$/, "")
49
+ .trim();
50
+ }
51
+ /**
52
+ * Normalize an ordered action list: each step token-normalized (case /
53
+ * whitespace / trailing-punctuation insensitive), with empties dropped. The
54
+ * result is ORDER-SENSITIVE — reordered sequences normalize distinctly.
55
+ */
56
+ export function normalizeSequence(actions) {
57
+ return actions.map(normalizeStep).filter((s) => s.length > 0);
58
+ }
59
+ /** A member counts toward recurrence only when its outcome is present, non-empty, and not a failure signal. */
60
+ function isSuccessfulOutcome(outcome) {
61
+ if (!outcome)
62
+ return false;
63
+ const trimmed = outcome.trim();
64
+ if (!trimmed)
65
+ return false;
66
+ return !FAILURE_SIGNAL.test(trimmed);
67
+ }
68
+ /** Recover the ordered-action sequence + outcome from an asset's frontmatter (NOT a DB column). */
69
+ function readOrderedSequence(entry) {
70
+ let data;
71
+ try {
72
+ const raw = fs.readFileSync(entry.filePath, "utf8");
73
+ data = parseFrontmatter(raw).data;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ const rawActions = data.orderedActions;
79
+ if (!Array.isArray(rawActions))
80
+ return null;
81
+ const actions = rawActions.filter((a) => typeof a === "string");
82
+ if (actions.length === 0)
83
+ return null;
84
+ const outcome = typeof data.outcomeData === "string" ? data.outcomeData : undefined;
85
+ const ref = `${entry.entry.type}:${entry.entry.name}`;
86
+ return { actions, ...(outcome !== undefined ? { outcome } : {}), ref, entryKey: entry.entryKey };
87
+ }
88
+ /**
89
+ * Group entries by their normalized ordered sequence. Only successful members
90
+ * (non-failure outcome) count toward recurrence. Keep groups whose successful
91
+ * member count >= `minRecurrence`, sort deterministically (member-count desc
92
+ * then groupKey asc), and cap to `maxProposalsPerRun`. Exported for unit tests.
93
+ */
94
+ export function buildSequenceClusters(entries, opts) {
95
+ const groups = new Map();
96
+ for (const entry of entries) {
97
+ const seq = readOrderedSequence(entry);
98
+ if (!seq)
99
+ continue;
100
+ const normalized = normalizeSequence(seq.actions);
101
+ if (normalized.length === 0)
102
+ continue;
103
+ if (!isSuccessfulOutcome(seq.outcome))
104
+ continue;
105
+ const groupKey = JSON.stringify(normalized);
106
+ const existing = groups.get(groupKey);
107
+ const member = { ref: seq.ref, entryKey: seq.entryKey, outcome: seq.outcome ?? "" };
108
+ if (existing) {
109
+ if (!existing.members.some((m) => m.entryKey === member.entryKey))
110
+ existing.members.push(member);
111
+ }
112
+ else {
113
+ groups.set(groupKey, { groupKey, normalized, members: [member] });
114
+ }
115
+ }
116
+ const clusters = [...groups.values()].filter((c) => c.members.length >= opts.minRecurrence);
117
+ clusters.sort((a, b) => b.members.length - a.members.length || a.groupKey.localeCompare(b.groupKey));
118
+ return clusters.slice(0, Math.max(0, opts.maxProposalsPerRun));
119
+ }
120
+ /**
121
+ * Stable workflow ref for a sequence cluster. The hash of the sorted member keys
122
+ * + the normalized sequence keeps the ref deterministic across runs, so
123
+ * re-detection maps to the same ref and the content-hash dedup in createProposal
124
+ * suppresses queue churn.
125
+ */
126
+ export function deriveProceduralWorkflowRef(cluster) {
127
+ const slug = cluster.normalized
128
+ .slice(0, 3)
129
+ .join("-")
130
+ .toLowerCase()
131
+ .replace(/[^a-z0-9-]+/g, "-")
132
+ .replace(/-+/g, "-")
133
+ .replace(/^-|-$/g, "");
134
+ const memberKey = cluster.members
135
+ .map((m) => m.entryKey)
136
+ .sort()
137
+ .join("|");
138
+ const hash = createHash("sha256")
139
+ .update(`${memberKey} ${JSON.stringify(cluster.normalized)}`, "utf8")
140
+ .digest("hex")
141
+ .slice(0, 8);
142
+ return `workflow:compiled/${slug || "sequence"}-${hash}`;
143
+ }
144
+ // ── Prompt + parse ──────────────────────────────────────────────────────────────
145
+ /** Assemble the per-sequence user prompt fed to the procedural LLM. */
146
+ export function buildProceduralPrompt(cluster, standardsContext = "") {
147
+ const lines = [
148
+ `A recurring successful action sequence observed across ${cluster.members.length} sessions.`,
149
+ "",
150
+ ];
151
+ if (standardsContext.trim()) {
152
+ lines.push("Standards to follow (the rulebook for this target):");
153
+ lines.push(standardsContext.trim());
154
+ lines.push("");
155
+ }
156
+ lines.push("Ordered actions (turn EACH into exactly one step, in this order):");
157
+ cluster.normalized.forEach((step, i) => {
158
+ lines.push(`${i + 1}. ${step}`);
159
+ });
160
+ lines.push("", "Sample successful outcomes:");
161
+ for (const m of cluster.members.slice(0, 5)) {
162
+ if (m.outcome)
163
+ lines.push(`- ${m.outcome}`);
164
+ }
165
+ lines.push("", `Return strict JSON with EXACTLY ${cluster.normalized.length} steps in the same order, or an explicit null.`);
166
+ return lines.join("\n");
167
+ }
168
+ /** Parse the raw LLM output into a workflow, or `null` for the justified-null path. */
169
+ function parseProceduralWorkflow(raw, expectedSteps) {
170
+ if (raw === null)
171
+ return null;
172
+ const trimmed = raw.trim();
173
+ if (!trimmed || trimmed.toLowerCase() === "null")
174
+ return null;
175
+ const parsed = parseEmbeddedJsonResponse(trimmed);
176
+ if (parsed === undefined || parsed === null || typeof parsed !== "object")
177
+ return null;
178
+ const obj = parsed;
179
+ const title = typeof obj.title === "string" ? obj.title.trim() : "";
180
+ const description = typeof obj.description === "string" ? obj.description : "";
181
+ if (!title && !description)
182
+ return null;
183
+ if (!Array.isArray(obj.steps) || obj.steps.length !== expectedSteps)
184
+ return null;
185
+ const steps = [];
186
+ for (const rawStep of obj.steps) {
187
+ if (typeof rawStep !== "object" || rawStep === null)
188
+ return null;
189
+ const s = rawStep;
190
+ const stepTitle = typeof s.title === "string" ? s.title.trim() : "";
191
+ const instructions = typeof s.instructions === "string" ? s.instructions.trim() : "";
192
+ if (!stepTitle || !instructions)
193
+ return null;
194
+ const completionCriteria = Array.isArray(s.completionCriteria)
195
+ ? s.completionCriteria.filter((c) => typeof c === "string" && c.trim().length > 0)
196
+ : undefined;
197
+ steps.push({
198
+ title: stepTitle,
199
+ instructions,
200
+ ...(completionCriteria && completionCriteria.length > 0 ? { completionCriteria } : {}),
201
+ });
202
+ }
203
+ return { title: title || "Compiled Workflow", description, steps };
204
+ }
205
+ // ── Workflow markdown assembly ────────────────────────────────────────────────
206
+ /** Convert a step title into a kebab-case step id. */
207
+ function kebab(s) {
208
+ return (s
209
+ .toLowerCase()
210
+ .replace(/[^a-z0-9]+/g, "-")
211
+ .replace(/-+/g, "-")
212
+ .replace(/^-|-$/g, "") || "step");
213
+ }
214
+ /** Build the exact workflow markdown the parser accepts. */
215
+ export function assembleWorkflowMarkdown(doc) {
216
+ const lines = [
217
+ "---",
218
+ `description: ${JSON.stringify(doc.description)}`,
219
+ "---",
220
+ "",
221
+ `# Workflow: ${doc.title}`,
222
+ "",
223
+ ];
224
+ const usedIds = new Set();
225
+ doc.steps.forEach((step, idx) => {
226
+ let id = kebab(step.title);
227
+ if (usedIds.has(id))
228
+ id = `${id}-${idx + 1}`;
229
+ usedIds.add(id);
230
+ lines.push(`## Step: ${step.title}`, `Step ID: ${id}`, "", "### Instructions", step.instructions, "");
231
+ if (step.completionCriteria && step.completionCriteria.length > 0) {
232
+ lines.push("### Completion Criteria");
233
+ for (const c of step.completionCriteria)
234
+ lines.push(`- ${c}`);
235
+ lines.push("");
236
+ }
237
+ });
238
+ return lines.join("\n");
239
+ }
240
+ // ── Production LLM seam ───────────────────────────────────────────────────────
241
+ // ── Main entry point ───────────────────────────────────────────────────────────
242
+ export async function akmProcedural(opts) {
243
+ const startMs = Date.now();
244
+ const config = opts.config ?? loadConfig();
245
+ const stashDir = opts.stashDir ?? resolveStashDir();
246
+ const sourceRun = opts.sourceRun ?? `procedural-${startMs}`;
247
+ const eligibilitySource = opts.eligibilitySource ?? "procedural";
248
+ const minRecurrence = opts.minRecurrence ?? DEFAULT_MIN_RECURRENCE;
249
+ const maxProposalsPerRun = opts.maxProposalsPerRun ?? DEFAULT_MAX_PROPOSALS_PER_RUN;
250
+ const warnings = [];
251
+ const finish = (over) => ({
252
+ schemaVersion: 1,
253
+ ok: true,
254
+ sequencesScanned: 0,
255
+ clustersFormed: 0,
256
+ proposalsEmitted: 0,
257
+ nullsReturned: 0,
258
+ durationMs: Date.now() - startMs,
259
+ warnings,
260
+ ...over,
261
+ });
262
+ // Budget guard: an already-aborted signal short-circuits before any LLM call.
263
+ if (opts.signal?.aborted) {
264
+ return finish({ ok: false, warnings: [...warnings, "aborted-before-start"] });
265
+ }
266
+ // Load all entries from the index (orderedActions can ride any asset type).
267
+ let entries = [];
268
+ let db;
269
+ try {
270
+ db = openExistingDatabase();
271
+ entries = getAllEntries(db);
272
+ }
273
+ catch (e) {
274
+ warnings.push(`procedural: failed to open index — ${String(e)}`);
275
+ return finish({ ok: false });
276
+ }
277
+ finally {
278
+ if (db)
279
+ closeDatabase(db);
280
+ }
281
+ const clusters = buildSequenceClusters(entries, { minRecurrence, maxProposalsPerRun });
282
+ const sequencesScanned = entries.length;
283
+ let clustersFormed = 0;
284
+ let proposalsEmitted = 0;
285
+ let nullsReturned = 0;
286
+ if (clusters.length === 0) {
287
+ return finish({ sequencesScanned, clustersFormed: 0 });
288
+ }
289
+ const llmFn = opts.proceduralLlmFn ??
290
+ resolveImproveLlmFn(config, {
291
+ processKey: "procedural",
292
+ systemPrompt: PROCEDURAL_SYSTEM_PROMPT,
293
+ tag: "[procedural]",
294
+ signal: opts.signal,
295
+ activeProfile: opts.improveProfile,
296
+ });
297
+ if (!llmFn) {
298
+ warnings.push("procedural: no LLM configured — skipping");
299
+ return finish({ sequencesScanned, clustersFormed: 0 });
300
+ }
301
+ // Procedural output is a workflow (non-wiki) → stash authoring standards.
302
+ // Resolved ONCE per run and passed to each sequence prompt.
303
+ const standardsContext = resolveStashStandards(stashDir);
304
+ for (const cluster of clusters) {
305
+ if (opts.signal?.aborted) {
306
+ warnings.push("aborted-mid-run");
307
+ break;
308
+ }
309
+ clustersFormed += 1;
310
+ const workflowRef = deriveProceduralWorkflowRef(cluster);
311
+ const prompt = buildProceduralPrompt(cluster, standardsContext);
312
+ const raw = await llmFn(prompt);
313
+ const doc = parseProceduralWorkflow(raw, cluster.normalized.length);
314
+ if (!doc) {
315
+ nullsReturned += 1;
316
+ appendEvent({
317
+ eventType: "procedural_compiled",
318
+ ref: workflowRef,
319
+ metadata: {
320
+ groupKey: cluster.groupKey,
321
+ memberCount: cluster.members.length,
322
+ outcome: "null_returned",
323
+ sourceRun,
324
+ },
325
+ }, opts.ctx);
326
+ continue;
327
+ }
328
+ // Quality gate (always-run, never bypassed): the description must be present
329
+ // and non-truncated. Runs BEFORE createProposal.
330
+ const fmCheck = validateProposalFrontmatter({ description: doc.description });
331
+ if (!fmCheck.ok) {
332
+ appendEvent({
333
+ eventType: "procedural_compiled",
334
+ ref: workflowRef,
335
+ metadata: {
336
+ groupKey: cluster.groupKey,
337
+ memberCount: cluster.members.length,
338
+ outcome: "quality_rejected",
339
+ reason: fmCheck.reason,
340
+ sourceRun,
341
+ },
342
+ }, opts.ctx);
343
+ continue;
344
+ }
345
+ // Assemble + locally validate the workflow markdown. Never queue an
346
+ // unparseable workflow.
347
+ const content = assembleWorkflowMarkdown(doc);
348
+ const parsed = parseWorkflow(content, { path: workflowRef });
349
+ if (!parsed.ok) {
350
+ appendEvent({
351
+ eventType: "procedural_compiled",
352
+ ref: workflowRef,
353
+ metadata: {
354
+ groupKey: cluster.groupKey,
355
+ memberCount: cluster.members.length,
356
+ outcome: "invalid_workflow",
357
+ reason: parsed.errors[0]?.message,
358
+ sourceRun,
359
+ },
360
+ }, opts.ctx);
361
+ continue;
362
+ }
363
+ const proposalResult = createProposal(stashDir, {
364
+ ref: workflowRef,
365
+ source: "procedural",
366
+ sourceRun,
367
+ payload: { content, frontmatter: { description: doc.description } },
368
+ eligibilitySource,
369
+ }, opts.ctx);
370
+ if (isProposalSkipped(proposalResult)) {
371
+ appendEvent({
372
+ eventType: "procedural_compiled",
373
+ ref: workflowRef,
374
+ metadata: {
375
+ groupKey: cluster.groupKey,
376
+ memberCount: cluster.members.length,
377
+ outcome: "skipped",
378
+ skipReason: proposalResult.reason,
379
+ sourceRun,
380
+ },
381
+ }, opts.ctx);
382
+ continue;
383
+ }
384
+ proposalsEmitted += 1;
385
+ appendEvent({
386
+ eventType: "procedural_compiled",
387
+ ref: workflowRef,
388
+ metadata: {
389
+ groupKey: cluster.groupKey,
390
+ memberCount: cluster.members.length,
391
+ outcome: "queued",
392
+ proposalId: proposalResult.id,
393
+ sourceRun,
394
+ },
395
+ }, opts.ctx);
396
+ }
397
+ return finish({ sequencesScanned, clustersFormed, proposalsEmitted, nullsReturned });
398
+ }