akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
379 lines (378 loc) • 20.6 kB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/**
* `akm health --format html` — token builder for the full HTML health report
* (#582). Ports the external akm-health-report skill's collect.py + render.py
* to TypeScript so the report is generated in-process (no python, no
* shell-out). The template (`src/assets/templates/html/health.html`) is a
* strict superset of the skill's report.html; this module computes the 17
* `%%TOKEN%%` replacements it consumes.
*
* Structure (chunk-9 WI-9.5a / H2, anchors C.6): `./report-view-model.ts`
* holds the pure `AkmHealthResult`→`HealthReportViewModel` extractor — every
* computed number/string/series the report shows, with NO HTML. This module
* holds the thin VM→HTML fragment renderers (one per template section) plus
* `buildHealthHtmlReplacements`, the extract-then-render glue that produces
* the 17-token replacement map.
*
* Determinism: nothing here depends on Date.now()/Math.random(). Runs are
* sorted by startedAt; `%%GENERATED_AT%%` derives from the latest run (or the
* window anchor), so output is byte-identical for identical inputs.
*
* ECharts delivery (chunk-9 WI-9.4d, anchors C.1): CDN-only. The vendored
* `~1MB` `echarts.min.js` self-contained/"inline" mode was removed along with
* the asset itself — `buildEchartsTag` always emits a `<script src>` tag
* pointed at the jsDelivr CDN, so viewing the report now requires network
* access. There is no env var or option to opt back into an offline report.
*/
import { escapeHtml } from "../../output/html-render.js";
import { pkgVersion } from "../../version.js";
import { buildHealthReportViewModel, compact, fmtMs, humanize, num, } from "./report-view-model.js";
const esc = escapeHtml;
/** Emit a <time> element that the browser's JS will reformat to the viewer's local timezone. */
function isoTimeTag(iso) {
const fallback = iso.slice(0, 16).replace("T", " ");
return `<time data-iso="${esc(iso)}">${esc(fallback)}</time>`;
}
function trendClass(direction) {
return direction === "up" ? "trend-up" : direction === "down" ? "trend-down" : "trend-flat";
}
function trendLabel(direction) {
return direction === "up" ? "▲ up" : direction === "down" ? "▼ watch" : "— flat";
}
function deltaPill(deltas, key, lowerIsBetter = false) {
const raw = deltas[key]?.pctChange;
if (raw === undefined)
return '<span class="trend-pill flat">— n/a</span>';
if (typeof raw === "string") {
const sign = raw.trim().startsWith("-") ? -1 : 1;
const good = lowerIsBetter ? sign < 0 : sign > 0;
return `<span class="trend-pill ${good ? "up" : "down"}">${sign > 0 ? "▲ new" : "▼ gone"}</span>`;
}
const good = lowerIsBetter ? raw < 0 : raw > 0;
const cls = Math.abs(raw) < 1 ? "flat" : good ? "up" : "down";
const arrow = raw > 0 ? "▲" : raw < 0 ? "▼" : "—";
const signed = `${raw > 0 ? "+" : ""}${Math.round(raw)}%`;
return `<span class="trend-pill ${cls}">${arrow} ${signed}</span>`;
}
const PRIO_RANK = { P1: 0, P2: 1, P3: 2 };
function actionItemCard(item) {
const icon = item.cls === "fail" ? "🔴" : item.prio === "P3" ? "🟡" : "⚠️";
const remedy = item.remedy ? `<div class="remedy">Fix: <code>${esc(item.remedy)}</code></div>` : "";
return (`<div class="advisory ${item.cls}"><div class="advisory-icon">${icon}</div>` +
`<div class="advisory-body">` +
`<div class="title"><span class="prio ${item.prio.toLowerCase()}">${item.prio}</span>${item.title}</div>` +
`<div class="desc">${item.descHtml}</div>${remedy}</div></div>`);
}
function passCard(title, desc) {
return ('<div class="advisory" style="border-left:3px solid var(--green);">' +
'<div class="advisory-icon">✅</div><div class="advisory-body">' +
`<div class="title">${esc(title)}</div>` +
`<div class="desc">${esc(desc)}</div></div></div>`);
}
// ── Status badges (presentation constants) ───────────────────────────────────
const badgeByStatus = {
pass: { badge: "badge-pass", dot: "dot-pass", label: "PASS" },
warn: { badge: "badge-warn", dot: "dot-warn", label: "WARN" },
fail: { badge: "badge-fail", dot: "dot-fail", label: "FAIL" },
};
// ── ECharts delivery ─────────────────────────────────────────────────────────
const ECHARTS_CDN = "https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";
/** Always CDN — see the module docstring's chunk-9 WI-9.4d note. */
function buildEchartsTag() {
return `<script src="${ECHARTS_CDN}"></script>`;
}
/**
* Build the time-slice `<option>`s for the report's filter bar, DERIVED from
* the actual report window (`vm.sliceCandidates`) so the choices always make
* sense (the old hard-coded 1d–21d list was useless on a 24h or 7d report).
* "All" is the default; the sub-window options carry their cutoff in
* milliseconds (consumed by filteredRuns), largest first.
*/
function buildSliceOptions(vm) {
const opts = [`<option value="all" selected>All (${esc(vm.window)})</option>`];
for (const [ms, label] of vm.sliceCandidates)
opts.push(`<option value="${ms}">Last ${label}</option>`);
return opts.join("\n ");
}
// ── Fragment renderers (VM → HTML; one per template section) ────────────────
function renderSinceHuman(vm) {
return vm.sinceIso ? `${isoTimeTag(vm.sinceIso)} → now` : `last ${esc(vm.window)}`;
}
function renderStatusBadgeHtml(vm) {
const badge = badgeByStatus[vm.status];
const statusBadge = `<span class="badge-pill ${badge.badge}"><span class="dot ${badge.dot}"></span>${badge.label}</span>`;
const failBadge = `<span class="badge-pill ${vm.failOk ? "badge-pass" : "badge-warn"}">` +
`<span class="dot ${vm.failOk ? "dot-pass" : "dot-warn"}"></span>${vm.taskFailRate} Fail Rate</span>`;
return `${statusBadge}\n ${failBadge}`;
}
/**
* Executive-summary block: quick numbers, trend pills, period-over-period
* deltas, the current-run snapshot, the window facts, the synthesized verdict
* sentence, and the freshness line.
*/
function renderExecSummary(vm) {
const badge = badgeByStatus[vm.status];
const li = (k, vHtml) => `<li><span class="k">${esc(k)}</span><span class="v">${vHtml}</span></li>`;
const trendLi = (k, d) => li(k, `<span class="trend-pill ${d === "flat" ? "flat" : d}">${d}</span>`);
const quickNumbers = [
li("Task fail rate", vm.taskFailRate),
li("Agent fail rate", vm.agentFailRate),
li("Improve completion", `${num(vm.completed)} / ${num(vm.invoked)} (${vm.completionRate})`),
li("MI yield rate", vm.miYieldRate),
li("MI written", num(vm.miWritten)),
li("Consolidation promoted", num(vm.consolidation.promoted)),
li("Consolidation judged: no action", `<abbr title="Candidates the consolidator reviewed but intentionally left unchanged (the 'judgedNoAction' field).">${num(vm.consolidation.judgedNoAction)}</abbr>`),
li("Chunk failure", vm.chunkFail),
li("Median wall time", fmtMs(vm.wallTime.medianMs)),
li("P95 wall time", fmtMs(vm.wallTime.p95Ms)),
].join("");
const trendRows = [
trendLi("Decision quality", vm.trend.decisionQuality),
trendLi("Output volume", vm.trend.outputVolume),
trendLi("Latency", vm.trend.latency),
].join("");
const deltaRows = [
li("Promoted", deltaPill(vm.deltas, "improve.consolidation.promoted")),
li("MI written", deltaPill(vm.deltas, "improve.memoryInference.written")),
li("MI yield", deltaPill(vm.deltas, "improve.memoryInference.yieldRate")),
li("Median wall", deltaPill(vm.deltas, "improve.wallTime.medianMs", true)),
li("P95 wall", deltaPill(vm.deltas, "improve.wallTime.p95Ms", true)),
].join("");
const snapRows = vm.latest
? [
li("Run id", `<code>${esc(vm.latest.id.slice(0, 28))}</code>`),
li("Completed", isoTimeTag(vm.latest.completedAt || vm.latest.startedAt)),
li("Status", vm.latest.ok ? "✅ ok" : "❌ failed"),
li("Wall time", fmtMs(vm.latest.wallTimeMs)),
li("Reflect ok/fail", `${vm.latest.reflectOk} / ${vm.latest.reflectFailed}`),
li("Promoted", String(vm.latest.promoted)),
li("Judged: no action", `<abbr title="Candidates reviewed but intentionally left unchanged on this run.">${vm.latest.judgedNoAction}</abbr>`),
li("MI written", String(vm.latest.miWritten)),
].join("")
: '<li><span class="k">No runs in window</span><span class="v">—</span></li>';
const windowRows = [
li("Report window", esc(vm.window)),
li(vm.comparisonMode === "custom" ? "Comparison windows" : "Compare window", esc(vm.compare)),
li("Runs", `${num(vm.totalRuns)} (${vm.failedRuns} failed)`),
li("Included result rows", num(vm.includedResultRows)),
li("Invalid result rows skipped", num(vm.skippedInvalidResultRows)),
li("Stash derived", `<abbr title="Whole-stash recount of derived assets at report time — not a per-run sum.">${num(vm.memorySummary.derived)}</abbr>`),
li("Stash eligible", `<abbr title="Whole-stash recount of eligible assets at report time — not a per-run sum.">${num(vm.memorySummary.eligible)}</abbr>`),
li("Pending proposals", String(vm.proposals.length)),
li("Semantic search", vm.sem.blocked ? "BLOCKED" : "OK"),
].join("");
const overallEmoji = vm.trend.overall === "improving" ? "📈" : vm.trend.overall === "degrading" ? "📉" : "↔️";
const verdictSentence = `${vm.verdictWord} — ${esc(vm.verdictRest)}.`;
const verdictHtml = `<div class="verdict ${vm.status}"><b>Verdict:</b> ${verdictSentence}</div>`;
const latestRunHuman = vm.lastRun ? isoTimeTag(vm.lastRun.completedAt || vm.lastRun.startedAt) : "—";
const freshnessHtml = `<div class="freshness${vm.isStale ? " stale" : ""}">${vm.isStale ? "⚠️ Stale: " : ""}Latest run ${latestRunHuman} · generated ${isoTimeTag(vm.generatedAt)}${vm.isStale ? " — no recent activity (newest run older than the 6h freshness threshold)." : "."}</div>`;
return `
<h2>${overallEmoji} Executive Summary
<span class="badge-pill ${badge.badge}" style="font-size:11px;">
<span class="dot ${badge.dot}"></span>${badge.label}</span></h2>
${verdictHtml}
${freshnessHtml}
<div class="exec-grid">
<div>
<h4>Quick Numbers</h4>
<ul>${quickNumbers}</ul>
</div>
<div>
<h4>${vm.comparisonMode === "custom" ? `Trend: ${esc(vm.compare)}` : `Trend vs prior ${esc(vm.compare)}`}</h4>
<ul>${trendRows}</ul>
<h4 style="margin-top:14px;">Period-over-period deltas</h4>
<ul>${deltaRows}</ul>
</div>
<div>
<h4>Current Run Snapshot</h4>
<ul>${snapRows}</ul>
</div>
<div>
<h4>Window</h4>
<ul>${windowRows}</ul>
</div>
</div>
<div class="overall">Overall trend: <b>${esc(vm.trend.overall)}</b> ${overallEmoji}
· based on decision quality, output volume, and latency ${vm.comparisonMode === "custom" ? "across the selected windows" : "vs the prior window"}.</div>`.trim();
}
/**
* Color is a health SIGNAL, not decoration: green/yellow/red where a card has
* a meaningful threshold; "neutral" for purely-informational counts. Cards
* are ordered by operator priority (failures first, informational counts
* last).
*/
function renderKpiCards(vm) {
const semValue = vm.sem.blocked ? "BLOCKED" : "OK";
const semColor = vm.sem.blocked ? "red" : "green";
const semStyle = vm.sem.blocked ? "font-size:18px;" : "";
const completionColor = vm.completionPct >= 99 ? "green" : vm.completionPct >= 90 ? "yellow" : "red";
const kpiCard = (color, label, value, sub, valueStyle = "") => `<div class="kpi-card ${color}">
<div class="label">${label}</div>
<div class="value"${valueStyle ? ` style="${valueStyle}"` : ""}>${value}</div>
<div class="sub">${sub}</div>
</div>`;
return [
kpiCard(vm.failedRuns === 0 ? "green" : "red", "Failed Runs", String(vm.failedRuns), `of ${num(vm.totalRuns)} runs · ${vm.taskFailRate} task fail`),
kpiCard(completionColor, "Completion Rate", vm.completionRate, `${num(vm.completed)} / ${num(vm.invoked)} invoked`),
kpiCard("neutral", "Median Duration", `${vm.medianDurMin}m`, `p95 = ${vm.p95DurMin}m`),
kpiCard("blue", "Total Promoted", num(vm.consolidation.promoted), `avg ${vm.avgPromoted} / run`),
kpiCard("blue", "MI Written", num(vm.miWritten), `${vm.miYieldRate} yield rate`),
kpiCard("neutral", "Stash Derived", num(vm.memorySummary.derived), `of ${num(vm.memorySummary.eligible)} eligible (whole-stash)`),
// #576: real LLM work — duration leads, tokens compact, not a GPU proxy.
kpiCard(vm.llm.calls > 0 ? "purple" : "neutral", "🧠 LLM Work", `${vm.llmTokensCompact} tok`, `${fmtMs(vm.llm.totalDurationMs)} · ${num(vm.llm.calls)} calls · ${compact(vm.llm.reasoningTokens)} reasoning`),
kpiCard(semColor, "Semantic Search", semValue, esc(vm.sem.detail), semStyle),
kpiCard(vm.proposals.length > 0 ? "yellow" : "neutral", "Pending Proposals", String(vm.proposals.length), `from ${esc(vm.window)} batch`),
].join("\n");
}
function renderSummaryRowsHtml(vm) {
return vm.summaryRows
.map(([label, value, t, tip]) => {
const labelHtml = tip ? `<abbr title="${esc(tip)}">${esc(label)}</abbr>` : esc(label);
return (` <tr><td>${labelHtml}</td><td>${esc(value)}</td>` +
`<td class="trend ${trendClass(t)}">${trendLabel(t)}</td></tr>`);
})
.join("\n");
}
/**
* Merge + de-duplicate advisories and the old "what to watch" cards (built
* from the same VM data — invalid rows, sem-blocked, proposals, tail-latency,
* failed-runs, …) into ONE prioritized list (P1/P2/P3 + a remediation command
* per item), keyed so each concern appears exactly once.
*/
function renderActionItems(vm) {
const items = [];
const seen = new Set();
const pushItem = (item) => {
if (seen.has(item.key))
return;
seen.add(item.key);
items.push(item);
};
if (vm.invalidRuns.length > 0) {
pushItem({
key: "invalid-improve-result-rows",
prio: "P2",
cls: "warn",
title: `${vm.invalidRuns.length} invalid improve result row${vm.invalidRuns.length === 1 ? "" : "s"} skipped`,
descHtml: `Excluded from result-derived metrics: ${vm.invalidRuns.map((run) => `<code>${esc(run.id)}</code>`).join(", ")}.`,
remedy: `akm health --since=${vm.window} --group-by run`,
});
}
// Hard advisories from the health check engine (own remediation in message).
for (const a of vm.advisories) {
if (a.status !== "warn" && a.status !== "fail")
continue;
pushItem({
key: `advisory:${a.name}`,
prio: a.status === "fail" ? "P1" : "P2",
cls: a.status === "fail" ? "fail" : "warn",
title: esc(humanize(a.name)),
descHtml: esc(a.message),
});
}
// Failed runs in window.
if (vm.failedRuns > 0) {
pushItem({
key: "failed-runs",
prio: "P1",
cls: "fail",
title: `${vm.failedRuns} failed run${vm.failedRuns === 1 ? "" : "s"} in window`,
descHtml: `Task fail rate ${esc(vm.taskFailRate)}. Inspect failed runs (ok=false) for early-exit or harness errors.`,
remedy: `akm health --since=${vm.window} --group-by run`,
});
}
// Semantic search blocked.
if (vm.sem.blocked) {
pushItem({
key: "semantic-search-blocked",
prio: "P2",
cls: "warn",
title: "Semantic search blocked",
descHtml: `Embedding provider unreachable. ${esc(vm.sem.detail)}. Curate falls back to keyword search — relevance scoring degraded.`,
remedy: "akm config list",
});
}
// Pending proposals to drain.
if (vm.proposals.length > 0) {
const srcSummary = vm.proposalsBySource.map(([source, count]) => `${count} via ${esc(source)}`).join(", ");
pushItem({
key: "drain-proposals",
prio: "P2",
cls: "warn",
title: `Drain ${vm.proposals.length} pending proposal${vm.proposals.length === 1 ? "" : "s"}`,
descHtml: `Proposals generated this batch (${srcSummary}). Review before the queue grows further.`,
remedy: "akm proposal list",
});
}
// High tail latency.
if (vm.wallTime.p95Ms && vm.wallTime.medianMs && vm.wallTime.p95Ms / vm.wallTime.medianMs > 2.5) {
pushItem({
key: "tail-latency",
prio: "P3",
cls: "warn",
title: `High tail latency: p95=${fmtMs(vm.wallTime.p95Ms)}, median=${fmtMs(vm.wallTime.medianMs)}`,
descHtml: "P95 is well above median. Consolidation/LLM phase dominates wall time on slow runs. " +
"Check for slow chunks or LLM rate limiting.",
});
}
// Stale freshness.
if (vm.isStale && vm.totalRuns > 0) {
const latestRunHuman = vm.lastRun ? isoTimeTag(vm.lastRun.completedAt || vm.lastRun.startedAt) : "—";
pushItem({
key: "stale",
prio: "P3",
cls: "warn",
title: "No recent improve runs",
descHtml: `Newest run is ${esc(latestRunHuman)} — older than the 6h freshness threshold. Check the improve scheduler/cron.`,
});
}
items.sort((a, b) => PRIO_RANK[a.prio] - PRIO_RANK[b.prio]);
return items.length > 0
? items.map(actionItemCard).join("\n")
: passCard("No action items", "All checks passed and nothing needs attention for this window.");
}
function renderProposalRowsHtml(vm) {
return vm.proposals.length > 0
? vm.proposals
.map((p, i) => {
const tagCls = p.source === "extract" ? "tag-extract" : "tag-consolidate";
return (`<tr><td>${i + 1}</td><td><code>${esc(p.ref)}</code></td>` +
`<td><span class="tag ${tagCls}">${esc(p.source)}</span></td>` +
`<td>${isoTimeTag(p.createdAt)}</td></tr>`);
})
.join("\n")
: '<tr><td colspan="4" style="text-align:center;color:var(--muted);">No pending proposals</td></tr>';
}
// ── Main builder: extract-then-render glue ──────────────────────────────────
/**
* Compute all 17 `%%TOKEN%%` replacements for the health HTML template.
* There is deliberately NO standalone `%%OVERALL_STATUS%%` token — the
* overall status is embedded in the pre-rendered badge / exec-summary
* fragments, matching the skill template. Advisories and "what to watch" are
* merged + de-duplicated into a single prioritized `%%ACTION_ITEMS_HTML%%`.
*/
export function buildHealthHtmlReplacements(result, opts) {
const vm = buildHealthReportViewModel(result, opts);
return {
"%%ECHARTS_TAG%%": buildEchartsTag(),
"%%REPORT_TITLE%%": esc(vm.reportTitle),
"%%WINDOW%%": esc(vm.window),
"%%SINCE_HUMAN%%": renderSinceHuman(vm),
"%%RUN_COUNT%%": num(vm.totalRuns),
"%%STATUS_BADGE_HTML%%": renderStatusBadgeHtml(vm),
"%%EXEC_SUMMARY_HTML%%": renderExecSummary(vm),
"%%KPI_CARDS_HTML%%": renderKpiCards(vm),
"%%RUNS_JS_CONST%%": `const RUNS = ${JSON.stringify(vm.runs)};`,
"%%DISTILL_REASONS_JSON%%": JSON.stringify(vm.distillReasons),
"%%SLICE_OPTIONS_HTML%%": buildSliceOptions(vm),
"%%LLM_BY_STAGE_JSON%%": JSON.stringify(vm.llm.byStage ?? {}),
"%%SUMMARY_ROWS_HTML%%": renderSummaryRowsHtml(vm),
"%%ACTION_ITEMS_HTML%%": renderActionItems(vm),
"%%PROPOSAL_ROWS_HTML%%": renderProposalRowsHtml(vm),
"%%PROPOSAL_COUNT%%": String(vm.proposals.length),
"%%GENERATED_AT%%": esc(vm.generatedAt),
"%%AKM_VERSION%%": esc(pkgVersion),
};
}