@devflow-tools/cli 0.16.9 → 0.16.11
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.
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +354 -13
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/setup.d.ts +25 -2
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +71 -63
- package/dist/commands/setup.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/plugin-files/.claude-plugin/plugin.json +1 -1
- package/dist/plugin-files/dist/command-registry.json +39 -13
- package/dist/plugin-files/dist/hooks/hook-client.js +6 -6
- package/dist/plugin-files/dist/hooks/hook-daemon.js +14 -13
- package/dist/plugin-files/dist/hooks/post-tool-use-failure.js +1 -1
- package/dist/plugin-files/dist/hooks/post-tool-use.js +2 -2
- package/dist/plugin-files/dist/hooks/pre-compact.js +1 -1
- package/dist/plugin-files/dist/hooks/pre-tool-use.js +2 -2
- package/dist/plugin-files/dist/hooks/session-end.js +2 -2
- package/dist/plugin-files/dist/hooks/stop.js +2 -2
- package/dist/plugin-files/dist/hooks/user-prompt-submit.js +4 -3
- package/dist/plugin-files/dist/skills/devflow:animation/SKILL.md +3 -1
- package/dist/plugin-files/dist/skills/devflow:css/SKILL.md +3 -1
- package/dist/plugin-files/dist/skills/devflow:docker/SKILL.md +3 -1
- package/dist/plugin-files/dist/skills/devflow:electron/SKILL.md +3 -1
- package/dist/plugin-files/dist/skills/devflow:git/SKILL.md +3 -1
- package/dist/plugin-files/dist/skills/devflow:graphql/SKILL.md +3 -1
- package/dist/plugin-files/dist/skills/devflow:nest/SKILL.md +3 -1
- package/dist/plugin-files/dist/skills/devflow:nextjs/SKILL.md +3 -1
- package/dist/plugin-files/dist/skills/devflow:performance/SKILL.md +3 -1
- package/dist/plugin-files/dist/skills/devflow:react/SKILL.md +6 -4
- package/dist/plugin-files/dist/skills/devflow:tailwind/SKILL.md +3 -1
- package/dist/plugin-files/dist/skills/devflow:taro/SKILL.md +3 -1
- package/dist/plugin-files/dist/skills/devflow:ui-layout/SKILL.md +3 -1
- package/dist/plugin-files/dist/skills/devflow:vue/SKILL.md +3 -1
- package/dist/plugin-files/hooks/hooks.json +3 -2
- package/dist/plugin-files/hooks/session-start +54 -67
- package/dist/plugin-files/hooks/stop +6 -4
- package/dist/plugin-files/package.json +7 -7
- package/dist/plugin-files/skills/animation/SKILL.md +3 -1
- package/dist/plugin-files/skills/css/SKILL.md +3 -1
- package/dist/plugin-files/skills/docker/SKILL.md +3 -1
- package/dist/plugin-files/skills/doctor/SKILL.md +1 -1
- package/dist/plugin-files/skills/electron/SKILL.md +3 -1
- package/dist/plugin-files/skills/git/SKILL.md +3 -1
- package/dist/plugin-files/skills/graphql/SKILL.md +3 -1
- package/dist/plugin-files/skills/nest/SKILL.md +3 -1
- package/dist/plugin-files/skills/nextjs/SKILL.md +3 -1
- package/dist/plugin-files/skills/performance/SKILL.md +3 -1
- package/dist/plugin-files/skills/react/SKILL.md +6 -4
- package/dist/plugin-files/skills/tailwind/SKILL.md +3 -1
- package/dist/plugin-files/skills/taro/SKILL.md +3 -1
- package/dist/plugin-files/skills/ui-layout/SKILL.md +3 -1
- package/dist/plugin-files/skills/vue/SKILL.md +3 -1
- package/package.json +27 -27
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep }
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import chalk from "chalk";
|
|
9
|
-
import { DOCTOR_SCHEMA_VERSION, checkMcpHealth, loadPluginsWithDiagnostics, readMcpInventory, redactMcpInventory, } from "@devflow-tools/sdk";
|
|
9
|
+
import { DOCTOR_SCHEMA_VERSION, checkMcpHealth, loadPluginsWithDiagnostics, readMcpInventory, redactMcpInventory, selectHostAdapter, } from "@devflow-tools/sdk";
|
|
10
10
|
import { resolveActivePlugin, scanClaudeSkillShadowing } from "../lib/plugin-setup.js";
|
|
11
11
|
import { serverStatus } from "./server.js";
|
|
12
12
|
import { inspectSkillHealth } from "./skill-health.js";
|
|
@@ -47,6 +47,41 @@ function sectionStatus(metrics) {
|
|
|
47
47
|
async function checkRuntimeControlPlane(project, dependencies) {
|
|
48
48
|
const home = dependencies.homePath ?? homedir();
|
|
49
49
|
const metrics = [];
|
|
50
|
+
const hostSelection = await selectHostAdapter({
|
|
51
|
+
projectRoot: project,
|
|
52
|
+
homeDir: home,
|
|
53
|
+
env: dependencies.env ?? process.env,
|
|
54
|
+
explicitHost: (dependencies.env ?? process.env).DEVFLOW_HOST_ADAPTER,
|
|
55
|
+
});
|
|
56
|
+
metrics.push({
|
|
57
|
+
key: "host_adapter",
|
|
58
|
+
value: hostSelection.status === "selected"
|
|
59
|
+
? { id: hostSelection.adapter.id, candidates: hostSelection.candidates, explicit: hostSelection.explicit }
|
|
60
|
+
: {
|
|
61
|
+
status: "failed",
|
|
62
|
+
code: hostSelection.code,
|
|
63
|
+
candidates: hostSelection.candidates,
|
|
64
|
+
reason: hostSelection.reason,
|
|
65
|
+
action: hostSelection.action,
|
|
66
|
+
},
|
|
67
|
+
target: "one explicitly selected or uniquely detected host",
|
|
68
|
+
ok: hostSelection.status === "selected",
|
|
69
|
+
severity: "warning",
|
|
70
|
+
});
|
|
71
|
+
if (hostSelection.status === "selected") {
|
|
72
|
+
const capabilities = await hostSelection.adapter.capabilities({
|
|
73
|
+
projectRoot: project,
|
|
74
|
+
homeDir: home,
|
|
75
|
+
env: dependencies.env ?? process.env,
|
|
76
|
+
});
|
|
77
|
+
metrics.push({
|
|
78
|
+
key: "host_capabilities",
|
|
79
|
+
value: capabilities,
|
|
80
|
+
target: "unsupported capabilities have explicit degradation",
|
|
81
|
+
ok: capabilities.every(entry => entry.status === "supported" || Boolean(entry.reason && entry.fallback)),
|
|
82
|
+
severity: "error",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
50
85
|
const loaded = await loadPluginsWithDiagnostics({ projectRoot: project, homeDir: home });
|
|
51
86
|
const failedPlugins = loaded.diagnostics.filter((diagnostic) => diagnostic.status === "failed");
|
|
52
87
|
metrics.push({
|
|
@@ -56,7 +91,9 @@ async function checkRuntimeControlPlane(project, dependencies) {
|
|
|
56
91
|
ok: failedPlugins.length === 0,
|
|
57
92
|
severity: "warning",
|
|
58
93
|
});
|
|
59
|
-
const inventory = redactMcpInventory(readMcpInventory(project, home
|
|
94
|
+
const inventory = redactMcpInventory(readMcpInventory(project, home, {
|
|
95
|
+
env: dependencies.env ?? process.env,
|
|
96
|
+
}));
|
|
60
97
|
metrics.push({
|
|
61
98
|
key: "mcp_inventory",
|
|
62
99
|
value: { servers: inventory.servers.length, duplicates: inventory.duplicates.length, unreadable: inventory.unreadable.length },
|
|
@@ -517,6 +554,60 @@ function doctorCount(db, sql, ...params) {
|
|
|
517
554
|
const row = db.prepare(sql).get(...params);
|
|
518
555
|
return Number(row?.count ?? 0);
|
|
519
556
|
}
|
|
557
|
+
function parseDoctorJson(value) {
|
|
558
|
+
if (typeof value !== "string")
|
|
559
|
+
return value;
|
|
560
|
+
try {
|
|
561
|
+
return JSON.parse(value);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function doctorRecord(value) {
|
|
568
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
569
|
+
? value
|
|
570
|
+
: undefined;
|
|
571
|
+
}
|
|
572
|
+
function semanticToolOutcome(row) {
|
|
573
|
+
const output = doctorRecord(parseDoctorJson(row.output));
|
|
574
|
+
const data = doctorRecord(output?.data);
|
|
575
|
+
const status = typeof output?.status === "string" ? output.status : undefined;
|
|
576
|
+
const metricStatus = typeof row.metric_status === "string" ? row.metric_status : undefined;
|
|
577
|
+
const failed = Boolean(row.error)
|
|
578
|
+
|| Boolean(row.failure_category)
|
|
579
|
+
|| output?.ok === false
|
|
580
|
+
|| output?.success === false
|
|
581
|
+
|| output?.isError === true
|
|
582
|
+
|| Boolean(output?.error)
|
|
583
|
+
|| status === "failed"
|
|
584
|
+
|| status === "rejected"
|
|
585
|
+
|| metricStatus === "error";
|
|
586
|
+
const degraded = !failed && (output?.degraded === true
|
|
587
|
+
|| Boolean(output?.degradation)
|
|
588
|
+
|| data?.degraded === true
|
|
589
|
+
|| status === "degraded"
|
|
590
|
+
|| metricStatus === "degraded");
|
|
591
|
+
return { failed, degraded };
|
|
592
|
+
}
|
|
593
|
+
function fallbackReasons(value) {
|
|
594
|
+
const parsed = parseDoctorJson(value);
|
|
595
|
+
if (typeof parsed === "string" && parsed.trim())
|
|
596
|
+
return [parsed.trim()];
|
|
597
|
+
if (Array.isArray(parsed)) {
|
|
598
|
+
return parsed.filter((reason) => typeof reason === "string" && reason.trim().length > 0)
|
|
599
|
+
.map(reason => reason.trim());
|
|
600
|
+
}
|
|
601
|
+
const record = doctorRecord(parsed);
|
|
602
|
+
return record
|
|
603
|
+
? Object.entries(record)
|
|
604
|
+
.filter(([, count]) => typeof count === "number" && count > 0)
|
|
605
|
+
.map(([reason]) => reason)
|
|
606
|
+
: [];
|
|
607
|
+
}
|
|
608
|
+
function rate(count, total) {
|
|
609
|
+
return total === 0 ? 0 : count / total;
|
|
610
|
+
}
|
|
520
611
|
function coverage(total, vectors) {
|
|
521
612
|
return total === 0 ? 1 : vectors / total;
|
|
522
613
|
}
|
|
@@ -690,6 +781,7 @@ function checkTelemetry(project, strict, dependencies) {
|
|
|
690
781
|
? join(home, ".devflow", "global", "devflow.db")
|
|
691
782
|
: getGlobalDevFlowDbPath(home);
|
|
692
783
|
const cutoff = (dependencies.now ?? Date.now)() - 24 * 60 * 60 * 1000;
|
|
784
|
+
const now = (dependencies.now ?? Date.now)();
|
|
693
785
|
const strictSeverity = strict ? "error" : "warning";
|
|
694
786
|
if (!existsSync(globalDbPath)) {
|
|
695
787
|
metrics.push({ key: "global_telemetry_db", value: "missing", target: globalDbPath, ok: false, severity: "warning" });
|
|
@@ -744,6 +836,67 @@ function checkTelemetry(project, strict, dependencies) {
|
|
|
744
836
|
ok: staleSessions === 0,
|
|
745
837
|
severity: "error",
|
|
746
838
|
});
|
|
839
|
+
const closureRows = db.prepare(`
|
|
840
|
+
SELECT status, COUNT(*) count
|
|
841
|
+
FROM sessions
|
|
842
|
+
WHERE project_root = ?
|
|
843
|
+
GROUP BY status
|
|
844
|
+
`).all(project);
|
|
845
|
+
const closureCounts = { running: 0, completed: 0, failed: 0 };
|
|
846
|
+
for (const row of closureRows) {
|
|
847
|
+
if (row.status in closureCounts) {
|
|
848
|
+
closureCounts[row.status] = Number(row.count);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const totalSessions = closureCounts.running + closureCounts.completed + closureCounts.failed;
|
|
852
|
+
const closedSessions = closureCounts.completed + closureCounts.failed;
|
|
853
|
+
metrics.push({
|
|
854
|
+
key: "project_session_closure_state",
|
|
855
|
+
value: {
|
|
856
|
+
total: totalSessions,
|
|
857
|
+
...closureCounts,
|
|
858
|
+
closed: closedSessions,
|
|
859
|
+
closureRate: totalSessions === 0 ? 1 : closedSessions / totalSessions,
|
|
860
|
+
},
|
|
861
|
+
target: "running sessions close through SessionEnd",
|
|
862
|
+
ok: true,
|
|
863
|
+
severity: "info",
|
|
864
|
+
});
|
|
865
|
+
if (doctorTableExists(db, "devflow_session_closures")) {
|
|
866
|
+
const durableRows = db.prepare(`
|
|
867
|
+
SELECT state, COUNT(*) count
|
|
868
|
+
FROM devflow_session_closures
|
|
869
|
+
WHERE project_root = ?
|
|
870
|
+
GROUP BY state
|
|
871
|
+
`).all(project);
|
|
872
|
+
const durable = { active: 0, closing: 0, closed: 0, closed_with_pending_work: 0 };
|
|
873
|
+
for (const row of durableRows) {
|
|
874
|
+
if (row.state in durable) {
|
|
875
|
+
durable[row.state] = Number(row.count);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
const staleClosing = doctorCount(db, `
|
|
879
|
+
SELECT COUNT(*) count
|
|
880
|
+
FROM devflow_session_closures
|
|
881
|
+
WHERE project_root = ? AND state = 'closing' AND updated_at < ?
|
|
882
|
+
`, project, now - SESSION_STALE_MS);
|
|
883
|
+
metrics.push({
|
|
884
|
+
key: "durable_session_closure_state",
|
|
885
|
+
value: { ...durable, staleClosing },
|
|
886
|
+
target: { active: 0, closing: 0, staleClosing: 0 },
|
|
887
|
+
ok: durable.active === 0 && durable.closing === 0 && staleClosing === 0,
|
|
888
|
+
severity: strictSeverity,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
metrics.push({
|
|
893
|
+
key: "durable_session_closure_state",
|
|
894
|
+
value: "closure table missing",
|
|
895
|
+
target: "active -> closing -> closed | closed_with_pending_work",
|
|
896
|
+
ok: false,
|
|
897
|
+
severity: strictSeverity,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
747
900
|
let orphanToolEvents = 0;
|
|
748
901
|
if (toolEventColumns.has("session_id")) {
|
|
749
902
|
orphanToolEvents = doctorCount(db, `
|
|
@@ -769,20 +922,172 @@ function checkTelemetry(project, strict, dependencies) {
|
|
|
769
922
|
ok: orphanToolEvents === 0 && orphanExecutions === 0,
|
|
770
923
|
severity: "error",
|
|
771
924
|
});
|
|
925
|
+
const staleExecutionSchemaReady = ["session_id", "status", "started_at"]
|
|
926
|
+
.every(column => executionColumns.has(column));
|
|
927
|
+
if (staleExecutionSchemaReady) {
|
|
928
|
+
const staleCutoff = now - 30 * 60 * 1000;
|
|
929
|
+
const staleExecutions = db.prepare(`
|
|
930
|
+
SELECT
|
|
931
|
+
COUNT(*) total,
|
|
932
|
+
COALESCE(SUM(CASE WHEN execution.started_at < ? THEN 1 ELSE 0 END), 0) older_than_30m,
|
|
933
|
+
COALESCE(SUM(CASE WHEN session.status <> 'running' THEN 1 ELSE 0 END), 0) non_running_session
|
|
934
|
+
FROM skill_executions execution
|
|
935
|
+
JOIN sessions session ON session.id = execution.session_id
|
|
936
|
+
WHERE session.project_root = ?
|
|
937
|
+
AND execution.status = 'running'
|
|
938
|
+
AND (execution.started_at < ? OR session.status <> 'running')
|
|
939
|
+
`).get(staleCutoff, project, staleCutoff);
|
|
940
|
+
const staleTotal = Number(staleExecutions?.total ?? 0);
|
|
941
|
+
metrics.push({
|
|
942
|
+
key: "stale_skill_executions",
|
|
943
|
+
value: {
|
|
944
|
+
total: staleTotal,
|
|
945
|
+
olderThan30m: Number(staleExecutions?.older_than_30m ?? 0),
|
|
946
|
+
attachedToNonRunningSession: Number(staleExecutions?.non_running_session ?? 0),
|
|
947
|
+
},
|
|
948
|
+
target: { total: 0 },
|
|
949
|
+
ok: staleTotal === 0,
|
|
950
|
+
severity: "error",
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
metrics.push({
|
|
955
|
+
key: "skill_execution_schema",
|
|
956
|
+
value: "status, started_at, or session_id column missing",
|
|
957
|
+
target: "canonical Skill execution lifecycle schema",
|
|
958
|
+
ok: false,
|
|
959
|
+
severity: "warning",
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (!doctorTableExists(db, "devflow_work_items")) {
|
|
964
|
+
metrics.push({
|
|
965
|
+
key: "work_queue_schema",
|
|
966
|
+
value: "devflow_work_items table missing; queue diagnostics degraded",
|
|
967
|
+
target: "canonical global work queue schema",
|
|
968
|
+
ok: false,
|
|
969
|
+
severity: "error",
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
const queueColumns = doctorTableColumns(db, "devflow_work_items");
|
|
974
|
+
const requiredQueueColumns = [
|
|
975
|
+
"project_root", "state", "lease_expires_at", "created_at", "completed_at",
|
|
976
|
+
];
|
|
977
|
+
const missingQueueColumns = requiredQueueColumns.filter(column => !queueColumns.has(column));
|
|
978
|
+
if (missingQueueColumns.length > 0) {
|
|
979
|
+
metrics.push({
|
|
980
|
+
key: "work_queue_schema",
|
|
981
|
+
value: { missingColumns: missingQueueColumns, diagnostic: "queue diagnostics degraded" },
|
|
982
|
+
target: "canonical global work queue schema",
|
|
983
|
+
ok: false,
|
|
984
|
+
severity: "error",
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
const queue = db.prepare(`
|
|
989
|
+
SELECT
|
|
990
|
+
COALESCE(SUM(CASE WHEN state IN ('pending', 'leased', 'failed') THEN 1 ELSE 0 END), 0) queue_depth,
|
|
991
|
+
COALESCE(SUM(CASE WHEN state = 'pending' THEN 1 ELSE 0 END), 0) pending,
|
|
992
|
+
COALESCE(SUM(CASE WHEN state = 'leased' THEN 1 ELSE 0 END), 0) leased,
|
|
993
|
+
COALESCE(SUM(CASE WHEN state = 'failed' THEN 1 ELSE 0 END), 0) failed,
|
|
994
|
+
COALESCE(SUM(CASE WHEN state = 'dead_letter' THEN 1 ELSE 0 END), 0) dead_letters,
|
|
995
|
+
COALESCE(SUM(CASE WHEN state = 'leased' AND lease_expires_at <= ? THEN 1 ELSE 0 END), 0) expired_leases,
|
|
996
|
+
MIN(CASE WHEN state IN ('pending', 'leased', 'failed') THEN created_at END) oldest_pending_at,
|
|
997
|
+
MAX(CASE WHEN state = 'completed' THEN completed_at END) last_successful_drain_at
|
|
998
|
+
FROM devflow_work_items
|
|
999
|
+
WHERE project_root = ?
|
|
1000
|
+
`).get(now, project);
|
|
1001
|
+
const queueDepth = Number(queue?.queue_depth ?? 0);
|
|
1002
|
+
const pending = Number(queue?.pending ?? 0);
|
|
1003
|
+
const leased = Number(queue?.leased ?? 0);
|
|
1004
|
+
const failed = Number(queue?.failed ?? 0);
|
|
1005
|
+
const deadLetters = Number(queue?.dead_letters ?? 0);
|
|
1006
|
+
const expiredLeases = Number(queue?.expired_leases ?? 0);
|
|
1007
|
+
const oldestPendingAt = queue?.oldest_pending_at == null
|
|
1008
|
+
? undefined
|
|
1009
|
+
: Number(queue.oldest_pending_at);
|
|
1010
|
+
const lastSuccessfulDrainAt = queue?.last_successful_drain_at == null
|
|
1011
|
+
? undefined
|
|
1012
|
+
: Number(queue.last_successful_drain_at);
|
|
1013
|
+
metrics.push({ key: "work_queue_schema", value: "canonical", target: "canonical global work queue schema", ok: true, severity: "info" });
|
|
1014
|
+
metrics.push({ key: "work_queue_depth", value: queueDepth, ok: true, severity: "info" });
|
|
1015
|
+
metrics.push({ key: "work_queue_pending", value: pending, ok: true, severity: "info" });
|
|
1016
|
+
metrics.push({ key: "work_queue_leased", value: leased, ok: true, severity: "info" });
|
|
1017
|
+
metrics.push({ key: "work_queue_failed", value: failed, target: 0, ok: failed === 0, severity: "warning" });
|
|
1018
|
+
metrics.push({
|
|
1019
|
+
key: "work_queue_oldest_pending_age_ms",
|
|
1020
|
+
value: oldestPendingAt === undefined ? 0 : Math.max(0, now - oldestPendingAt),
|
|
1021
|
+
ok: true,
|
|
1022
|
+
severity: "info",
|
|
1023
|
+
});
|
|
1024
|
+
metrics.push({ key: "work_queue_dead_letters", value: deadLetters, target: 0, ok: deadLetters === 0, severity: "error" });
|
|
1025
|
+
metrics.push({ key: "work_queue_expired_leases", value: expiredLeases, target: 0, ok: expiredLeases === 0, severity: "error" });
|
|
1026
|
+
metrics.push({ key: "work_queue_last_successful_drain_at", value: lastSuccessfulDrainAt ?? null, ok: true, severity: "info" });
|
|
1027
|
+
}
|
|
772
1028
|
}
|
|
1029
|
+
const hookStateDir = dependencies.env?.DEVFLOW_STATE_DIR
|
|
1030
|
+
?? join(home, ".devflow", "state", getProjectHash(project));
|
|
1031
|
+
const spoolKinds = ["session-start-pending", "memory-turn-pending", "session-finalize-pending"];
|
|
1032
|
+
const spoolBacklog = Object.fromEntries(spoolKinds.map(kind => {
|
|
1033
|
+
const directory = join(hookStateDir, kind);
|
|
1034
|
+
const count = existsSync(directory)
|
|
1035
|
+
? readdirSync(directory).filter(name => name.includes(".json")).length
|
|
1036
|
+
: 0;
|
|
1037
|
+
return [kind, count];
|
|
1038
|
+
}));
|
|
1039
|
+
const totalSpoolBacklog = Object.values(spoolBacklog).reduce((sum, count) => sum + count, 0);
|
|
1040
|
+
metrics.push({
|
|
1041
|
+
key: "hook_spool_backlog",
|
|
1042
|
+
value: { total: totalSpoolBacklog, ...spoolBacklog },
|
|
1043
|
+
target: { total: 0 },
|
|
1044
|
+
ok: totalSpoolBacklog === 0,
|
|
1045
|
+
severity: strictSeverity,
|
|
1046
|
+
});
|
|
773
1047
|
if (doctorTableExists(db, "tool_call_events")) {
|
|
1048
|
+
const canProjectFilterEvents = sessionSchemaReady && toolEventColumns.has("session_id");
|
|
1049
|
+
const metricColumns = doctorTableColumns(db, "tool_metrics");
|
|
1050
|
+
const canJoinMetrics = toolEventColumns.has("event_id")
|
|
1051
|
+
&& metricColumns.has("tool_call_event_id")
|
|
1052
|
+
&& metricColumns.has("status");
|
|
1053
|
+
const selectedColumns = [
|
|
1054
|
+
toolEventColumns.has("error") ? "event.error" : "NULL error",
|
|
1055
|
+
toolEventColumns.has("failure_category") ? "event.failure_category" : "NULL failure_category",
|
|
1056
|
+
toolEventColumns.has("output") ? "event.output" : "NULL output",
|
|
1057
|
+
canJoinMetrics ? "metric.status metric_status" : "NULL metric_status",
|
|
1058
|
+
].join(", ");
|
|
1059
|
+
const eventRows = db.prepare(`
|
|
1060
|
+
SELECT ${selectedColumns}
|
|
1061
|
+
FROM tool_call_events event
|
|
1062
|
+
${canJoinMetrics ? "LEFT JOIN tool_metrics metric ON metric.tool_call_event_id = event.event_id" : ""}
|
|
1063
|
+
${canProjectFilterEvents ? "JOIN sessions event_session ON event_session.id = event.session_id" : ""}
|
|
1064
|
+
WHERE event.timestamp >= ?
|
|
1065
|
+
${canProjectFilterEvents ? "AND event_session.project_root = ?" : ""}
|
|
1066
|
+
`).all(...(canProjectFilterEvents ? [cutoff, project] : [cutoff]));
|
|
1067
|
+
const semantic = eventRows.map(semanticToolOutcome);
|
|
1068
|
+
const semanticFailures = semantic.filter(outcome => outcome.failed).length;
|
|
1069
|
+
const semanticDegradations = semantic.filter(outcome => outcome.degraded).length;
|
|
1070
|
+
metrics.push({ key: "semantic_tool_calls_24h", value: eventRows.length, ok: true, severity: "info" });
|
|
1071
|
+
metrics.push({ key: "semantic_tool_failures_24h", value: semanticFailures, target: 0, ok: semanticFailures === 0, severity: strictSeverity });
|
|
1072
|
+
metrics.push({ key: "semantic_tool_degradations_24h", value: semanticDegradations, target: 0, ok: semanticDegradations === 0, severity: strictSeverity });
|
|
1073
|
+
metrics.push({ key: "semantic_tool_failure_rate_24h", value: rate(semanticFailures, eventRows.length), target: 0, ok: semanticFailures === 0, severity: strictSeverity });
|
|
1074
|
+
metrics.push({ key: "semantic_tool_degradation_rate_24h", value: rate(semanticDegradations, eventRows.length), target: 0, ok: semanticDegradations === 0, severity: strictSeverity });
|
|
1075
|
+
metrics.push({
|
|
1076
|
+
key: "semantic_tool_project_scope",
|
|
1077
|
+
value: canProjectFilterEvents ? "project" : "global fallback; session identity unavailable",
|
|
1078
|
+
target: "project",
|
|
1079
|
+
ok: canProjectFilterEvents,
|
|
1080
|
+
severity: "warning",
|
|
1081
|
+
});
|
|
774
1082
|
if (toolEventColumns.has("failure_category")) {
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
const failedEvents = doctorCount(db, "SELECT COUNT(*) count FROM tool_call_events WHERE timestamp >= ? AND error IS NOT NULL", cutoff);
|
|
784
|
-
const classifiedEvents = rows.reduce((sum, row) => sum + Number(row.count), 0);
|
|
785
|
-
const classificationCoverage = failedEvents === 0 ? 1 : classifiedEvents / failedEvents;
|
|
1083
|
+
const distribution = {};
|
|
1084
|
+
for (const row of eventRows) {
|
|
1085
|
+
if (typeof row.failure_category !== "string" || !row.failure_category.trim())
|
|
1086
|
+
continue;
|
|
1087
|
+
distribution[row.failure_category] = (distribution[row.failure_category] ?? 0) + 1;
|
|
1088
|
+
}
|
|
1089
|
+
const classifiedEvents = Object.values(distribution).reduce((sum, count) => sum + count, 0);
|
|
1090
|
+
const classificationCoverage = semanticFailures === 0 ? 1 : classifiedEvents / semanticFailures;
|
|
786
1091
|
metrics.push({
|
|
787
1092
|
key: "failure_categories_24h",
|
|
788
1093
|
value: distribution,
|
|
@@ -807,6 +1112,42 @@ function checkTelemetry(project, strict, dependencies) {
|
|
|
807
1112
|
});
|
|
808
1113
|
}
|
|
809
1114
|
}
|
|
1115
|
+
const skillFallbackSchemaReady = sessionSchemaReady
|
|
1116
|
+
&& ["session_id", "started_at", "fallback_count", "fallback_reasons"]
|
|
1117
|
+
.every(column => executionColumns.has(column));
|
|
1118
|
+
let skillFallbackCount = 0;
|
|
1119
|
+
const fallbackReasonCounts = {};
|
|
1120
|
+
if (skillFallbackSchemaReady) {
|
|
1121
|
+
const rows = db.prepare(`
|
|
1122
|
+
SELECT execution.fallback_count, execution.fallback_reasons
|
|
1123
|
+
FROM skill_executions execution
|
|
1124
|
+
JOIN sessions session ON session.id = execution.session_id
|
|
1125
|
+
WHERE session.project_root = ? AND execution.started_at >= ?
|
|
1126
|
+
`).all(project, cutoff);
|
|
1127
|
+
for (const row of rows) {
|
|
1128
|
+
skillFallbackCount += Number(row.fallback_count ?? 0);
|
|
1129
|
+
for (const reason of fallbackReasons(row.fallback_reasons)) {
|
|
1130
|
+
fallbackReasonCounts[reason] = (fallbackReasonCounts[reason] ?? 0) + 1;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
let hookFallbackCount = 0;
|
|
1135
|
+
if (doctorTableExists(db, "devflow_hook_fallbacks")) {
|
|
1136
|
+
const rows = db.prepare(`
|
|
1137
|
+
SELECT reason, COUNT(*) count
|
|
1138
|
+
FROM devflow_hook_fallbacks
|
|
1139
|
+
WHERE project_root = ? AND created_at >= ?
|
|
1140
|
+
GROUP BY reason ORDER BY reason
|
|
1141
|
+
`).all(project, cutoff);
|
|
1142
|
+
for (const row of rows) {
|
|
1143
|
+
const count = Number(row.count);
|
|
1144
|
+
hookFallbackCount += count;
|
|
1145
|
+
fallbackReasonCounts[`hook:${row.reason}`] = count;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
metrics.push({ key: "skill_fallbacks_24h", value: skillFallbackCount, target: 0, ok: skillFallbackCount === 0, severity: "warning" });
|
|
1149
|
+
metrics.push({ key: "hook_fallbacks_24h", value: hookFallbackCount, target: 0, ok: hookFallbackCount === 0, severity: "warning" });
|
|
1150
|
+
metrics.push({ key: "fallback_reasons_24h", value: fallbackReasonCounts, target: {}, ok: Object.keys(fallbackReasonCounts).length === 0, severity: "warning" });
|
|
810
1151
|
}
|
|
811
1152
|
catch (error) {
|
|
812
1153
|
metrics.push({ key: "global_telemetry_db_read", value: error.message, target: "readable SQLite database", ok: false, severity: "error" });
|