akm-cli 0.9.0-beta.6 → 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 (326) hide show
  1. package/CHANGELOG.md +663 -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 -14
  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 +153 -147
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +76 -72
  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 -1083
  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 +1313 -1278
  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 +243 -599
  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 +55 -4
  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 +160 -7
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +32 -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 +510 -2537
  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 +115 -0
  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 +212 -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 +7 -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 -882
  138. package/dist/commands/read/curate.js +419 -103
  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 +29 -41
  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 +389 -58
  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 +21 -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 +142 -1091
  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 +356 -780
  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 +103 -24
  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 +106 -0
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +291 -310
  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 +11 -1
  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 +57 -15
  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 +2718 -2354
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
  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/tar-utils.js +16 -8
  286. package/dist/sources/providers/website.js +9 -5
  287. package/dist/sources/website-ingest.js +187 -29
  288. package/dist/sources/wiki-fetchers/registry.js +53 -0
  289. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  290. package/dist/storage/database.js +45 -10
  291. package/dist/storage/managed-db.js +82 -0
  292. package/dist/storage/repositories/canaries-repository.js +107 -0
  293. package/dist/storage/repositories/consolidation-repository.js +38 -0
  294. package/dist/storage/repositories/embeddings-repository.js +72 -0
  295. package/dist/storage/repositories/events-repository.js +187 -0
  296. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  297. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  298. package/dist/storage/repositories/index-db.js +14 -8
  299. package/dist/storage/repositories/proposals-repository.js +220 -0
  300. package/dist/storage/repositories/recombine-repository.js +213 -0
  301. package/dist/storage/repositories/registry-cache.js +93 -0
  302. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  303. package/dist/storage/repositories/task-history-repository.js +93 -0
  304. package/dist/storage/sqlite-pragmas.js +146 -0
  305. package/dist/tasks/backends/cron.js +1 -1
  306. package/dist/tasks/backends/index.js +9 -0
  307. package/dist/tasks/backends/launchd.js +1 -1
  308. package/dist/tasks/backends/schtasks.js +1 -1
  309. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  310. package/dist/tasks/runner.js +15 -13
  311. package/dist/text-import-hook.mjs +0 -0
  312. package/dist/wiki/wiki.js +52 -11
  313. package/dist/workflows/cli.js +1 -0
  314. package/dist/workflows/db.js +3 -4
  315. package/dist/workflows/runtime/runs.js +43 -118
  316. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  317. package/dist/workflows/validate-summary.js +2 -7
  318. package/docs/README.md +69 -18
  319. package/docs/data-and-telemetry.md +5 -4
  320. package/docs/migration/release-notes/0.7.0.md +1 -1
  321. package/docs/migration/release-notes/0.9.0.md +39 -0
  322. package/package.json +10 -10
  323. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  324. package/dist/commands/db-cli.js +0 -23
  325. package/dist/indexer/db/db-backup.js +0 -376
  326. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -0,0 +1,115 @@
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
+ import { computeSalience } from "./salience.js";
5
+ /** One day in milliseconds. */
6
+ const DAY_MS = 86_400_000;
7
+ /** Default staleness gate: an asset is due when last reflected > this many days ago (or never). */
8
+ export const DEFAULT_DUE_DAYS = 30;
9
+ /** Default bound on how many assets the selector surfaces per run. */
10
+ export const DEFAULT_MAX_PER_RUN = 25;
11
+ /** Lower bound on size used in the cost denominator so tiny files don't divide by ~0. */
12
+ const SIZE_FLOOR_BYTES = 200;
13
+ /** Parse the bare asset type out of a `type:name` ref. Returns "" when unparseable. */
14
+ function refType(ref) {
15
+ const i = ref.indexOf(":");
16
+ return i > 0 ? ref.slice(0, i) : "";
17
+ }
18
+ /**
19
+ * Score and select due assets for proactive maintenance.
20
+ *
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.
24
+ *
25
+ * DUE gate: an asset is eligible only if it was never reflected OR last
26
+ * reflected/distilled more than `dueDays` ago. The same gate doubles as the
27
+ * ROTATION cooldown — a freshly-reflected asset is excluded until it ages back
28
+ * past `dueDays`, so successive runs rotate through the due pool rather than
29
+ * re-selecting the same heads. Non-due assets never enter the selection.
30
+ */
31
+ export function selectProactiveMaintenanceRefs(params) {
32
+ const now = params.now ?? Date.now();
33
+ const dueDays = params.dueDays ?? DEFAULT_DUE_DAYS;
34
+ const maxPerRun = params.maxPerRun ?? DEFAULT_MAX_PER_RUN;
35
+ const scored = [];
36
+ for (const candidate of params.candidates) {
37
+ const ref = candidate.ref;
38
+ const type = refType(ref);
39
+ // Staleness from the most recent of reflect/distill — either one touching
40
+ // the asset resets its maintenance clock.
41
+ const reflectIso = params.lastReflectTs.get(ref);
42
+ const distillIso = params.lastDistillTs.get(ref);
43
+ let lastTouchMs = 0;
44
+ if (reflectIso)
45
+ lastTouchMs = Math.max(lastTouchMs, Date.parse(reflectIso) || 0);
46
+ if (distillIso)
47
+ lastTouchMs = Math.max(lastTouchMs, Date.parse(distillIso) || 0);
48
+ const neverReflected = lastTouchMs === 0;
49
+ const staleDays = neverReflected ? Number.POSITIVE_INFINITY : (now - lastTouchMs) / DAY_MS;
50
+ // DUE / rotation gate.
51
+ const due = neverReflected || staleDays > dueDays;
52
+ // Retrieval frequency (for salience inputs).
53
+ const retrievalFreq = params.retrievalCounts.get(ref) ?? 0;
54
+ const lastUse = params.lastUseMs?.get(ref) ?? 0;
55
+ // Size proxy (cost): kept for salience input — computeSalience applies
56
+ // the log10 denominator internally.
57
+ let sizeBytes = params.sizeBytesOf?.(candidate) ?? 0;
58
+ if (!sizeBytes || sizeBytes < 0)
59
+ sizeBytes = SIZE_FLOOR_BYTES;
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;
62
+ scored.push({
63
+ ref: candidate,
64
+ type,
65
+ staleDays,
66
+ neverReflected,
67
+ retrievalFreq,
68
+ sizeBytes,
69
+ priority,
70
+ due,
71
+ });
72
+ }
73
+ const dueScored = scored.filter((s) => s.due);
74
+ const dueTotal = dueScored.length;
75
+ const neverReflected = dueScored.filter((s) => s.neverReflected).length;
76
+ // Rank due assets by composite priority (desc). Ties broken by staleness
77
+ // (older first) then ref string for deterministic ordering.
78
+ const ranked = dueScored.slice().sort((a, b) => {
79
+ if (b.priority !== a.priority)
80
+ return b.priority - a.priority;
81
+ if (b.staleDays !== a.staleDays)
82
+ return b.staleDays - a.staleDays;
83
+ return a.ref.ref < b.ref.ref ? -1 : a.ref.ref > b.ref.ref ? 1 : 0;
84
+ });
85
+ const selected = ranked.slice(0, Math.max(0, maxPerRun)).map((s) => s.ref);
86
+ return { selected, dueTotal, neverReflected, scored };
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
+ }