@devflow-tools/cli 0.16.10 → 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.
Files changed (53) hide show
  1. package/dist/commands/doctor.d.ts.map +1 -1
  2. package/dist/commands/doctor.js +354 -13
  3. package/dist/commands/doctor.js.map +1 -1
  4. package/dist/commands/setup.d.ts +25 -2
  5. package/dist/commands/setup.d.ts.map +1 -1
  6. package/dist/commands/setup.js +71 -63
  7. package/dist/commands/setup.js.map +1 -1
  8. package/dist/index.js +2 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/plugin-files/.claude-plugin/plugin.json +1 -1
  11. package/dist/plugin-files/dist/command-registry.json +39 -13
  12. package/dist/plugin-files/dist/hooks/hook-client.js +5 -5
  13. package/dist/plugin-files/dist/hooks/hook-daemon.js +13 -13
  14. package/dist/plugin-files/dist/hooks/post-tool-use-failure.js +1 -1
  15. package/dist/plugin-files/dist/hooks/post-tool-use.js +2 -2
  16. package/dist/plugin-files/dist/hooks/pre-compact.js +1 -1
  17. package/dist/plugin-files/dist/hooks/pre-tool-use.js +2 -2
  18. package/dist/plugin-files/dist/hooks/session-end.js +2 -2
  19. package/dist/plugin-files/dist/hooks/stop.js +2 -2
  20. package/dist/plugin-files/dist/hooks/user-prompt-submit.js +3 -4
  21. package/dist/plugin-files/dist/skills/devflow:animation/SKILL.md +3 -1
  22. package/dist/plugin-files/dist/skills/devflow:css/SKILL.md +3 -1
  23. package/dist/plugin-files/dist/skills/devflow:docker/SKILL.md +3 -1
  24. package/dist/plugin-files/dist/skills/devflow:electron/SKILL.md +3 -1
  25. package/dist/plugin-files/dist/skills/devflow:git/SKILL.md +3 -1
  26. package/dist/plugin-files/dist/skills/devflow:graphql/SKILL.md +3 -1
  27. package/dist/plugin-files/dist/skills/devflow:nest/SKILL.md +3 -1
  28. package/dist/plugin-files/dist/skills/devflow:nextjs/SKILL.md +3 -1
  29. package/dist/plugin-files/dist/skills/devflow:performance/SKILL.md +3 -1
  30. package/dist/plugin-files/dist/skills/devflow:react/SKILL.md +6 -4
  31. package/dist/plugin-files/dist/skills/devflow:tailwind/SKILL.md +3 -1
  32. package/dist/plugin-files/dist/skills/devflow:taro/SKILL.md +3 -1
  33. package/dist/plugin-files/dist/skills/devflow:ui-layout/SKILL.md +3 -1
  34. package/dist/plugin-files/dist/skills/devflow:vue/SKILL.md +3 -1
  35. package/dist/plugin-files/hooks/hooks.json +3 -2
  36. package/dist/plugin-files/hooks/session-start +54 -67
  37. package/dist/plugin-files/package.json +7 -7
  38. package/dist/plugin-files/skills/animation/SKILL.md +3 -1
  39. package/dist/plugin-files/skills/css/SKILL.md +3 -1
  40. package/dist/plugin-files/skills/docker/SKILL.md +3 -1
  41. package/dist/plugin-files/skills/doctor/SKILL.md +1 -1
  42. package/dist/plugin-files/skills/electron/SKILL.md +3 -1
  43. package/dist/plugin-files/skills/git/SKILL.md +3 -1
  44. package/dist/plugin-files/skills/graphql/SKILL.md +3 -1
  45. package/dist/plugin-files/skills/nest/SKILL.md +3 -1
  46. package/dist/plugin-files/skills/nextjs/SKILL.md +3 -1
  47. package/dist/plugin-files/skills/performance/SKILL.md +3 -1
  48. package/dist/plugin-files/skills/react/SKILL.md +6 -4
  49. package/dist/plugin-files/skills/tailwind/SKILL.md +3 -1
  50. package/dist/plugin-files/skills/taro/SKILL.md +3 -1
  51. package/dist/plugin-files/skills/ui-layout/SKILL.md +3 -1
  52. package/dist/plugin-files/skills/vue/SKILL.md +3 -1
  53. package/package.json +27 -27
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAEV,cAAc,EAIf,MAAM,oBAAoB,CAAC;AAM5B,YAAY,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,cAAc,EACd,YAAY,GACb,MAAM,oBAAoB,CAAC;AAoC5B,MAAM,MAAM,YAAY,GAAG,cAAc,CAAC;AAE1C,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,UAAU,eAAe;IACvB,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;IACrC,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IACnC,cAAc,CAAC,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACzC;AAED,UAAU,cAAc;IACtB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;IACtC,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,cAAc,CAAC;IAChD,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAu/ID,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,aAAkB,EAC3B,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,YAAY,CAAC,CA4BvB;AAED,wBAAgB,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;AACnD,wBAAgB,SAAS,CACvB,OAAO,EAAE,aAAa,EACtB,YAAY,CAAC,EAAE,kBAAkB,GAChC,OAAO,CAAC,YAAY,CAAC,CAAC;AAezB,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAoB7D;AAED,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,aAAkB,EAC3B,EAAE,GAAE,eAGH,EACD,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;IACvE,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,MAAM,EAAE;QAAE,YAAY,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,SAAS,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/B,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChD,SAAS,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAClC,MAAM,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAC7B,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD,wBAAsB,qBAAqB,CACzC,OAAO,SAAgB,GACtB,OAAO,CAAC,YAAY,CAAC,CA8DvB;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC;IAC9C,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CAiBD;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAevE;AAED,wBAAsB,mBAAmB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAChE,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;CACJ,CAAC,CAsBD;AAED,wBAAgB,iBAAiB,IAAI;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CASxE"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"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"}
@@ -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 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;
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" });