@devflow-tools/cli 0.16.10 → 0.16.12
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 +5 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +408 -16
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/embeddings.d.ts +8 -0
- package/dist/commands/embeddings.d.ts.map +1 -0
- package/dist/commands/embeddings.js +26 -0
- package/dist/commands/embeddings.js.map +1 -0
- 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 +52 -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 +5 -5
- package/dist/plugin-files/dist/hooks/hook-daemon.js +32 -14
- package/dist/plugin-files/dist/hooks/memory-snapshot-cache.js +11 -0
- package/dist/plugin-files/dist/hooks/post-tool-use-failure.js +2 -2
- package/dist/plugin-files/dist/hooks/post-tool-use.js +6 -3
- package/dist/plugin-files/dist/hooks/pre-compact.js +2 -2
- 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 +15 -2
- package/dist/plugin-files/dist/hooks/user-prompt-submit.js +14 -5
- 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 +14 -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 +66 -82
- 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 +14 -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,4 +1,5 @@
|
|
|
1
1
|
import type { DoctorReportV1 } from "@devflow-tools/sdk";
|
|
2
|
+
import type { EmbeddingModelStatus } from "@devflow-tools/memory-engine";
|
|
2
3
|
export type { DoctorMetric, DoctorReportV1, DoctorSection, DoctorSeverity, DoctorStatus, } from "@devflow-tools/sdk";
|
|
3
4
|
export type DoctorResult = DoctorReportV1;
|
|
4
5
|
export interface DoctorOptions {
|
|
@@ -31,6 +32,10 @@ export interface DoctorDependencies {
|
|
|
31
32
|
httpGet?: (url: string) => Promise<{
|
|
32
33
|
status: number;
|
|
33
34
|
}>;
|
|
35
|
+
inspectEmbeddingModel?: (options: {
|
|
36
|
+
homeDir?: string;
|
|
37
|
+
env?: NodeJS.ProcessEnv;
|
|
38
|
+
}) => Promise<EmbeddingModelStatus>;
|
|
34
39
|
}
|
|
35
40
|
export interface DoctorCommandIO {
|
|
36
41
|
stdout(output: string): void;
|
|
@@ -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,11 +6,12 @@ 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";
|
|
13
13
|
import { getGlobalDevFlowDbPath } from "@devflow-tools/database";
|
|
14
|
+
import { inspectEmbeddingModel } from "@devflow-tools/memory-engine";
|
|
14
15
|
function getProjectHash(rootPath) {
|
|
15
16
|
return createHash("sha256").update(rootPath).digest("hex").slice(0, 16);
|
|
16
17
|
}
|
|
@@ -47,6 +48,41 @@ function sectionStatus(metrics) {
|
|
|
47
48
|
async function checkRuntimeControlPlane(project, dependencies) {
|
|
48
49
|
const home = dependencies.homePath ?? homedir();
|
|
49
50
|
const metrics = [];
|
|
51
|
+
const hostSelection = await selectHostAdapter({
|
|
52
|
+
projectRoot: project,
|
|
53
|
+
homeDir: home,
|
|
54
|
+
env: dependencies.env ?? process.env,
|
|
55
|
+
explicitHost: (dependencies.env ?? process.env).DEVFLOW_HOST_ADAPTER,
|
|
56
|
+
});
|
|
57
|
+
metrics.push({
|
|
58
|
+
key: "host_adapter",
|
|
59
|
+
value: hostSelection.status === "selected"
|
|
60
|
+
? { id: hostSelection.adapter.id, candidates: hostSelection.candidates, explicit: hostSelection.explicit }
|
|
61
|
+
: {
|
|
62
|
+
status: "failed",
|
|
63
|
+
code: hostSelection.code,
|
|
64
|
+
candidates: hostSelection.candidates,
|
|
65
|
+
reason: hostSelection.reason,
|
|
66
|
+
action: hostSelection.action,
|
|
67
|
+
},
|
|
68
|
+
target: "one explicitly selected or uniquely detected host",
|
|
69
|
+
ok: hostSelection.status === "selected",
|
|
70
|
+
severity: "warning",
|
|
71
|
+
});
|
|
72
|
+
if (hostSelection.status === "selected") {
|
|
73
|
+
const capabilities = await hostSelection.adapter.capabilities({
|
|
74
|
+
projectRoot: project,
|
|
75
|
+
homeDir: home,
|
|
76
|
+
env: dependencies.env ?? process.env,
|
|
77
|
+
});
|
|
78
|
+
metrics.push({
|
|
79
|
+
key: "host_capabilities",
|
|
80
|
+
value: capabilities,
|
|
81
|
+
target: "unsupported capabilities have explicit degradation",
|
|
82
|
+
ok: capabilities.every(entry => entry.status === "supported" || Boolean(entry.reason && entry.fallback)),
|
|
83
|
+
severity: "error",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
50
86
|
const loaded = await loadPluginsWithDiagnostics({ projectRoot: project, homeDir: home });
|
|
51
87
|
const failedPlugins = loaded.diagnostics.filter((diagnostic) => diagnostic.status === "failed");
|
|
52
88
|
metrics.push({
|
|
@@ -56,7 +92,9 @@ async function checkRuntimeControlPlane(project, dependencies) {
|
|
|
56
92
|
ok: failedPlugins.length === 0,
|
|
57
93
|
severity: "warning",
|
|
58
94
|
});
|
|
59
|
-
const inventory = redactMcpInventory(readMcpInventory(project, home
|
|
95
|
+
const inventory = redactMcpInventory(readMcpInventory(project, home, {
|
|
96
|
+
env: dependencies.env ?? process.env,
|
|
97
|
+
}));
|
|
60
98
|
metrics.push({
|
|
61
99
|
key: "mcp_inventory",
|
|
62
100
|
value: { servers: inventory.servers.length, duplicates: inventory.duplicates.length, unreadable: inventory.unreadable.length },
|
|
@@ -517,10 +555,64 @@ function doctorCount(db, sql, ...params) {
|
|
|
517
555
|
const row = db.prepare(sql).get(...params);
|
|
518
556
|
return Number(row?.count ?? 0);
|
|
519
557
|
}
|
|
558
|
+
function parseDoctorJson(value) {
|
|
559
|
+
if (typeof value !== "string")
|
|
560
|
+
return value;
|
|
561
|
+
try {
|
|
562
|
+
return JSON.parse(value);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function doctorRecord(value) {
|
|
569
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
570
|
+
? value
|
|
571
|
+
: undefined;
|
|
572
|
+
}
|
|
573
|
+
function semanticToolOutcome(row) {
|
|
574
|
+
const output = doctorRecord(parseDoctorJson(row.output));
|
|
575
|
+
const data = doctorRecord(output?.data);
|
|
576
|
+
const status = typeof output?.status === "string" ? output.status : undefined;
|
|
577
|
+
const metricStatus = typeof row.metric_status === "string" ? row.metric_status : undefined;
|
|
578
|
+
const failed = Boolean(row.error)
|
|
579
|
+
|| Boolean(row.failure_category)
|
|
580
|
+
|| output?.ok === false
|
|
581
|
+
|| output?.success === false
|
|
582
|
+
|| output?.isError === true
|
|
583
|
+
|| Boolean(output?.error)
|
|
584
|
+
|| status === "failed"
|
|
585
|
+
|| status === "rejected"
|
|
586
|
+
|| metricStatus === "error";
|
|
587
|
+
const degraded = !failed && (output?.degraded === true
|
|
588
|
+
|| Boolean(output?.degradation)
|
|
589
|
+
|| data?.degraded === true
|
|
590
|
+
|| status === "degraded"
|
|
591
|
+
|| metricStatus === "degraded");
|
|
592
|
+
return { failed, degraded };
|
|
593
|
+
}
|
|
594
|
+
function fallbackReasons(value) {
|
|
595
|
+
const parsed = parseDoctorJson(value);
|
|
596
|
+
if (typeof parsed === "string" && parsed.trim())
|
|
597
|
+
return [parsed.trim()];
|
|
598
|
+
if (Array.isArray(parsed)) {
|
|
599
|
+
return parsed.filter((reason) => typeof reason === "string" && reason.trim().length > 0)
|
|
600
|
+
.map(reason => reason.trim());
|
|
601
|
+
}
|
|
602
|
+
const record = doctorRecord(parsed);
|
|
603
|
+
return record
|
|
604
|
+
? Object.entries(record)
|
|
605
|
+
.filter(([, count]) => typeof count === "number" && count > 0)
|
|
606
|
+
.map(([reason]) => reason)
|
|
607
|
+
: [];
|
|
608
|
+
}
|
|
609
|
+
function rate(count, total) {
|
|
610
|
+
return total === 0 ? 0 : count / total;
|
|
611
|
+
}
|
|
520
612
|
function coverage(total, vectors) {
|
|
521
613
|
return total === 0 ? 1 : vectors / total;
|
|
522
614
|
}
|
|
523
|
-
function checkVectors(project, dependencies) {
|
|
615
|
+
async function checkVectors(project, dependencies) {
|
|
524
616
|
const metrics = [];
|
|
525
617
|
let codegraphNodes = null;
|
|
526
618
|
let symbolVectors = null;
|
|
@@ -646,11 +738,18 @@ function checkVectors(project, dependencies) {
|
|
|
646
738
|
metrics.push({ key: "observation_vector_schema", value: "observations/embeddings missing", target: "Batch 2 vector schema", ok: false, severity: "warning" });
|
|
647
739
|
}
|
|
648
740
|
else {
|
|
649
|
-
const
|
|
741
|
+
const observationColumns = doctorTableColumns(db, "observations");
|
|
742
|
+
const hasEligibilityColumns = observationColumns.has("quality_status")
|
|
743
|
+
&& observationColumns.has("resolution_state");
|
|
744
|
+
const eligibilityClause = hasEligibilityColumns
|
|
745
|
+
? "WHERE quality_status = 'accepted' AND resolution_state <> 'superseded'"
|
|
746
|
+
: "";
|
|
747
|
+
const observations = doctorCount(db, `SELECT COUNT(*) count FROM observations ${eligibilityClause}`);
|
|
650
748
|
const vectors = doctorCount(db, `
|
|
651
749
|
SELECT COUNT(DISTINCT e.source_id) count
|
|
652
750
|
FROM embeddings e JOIN observations o ON o.id = e.source_id
|
|
653
751
|
WHERE e.source_type = 'observation'
|
|
752
|
+
${hasEligibilityColumns ? "AND o.quality_status = 'accepted' AND o.resolution_state <> 'superseded'" : ""}
|
|
654
753
|
`);
|
|
655
754
|
const missing = Math.max(0, observations - vectors);
|
|
656
755
|
metrics.push({ key: "observations", value: observations, ok: true, severity: "info" });
|
|
@@ -681,6 +780,49 @@ function checkVectors(project, dependencies) {
|
|
|
681
780
|
}
|
|
682
781
|
}
|
|
683
782
|
metrics.push({ key: "embedder_errors_24h", value: recentErrors, target: 0, ok: recentErrors === 0, severity: "warning" });
|
|
783
|
+
try {
|
|
784
|
+
const model = await (dependencies.inspectEmbeddingModel ?? inspectEmbeddingModel)({
|
|
785
|
+
homeDir: dependencies.homePath,
|
|
786
|
+
env: dependencies.env ?? process.env,
|
|
787
|
+
});
|
|
788
|
+
metrics.push({
|
|
789
|
+
key: "embedding_model_state",
|
|
790
|
+
value: {
|
|
791
|
+
state: model.state,
|
|
792
|
+
modelId: model.modelId,
|
|
793
|
+
revision: model.revision,
|
|
794
|
+
},
|
|
795
|
+
target: { state: "ready" },
|
|
796
|
+
ok: model.ready,
|
|
797
|
+
severity: "warning",
|
|
798
|
+
});
|
|
799
|
+
metrics.push({
|
|
800
|
+
key: "embedding_model_cache_root",
|
|
801
|
+
value: model.cacheRoot,
|
|
802
|
+
target: "stable shared DevFlow cache",
|
|
803
|
+
ok: true,
|
|
804
|
+
severity: "info",
|
|
805
|
+
});
|
|
806
|
+
metrics.push({
|
|
807
|
+
key: "embedding_model_integrity",
|
|
808
|
+
value: {
|
|
809
|
+
missingFiles: model.missingFiles,
|
|
810
|
+
corruptFiles: model.corruptFiles,
|
|
811
|
+
},
|
|
812
|
+
target: { missingFiles: [], corruptFiles: [] },
|
|
813
|
+
ok: model.missingFiles.length === 0 && model.corruptFiles.length === 0,
|
|
814
|
+
severity: "warning",
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
metrics.push({
|
|
819
|
+
key: "embedding_model_inspection",
|
|
820
|
+
value: error.message,
|
|
821
|
+
target: "readable verified shared model cache",
|
|
822
|
+
ok: false,
|
|
823
|
+
severity: "warning",
|
|
824
|
+
});
|
|
825
|
+
}
|
|
684
826
|
return { name: "Vectors", status: sectionStatus(metrics), metrics };
|
|
685
827
|
}
|
|
686
828
|
function checkTelemetry(project, strict, dependencies) {
|
|
@@ -690,6 +832,7 @@ function checkTelemetry(project, strict, dependencies) {
|
|
|
690
832
|
? join(home, ".devflow", "global", "devflow.db")
|
|
691
833
|
: getGlobalDevFlowDbPath(home);
|
|
692
834
|
const cutoff = (dependencies.now ?? Date.now)() - 24 * 60 * 60 * 1000;
|
|
835
|
+
const now = (dependencies.now ?? Date.now)();
|
|
693
836
|
const strictSeverity = strict ? "error" : "warning";
|
|
694
837
|
if (!existsSync(globalDbPath)) {
|
|
695
838
|
metrics.push({ key: "global_telemetry_db", value: "missing", target: globalDbPath, ok: false, severity: "warning" });
|
|
@@ -744,6 +887,67 @@ function checkTelemetry(project, strict, dependencies) {
|
|
|
744
887
|
ok: staleSessions === 0,
|
|
745
888
|
severity: "error",
|
|
746
889
|
});
|
|
890
|
+
const closureRows = db.prepare(`
|
|
891
|
+
SELECT status, COUNT(*) count
|
|
892
|
+
FROM sessions
|
|
893
|
+
WHERE project_root = ?
|
|
894
|
+
GROUP BY status
|
|
895
|
+
`).all(project);
|
|
896
|
+
const closureCounts = { running: 0, completed: 0, failed: 0 };
|
|
897
|
+
for (const row of closureRows) {
|
|
898
|
+
if (row.status in closureCounts) {
|
|
899
|
+
closureCounts[row.status] = Number(row.count);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const totalSessions = closureCounts.running + closureCounts.completed + closureCounts.failed;
|
|
903
|
+
const closedSessions = closureCounts.completed + closureCounts.failed;
|
|
904
|
+
metrics.push({
|
|
905
|
+
key: "project_session_closure_state",
|
|
906
|
+
value: {
|
|
907
|
+
total: totalSessions,
|
|
908
|
+
...closureCounts,
|
|
909
|
+
closed: closedSessions,
|
|
910
|
+
closureRate: totalSessions === 0 ? 1 : closedSessions / totalSessions,
|
|
911
|
+
},
|
|
912
|
+
target: "running sessions close through SessionEnd",
|
|
913
|
+
ok: true,
|
|
914
|
+
severity: "info",
|
|
915
|
+
});
|
|
916
|
+
if (doctorTableExists(db, "devflow_session_closures")) {
|
|
917
|
+
const durableRows = db.prepare(`
|
|
918
|
+
SELECT state, COUNT(*) count
|
|
919
|
+
FROM devflow_session_closures
|
|
920
|
+
WHERE project_root = ?
|
|
921
|
+
GROUP BY state
|
|
922
|
+
`).all(project);
|
|
923
|
+
const durable = { active: 0, closing: 0, closed: 0, closed_with_pending_work: 0 };
|
|
924
|
+
for (const row of durableRows) {
|
|
925
|
+
if (row.state in durable) {
|
|
926
|
+
durable[row.state] = Number(row.count);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const staleClosing = doctorCount(db, `
|
|
930
|
+
SELECT COUNT(*) count
|
|
931
|
+
FROM devflow_session_closures
|
|
932
|
+
WHERE project_root = ? AND state = 'closing' AND updated_at < ?
|
|
933
|
+
`, project, now - SESSION_STALE_MS);
|
|
934
|
+
metrics.push({
|
|
935
|
+
key: "durable_session_closure_state",
|
|
936
|
+
value: { ...durable, staleClosing },
|
|
937
|
+
target: { active: 0, closing: 0, staleClosing: 0 },
|
|
938
|
+
ok: durable.active === 0 && durable.closing === 0 && staleClosing === 0,
|
|
939
|
+
severity: strictSeverity,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
metrics.push({
|
|
944
|
+
key: "durable_session_closure_state",
|
|
945
|
+
value: "closure table missing",
|
|
946
|
+
target: "active -> closing -> closed | closed_with_pending_work",
|
|
947
|
+
ok: false,
|
|
948
|
+
severity: strictSeverity,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
747
951
|
let orphanToolEvents = 0;
|
|
748
952
|
if (toolEventColumns.has("session_id")) {
|
|
749
953
|
orphanToolEvents = doctorCount(db, `
|
|
@@ -769,20 +973,172 @@ function checkTelemetry(project, strict, dependencies) {
|
|
|
769
973
|
ok: orphanToolEvents === 0 && orphanExecutions === 0,
|
|
770
974
|
severity: "error",
|
|
771
975
|
});
|
|
976
|
+
const staleExecutionSchemaReady = ["session_id", "status", "started_at"]
|
|
977
|
+
.every(column => executionColumns.has(column));
|
|
978
|
+
if (staleExecutionSchemaReady) {
|
|
979
|
+
const staleCutoff = now - 30 * 60 * 1000;
|
|
980
|
+
const staleExecutions = db.prepare(`
|
|
981
|
+
SELECT
|
|
982
|
+
COUNT(*) total,
|
|
983
|
+
COALESCE(SUM(CASE WHEN execution.started_at < ? THEN 1 ELSE 0 END), 0) older_than_30m,
|
|
984
|
+
COALESCE(SUM(CASE WHEN session.status <> 'running' THEN 1 ELSE 0 END), 0) non_running_session
|
|
985
|
+
FROM skill_executions execution
|
|
986
|
+
JOIN sessions session ON session.id = execution.session_id
|
|
987
|
+
WHERE session.project_root = ?
|
|
988
|
+
AND execution.status = 'running'
|
|
989
|
+
AND (execution.started_at < ? OR session.status <> 'running')
|
|
990
|
+
`).get(staleCutoff, project, staleCutoff);
|
|
991
|
+
const staleTotal = Number(staleExecutions?.total ?? 0);
|
|
992
|
+
metrics.push({
|
|
993
|
+
key: "stale_skill_executions",
|
|
994
|
+
value: {
|
|
995
|
+
total: staleTotal,
|
|
996
|
+
olderThan30m: Number(staleExecutions?.older_than_30m ?? 0),
|
|
997
|
+
attachedToNonRunningSession: Number(staleExecutions?.non_running_session ?? 0),
|
|
998
|
+
},
|
|
999
|
+
target: { total: 0 },
|
|
1000
|
+
ok: staleTotal === 0,
|
|
1001
|
+
severity: "error",
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
metrics.push({
|
|
1006
|
+
key: "skill_execution_schema",
|
|
1007
|
+
value: "status, started_at, or session_id column missing",
|
|
1008
|
+
target: "canonical Skill execution lifecycle schema",
|
|
1009
|
+
ok: false,
|
|
1010
|
+
severity: "warning",
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (!doctorTableExists(db, "devflow_work_items")) {
|
|
1015
|
+
metrics.push({
|
|
1016
|
+
key: "work_queue_schema",
|
|
1017
|
+
value: "devflow_work_items table missing; queue diagnostics degraded",
|
|
1018
|
+
target: "canonical global work queue schema",
|
|
1019
|
+
ok: false,
|
|
1020
|
+
severity: "error",
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
const queueColumns = doctorTableColumns(db, "devflow_work_items");
|
|
1025
|
+
const requiredQueueColumns = [
|
|
1026
|
+
"project_root", "state", "lease_expires_at", "created_at", "completed_at",
|
|
1027
|
+
];
|
|
1028
|
+
const missingQueueColumns = requiredQueueColumns.filter(column => !queueColumns.has(column));
|
|
1029
|
+
if (missingQueueColumns.length > 0) {
|
|
1030
|
+
metrics.push({
|
|
1031
|
+
key: "work_queue_schema",
|
|
1032
|
+
value: { missingColumns: missingQueueColumns, diagnostic: "queue diagnostics degraded" },
|
|
1033
|
+
target: "canonical global work queue schema",
|
|
1034
|
+
ok: false,
|
|
1035
|
+
severity: "error",
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
const queue = db.prepare(`
|
|
1040
|
+
SELECT
|
|
1041
|
+
COALESCE(SUM(CASE WHEN state IN ('pending', 'leased', 'failed') THEN 1 ELSE 0 END), 0) queue_depth,
|
|
1042
|
+
COALESCE(SUM(CASE WHEN state = 'pending' THEN 1 ELSE 0 END), 0) pending,
|
|
1043
|
+
COALESCE(SUM(CASE WHEN state = 'leased' THEN 1 ELSE 0 END), 0) leased,
|
|
1044
|
+
COALESCE(SUM(CASE WHEN state = 'failed' THEN 1 ELSE 0 END), 0) failed,
|
|
1045
|
+
COALESCE(SUM(CASE WHEN state = 'dead_letter' THEN 1 ELSE 0 END), 0) dead_letters,
|
|
1046
|
+
COALESCE(SUM(CASE WHEN state = 'leased' AND lease_expires_at <= ? THEN 1 ELSE 0 END), 0) expired_leases,
|
|
1047
|
+
MIN(CASE WHEN state IN ('pending', 'leased', 'failed') THEN created_at END) oldest_pending_at,
|
|
1048
|
+
MAX(CASE WHEN state = 'completed' THEN completed_at END) last_successful_drain_at
|
|
1049
|
+
FROM devflow_work_items
|
|
1050
|
+
WHERE project_root = ?
|
|
1051
|
+
`).get(now, project);
|
|
1052
|
+
const queueDepth = Number(queue?.queue_depth ?? 0);
|
|
1053
|
+
const pending = Number(queue?.pending ?? 0);
|
|
1054
|
+
const leased = Number(queue?.leased ?? 0);
|
|
1055
|
+
const failed = Number(queue?.failed ?? 0);
|
|
1056
|
+
const deadLetters = Number(queue?.dead_letters ?? 0);
|
|
1057
|
+
const expiredLeases = Number(queue?.expired_leases ?? 0);
|
|
1058
|
+
const oldestPendingAt = queue?.oldest_pending_at == null
|
|
1059
|
+
? undefined
|
|
1060
|
+
: Number(queue.oldest_pending_at);
|
|
1061
|
+
const lastSuccessfulDrainAt = queue?.last_successful_drain_at == null
|
|
1062
|
+
? undefined
|
|
1063
|
+
: Number(queue.last_successful_drain_at);
|
|
1064
|
+
metrics.push({ key: "work_queue_schema", value: "canonical", target: "canonical global work queue schema", ok: true, severity: "info" });
|
|
1065
|
+
metrics.push({ key: "work_queue_depth", value: queueDepth, ok: true, severity: "info" });
|
|
1066
|
+
metrics.push({ key: "work_queue_pending", value: pending, ok: true, severity: "info" });
|
|
1067
|
+
metrics.push({ key: "work_queue_leased", value: leased, ok: true, severity: "info" });
|
|
1068
|
+
metrics.push({ key: "work_queue_failed", value: failed, target: 0, ok: failed === 0, severity: "warning" });
|
|
1069
|
+
metrics.push({
|
|
1070
|
+
key: "work_queue_oldest_pending_age_ms",
|
|
1071
|
+
value: oldestPendingAt === undefined ? 0 : Math.max(0, now - oldestPendingAt),
|
|
1072
|
+
ok: true,
|
|
1073
|
+
severity: "info",
|
|
1074
|
+
});
|
|
1075
|
+
metrics.push({ key: "work_queue_dead_letters", value: deadLetters, target: 0, ok: deadLetters === 0, severity: "error" });
|
|
1076
|
+
metrics.push({ key: "work_queue_expired_leases", value: expiredLeases, target: 0, ok: expiredLeases === 0, severity: "error" });
|
|
1077
|
+
metrics.push({ key: "work_queue_last_successful_drain_at", value: lastSuccessfulDrainAt ?? null, ok: true, severity: "info" });
|
|
1078
|
+
}
|
|
772
1079
|
}
|
|
1080
|
+
const hookStateDir = dependencies.env?.DEVFLOW_STATE_DIR
|
|
1081
|
+
?? join(home, ".devflow", "state", getProjectHash(project));
|
|
1082
|
+
const spoolKinds = ["session-start-pending", "memory-turn-pending", "session-finalize-pending"];
|
|
1083
|
+
const spoolBacklog = Object.fromEntries(spoolKinds.map(kind => {
|
|
1084
|
+
const directory = join(hookStateDir, kind);
|
|
1085
|
+
const count = existsSync(directory)
|
|
1086
|
+
? readdirSync(directory).filter(name => name.includes(".json")).length
|
|
1087
|
+
: 0;
|
|
1088
|
+
return [kind, count];
|
|
1089
|
+
}));
|
|
1090
|
+
const totalSpoolBacklog = Object.values(spoolBacklog).reduce((sum, count) => sum + count, 0);
|
|
1091
|
+
metrics.push({
|
|
1092
|
+
key: "hook_spool_backlog",
|
|
1093
|
+
value: { total: totalSpoolBacklog, ...spoolBacklog },
|
|
1094
|
+
target: { total: 0 },
|
|
1095
|
+
ok: totalSpoolBacklog === 0,
|
|
1096
|
+
severity: strictSeverity,
|
|
1097
|
+
});
|
|
773
1098
|
if (doctorTableExists(db, "tool_call_events")) {
|
|
1099
|
+
const canProjectFilterEvents = sessionSchemaReady && toolEventColumns.has("session_id");
|
|
1100
|
+
const metricColumns = doctorTableColumns(db, "tool_metrics");
|
|
1101
|
+
const canJoinMetrics = toolEventColumns.has("event_id")
|
|
1102
|
+
&& metricColumns.has("tool_call_event_id")
|
|
1103
|
+
&& metricColumns.has("status");
|
|
1104
|
+
const selectedColumns = [
|
|
1105
|
+
toolEventColumns.has("error") ? "event.error" : "NULL error",
|
|
1106
|
+
toolEventColumns.has("failure_category") ? "event.failure_category" : "NULL failure_category",
|
|
1107
|
+
toolEventColumns.has("output") ? "event.output" : "NULL output",
|
|
1108
|
+
canJoinMetrics ? "metric.status metric_status" : "NULL metric_status",
|
|
1109
|
+
].join(", ");
|
|
1110
|
+
const eventRows = db.prepare(`
|
|
1111
|
+
SELECT ${selectedColumns}
|
|
1112
|
+
FROM tool_call_events event
|
|
1113
|
+
${canJoinMetrics ? "LEFT JOIN tool_metrics metric ON metric.tool_call_event_id = event.event_id" : ""}
|
|
1114
|
+
${canProjectFilterEvents ? "JOIN sessions event_session ON event_session.id = event.session_id" : ""}
|
|
1115
|
+
WHERE event.timestamp >= ?
|
|
1116
|
+
${canProjectFilterEvents ? "AND event_session.project_root = ?" : ""}
|
|
1117
|
+
`).all(...(canProjectFilterEvents ? [cutoff, project] : [cutoff]));
|
|
1118
|
+
const semantic = eventRows.map(semanticToolOutcome);
|
|
1119
|
+
const semanticFailures = semantic.filter(outcome => outcome.failed).length;
|
|
1120
|
+
const semanticDegradations = semantic.filter(outcome => outcome.degraded).length;
|
|
1121
|
+
metrics.push({ key: "semantic_tool_calls_24h", value: eventRows.length, ok: true, severity: "info" });
|
|
1122
|
+
metrics.push({ key: "semantic_tool_failures_24h", value: semanticFailures, target: 0, ok: semanticFailures === 0, severity: strictSeverity });
|
|
1123
|
+
metrics.push({ key: "semantic_tool_degradations_24h", value: semanticDegradations, target: 0, ok: semanticDegradations === 0, severity: strictSeverity });
|
|
1124
|
+
metrics.push({ key: "semantic_tool_failure_rate_24h", value: rate(semanticFailures, eventRows.length), target: 0, ok: semanticFailures === 0, severity: strictSeverity });
|
|
1125
|
+
metrics.push({ key: "semantic_tool_degradation_rate_24h", value: rate(semanticDegradations, eventRows.length), target: 0, ok: semanticDegradations === 0, severity: strictSeverity });
|
|
1126
|
+
metrics.push({
|
|
1127
|
+
key: "semantic_tool_project_scope",
|
|
1128
|
+
value: canProjectFilterEvents ? "project" : "global fallback; session identity unavailable",
|
|
1129
|
+
target: "project",
|
|
1130
|
+
ok: canProjectFilterEvents,
|
|
1131
|
+
severity: "warning",
|
|
1132
|
+
});
|
|
774
1133
|
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;
|
|
1134
|
+
const distribution = {};
|
|
1135
|
+
for (const row of eventRows) {
|
|
1136
|
+
if (typeof row.failure_category !== "string" || !row.failure_category.trim())
|
|
1137
|
+
continue;
|
|
1138
|
+
distribution[row.failure_category] = (distribution[row.failure_category] ?? 0) + 1;
|
|
1139
|
+
}
|
|
1140
|
+
const classifiedEvents = Object.values(distribution).reduce((sum, count) => sum + count, 0);
|
|
1141
|
+
const classificationCoverage = semanticFailures === 0 ? 1 : classifiedEvents / semanticFailures;
|
|
786
1142
|
metrics.push({
|
|
787
1143
|
key: "failure_categories_24h",
|
|
788
1144
|
value: distribution,
|
|
@@ -807,6 +1163,42 @@ function checkTelemetry(project, strict, dependencies) {
|
|
|
807
1163
|
});
|
|
808
1164
|
}
|
|
809
1165
|
}
|
|
1166
|
+
const skillFallbackSchemaReady = sessionSchemaReady
|
|
1167
|
+
&& ["session_id", "started_at", "fallback_count", "fallback_reasons"]
|
|
1168
|
+
.every(column => executionColumns.has(column));
|
|
1169
|
+
let skillFallbackCount = 0;
|
|
1170
|
+
const fallbackReasonCounts = {};
|
|
1171
|
+
if (skillFallbackSchemaReady) {
|
|
1172
|
+
const rows = db.prepare(`
|
|
1173
|
+
SELECT execution.fallback_count, execution.fallback_reasons
|
|
1174
|
+
FROM skill_executions execution
|
|
1175
|
+
JOIN sessions session ON session.id = execution.session_id
|
|
1176
|
+
WHERE session.project_root = ? AND execution.started_at >= ?
|
|
1177
|
+
`).all(project, cutoff);
|
|
1178
|
+
for (const row of rows) {
|
|
1179
|
+
skillFallbackCount += Number(row.fallback_count ?? 0);
|
|
1180
|
+
for (const reason of fallbackReasons(row.fallback_reasons)) {
|
|
1181
|
+
fallbackReasonCounts[reason] = (fallbackReasonCounts[reason] ?? 0) + 1;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
let hookFallbackCount = 0;
|
|
1186
|
+
if (doctorTableExists(db, "devflow_hook_fallbacks")) {
|
|
1187
|
+
const rows = db.prepare(`
|
|
1188
|
+
SELECT reason, COUNT(*) count
|
|
1189
|
+
FROM devflow_hook_fallbacks
|
|
1190
|
+
WHERE project_root = ? AND created_at >= ?
|
|
1191
|
+
GROUP BY reason ORDER BY reason
|
|
1192
|
+
`).all(project, cutoff);
|
|
1193
|
+
for (const row of rows) {
|
|
1194
|
+
const count = Number(row.count);
|
|
1195
|
+
hookFallbackCount += count;
|
|
1196
|
+
fallbackReasonCounts[`hook:${row.reason}`] = count;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
metrics.push({ key: "skill_fallbacks_24h", value: skillFallbackCount, target: 0, ok: skillFallbackCount === 0, severity: "warning" });
|
|
1200
|
+
metrics.push({ key: "hook_fallbacks_24h", value: hookFallbackCount, target: 0, ok: hookFallbackCount === 0, severity: "warning" });
|
|
1201
|
+
metrics.push({ key: "fallback_reasons_24h", value: fallbackReasonCounts, target: {}, ok: Object.keys(fallbackReasonCounts).length === 0, severity: "warning" });
|
|
810
1202
|
}
|
|
811
1203
|
catch (error) {
|
|
812
1204
|
metrics.push({ key: "global_telemetry_db_read", value: error.message, target: "readable SQLite database", ok: false, severity: "error" });
|
|
@@ -4226,7 +4618,7 @@ export async function runStructuredDoctor(options = {}, dependencies = {}) {
|
|
|
4226
4618
|
checkIsolation(requestedProject, dependencies),
|
|
4227
4619
|
checkDistill(canonical.path, dependencies),
|
|
4228
4620
|
checkLegacyPaths(canonical.path, dependencies),
|
|
4229
|
-
checkVectors(canonical.path, dependencies),
|
|
4621
|
+
await checkVectors(canonical.path, dependencies),
|
|
4230
4622
|
checkTelemetry(canonical.path, Boolean(options.strict), dependencies),
|
|
4231
4623
|
checkInjection(canonical.path, dependencies),
|
|
4232
4624
|
checkMemoryQuality(canonical.path, dependencies),
|