akm-cli 0.9.0-beta.11 → 0.9.0-beta.26

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 (88) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/dist/assets/prompts/consolidate-system.md +23 -0
  3. package/dist/assets/prompts/contradiction-judge.md +33 -0
  4. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  5. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  6. package/dist/assets/prompts/extract-session.md +5 -1
  7. package/dist/assets/prompts/graph-extract-system.md +1 -0
  8. package/dist/assets/prompts/memory-infer-system.md +1 -0
  9. package/dist/assets/prompts/memory-infer-user.md +5 -0
  10. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  11. package/dist/assets/prompts/procedural-system.md +44 -0
  12. package/dist/assets/prompts/recombine-system.md +40 -0
  13. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  14. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  15. package/dist/assets/templates/html/health.html +25 -27
  16. package/dist/cli.js +2 -2
  17. package/dist/commands/agent/contribute-cli.js +16 -3
  18. package/dist/commands/feedback-cli.js +48 -44
  19. package/dist/commands/health/html-report.js +140 -16
  20. package/dist/commands/health.js +277 -1
  21. package/dist/commands/improve/calibration.js +161 -0
  22. package/dist/commands/improve/consolidate.js +595 -105
  23. package/dist/commands/improve/dedup.js +482 -0
  24. package/dist/commands/improve/distill.js +119 -64
  25. package/dist/commands/improve/encoding-salience.js +205 -0
  26. package/dist/commands/improve/extract-cli.js +115 -1
  27. package/dist/commands/improve/extract-prompt.js +32 -1
  28. package/dist/commands/improve/extract-watch.js +140 -0
  29. package/dist/commands/improve/extract.js +210 -30
  30. package/dist/commands/improve/feedback-valence.js +54 -0
  31. package/dist/commands/improve/homeostatic.js +467 -0
  32. package/dist/commands/improve/improve-auto-accept.js +80 -7
  33. package/dist/commands/improve/improve-profiles.js +8 -0
  34. package/dist/commands/improve/improve.js +991 -61
  35. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  36. package/dist/commands/improve/outcome-loop.js +256 -0
  37. package/dist/commands/improve/proactive-maintenance.js +9 -35
  38. package/dist/commands/improve/procedural.js +409 -0
  39. package/dist/commands/improve/recombine.js +488 -0
  40. package/dist/commands/improve/reflect.js +20 -1
  41. package/dist/commands/improve/related-sessions.js +120 -0
  42. package/dist/commands/improve/salience.js +386 -0
  43. package/dist/commands/improve/triage.js +95 -0
  44. package/dist/commands/lint/agent-linter.js +19 -24
  45. package/dist/commands/lint/base-linter.js +173 -60
  46. package/dist/commands/lint/command-linter.js +19 -24
  47. package/dist/commands/lint/env-key-rules.js +34 -1
  48. package/dist/commands/lint/index.js +30 -13
  49. package/dist/commands/lint/memory-linter.js +1 -1
  50. package/dist/commands/lint/registry.js +5 -2
  51. package/dist/commands/lint/task-linter.js +3 -3
  52. package/dist/commands/lint/workflow-linter.js +26 -1
  53. package/dist/commands/proposal/validators/proposals.js +4 -0
  54. package/dist/commands/read/curate.js +284 -86
  55. package/dist/commands/read/search-cli.js +7 -0
  56. package/dist/commands/read/search.js +1 -0
  57. package/dist/commands/sources/installed-stashes.js +5 -1
  58. package/dist/core/asset/frontmatter.js +166 -167
  59. package/dist/core/asset/markdown.js +8 -0
  60. package/dist/core/config/config-schema.js +211 -3
  61. package/dist/core/config/config.js +2 -2
  62. package/dist/core/logs-db.js +4 -3
  63. package/dist/core/state-db.js +555 -29
  64. package/dist/indexer/db/db.js +250 -27
  65. package/dist/indexer/db/graph-db.js +81 -86
  66. package/dist/indexer/graph/graph-boost.js +51 -41
  67. package/dist/indexer/passes/memory-inference.js +10 -3
  68. package/dist/indexer/passes/staleness-detect.js +2 -5
  69. package/dist/indexer/search/db-search.js +15 -4
  70. package/dist/indexer/search/ranking.js +4 -0
  71. package/dist/integrations/harnesses/claude/session-log.js +10 -0
  72. package/dist/integrations/harnesses/opencode/session-log.js +9 -0
  73. package/dist/integrations/session-logs/index.js +16 -0
  74. package/dist/llm/embedder.js +27 -3
  75. package/dist/llm/embedders/local.js +66 -2
  76. package/dist/llm/graph-extract.js +2 -1
  77. package/dist/llm/memory-infer.js +4 -8
  78. package/dist/llm/metadata-enhance.js +9 -1
  79. package/dist/output/shapes/curate.js +14 -2
  80. package/dist/output/text/helpers.js +9 -0
  81. package/dist/runtime.js +25 -1
  82. package/dist/scripts/migrate-storage.js +1025 -567
  83. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +435 -269
  84. package/dist/storage/sqlite-pragmas.js +146 -0
  85. package/dist/workflows/db.js +3 -4
  86. package/dist/workflows/validate-summary.js +2 -7
  87. package/docs/data-and-telemetry.md +1 -0
  88. package/package.json +5 -4
@@ -11,11 +11,9 @@ import { writeFileAtomic } from "../core/common.js";
11
11
  import { FEEDBACK_FAILURE_MODES, loadConfig } from "../core/config/config.js";
12
12
  import { UsageError } from "../core/errors.js";
13
13
  import { appendEvent } from "../core/events.js";
14
+ import { getDbPath } from "../core/paths.js";
14
15
  import { warn } from "../core/warn.js";
15
16
  import { applyFeedbackToUtilityScore, closeDatabase, findEntryIdByRef, getEntryFilePathById, openExistingDatabase, } from "../indexer/db/db.js";
16
- import { ensureIndex } from "../indexer/ensure-index.js";
17
- import { withIndexWriterLease } from "../indexer/index-writer-lock.js";
18
- import { resolveSourceEntries } from "../indexer/search/search-source.js";
19
17
  import { countFeedbackSignals, insertUsageEvent } from "../indexer/usage/usage-events.js";
20
18
  // ── Tag validation ────────────────────────────────────────────────────────────
21
19
  const TAG_KEY_RE = /^[a-z_][a-z0-9_]*$/;
@@ -204,51 +202,57 @@ export const feedbackCommand = defineCommand({
204
202
  ...(validatedTags.length > 0 ? { tags: validatedTags } : {}),
205
203
  };
206
204
  const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
207
- const utilityResult = await withIndexWriterLease({ purpose: "feedback-write" }, async () => {
208
- // Feedback is itself an index.db writer, so it must not spawn a detached
209
- // reindex and then compete with it for the same database file.
210
- const sources = resolveSourceEntries();
211
- if (sources.length > 0) {
212
- await ensureIndex(sources[0].path, { mode: "blocking" });
205
+ // Feedback only needs the index to exist, not to be current. A stale index
206
+ // is fine the ref lookup works against any populated DB. We do NOT call
207
+ // ensureIndex here: it either blocks (3+ min inline reindex) or spawns a
208
+ // background process that holds the writer lock, causing the feedback write
209
+ // to spin-wait for the full reindex duration. If the DB is absent we give a
210
+ // clear error below rather than silently triggering a rebuild.
211
+ if (!fs.existsSync(getDbPath())) {
212
+ throw new UsageError("Index not found. Run 'akm index' first to build the index before recording feedback.", "MISSING_REQUIRED_ARGUMENT", "akm index");
213
+ }
214
+ // Feedback writes exactly 2 rows (usage_events + utility_score). SQLite
215
+ // WAL mode + busy_timeout=30s handles concurrent access with an ongoing
216
+ // `akm improve` run without needing the application-level writer lock.
217
+ // The lock was originally needed to prevent feedback from racing a
218
+ // background reindex it spawned — now that ensureIndex is removed, holding
219
+ // the lock only causes feedback to block for the full improve run duration.
220
+ let utilityResult;
221
+ const db = openExistingDatabase();
222
+ try {
223
+ const entryId = findEntryIdByRef(db, ref);
224
+ if (entryId === undefined) {
225
+ throw new UsageError(`Ref "${ref}" is not in the index. ` +
226
+ "Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.");
213
227
  }
214
- let scopedUtilityResult;
215
- const db = openExistingDatabase();
228
+ // Persist the feedback signal into usage_events. For positive signals,
229
+ // the EMA utility score is updated immediately on the next read path.
230
+ // For negative signals, the score is adjusted the next time `akm index`
231
+ // runs — the signal is durable in the DB but does NOT suppress ranking
232
+ // in search results until after reindexing.
233
+ insertUsageEvent(db, {
234
+ event_type: "feedback",
235
+ entry_ref: ref,
236
+ entry_id: entryId,
237
+ signal,
238
+ metadata: metadataStr,
239
+ });
240
+ // Apply feedback-derived utility score adjustment immediately so that
241
+ // positive/negative signals influence search ranking without requiring
242
+ // a full reindex. We query the total accumulated feedback counts from
243
+ // usage_events so the delta reflects the entire signal history.
244
+ // Uses MemRL bounded-step EMA (F-5 / #386, arXiv:2601.03192).
216
245
  try {
217
- const entryId = findEntryIdByRef(db, ref);
218
- if (entryId === undefined) {
219
- throw new UsageError(`Ref "${ref}" is not in the index. ` +
220
- "Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.");
221
- }
222
- // Persist the feedback signal into usage_events. For positive signals,
223
- // the EMA utility score is updated immediately on the next read path.
224
- // For negative signals, the score is adjusted the next time `akm index`
225
- // runs — the signal is durable in the DB but does NOT suppress ranking
226
- // in search results until after reindexing.
227
- insertUsageEvent(db, {
228
- event_type: "feedback",
229
- entry_ref: ref,
230
- entry_id: entryId,
231
- signal,
232
- metadata: metadataStr,
233
- });
234
- // Apply feedback-derived utility score adjustment immediately so that
235
- // positive/negative signals influence search ranking without requiring
236
- // a full reindex. We query the total accumulated feedback counts from
237
- // usage_events so the delta reflects the entire signal history.
238
- // Uses MemRL bounded-step EMA (F-5 / #386, arXiv:2601.03192).
239
- try {
240
- const { pos, neg } = countFeedbackSignals(db, entryId);
241
- scopedUtilityResult = applyFeedbackToUtilityScore(db, entryId, pos, neg);
242
- }
243
- catch {
244
- // best-effort — feedback recording succeeds even if utility update fails
245
- }
246
+ const { pos, neg } = countFeedbackSignals(db, entryId);
247
+ utilityResult = applyFeedbackToUtilityScore(db, entryId, pos, neg);
246
248
  }
247
- finally {
248
- closeDatabase(db);
249
+ catch {
250
+ // best-effort — feedback recording succeeds even if utility update fails
249
251
  }
250
- return scopedUtilityResult;
251
- });
252
+ }
253
+ finally {
254
+ closeDatabase(db);
255
+ }
252
256
  appendEvent({
253
257
  eventType: "feedback",
254
258
  ref,
@@ -29,6 +29,11 @@ const ECHARTS_CDN = "https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"
29
29
  const ECHARTS_VENDOR_PATH = path.join(getDirname(import.meta.url), "../../assets/templates/html/vendor/echarts.min.js");
30
30
  // ── Small formatters (ports of render.py helpers) ───────────────────────────
31
31
  const esc = escapeHtml;
32
+ /** Emit a <time> element that the browser's JS will reformat to the viewer's local timezone. */
33
+ function isoTimeTag(iso) {
34
+ const fallback = iso.slice(0, 16).replace("T", " ");
35
+ return `<time data-iso="${esc(iso)}">${esc(fallback)}</time>`;
36
+ }
32
37
  function num(value) {
33
38
  return Math.round(value).toLocaleString("en-US");
34
39
  }
@@ -264,6 +269,19 @@ export function buildHealthHtmlReplacements(result, opts) {
264
269
  const mi = improve.memoryInference;
265
270
  const ge = improve.graphExtraction;
266
271
  const wallTime = improve.wallTime;
272
+ // WS-5: perf telemetry, coverage, and degradation metrics.
273
+ const perf = improve.perfTelemetry ?? {
274
+ dedupPoolSize: 0,
275
+ llmPoolSize: 0,
276
+ judgedCacheSkipped: 0,
277
+ embedMs: 0,
278
+ embedCacheHits: 0,
279
+ embedCacheMisses: 0,
280
+ overBudgetRuns: 0,
281
+ runsWithTelemetry: 0,
282
+ };
283
+ const coverage = improve.coverage;
284
+ const degradation = improve.degradation;
267
285
  // #576: real per-stage LLM token/time accounting (replaces the GPU-time
268
286
  // proxy). Optional-guarded so reports built from older health JSON without
269
287
  // the aggregate still render.
@@ -292,7 +310,7 @@ export function buildHealthHtmlReplacements(result, opts) {
292
310
  // ── Meta (collect.py steps 10-11) ──────────────────────────────────────────
293
311
  const sinceIso = result.since;
294
312
  const reportDate = sinceIso.slice(0, 10);
295
- const sinceHuman = sinceIso ? `${sinceIso.slice(0, 16).replace("T", " ")} UTC → now` : `last ${opts.window}`;
313
+ const sinceHuman = sinceIso ? `${isoTimeTag(sinceIso)} → now` : `last ${esc(opts.window)}`;
296
314
  const reportTitle = reportDate ? `AKM Health Report — ${reportDate}` : "AKM Health Report";
297
315
  const lastRun = runs[runs.length - 1];
298
316
  const generatedAt = lastRun ? lastRun.completedAt || lastRun.startedAt || sinceIso : sinceIso;
@@ -308,9 +326,7 @@ export function buildHealthHtmlReplacements(result, opts) {
308
326
  const generatedMs = Date.parse(generatedAt);
309
327
  const idleTailMs = Number.isFinite(latestRunMs) && Number.isFinite(generatedMs) ? Math.max(0, generatedMs - latestRunMs) : 0;
310
328
  const isStale = totalRuns === 0 || (Number.isFinite(latestRunMs) && Number.isFinite(sinceMs) && idleTailMs > STALE_MS);
311
- const latestRunHuman = lastRun
312
- ? `${(lastRun.completedAt || lastRun.startedAt).slice(0, 16).replace("T", " ")} UTC`
313
- : "—";
329
+ const latestRunHuman = lastRun ? isoTimeTag(lastRun.completedAt || lastRun.startedAt) : "—";
314
330
  // ── Status badges ──────────────────────────────────────────────────────────
315
331
  const badgeByStatus = {
316
332
  pass: { badge: "badge-pass", dot: "dot-pass", label: "PASS" },
@@ -353,7 +369,7 @@ export function buildHealthHtmlReplacements(result, opts) {
353
369
  const snapRows = latest
354
370
  ? [
355
371
  li("Run id", `<code>${esc(latest.id.slice(0, 28))}</code>`),
356
- li("Completed", `${esc((latest.completedAt || latest.startedAt).slice(0, 16).replace("T", " "))} UTC`),
372
+ li("Completed", isoTimeTag(latest.completedAt || latest.startedAt)),
357
373
  li("Status", latest.ok ? "✅ ok" : "❌ failed"),
358
374
  li("Wall time", fmtMs(latest.wallTimeMs)),
359
375
  li("Reflect ok/fail", `${latest.reflectOk} / ${latest.reflectFailed}`),
@@ -404,7 +420,7 @@ export function buildHealthHtmlReplacements(result, opts) {
404
420
  const verdictSentence = `${verdictWord} — ${esc(verdictRest)}.`;
405
421
  const verdictHtml = `<div class="verdict ${result.status}"><b>Verdict:</b> ${verdictSentence}</div>`;
406
422
  // ── Freshness line ─────────────────────────────────────────────────────────
407
- const freshnessHtml = `<div class="freshness${isStale ? " stale" : ""}">${isStale ? "⚠️ Stale: " : ""}Latest run ${esc(latestRunHuman)} &nbsp;·&nbsp; generated ${esc(generatedAt)}${isStale ? " — no recent activity (newest run older than the 6h freshness threshold)." : "."}</div>`;
423
+ 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>`;
408
424
  const execSummary = `
409
425
  <h2>${overallEmoji} Executive Summary
410
426
  <span class="badge-pill ${badge.badge}" style="font-size:11px;">
@@ -513,6 +529,97 @@ export function buildHealthHtmlReplacements(result, opts) {
513
529
  ],
514
530
  ["LLM wall time", fmtMs(llm.totalDurationMs), trend.latency],
515
531
  ];
532
+ // #612 — auto-accept gate calibration. Only surface when the gate actually
533
+ // acted on proposals in the window (samples > 0); a default ungated install
534
+ // reports an empty summary and we omit the rows to keep the table parity-clean.
535
+ const calibration = improve.calibration;
536
+ if (calibration && calibration.samples > 0) {
537
+ summaryRows.push([
538
+ "Calibration samples",
539
+ num(calibration.samples),
540
+ "flat",
541
+ "Auto-accept gate decisions (auto-accepted + auto-rejected) the calibration join measured this window.",
542
+ ], [
543
+ "Calibration accept rate",
544
+ String(calibration.overallAcceptRate),
545
+ "flat",
546
+ "Realized accept rate of acted-on gate decisions (auto-accepted / total acted-on).",
547
+ ], [
548
+ "Calibration gap",
549
+ String(calibration.calibrationGap),
550
+ "flat",
551
+ "Mean predicted confidence minus realized accept rate. Positive = the gate is over-confident.",
552
+ ]);
553
+ }
554
+ // WS-5: denominator-fixed coverage rows (only when we have real data).
555
+ if (coverage && !Number.isNaN(coverage.rate)) {
556
+ summaryRows.push([
557
+ "Coverage rate",
558
+ pct(coverage.rate, 1),
559
+ "flat",
560
+ "Accepted proposals / total stash assets (denominator-fixed). Shows what fraction of the corpus has been touched.",
561
+ ], [
562
+ "Eligible fraction",
563
+ pct(coverage.eligibleFraction, 1),
564
+ "flat",
565
+ "Eligible assets / total stash assets. Fraction the improve pipeline actively considers.",
566
+ ], [
567
+ "Coverage accepted",
568
+ num(coverage.acceptedProposals),
569
+ "up",
570
+ "Total accepted proposals used for the denominator-fixed coverage rate.",
571
+ ]);
572
+ }
573
+ // WS-5: perf telemetry rows (only when at least one run reported telemetry).
574
+ if (perf.runsWithTelemetry > 0) {
575
+ const embedCacheTotal = perf.embedCacheHits + perf.embedCacheMisses;
576
+ const embedCacheHitRate = embedCacheTotal > 0 ? pct(perf.embedCacheHits / embedCacheTotal, 1) : "—";
577
+ summaryRows.push([
578
+ "Embed cache hit rate",
579
+ embedCacheHitRate,
580
+ "flat",
581
+ "Fraction of embedding lookups served from cache (>95% is healthy). Aggregated across WS-5 runs.",
582
+ ], [
583
+ "Embed wall time",
584
+ fmtMs(perf.embedMs),
585
+ "flat",
586
+ "Cumulative embedding wall-clock time across consolidation runs in the window.",
587
+ ], [
588
+ "Judged-cache skipped",
589
+ num(perf.judgedCacheSkipped),
590
+ "flat",
591
+ "Candidates skipped by the judged-cache (not sent to LLM). Higher = more efficient reuse of prior judgments.",
592
+ ], [
593
+ "Dedup pool size",
594
+ num(perf.dedupPoolSize),
595
+ "flat",
596
+ "Average memory pool size after deduplication (before judged-cache narrowing). WS-5 perf telemetry.",
597
+ ], [
598
+ "Over-budget consolidation runs",
599
+ String(perf.overBudgetRuns),
600
+ perf.overBudgetRuns > 0 ? "down" : "flat",
601
+ "Runs where consolidation alone exceeded the total run budget (estimatedBudgetFractionUsed > 1.0).",
602
+ ]);
603
+ }
604
+ // WS-5: degradation metrics rows.
605
+ if (degradation) {
606
+ summaryRows.push([
607
+ "Corpus diversity (Gini)",
608
+ num(degradation.corpusCentroidDistance),
609
+ degradation.entrenchmentFlagged ? "down" : "flat",
610
+ "Gini coefficient of retrieval_salience for top-100 ranked assets. High Gini (>0.35) = entrenchment risk.",
611
+ ], [
612
+ "Merge fidelity contradiction rate",
613
+ pct(degradation.mergeFidelityContradictionRate, 1),
614
+ "flat",
615
+ "Fraction of consolidated proposals that involved a contradiction, from consolidation result envelopes.",
616
+ ], [
617
+ "High-generation fraction",
618
+ pct(degradation.highGenerationFraction, 1),
619
+ "flat",
620
+ "Fraction of assets with consecutive_no_ops >= 2 (proxy for high-generation assets in the salience table).",
621
+ ]);
622
+ }
516
623
  const summaryRowsHtml = summaryRows
517
624
  .map(([label, value, t, tip]) => {
518
625
  const labelHtml = tip ? `<abbr title="${esc(tip)}">${esc(label)}</abbr>` : esc(label);
@@ -606,6 +713,31 @@ export function buildHealthHtmlReplacements(result, opts) {
606
713
  descHtml: `Newest run is ${esc(latestRunHuman)} — older than the 6h freshness threshold. Check the improve scheduler/cron.`,
607
714
  });
608
715
  }
716
+ // WS-5: corpus entrenchment flag.
717
+ if (degradation?.entrenchmentFlagged) {
718
+ pushItem({
719
+ key: "corpus-entrenchment",
720
+ prio: "P2",
721
+ cls: "warn",
722
+ title: "Corpus entrenchment risk: retrieval_salience Gini > 0.35",
723
+ descHtml: "A small set of assets dominates retrieval — retrieval diversity is low. " +
724
+ "Review top-ranked assets for stale or over-represented content. " +
725
+ `Corpus diversity proxy: ${esc(String(degradation.corpusCentroidDistance))}.`,
726
+ remedy: "akm health --format json | jq '.improve.degradation'",
727
+ });
728
+ }
729
+ // WS-5: over-budget consolidation advisory.
730
+ if (perf.overBudgetRuns > 0) {
731
+ pushItem({
732
+ key: "over-budget-consolidation",
733
+ prio: "P2",
734
+ cls: "warn",
735
+ title: `${perf.overBudgetRuns} consolidation run${perf.overBudgetRuns === 1 ? "" : "s"} exceeded budget`,
736
+ descHtml: "Consolidation phase wall time exceeded the total run budget on these runs. " +
737
+ "Consider increasing the timeout or reducing the consolidation pool via profile config.",
738
+ remedy: "akm config show",
739
+ });
740
+ }
609
741
  // Session-log notes (informational, lowest priority).
610
742
  if (result.sessionLogAdvisories.length > 0) {
611
743
  const patterns = result.sessionLogAdvisories
@@ -629,24 +761,17 @@ export function buildHealthHtmlReplacements(result, opts) {
629
761
  ? proposals
630
762
  .map((p, i) => {
631
763
  const tagCls = p.source === "extract" ? "tag-extract" : "tag-consolidate";
632
- const ts = p.createdAt.slice(0, 16).replace("T", " ");
633
764
  return (`<tr><td>${i + 1}</td><td><code>${esc(p.ref)}</code></td>` +
634
765
  `<td><span class="tag ${tagCls}">${esc(p.source)}</span></td>` +
635
- `<td>${esc(ts)}</td></tr>`);
766
+ `<td>${isoTimeTag(p.createdAt)}</td></tr>`);
636
767
  })
637
768
  .join("\n")
638
769
  : '<tr><td colspan="4" style="text-align:center;color:var(--muted);">No pending proposals</td></tr>';
639
- // ── Commands used ──────────────────────────────────────────────────────────
640
- const commandsHtml = [
641
- ` <div><span>akm health --since=${esc(opts.window)} --group-by run --format json</span></div>`,
642
- ` <div><span>akm health --since=${esc(opts.window)} --window-compare=${esc(opts.compare)} --format json</span></div>`,
643
- " <div><span>akm proposal list</span></div>",
644
- ].join("\n");
645
770
  return {
646
771
  "%%ECHARTS_TAG%%": buildEchartsTag(opts),
647
772
  "%%REPORT_TITLE%%": esc(reportTitle),
648
773
  "%%WINDOW%%": esc(opts.window),
649
- "%%SINCE_HUMAN%%": esc(sinceHuman),
774
+ "%%SINCE_HUMAN%%": sinceHuman,
650
775
  "%%RUN_COUNT%%": num(totalRuns),
651
776
  "%%STATUS_BADGE_HTML%%": `${statusBadge}\n ${failBadge}`,
652
777
  "%%EXEC_SUMMARY_HTML%%": execSummary,
@@ -659,7 +784,6 @@ export function buildHealthHtmlReplacements(result, opts) {
659
784
  "%%ACTION_ITEMS_HTML%%": actionItemsHtml,
660
785
  "%%PROPOSAL_ROWS_HTML%%": proposalRowsHtml,
661
786
  "%%PROPOSAL_COUNT%%": String(proposals.length),
662
- "%%COMMANDS_HTML%%": commandsHtml,
663
787
  "%%GENERATED_AT%%": esc(generatedAt),
664
788
  "%%AKM_VERSION%%": esc(pkgVersion),
665
789
  };