@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.
Files changed (59) hide show
  1. package/dist/commands/doctor.d.ts +5 -0
  2. package/dist/commands/doctor.d.ts.map +1 -1
  3. package/dist/commands/doctor.js +408 -16
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/embeddings.d.ts +8 -0
  6. package/dist/commands/embeddings.d.ts.map +1 -0
  7. package/dist/commands/embeddings.js +26 -0
  8. package/dist/commands/embeddings.js.map +1 -0
  9. package/dist/commands/setup.d.ts +25 -2
  10. package/dist/commands/setup.d.ts.map +1 -1
  11. package/dist/commands/setup.js +71 -63
  12. package/dist/commands/setup.js.map +1 -1
  13. package/dist/index.js +52 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/plugin-files/.claude-plugin/plugin.json +1 -1
  16. package/dist/plugin-files/dist/command-registry.json +39 -13
  17. package/dist/plugin-files/dist/hooks/hook-client.js +5 -5
  18. package/dist/plugin-files/dist/hooks/hook-daemon.js +32 -14
  19. package/dist/plugin-files/dist/hooks/memory-snapshot-cache.js +11 -0
  20. package/dist/plugin-files/dist/hooks/post-tool-use-failure.js +2 -2
  21. package/dist/plugin-files/dist/hooks/post-tool-use.js +6 -3
  22. package/dist/plugin-files/dist/hooks/pre-compact.js +2 -2
  23. package/dist/plugin-files/dist/hooks/pre-tool-use.js +2 -2
  24. package/dist/plugin-files/dist/hooks/session-end.js +2 -2
  25. package/dist/plugin-files/dist/hooks/stop.js +15 -2
  26. package/dist/plugin-files/dist/hooks/user-prompt-submit.js +14 -5
  27. package/dist/plugin-files/dist/skills/devflow:animation/SKILL.md +3 -1
  28. package/dist/plugin-files/dist/skills/devflow:css/SKILL.md +3 -1
  29. package/dist/plugin-files/dist/skills/devflow:docker/SKILL.md +3 -1
  30. package/dist/plugin-files/dist/skills/devflow:electron/SKILL.md +3 -1
  31. package/dist/plugin-files/dist/skills/devflow:git/SKILL.md +3 -1
  32. package/dist/plugin-files/dist/skills/devflow:graphql/SKILL.md +3 -1
  33. package/dist/plugin-files/dist/skills/devflow:nest/SKILL.md +3 -1
  34. package/dist/plugin-files/dist/skills/devflow:nextjs/SKILL.md +3 -1
  35. package/dist/plugin-files/dist/skills/devflow:performance/SKILL.md +3 -1
  36. package/dist/plugin-files/dist/skills/devflow:react/SKILL.md +14 -4
  37. package/dist/plugin-files/dist/skills/devflow:tailwind/SKILL.md +3 -1
  38. package/dist/plugin-files/dist/skills/devflow:taro/SKILL.md +3 -1
  39. package/dist/plugin-files/dist/skills/devflow:ui-layout/SKILL.md +3 -1
  40. package/dist/plugin-files/dist/skills/devflow:vue/SKILL.md +3 -1
  41. package/dist/plugin-files/hooks/hooks.json +3 -2
  42. package/dist/plugin-files/hooks/session-start +66 -82
  43. package/dist/plugin-files/package.json +7 -7
  44. package/dist/plugin-files/skills/animation/SKILL.md +3 -1
  45. package/dist/plugin-files/skills/css/SKILL.md +3 -1
  46. package/dist/plugin-files/skills/docker/SKILL.md +3 -1
  47. package/dist/plugin-files/skills/doctor/SKILL.md +1 -1
  48. package/dist/plugin-files/skills/electron/SKILL.md +3 -1
  49. package/dist/plugin-files/skills/git/SKILL.md +3 -1
  50. package/dist/plugin-files/skills/graphql/SKILL.md +3 -1
  51. package/dist/plugin-files/skills/nest/SKILL.md +3 -1
  52. package/dist/plugin-files/skills/nextjs/SKILL.md +3 -1
  53. package/dist/plugin-files/skills/performance/SKILL.md +3 -1
  54. package/dist/plugin-files/skills/react/SKILL.md +14 -4
  55. package/dist/plugin-files/skills/tailwind/SKILL.md +3 -1
  56. package/dist/plugin-files/skills/taro/SKILL.md +3 -1
  57. package/dist/plugin-files/skills/ui-layout/SKILL.md +3 -1
  58. package/dist/plugin-files/skills/vue/SKILL.md +3 -1
  59. 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":"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"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"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"}
@@ -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 observations = doctorCount(db, "SELECT COUNT(*) count FROM observations");
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 rows = db.prepare(`
776
- SELECT failure_category category, COUNT(*) count
777
- FROM tool_call_events
778
- WHERE timestamp >= ? AND failure_category IS NOT NULL
779
- GROUP BY failure_category
780
- ORDER BY failure_category
781
- `).all(cutoff);
782
- const distribution = Object.fromEntries(rows.map(row => [row.category, Number(row.count)]));
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),