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
@@ -6,7 +6,7 @@
6
6
  * (#582). Ports the external akm-health-report skill's collect.py + render.py
7
7
  * to TypeScript so the report is generated in-process (no python, no
8
8
  * shell-out). The template (`src/assets/templates/html/health.html`) is a
9
- * verbatim copy of the skill's report.html; this module computes the 17
9
+ * strict superset of the skill's report.html; this module computes the 17
10
10
  * `%%TOKEN%%` replacements it consumes.
11
11
  *
12
12
  * Determinism: nothing here depends on Date.now()/Math.random(). Runs are
@@ -17,13 +17,45 @@ import fs from "node:fs";
17
17
  import path from "node:path";
18
18
  import { escapeHtml } from "../../output/html-render.js";
19
19
  import { getDirname } from "../../runtime.js";
20
+ import { pkgVersion } from "../../version.js";
21
+ import { TASK_FAIL_RATE_WARN, } from "./types.js";
22
+ /**
23
+ * Distill skip-reasons hidden from the breakdown chart. `no new signal since
24
+ * last proposal` is the steady-state "nothing changed, nothing to do" outcome —
25
+ * it dominates the histogram and drowns out the actionable reasons, so it is
26
+ * intentionally excluded from the chart (the count still lives in the data).
27
+ */
28
+ const DISTILL_REASONS_HIDDEN = new Set(["no new signal since last proposal"]);
20
29
  const ECHARTS_CDN = "https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";
21
30
  const ECHARTS_VENDOR_PATH = path.join(getDirname(import.meta.url), "../../assets/templates/html/vendor/echarts.min.js");
22
31
  // ── Small formatters (ports of render.py helpers) ───────────────────────────
23
32
  const esc = escapeHtml;
33
+ /** Emit a <time> element that the browser's JS will reformat to the viewer's local timezone. */
34
+ function isoTimeTag(iso) {
35
+ const fallback = iso.slice(0, 16).replace("T", " ");
36
+ return `<time data-iso="${esc(iso)}">${esc(fallback)}</time>`;
37
+ }
24
38
  function num(value) {
25
39
  return Math.round(value).toLocaleString("en-US");
26
40
  }
41
+ /** Compact token/count formatter, e.g. 127345 → "127K", 1_500_000 → "1.5M". */
42
+ function compact(value) {
43
+ const v = Math.round(value);
44
+ if (Math.abs(v) >= 1_000_000)
45
+ return `${(v / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
46
+ if (Math.abs(v) >= 1_000)
47
+ return `${(v / 1_000).toFixed(1).replace(/\.0$/, "")}K`;
48
+ return String(v);
49
+ }
50
+ /** Humanize a camelCase / kebab / snake enum into reader-facing text. */
51
+ function humanize(raw) {
52
+ return raw
53
+ .replace(/[-_]/g, " ")
54
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
55
+ .replace(/\s+/g, " ")
56
+ .trim()
57
+ .replace(/^./, (c) => c.toUpperCase());
58
+ }
27
59
  function fmtMs(ms) {
28
60
  return ms ? `${(ms / 60000).toFixed(1)}m` : "—";
29
61
  }
@@ -60,6 +92,7 @@ function reshapeRun(r) {
60
92
  const geMs = ge.durationMs || 0;
61
93
  return {
62
94
  id: r.id,
95
+ taskId: r.taskId ?? "manual",
63
96
  startedAt: r.startedAt,
64
97
  completedAt: r.completedAt,
65
98
  wallTimeMs: wall,
@@ -144,11 +177,14 @@ function deltaPill(deltas, key, lowerIsBetter = false) {
144
177
  const signed = `${raw > 0 ? "+" : ""}${Math.round(raw)}%`;
145
178
  return `<span class="trend-pill ${cls}">${arrow} ${signed}</span>`;
146
179
  }
147
- // ── Advisory / watch-item cards ──────────────────────────────────────────────
148
- function advisoryCard(cls, icon, title, descHtml) {
149
- return (`<div class="advisory ${cls}"><div class="advisory-icon">${icon}</div>` +
150
- `<div class="advisory-body"><div class="title">${title}</div>` +
151
- `<div class="desc">${descHtml}</div></div></div>`);
180
+ const PRIO_RANK = { P1: 0, P2: 1, P3: 2 };
181
+ function actionItemCard(item) {
182
+ const icon = item.cls === "fail" ? "🔴" : item.prio === "P3" ? "🟡" : "⚠️";
183
+ const remedy = item.remedy ? `<div class="remedy">Fix: <code>${esc(item.remedy)}</code></div>` : "";
184
+ return (`<div class="advisory ${item.cls}"><div class="advisory-icon">${icon}</div>` +
185
+ `<div class="advisory-body">` +
186
+ `<div class="title"><span class="prio ${item.prio.toLowerCase()}">${item.prio}</span>${item.title}</div>` +
187
+ `<div class="desc">${item.descHtml}</div>${remedy}</div></div>`);
152
188
  }
153
189
  function passCard(title, desc) {
154
190
  return ('<div class="advisory" style="border-left:3px solid var(--green);">' +
@@ -168,6 +204,43 @@ function readSemSearch(advisories) {
168
204
  return { blocked, detail: `${num(entries)} entries, ${num(embeddings)} embeddings` };
169
205
  }
170
206
  // ── ECharts delivery ─────────────────────────────────────────────────────────
207
+ /** Parse an akm window string (`24h`, `7d`, `30m`, `2w`) to milliseconds; 0 if unparseable. */
208
+ function windowToMs(window) {
209
+ const m = /^(\d+)\s*([mhdw])$/i.exec(window.trim());
210
+ if (!m)
211
+ return 0;
212
+ const n = Number(m[1]);
213
+ const mult = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }[m[2].toLowerCase()] ?? 0;
214
+ return n * mult;
215
+ }
216
+ /**
217
+ * Build the time-slice `<option>`s for the report's filter bar, DERIVED from the
218
+ * actual report window so the choices always make sense (the old hard-coded
219
+ * 1d–21d list was useless on a 24h or 7d report). "All" is the default; the
220
+ * sub-window options carry their cutoff in milliseconds (consumed by
221
+ * filteredRuns), largest first, only those strictly shorter than the window.
222
+ */
223
+ function buildSliceOptions(window) {
224
+ const windowMs = windowToMs(window);
225
+ const HOUR = 3_600_000;
226
+ const DAY = 86_400_000;
227
+ const candidates = [
228
+ [6 * HOUR, "6h"],
229
+ [12 * HOUR, "12h"],
230
+ [DAY, "1d"],
231
+ [3 * DAY, "3d"],
232
+ [7 * DAY, "7d"],
233
+ [14 * DAY, "14d"],
234
+ ];
235
+ const subs = candidates
236
+ .filter(([ms]) => windowMs > 0 && ms < windowMs)
237
+ .sort((a, b) => b[0] - a[0])
238
+ .slice(0, 4);
239
+ const opts = [`<option value="all" selected>All (${esc(window)})</option>`];
240
+ for (const [ms, label] of subs)
241
+ opts.push(`<option value="${ms}">Last ${label}</option>`);
242
+ return opts.join("\n ");
243
+ }
171
244
  function buildEchartsTag(opts) {
172
245
  const mode = opts.echarts ?? (process.env.AKM_ECHARTS === "cdn" ? "cdn" : "inline");
173
246
  if (mode === "cdn")
@@ -182,7 +255,8 @@ function buildEchartsTag(opts) {
182
255
  * Compute all 17 `%%TOKEN%%` replacements for the health HTML template.
183
256
  * There is deliberately NO standalone `%%OVERALL_STATUS%%` token — the
184
257
  * overall status is embedded in the pre-rendered badge / exec-summary
185
- * fragments, matching the skill template.
258
+ * fragments, matching the skill template. Advisories and "what to watch" are
259
+ * merged + de-duplicated into a single prioritized `%%ACTION_ITEMS_HTML%%`.
186
260
  */
187
261
  export function buildHealthHtmlReplacements(result, opts) {
188
262
  const deltas = opts.deltas ?? {};
@@ -196,6 +270,32 @@ export function buildHealthHtmlReplacements(result, opts) {
196
270
  const mi = improve.memoryInference;
197
271
  const ge = improve.graphExtraction;
198
272
  const wallTime = improve.wallTime;
273
+ // WS-5: perf telemetry, coverage, and degradation metrics.
274
+ const perf = improve.perfTelemetry ?? {
275
+ dedupPoolSize: 0,
276
+ llmPoolSize: 0,
277
+ judgedCacheSkipped: 0,
278
+ embedMs: 0,
279
+ embedCacheHits: 0,
280
+ embedCacheMisses: 0,
281
+ overBudgetRuns: 0,
282
+ runsWithTelemetry: 0,
283
+ };
284
+ const coverage = improve.coverage;
285
+ const degradation = improve.degradation;
286
+ const minting = improve.enrichmentMinting;
287
+ // #576: real per-stage LLM token/time accounting (replaces the GPU-time
288
+ // proxy). Optional-guarded so reports built from older health JSON without
289
+ // the aggregate still render.
290
+ const llm = result.metrics.llmUsage ?? {
291
+ calls: 0,
292
+ totalDurationMs: 0,
293
+ promptTokens: 0,
294
+ completionTokens: 0,
295
+ totalTokens: 0,
296
+ reasoningTokens: 0,
297
+ byStage: {},
298
+ };
199
299
  const totalRuns = runs.length;
200
300
  const failedRuns = runs.filter((r) => !r.ok).length;
201
301
  const invoked = improve.invoked || totalRuns;
@@ -212,11 +312,23 @@ export function buildHealthHtmlReplacements(result, opts) {
212
312
  // ── Meta (collect.py steps 10-11) ──────────────────────────────────────────
213
313
  const sinceIso = result.since;
214
314
  const reportDate = sinceIso.slice(0, 10);
215
- const sinceHuman = sinceIso ? `${sinceIso.slice(0, 16).replace("T", " ")} UTC → now` : `last ${opts.window}`;
315
+ const sinceHuman = sinceIso ? `${isoTimeTag(sinceIso)} → now` : `last ${esc(opts.window)}`;
216
316
  const reportTitle = reportDate ? `AKM Health Report — ${reportDate}` : "AKM Health Report";
217
317
  const lastRun = runs[runs.length - 1];
218
318
  const generatedAt = lastRun ? lastRun.completedAt || lastRun.startedAt || sinceIso : sinceIso;
219
319
  const latest = [...runs].reverse().find((r) => r.ok) ?? lastRun;
320
+ // Freshness: surface the newest run's timestamp + the generated-at anchor in
321
+ // the exec card. Staleness is computed deterministically (no Date.now()) from
322
+ // the gap between the window start (`since`) and the newest run we have: if no
323
+ // run landed in the window, or the newest run sits in the first 25% of a
324
+ // window wider than the 6h threshold (i.e. a long idle tail), we flag stale.
325
+ const STALE_MS = 6 * 60 * 60 * 1000;
326
+ const latestRunMs = lastRun ? Date.parse(lastRun.completedAt || lastRun.startedAt) : NaN;
327
+ const sinceMs = Date.parse(sinceIso);
328
+ const generatedMs = Date.parse(generatedAt);
329
+ const idleTailMs = Number.isFinite(latestRunMs) && Number.isFinite(generatedMs) ? Math.max(0, generatedMs - latestRunMs) : 0;
330
+ const isStale = totalRuns === 0 || (Number.isFinite(latestRunMs) && Number.isFinite(sinceMs) && idleTailMs > STALE_MS);
331
+ const latestRunHuman = lastRun ? isoTimeTag(lastRun.completedAt || lastRun.startedAt) : "—";
220
332
  // ── Status badges ──────────────────────────────────────────────────────────
221
333
  const badgeByStatus = {
222
334
  pass: { badge: "badge-pass", dot: "dot-pass", label: "PASS" },
@@ -225,7 +337,7 @@ export function buildHealthHtmlReplacements(result, opts) {
225
337
  };
226
338
  const badge = badgeByStatus[result.status];
227
339
  const statusBadge = `<span class="badge-pill ${badge.badge}"><span class="dot ${badge.dot}"></span>${badge.label}</span>`;
228
- const failOk = result.metrics.taskFailRate < 0.05;
340
+ const failOk = result.metrics.taskFailRate < TASK_FAIL_RATE_WARN;
229
341
  const failBadge = `<span class="badge-pill ${failOk ? "badge-pass" : "badge-warn"}">` +
230
342
  `<span class="dot ${failOk ? "dot-pass" : "dot-warn"}"></span>${taskFailRate} Fail Rate</span>`;
231
343
  // ── Executive summary ──────────────────────────────────────────────────────
@@ -238,7 +350,7 @@ export function buildHealthHtmlReplacements(result, opts) {
238
350
  li("MI yield rate", miYieldRate),
239
351
  li("MI written", num(miWritten)),
240
352
  li("Consolidation promoted", num(cons.promoted)),
241
- li("Consolidation judgedNoAction", num(cons.judgedNoAction)),
353
+ li("Consolidation judged: no action", `<abbr title="Candidates the consolidator reviewed but intentionally left unchanged (the 'judgedNoAction' field).">${num(cons.judgedNoAction)}</abbr>`),
242
354
  li("Chunk failure", chunkFail),
243
355
  li("Median wall time", fmtMs(wallTime.medianMs)),
244
356
  li("P95 wall time", fmtMs(wallTime.p95Ms)),
@@ -259,12 +371,12 @@ export function buildHealthHtmlReplacements(result, opts) {
259
371
  const snapRows = latest
260
372
  ? [
261
373
  li("Run id", `<code>${esc(latest.id.slice(0, 28))}</code>`),
262
- li("Completed", `${esc((latest.completedAt || latest.startedAt).slice(0, 16).replace("T", " "))} UTC`),
374
+ li("Completed", isoTimeTag(latest.completedAt || latest.startedAt)),
263
375
  li("Status", latest.ok ? "✅ ok" : "❌ failed"),
264
376
  li("Wall time", fmtMs(latest.wallTimeMs)),
265
377
  li("Reflect ok/fail", `${latest.reflectOk} / ${latest.reflectFailed}`),
266
378
  li("Promoted", String(latest.promoted)),
267
- li("judgedNoAction", String(latest.judgedNoAction)),
379
+ li("Judged: no action", `<abbr title="Candidates reviewed but intentionally left unchanged on this run.">${latest.judgedNoAction}</abbr>`),
268
380
  li("MI written", String(latest.miWritten)),
269
381
  li("Graph entities/relations", `${latest.geEntities} / ${latest.geRelations}`),
270
382
  ].join("")
@@ -273,16 +385,50 @@ export function buildHealthHtmlReplacements(result, opts) {
273
385
  li("Report window", esc(opts.window)),
274
386
  li("Compare window", esc(opts.compare)),
275
387
  li("Runs", `${num(totalRuns)} (${failedRuns} failed)`),
276
- li("Stash derived", num(improve.memorySummary.derived)),
277
- li("Stash eligible", num(improve.memorySummary.eligible)),
388
+ li("Stash derived", `<abbr title="Whole-stash recount of derived assets at report time — not a per-run sum.">${num(improve.memorySummary.derived)}</abbr>`),
389
+ li("Stash eligible", `<abbr title="Whole-stash recount of eligible assets at report time — not a per-run sum.">${num(improve.memorySummary.eligible)}</abbr>`),
278
390
  li("Pending proposals", String(proposals.length)),
279
391
  li("Semantic search", sem.blocked ? "BLOCKED" : "OK"),
280
392
  ].join("");
281
393
  const overallEmoji = trend.overall === "improving" ? "📈" : trend.overall === "degrading" ? "📉" : "↔️";
394
+ // ── Synthesized verdict (one sentence + 2-3 drivers) ───────────────────────
395
+ // The verdict WORD is the authoritative health status (hard checks). Concerns
396
+ // (failed improve runs, blocked search, pending proposals) are advisory and do
397
+ // NOT gate that word, so we must not pair a green PASS with red "drivers" as
398
+ // if they were failures. When the status is PASS we frame concerns as "watch:"
399
+ // alongside the trend; only a degraded status (WARN/FAIL) leads with them.
400
+ const concerns = [];
401
+ if (failedRuns > 0)
402
+ concerns.push(`${failedRuns} failed run${failedRuns === 1 ? "" : "s"}`);
403
+ if (sem.blocked)
404
+ concerns.push("semantic search blocked");
405
+ if (proposals.length > 0)
406
+ concerns.push(`${proposals.length} pending proposal${proposals.length === 1 ? "" : "s"}`);
407
+ const trendClause = trend.overall === "improving"
408
+ ? "throughput and latency improving"
409
+ : trend.overall === "degrading"
410
+ ? "throughput or latency degrading"
411
+ : "throughput and latency steady";
412
+ const verdictWord = badge.label;
413
+ let verdictRest;
414
+ if (result.status === "pass") {
415
+ const watch = concerns.length > 0 ? `; watch: ${concerns.slice(0, 3).join(", ")}` : "";
416
+ verdictRest = `healthy, ${trendClause}${watch}`;
417
+ }
418
+ else {
419
+ const lead = concerns.length > 0 ? concerns.slice(0, 3).join("; ") : trendClause;
420
+ verdictRest = trend.overall === "degrading" && concerns.length > 0 ? `${lead}; ${trendClause}` : lead;
421
+ }
422
+ const verdictSentence = `${verdictWord} — ${esc(verdictRest)}.`;
423
+ const verdictHtml = `<div class="verdict ${result.status}"><b>Verdict:</b> ${verdictSentence}</div>`;
424
+ // ── Freshness line ─────────────────────────────────────────────────────────
425
+ const freshnessHtml = `<div class="freshness${isStale ? " stale" : ""}">${isStale ? "⚠️ Stale: " : ""}Latest run ${latestRunHuman} &nbsp;·&nbsp; generated ${isoTimeTag(generatedAt)}${isStale ? " — no recent activity (newest run older than the 6h freshness threshold)." : "."}</div>`;
282
426
  const execSummary = `
283
427
  <h2>${overallEmoji} Executive Summary
284
428
  <span class="badge-pill ${badge.badge}" style="font-size:11px;">
285
429
  <span class="dot ${badge.dot}"></span>${badge.label}</span></h2>
430
+ ${verdictHtml}
431
+ ${freshnessHtml}
286
432
  <div class="exec-grid">
287
433
  <div>
288
434
  <h4>Quick Numbers</h4>
@@ -306,143 +452,363 @@ export function buildHealthHtmlReplacements(result, opts) {
306
452
  <div class="overall">Overall trend: <b>${esc(trend.overall)}</b> ${overallEmoji}
307
453
  &nbsp;·&nbsp; based on decision quality, output volume, failures, and latency vs the prior window.</div>`.trim();
308
454
  // ── KPI cards ──────────────────────────────────────────────────────────────
455
+ // Color is a health SIGNAL, not decoration: green/yellow/red where a card has
456
+ // a meaningful threshold; "neutral" for purely-informational counts. Cards are
457
+ // ordered by operator priority (failures first, informational counts last).
309
458
  const semValue = sem.blocked ? "BLOCKED" : "OK";
310
- const semColor = sem.blocked ? "yellow" : "green";
459
+ const semColor = sem.blocked ? "red" : "green";
311
460
  const semStyle = sem.blocked ? "font-size:18px;" : "";
461
+ const completionPct = invoked ? (100 * completed) / invoked : 100;
462
+ const completionColor = completionPct >= 99 ? "green" : completionPct >= 90 ? "yellow" : "red";
463
+ const llmTokensCompact = compact(llm.totalTokens);
312
464
  const kpiCard = (color, label, value, sub, valueStyle = "") => `<div class="kpi-card ${color}">
313
465
  <div class="label">${label}</div>
314
466
  <div class="value"${valueStyle ? ` style="${valueStyle}"` : ""}>${value}</div>
315
467
  <div class="sub">${sub}</div>
316
468
  </div>`;
317
469
  const kpiCards = [
318
- kpiCard(failedRuns === 0 ? "green" : "yellow", "Completion Rate", completionRate, `${num(completed)} / ${num(invoked)} invoked`),
319
470
  kpiCard(failedRuns === 0 ? "green" : "red", "Failed Runs", String(failedRuns), `of ${num(totalRuns)} runs · ${taskFailRate} task fail`),
471
+ kpiCard(completionColor, "Completion Rate", completionRate, `${num(completed)} / ${num(invoked)} invoked`),
472
+ kpiCard("neutral", "Median Duration", `${medianDurMin}m`, `p95 = ${p95DurMin}m`),
320
473
  kpiCard("blue", "Total Promoted", num(cons.promoted), `avg ${avgPromoted} / run`),
321
474
  kpiCard("blue", "MI Written", num(miWritten), `${miYieldRate} yield rate`),
322
475
  kpiCard("purple", "Graph Entities", num(ge.entities), `+${num(ge.relations)} relations`),
323
- kpiCard("green", "Stash Derived", num(improve.memorySummary.derived), `of ${num(improve.memorySummary.eligible)} eligible`),
324
- kpiCard("yellow", "Median Duration", `${medianDurMin}m`, `p95 = ${p95DurMin}m`),
476
+ kpiCard("neutral", "Stash Derived", num(improve.memorySummary.derived), `of ${num(improve.memorySummary.eligible)} eligible (whole-stash)`),
477
+ // #576: real LLM work — duration leads, tokens compact, not a GPU proxy.
478
+ kpiCard(llm.calls > 0 ? "purple" : "neutral", "🧠 LLM Work", `${llmTokensCompact} tok`, `${fmtMs(llm.totalDurationMs)} · ${num(llm.calls)} calls · ${compact(llm.reasoningTokens)} reasoning`),
325
479
  kpiCard(semColor, "Semantic Search", semValue, esc(sem.detail), semStyle),
326
- kpiCard("yellow", "Pending Proposals", String(proposals.length), `from ${esc(opts.window)} batch`),
480
+ kpiCard(proposals.length > 0 ? "yellow" : "neutral", "Pending Proposals", String(proposals.length), `from ${esc(opts.window)} batch`),
327
481
  ].join("\n");
328
482
  // ── Chart payload ──────────────────────────────────────────────────────────
329
- const distillReasons = [...new Set(runs.flatMap((r) => Object.keys(r.distillByReason)))].sort();
483
+ const distillReasons = [...new Set(runs.flatMap((r) => Object.keys(r.distillByReason)))]
484
+ .filter((reason) => !DISTILL_REASONS_HIDDEN.has(reason))
485
+ .sort();
330
486
  const runsJsConst = `const RUNS = ${JSON.stringify(runs)};`;
331
487
  // ── Summary table rows ─────────────────────────────────────────────────────
488
+ // Optional 4th element = a glossary tooltip rendered as <abbr title>.
332
489
  const summaryRows = [
333
490
  ["Task fail rate", taskFailRate, "flat"],
334
491
  ["Agent fail rate", agentFailRate, "flat"],
335
492
  ["Improve completion", `${num(completed)} / ${num(invoked)}`, "flat"],
336
- ["MI yield rate", miYieldRate, trend.decisionQuality],
337
- ["MI written", num(miWritten), trend.outputVolume],
493
+ [
494
+ "MI yield rate",
495
+ miYieldRate,
496
+ trend.decisionQuality,
497
+ "Memory-inference yield: share of considered candidates that produced a written fact.",
498
+ ],
499
+ ["MI written", num(miWritten), trend.outputVolume, "Memory-inference: facts written this window."],
338
500
  ["Consolidation promoted", num(cons.promoted), trend.outputVolume],
339
501
  ["Consolidation merged", num(cons.merged), "flat"],
340
502
  ["Consolidation deleted", num(cons.deleted), "flat"],
341
503
  ["Consolidation contradicted", num(cons.contradicted), "flat"],
342
- ["Consolidation judgedNoAction", num(cons.judgedNoAction), "flat"],
504
+ [
505
+ "Consolidation judged: no action",
506
+ num(cons.judgedNoAction),
507
+ "flat",
508
+ "Candidates reviewed but intentionally left unchanged (the 'judgedNoAction' field).",
509
+ ],
343
510
  ["Chunk failure", chunkFail, "flat"],
344
511
  ["Graph entities", num(ge.entities), "up"],
345
512
  ["Graph relations", num(ge.relations), "up"],
346
- ["Stash derived", num(improve.memorySummary.derived), "up"],
513
+ [
514
+ "Stash derived",
515
+ num(improve.memorySummary.derived),
516
+ "up",
517
+ "Whole-stash recount of derived assets at report time — not a per-run sum.",
518
+ ],
347
519
  ["Median wall time", fmtMs(wallTime.medianMs), trend.latency],
348
520
  ["P95 wall time", fmtMs(wallTime.p95Ms), trend.latency],
521
+ // #576: real LLM accounting (replaces the GPU-time proxy).
522
+ ["LLM calls", num(llm.calls), "flat"],
523
+ ["LLM total tokens", num(llm.totalTokens), "flat"],
524
+ ["LLM prompt tokens", num(llm.promptTokens), "flat"],
525
+ ["LLM completion tokens", num(llm.completionTokens), "flat"],
526
+ [
527
+ "LLM reasoning tokens",
528
+ num(llm.reasoningTokens),
529
+ "flat",
530
+ "Tokens spent on model reasoning/thinking, billed separately from prompt and completion.",
531
+ ],
532
+ ["LLM wall time", fmtMs(llm.totalDurationMs), trend.latency],
349
533
  ];
534
+ // #612 — auto-accept gate calibration. Only surface when the gate actually
535
+ // acted on proposals in the window (samples > 0); a default ungated install
536
+ // reports an empty summary and we omit the rows to keep the table parity-clean.
537
+ const calibration = improve.calibration;
538
+ if (calibration && calibration.samples > 0) {
539
+ summaryRows.push([
540
+ "Calibration samples",
541
+ num(calibration.samples),
542
+ "flat",
543
+ "Auto-accept gate decisions (auto-accepted + auto-rejected) the calibration join measured this window.",
544
+ ], [
545
+ "Calibration accept rate",
546
+ String(calibration.overallAcceptRate),
547
+ "flat",
548
+ "Realized accept rate of acted-on gate decisions (auto-accepted / total acted-on).",
549
+ ], [
550
+ "Calibration gap",
551
+ String(calibration.calibrationGap),
552
+ "flat",
553
+ "Mean predicted confidence minus realized accept rate. Positive = the gate is over-confident.",
554
+ ]);
555
+ }
556
+ // WS-5: denominator-fixed coverage rows (only when we have real data).
557
+ if (coverage && !Number.isNaN(coverage.rate)) {
558
+ summaryRows.push([
559
+ "Coverage rate",
560
+ pct(coverage.rate, 1),
561
+ "flat",
562
+ "Distinct accepted refs / total stash assets (denominator-fixed). Shows what fraction of the corpus has been touched.",
563
+ ], [
564
+ "Eligible fraction",
565
+ pct(coverage.eligibleFraction, 1),
566
+ "flat",
567
+ "Eligible assets / total stash assets. Fraction the improve pipeline actively considers.",
568
+ ], [
569
+ "Coverage accepted",
570
+ num(coverage.acceptedProposals),
571
+ "flat",
572
+ "Total accepted proposals in the window (raw volume — includes repeated rewrites of the same asset).",
573
+ ], [
574
+ "Churn ratio",
575
+ Number.isFinite(coverage.churnRatio) ? num(coverage.churnRatio) : "—",
576
+ Number.isFinite(coverage.churnRatio) && coverage.churnRatio > 1.5 ? "down" : "flat",
577
+ "Accepted proposals / distinct refs touched. >1.5 = the loop is repeatedly rewriting the same assets (churn, not coverage).",
578
+ ]);
579
+ }
580
+ // Enrichment-vs-minting policy rollup (reporting-only).
581
+ if (minting && Number.isFinite(minting.share)) {
582
+ summaryRows.push([
583
+ "Enrichment-lane minted share",
584
+ pct(minting.share, 1),
585
+ minting.share > 0.05 ? "down" : "flat",
586
+ `New assets minted by enrichment lanes / their accepted total (${minting.minted} minted vs ${minting.updated} updated). Enrichment lanes are ratified to edit existing assets only; WARN >5%, FAIL >15%.`,
587
+ ]);
588
+ }
589
+ // WS-5: perf telemetry rows (only when at least one run reported telemetry).
590
+ if (perf.runsWithTelemetry > 0) {
591
+ const embedCacheTotal = perf.embedCacheHits + perf.embedCacheMisses;
592
+ const embedCacheHitRate = embedCacheTotal > 0 ? pct(perf.embedCacheHits / embedCacheTotal, 1) : "—";
593
+ summaryRows.push([
594
+ "Embed cache hit rate",
595
+ embedCacheHitRate,
596
+ "flat",
597
+ "Fraction of embedding lookups served from cache (>95% is healthy). Aggregated across WS-5 runs.",
598
+ ], [
599
+ "Embed wall time",
600
+ fmtMs(perf.embedMs),
601
+ "flat",
602
+ "Cumulative embedding wall-clock time across consolidation runs in the window.",
603
+ ], [
604
+ "Judged-cache skipped",
605
+ num(perf.judgedCacheSkipped),
606
+ "flat",
607
+ "Candidates skipped by the judged-cache (not sent to LLM). Higher = more efficient reuse of prior judgments.",
608
+ ], [
609
+ "Dedup pool size",
610
+ num(perf.dedupPoolSize),
611
+ "flat",
612
+ "Average memory pool size after deduplication (before judged-cache narrowing). WS-5 perf telemetry.",
613
+ ], [
614
+ "Over-budget consolidation runs",
615
+ String(perf.overBudgetRuns),
616
+ perf.overBudgetRuns > 0 ? "down" : "flat",
617
+ "Runs where consolidation alone exceeded the total run budget (estimatedBudgetFractionUsed > 1.0).",
618
+ ]);
619
+ }
620
+ // WS-5: degradation metrics rows.
621
+ if (degradation) {
622
+ summaryRows.push([
623
+ "Corpus diversity (Gini)",
624
+ num(degradation.corpusCentroidDistance),
625
+ degradation.entrenchmentFlagged || degradation.salienceUniformityFlagged ? "down" : "flat",
626
+ "Gini coefficient of retrieval_salience for top-100 ranked assets. Two-tailed: >0.35 = entrenchment risk; <0.08 = collapsed toward uniform (ranking no longer discriminates).",
627
+ ], [
628
+ "Merge fidelity contradiction rate",
629
+ pct(degradation.mergeFidelityContradictionRate, 1),
630
+ "flat",
631
+ "Fraction of consolidated proposals that involved a contradiction, from consolidation result envelopes.",
632
+ ]);
633
+ }
350
634
  const summaryRowsHtml = summaryRows
351
- .map(([label, value, t]) => ` <tr><td>${esc(label)}</td><td>${esc(value)}</td>` +
352
- `<td class="trend ${trendClass(t)}">${trendLabel(t)}</td></tr>`)
635
+ .map(([label, value, t, tip]) => {
636
+ const labelHtml = tip ? `<abbr title="${esc(tip)}">${esc(label)}</abbr>` : esc(label);
637
+ return (` <tr><td>${labelHtml}</td><td>${esc(value)}</td>` +
638
+ `<td class="trend ${trendClass(t)}">${trendLabel(t)}</td></tr>`);
639
+ })
353
640
  .join("\n");
354
- // ── Advisory cards ─────────────────────────────────────────────────────────
355
- const advisoryParts = [];
641
+ // ── Action Items (merged + de-duplicated advisories + what-to-watch) ────────
642
+ // Advisories and the old "what to watch" cards were built from the same data
643
+ // (result.advisories, sem-blocked, proposals, tail-latency, failed-runs). We
644
+ // collapse them into ONE prioritized, de-duplicated list (P1/P2/P3 + a
645
+ // remediation command per item), keyed so each concern appears exactly once.
646
+ const items = [];
647
+ const seen = new Set();
648
+ const pushItem = (item) => {
649
+ if (seen.has(item.key))
650
+ return;
651
+ seen.add(item.key);
652
+ items.push(item);
653
+ };
654
+ // Hard advisories from the health check engine (own remediation in message).
356
655
  for (const a of result.advisories) {
357
656
  if (a.status !== "warn" && a.status !== "fail")
358
657
  continue;
359
- advisoryParts.push(advisoryCard(a.status === "fail" ? "fail" : "warn", a.status === "fail" ? "🔴" : "⚠️", esc(a.name), esc(a.message)));
658
+ pushItem({
659
+ key: `advisory:${a.name}`,
660
+ prio: a.status === "fail" ? "P1" : "P2",
661
+ cls: a.status === "fail" ? "fail" : "warn",
662
+ title: esc(humanize(a.name)),
663
+ descHtml: esc(a.message),
664
+ });
665
+ }
666
+ // Failed runs in window.
667
+ if (failedRuns > 0) {
668
+ pushItem({
669
+ key: "failed-runs",
670
+ prio: "P1",
671
+ cls: "fail",
672
+ title: `${failedRuns} failed run${failedRuns === 1 ? "" : "s"} in window`,
673
+ descHtml: `Task fail rate ${esc(taskFailRate)}. Inspect failed runs (ok=false) for early-exit or harness errors.`,
674
+ remedy: `akm health --since=${opts.window} --group-by run`,
675
+ });
360
676
  }
677
+ // Semantic search blocked.
361
678
  if (sem.blocked) {
362
- advisoryParts.push(advisoryCard("warn", "⚠️", "Semantic search blocked", `Embedding provider unreachable. ${esc(sem.detail)}. Curate falls back to keyword search — relevance scoring degraded.`));
679
+ pushItem({
680
+ key: "semantic-search-blocked",
681
+ prio: "P2",
682
+ cls: "warn",
683
+ title: "Semantic search blocked",
684
+ descHtml: `Embedding provider unreachable. ${esc(sem.detail)}. Curate falls back to keyword search — relevance scoring degraded.`,
685
+ remedy: "akm config show",
686
+ });
363
687
  }
688
+ // Pending proposals to drain.
364
689
  if (proposals.length > 0) {
365
- advisoryParts.push(advisoryCard("warn", "⚠️", `${proposals.length} proposals pending (drain needed)`, "Run <code>akm proposal list</code> to review and drain."));
690
+ const bySource = new Map();
691
+ for (const p of proposals)
692
+ bySource.set(p.source, (bySource.get(p.source) ?? 0) + 1);
693
+ const srcSummary = [...bySource.entries()]
694
+ .sort(([a], [b]) => a.localeCompare(b))
695
+ .map(([source, count]) => `${count} via ${esc(source)}`)
696
+ .join(", ");
697
+ pushItem({
698
+ key: "drain-proposals",
699
+ prio: "P2",
700
+ cls: "warn",
701
+ title: `Drain ${proposals.length} pending proposal${proposals.length === 1 ? "" : "s"}`,
702
+ descHtml: `Proposals generated this batch (${srcSummary}). Review before the queue grows further.`,
703
+ remedy: "akm proposal list",
704
+ });
705
+ }
706
+ // High tail latency.
707
+ if (wallTime.p95Ms && wallTime.medianMs && wallTime.p95Ms / wallTime.medianMs > 2.5) {
708
+ pushItem({
709
+ key: "tail-latency",
710
+ prio: "P3",
711
+ cls: "warn",
712
+ title: `High tail latency: p95=${fmtMs(wallTime.p95Ms)}, median=${fmtMs(wallTime.medianMs)}`,
713
+ descHtml: "P95 is well above median. Consolidation/LLM phase dominates wall time on slow runs. " +
714
+ "Check for slow chunks or LLM rate limiting.",
715
+ });
716
+ }
717
+ // Stale freshness.
718
+ if (isStale && totalRuns > 0) {
719
+ pushItem({
720
+ key: "stale",
721
+ prio: "P3",
722
+ cls: "warn",
723
+ title: "No recent improve runs",
724
+ descHtml: `Newest run is ${esc(latestRunHuman)} — older than the 6h freshness threshold. Check the improve scheduler/cron.`,
725
+ });
726
+ }
727
+ // WS-5: corpus entrenchment flag.
728
+ if (degradation?.entrenchmentFlagged) {
729
+ pushItem({
730
+ key: "corpus-entrenchment",
731
+ prio: "P2",
732
+ cls: "warn",
733
+ title: "Corpus entrenchment risk: retrieval_salience Gini > 0.35",
734
+ descHtml: "A small set of assets dominates retrieval — retrieval diversity is low. " +
735
+ "Review top-ranked assets for stale or over-represented content. " +
736
+ `Corpus diversity proxy: ${esc(String(degradation.corpusCentroidDistance))}.`,
737
+ remedy: "akm health --format json | jq '.improve.degradation'",
738
+ });
739
+ }
740
+ // Low-tail companion: salience distribution collapsed toward uniform.
741
+ if (degradation?.salienceUniformityFlagged) {
742
+ pushItem({
743
+ key: "salience-uniformity-collapse",
744
+ prio: "P2",
745
+ cls: "warn",
746
+ title: "Salience distribution collapsed: retrieval_salience Gini < 0.08",
747
+ descHtml: "The top-100 salience scores are near-uniform (uniform baseline ≈ 0.1) — " +
748
+ "ranking currently carries little to no discrimination between assets. " +
749
+ `Corpus diversity proxy: ${esc(String(degradation.corpusCentroidDistance))}.`,
750
+ remedy: "akm health --format json | jq '.improve.degradation'",
751
+ });
752
+ }
753
+ // WS-5: over-budget consolidation advisory.
754
+ if (perf.overBudgetRuns > 0) {
755
+ pushItem({
756
+ key: "over-budget-consolidation",
757
+ prio: "P2",
758
+ cls: "warn",
759
+ title: `${perf.overBudgetRuns} consolidation run${perf.overBudgetRuns === 1 ? "" : "s"} exceeded budget`,
760
+ descHtml: "Consolidation phase wall time exceeded the total run budget on these runs. " +
761
+ "Consider increasing the timeout or reducing the consolidation pool via profile config.",
762
+ remedy: "akm config show",
763
+ });
366
764
  }
765
+ // Session-log notes (informational, lowest priority).
367
766
  if (result.sessionLogAdvisories.length > 0) {
368
767
  const patterns = result.sessionLogAdvisories
369
768
  .slice(0, 6)
370
769
  .map((p) => `<li>${esc(p.topic)}</li>`)
371
770
  .join("");
372
- advisoryParts.push('<div class="advisory" style="border-left:3px solid var(--accent);">' +
373
- '<div class="advisory-icon">ℹ️</div><div class="advisory-body">' +
374
- `<div class="title">${result.sessionLogAdvisories.length} session-log note(s) (informational)</div>` +
375
- `<div class="desc"><ul style="margin:4px 0 0 16px;padding:0;">${patterns}</ul></div></div></div>`);
771
+ pushItem({
772
+ key: "session-log-notes",
773
+ prio: "P3",
774
+ cls: "warn",
775
+ title: `${result.sessionLogAdvisories.length} session-log note(s) (informational)`,
776
+ descHtml: `<ul style="margin:4px 0 0 16px;padding:0;">${patterns}</ul>`,
777
+ });
376
778
  }
377
- const advisoryCardsHtml = advisoryParts.length > 0
378
- ? advisoryParts.join("\n")
379
- : passCard("No active advisories", "All checks passed for this window.");
779
+ items.sort((a, b) => PRIO_RANK[a.prio] - PRIO_RANK[b.prio]);
780
+ const actionItemsHtml = items.length > 0
781
+ ? items.map(actionItemCard).join("\n")
782
+ : passCard("No action items", "All checks passed and nothing needs attention for this window.");
380
783
  // ── Proposal rows ──────────────────────────────────────────────────────────
381
784
  const proposalRowsHtml = proposals.length > 0
382
785
  ? proposals
383
786
  .map((p, i) => {
384
787
  const tagCls = p.source === "extract" ? "tag-extract" : "tag-consolidate";
385
- const ts = p.createdAt.slice(0, 16).replace("T", " ");
386
788
  return (`<tr><td>${i + 1}</td><td><code>${esc(p.ref)}</code></td>` +
387
789
  `<td><span class="tag ${tagCls}">${esc(p.source)}</span></td>` +
388
- `<td>${esc(ts)}</td></tr>`);
790
+ `<td>${isoTimeTag(p.createdAt)}</td></tr>`);
389
791
  })
390
792
  .join("\n")
391
793
  : '<tr><td colspan="4" style="text-align:center;color:var(--muted);">No pending proposals</td></tr>';
392
- // ── What to watch ──────────────────────────────────────────────────────────
393
- const watchParts = [];
394
- for (const a of result.advisories) {
395
- if (a.status !== "warn" && a.status !== "fail")
396
- continue;
397
- const prio = a.status === "fail" ? "P1" : "P2";
398
- watchParts.push(advisoryCard(a.status === "fail" ? "fail" : "warn", a.status === "fail" ? "🔴" : "🟡", `${esc(a.name)} (${prio})`, esc(a.message)));
399
- }
400
- if (sem.blocked) {
401
- watchParts.push(advisoryCard("warn", "🟡", "Embedding server unreachable (P2)", "Curate quality and semantic ranking are degraded. Check the embedding endpoint configured in config.json."));
402
- }
403
- if (proposals.length > 0) {
404
- const bySource = new Map();
405
- for (const p of proposals)
406
- bySource.set(p.source, (bySource.get(p.source) ?? 0) + 1);
407
- const srcSummary = [...bySource.entries()]
408
- .sort(([a], [b]) => a.localeCompare(b))
409
- .map(([source, count]) => `${count} via ${esc(source)}`)
410
- .join(", ");
411
- watchParts.push(advisoryCard("warn", "🟡", `Drain ${proposals.length} pending proposals (P2)`, `Proposals generated this batch (${srcSummary}). Run <code>akm proposal list</code> before the queue grows further.`));
412
- }
413
- if (wallTime.p95Ms && wallTime.medianMs && wallTime.p95Ms / wallTime.medianMs > 2.5) {
414
- watchParts.push(advisoryCard("warn", "🟡", `High tail latency (P3): p95=${fmtMs(wallTime.p95Ms)}, median=${fmtMs(wallTime.medianMs)}`, "P95 is well above median. Consolidation/LLM phase dominates wall time on slow runs. " +
415
- "Check for slow chunks or LLM rate limiting."));
416
- }
417
- if (failedRuns > 0) {
418
- watchParts.push(advisoryCard("warn", "🟡", `${failedRuns} failed run(s) in window (P2)`, `Task fail rate ${taskFailRate}. Inspect failed runs (ok=false) for early-exit or harness errors.`));
419
- }
420
- const watchItemsHtml = watchParts.length > 0
421
- ? watchParts.join("\n")
422
- : passCard("Nothing critical to watch", "All indicators are within normal range.");
423
- // ── Commands used ──────────────────────────────────────────────────────────
424
- const commandsHtml = [
425
- ` <div><span>akm health --since=${esc(opts.window)} --group-by run --format json</span></div>`,
426
- ` <div><span>akm health --since=${esc(opts.window)} --window-compare=${esc(opts.compare)} --format json</span></div>`,
427
- " <div><span>akm proposal list</span></div>",
428
- ].join("\n");
429
794
  return {
430
795
  "%%ECHARTS_TAG%%": buildEchartsTag(opts),
431
796
  "%%REPORT_TITLE%%": esc(reportTitle),
432
797
  "%%WINDOW%%": esc(opts.window),
433
- "%%SINCE_HUMAN%%": esc(sinceHuman),
798
+ "%%SINCE_HUMAN%%": sinceHuman,
434
799
  "%%RUN_COUNT%%": num(totalRuns),
435
800
  "%%STATUS_BADGE_HTML%%": `${statusBadge}\n ${failBadge}`,
436
801
  "%%EXEC_SUMMARY_HTML%%": execSummary,
437
802
  "%%KPI_CARDS_HTML%%": kpiCards,
438
803
  "%%RUNS_JS_CONST%%": runsJsConst,
439
804
  "%%DISTILL_REASONS_JSON%%": JSON.stringify(distillReasons),
805
+ "%%SLICE_OPTIONS_HTML%%": buildSliceOptions(opts.window),
806
+ "%%LLM_BY_STAGE_JSON%%": JSON.stringify(llm.byStage ?? {}),
440
807
  "%%SUMMARY_ROWS_HTML%%": summaryRowsHtml,
441
- "%%ADVISORY_CARDS_HTML%%": advisoryCardsHtml,
808
+ "%%ACTION_ITEMS_HTML%%": actionItemsHtml,
442
809
  "%%PROPOSAL_ROWS_HTML%%": proposalRowsHtml,
443
810
  "%%PROPOSAL_COUNT%%": String(proposals.length),
444
- "%%WATCH_ITEMS_HTML%%": watchItemsHtml,
445
- "%%COMMANDS_HTML%%": commandsHtml,
446
811
  "%%GENERATED_AT%%": esc(generatedAt),
812
+ "%%AKM_VERSION%%": esc(pkgVersion),
447
813
  };
448
814
  }