feat(workflows): pipeline pass rates and skill/plugin usage
Restores the five analytics sections that turned out to be worth keeping (patterns, model delegation, error propagation, session complexity, compaction impact) and reworks what the page opens on. The lane panel no longer mirrors the Workspace's live lane strip. It now answers the question the strip cannot: how often each pipeline node actually passes. GET /api/lanes/pipeline-stats aggregates every stage record of every lane AND every archived feature — a lane's own `stages` only holds its current feature, so without the archive the history resets each time a lane starts new work. Evidence-backed passes are counted apart from bare claims, the same line the pipeline map draws between `done` and `passed-no-evidence`. Beside it, GET /api/workflows/skill-usage reports which skills and plugins the sessions actually reach for. Only PreToolUse rows are counted: every call also emits a PostToolUse, and counting both would double every number. A plugin's total includes calls to the MCP tools it ships, so one used purely through MCP does not read as unused. Still dropped from issue #6: the agent collaboration network and the subagent effectiveness scorecards.
This commit is contained in:
@@ -39,6 +39,82 @@ router.get("/", (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* How much each skill and each plugin is actually used.
|
||||
*
|
||||
* Skill calls are read off `Skill` tool events, one row per invocation:
|
||||
* `PreToolUse` only, because every call also emits a `PostToolUse` and counting
|
||||
* both would double every number. The skill's own name carries its plugin as a
|
||||
* `plugin:skill` prefix; a bare name is a built-in or personal skill and is
|
||||
* grouped under `null`.
|
||||
*
|
||||
* A plugin's total also includes calls to the MCP tools it ships
|
||||
* (`mcp__plugin_<plugin>__<tool>`) — a plugin used entirely through its MCP
|
||||
* server would otherwise report zero usage.
|
||||
*/
|
||||
router.get("/skill-usage", (req, res) => {
|
||||
try {
|
||||
const sf = sessionIdFilter(req.query.status || null);
|
||||
|
||||
const skills = db
|
||||
.prepare(
|
||||
`SELECT json_extract(data, '$.tool_input.skill') AS name,
|
||||
COUNT(*) AS count,
|
||||
COUNT(DISTINCT session_id) AS sessions
|
||||
FROM events
|
||||
WHERE tool_name = 'Skill' AND event_type = 'PreToolUse'
|
||||
AND json_extract(data, '$.tool_input.skill') IS NOT NULL${sf.clause}
|
||||
GROUP BY name ORDER BY count DESC LIMIT 30`
|
||||
)
|
||||
.all(...sf.params)
|
||||
.map((r) => {
|
||||
const i = r.name.indexOf(":");
|
||||
return { ...r, plugin: i > 0 ? r.name.slice(0, i) : null };
|
||||
});
|
||||
|
||||
const mcpCalls = db
|
||||
.prepare(
|
||||
`SELECT tool_name, COUNT(*) AS count FROM events
|
||||
WHERE tool_name LIKE 'mcp__plugin\\_%' ESCAPE '\\'
|
||||
AND event_type = 'PreToolUse'${sf.clause}
|
||||
GROUP BY tool_name`
|
||||
)
|
||||
.all(...sf.params);
|
||||
|
||||
const byPlugin = new Map();
|
||||
const bump = (name, field, n) => {
|
||||
const e = byPlugin.get(name) || { name, skills: 0, skillCalls: 0, mcpCalls: 0 };
|
||||
e[field] += n;
|
||||
byPlugin.set(name, e);
|
||||
};
|
||||
for (const s of skills) {
|
||||
if (!s.plugin) continue;
|
||||
bump(s.plugin, "skills", 1);
|
||||
bump(s.plugin, "skillCalls", s.count);
|
||||
}
|
||||
for (const m of mcpCalls) {
|
||||
// mcp__plugin_<plugin>_<server>__<tool>. Both halves may contain
|
||||
// underscores, so the split is only unambiguous when the plugin names its
|
||||
// server after itself — the overwhelmingly common case (`ccam_ccam`,
|
||||
// `vercel_vercel`), which the dedupe below collapses back to one name.
|
||||
// ponytail: a plugin whose server has its own name reports under
|
||||
// "<plugin>_<server>"; read the real pairing off the MCP config if that
|
||||
// ever matters.
|
||||
const seg = m.tool_name.slice("mcp__plugin_".length).split("__")[0];
|
||||
const doubled = seg.match(/^(.+)_\1$/);
|
||||
const plugin = doubled ? doubled[1] : seg;
|
||||
if (plugin) bump(plugin, "mcpCalls", m.count);
|
||||
}
|
||||
const plugins = [...byPlugin.values()].sort(
|
||||
(a, b) => b.skillCalls + b.mcpCalls - (a.skillCalls + a.mcpCalls)
|
||||
);
|
||||
|
||||
res.json({ skills, plugins });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: { message: err.message } });
|
||||
}
|
||||
});
|
||||
|
||||
// ── GET /session/:id — Single session drill-in ──
|
||||
router.get("/session/:id", (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user