principles-disciple 1.71.0 → 1.73.0

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 (309) hide show
  1. package/openclaw.plugin.json +10 -5
  2. package/package.json +17 -19
  3. package/scripts/acceptance-test.mjs +16 -73
  4. package/scripts/sync-plugin.mjs +382 -77
  5. package/src/commands/archive-impl.ts +2 -1
  6. package/src/commands/capabilities.ts +2 -2
  7. package/src/commands/context.ts +2 -2
  8. package/src/commands/disable-impl.ts +2 -1
  9. package/src/commands/evolution-status.ts +16 -16
  10. package/src/commands/export.ts +12 -67
  11. package/src/commands/pain.ts +91 -1
  12. package/src/commands/principle-rollback.ts +2 -1
  13. package/src/commands/promote-impl.ts +7 -43
  14. package/src/commands/rollback-impl.ts +2 -1
  15. package/src/commands/rollback.ts +2 -1
  16. package/src/commands/samples.ts +2 -1
  17. package/src/commands/thinking-os.ts +2 -1
  18. package/src/config/errors.ts +18 -2
  19. package/src/constants/diagnostician.ts +2 -2
  20. package/src/constants/tools.ts +2 -1
  21. package/src/core/__tests__/focus-history.test.ts +210 -0
  22. package/src/core/config.ts +1 -1
  23. package/src/core/confirm-first-gate.ts +255 -0
  24. package/src/core/correction-cue-learner.ts +2 -136
  25. package/src/core/correction-types.ts +16 -88
  26. package/src/core/dictionary.ts +19 -20
  27. package/src/core/empathy-keyword-matcher.ts +17 -289
  28. package/src/core/empathy-types.ts +18 -229
  29. package/src/core/event-log.ts +38 -132
  30. package/src/core/evolution-reducer.ts +21 -2
  31. package/src/core/evolution-types.ts +76 -464
  32. package/src/core/file-store.ts +80 -0
  33. package/src/core/focus-history.ts +228 -955
  34. package/src/core/local-worker-routing.ts +34 -314
  35. package/src/core/merge-gate-audit.ts +0 -195
  36. package/src/core/pain-diagnostic-gate.ts +154 -0
  37. package/src/core/pain-signal.ts +21 -138
  38. package/src/core/pain.ts +15 -88
  39. package/src/core/pd-task-reconciler.ts +26 -115
  40. package/src/core/pd-task-service.ts +9 -9
  41. package/src/core/pd-task-types.ts +23 -127
  42. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  43. package/src/core/principle-compiler/code-validator.ts +15 -42
  44. package/src/core/principle-compiler/compiler.ts +100 -15
  45. package/src/core/principle-compiler/index.ts +5 -2
  46. package/src/core/principle-compiler/template-generator.ts +4 -104
  47. package/src/core/principle-injection.ts +10 -202
  48. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  49. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  50. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  51. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  52. package/src/core/principle-tree-ledger.ts +8 -6
  53. package/src/core/reflection/reflection-context.ts +14 -109
  54. package/src/core/replay-engine.ts +8 -500
  55. package/src/core/rule-host-helpers.ts +5 -35
  56. package/src/core/rule-host-types.ts +10 -82
  57. package/src/core/rule-host.ts +6 -63
  58. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  59. package/src/core/session-tracker.ts +87 -101
  60. package/src/core/shadow-observation-registry.ts +19 -48
  61. package/src/core/trajectory.ts +3 -1
  62. package/src/core/workflow-funnel-loader.ts +62 -68
  63. package/src/core/workspace-context.ts +46 -0
  64. package/src/core/workspace-dir-service.ts +1 -1
  65. package/src/core/workspace-dir-validation.ts +18 -9
  66. package/src/hooks/AGENTS.md +1 -1
  67. package/src/hooks/gate-block-helper.ts +46 -44
  68. package/src/hooks/gate.ts +207 -7
  69. package/src/hooks/lifecycle.ts +30 -32
  70. package/src/hooks/llm.ts +60 -32
  71. package/src/hooks/pain.ts +297 -103
  72. package/src/hooks/prompt.ts +469 -339
  73. package/src/hooks/subagent.ts +2 -29
  74. package/src/i18n/commands.ts +2 -10
  75. package/src/index.ts +95 -85
  76. package/src/openclaw-sdk.ts +311 -0
  77. package/src/service/central-database.ts +8 -4
  78. package/src/service/evolution-queue-migration.ts +2 -1
  79. package/src/service/evolution-worker.ts +163 -1786
  80. package/src/service/internalization-trigger-adapter.ts +302 -0
  81. package/src/service/keyword-optimization-service.ts +4 -4
  82. package/src/service/monitoring-query-service.ts +1 -215
  83. package/src/service/queue-io.ts +60 -331
  84. package/src/service/runtime-summary-service.ts +115 -18
  85. package/src/service/subagent-workflow/index.ts +0 -41
  86. package/src/service/subagent-workflow/types.ts +9 -120
  87. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  88. package/src/service/workflow-watchdog.ts +0 -43
  89. package/src/types/event-payload.ts +16 -74
  90. package/src/types/event-types.ts +39 -547
  91. package/src/types/hygiene-types.ts +7 -30
  92. package/src/types/principle-tree-schema.ts +20 -222
  93. package/src/types/queue.ts +15 -70
  94. package/src/types/runtime-summary.ts +5 -49
  95. package/src/utils/io.ts +10 -0
  96. package/src/utils/retry.ts +1 -1
  97. package/src/utils/shadow-fingerprint.ts +2 -2
  98. package/src/utils/workspace-resolver.ts +50 -0
  99. package/templates/langs/en/core/AGENTS.md +2 -2
  100. package/templates/langs/en/core/BOOT.md +1 -1
  101. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  102. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  103. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  104. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  105. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  107. package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  108. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  109. package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
  110. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  111. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  112. package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
  113. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  114. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  115. package/templates/langs/zh/core/AGENTS.md +2 -2
  116. package/templates/langs/zh/core/BOOT.md +1 -1
  117. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  118. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  119. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  120. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  121. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  127. package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
  128. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  129. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  130. package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
  131. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  132. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  133. package/tests/build-artifacts.test.ts +1 -3
  134. package/tests/commands/evolution-status.test.ts +0 -118
  135. package/tests/core/bootstrap-rules.test.ts +1 -1
  136. package/tests/core/config.test.ts +1 -1
  137. package/tests/core/event-log.test.ts +35 -0
  138. package/tests/core/evolution-engine.test.ts +610 -0
  139. package/tests/core/file-store.test.ts +102 -0
  140. package/tests/core/focus-history.test.ts +203 -11
  141. package/tests/core/merge-gate-audit.test.ts +2 -169
  142. package/tests/core/model-deployment-registry.test.ts +7 -1
  143. package/tests/core/model-training-registry.test.ts +19 -0
  144. package/tests/core/observability.test.ts +0 -1
  145. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  146. package/tests/core/pain.test.ts +0 -1
  147. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  148. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  149. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  150. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  151. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  152. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  153. package/tests/core/reflection-context.test.ts +0 -14
  154. package/tests/core/replay-engine.test.ts +127 -215
  155. package/tests/core/rule-host-helpers.test.ts +2 -2
  156. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  157. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  158. package/tests/core/workspace-dir-validation.test.ts +8 -1
  159. package/tests/core-anti-growth.test.ts +192 -0
  160. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  161. package/tests/hooks/confirm-first-gate.test.ts +333 -0
  162. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  163. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  164. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  165. package/tests/hooks/pain.test.ts +269 -12
  166. package/tests/hooks/prompt-characterization.test.ts +500 -0
  167. package/tests/hooks/prompt-size-guard.test.ts +329 -0
  168. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  169. package/tests/index.test.ts +94 -1
  170. package/tests/integration/auto-entry-gate.test.ts +248 -0
  171. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  172. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  173. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  174. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  175. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  176. package/tests/service/central-database.test.ts +457 -0
  177. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  178. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  179. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  180. package/tests/service/monitoring-query-service.test.ts +1 -47
  181. package/tests/service/queue-io.test.ts +1 -62
  182. package/tests/service/runtime-summary-service.test.ts +184 -3
  183. package/tests/service/workflow-watchdog.test.ts +0 -91
  184. package/tests/utils/file-lock.test.ts +5 -3
  185. package/tests/utils/session-key.test.ts +52 -0
  186. package/tests/utils/subagent-probe.test.ts +48 -1
  187. package/vitest.config.ts +4 -11
  188. package/.planning/codebase/ARCHITECTURE.md +0 -157
  189. package/.planning/codebase/CONCERNS.md +0 -145
  190. package/.planning/codebase/CONVENTIONS.md +0 -148
  191. package/.planning/codebase/INTEGRATIONS.md +0 -81
  192. package/.planning/codebase/STACK.md +0 -87
  193. package/.planning/codebase/STRUCTURE.md +0 -193
  194. package/.planning/codebase/TESTING.md +0 -243
  195. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  196. package/docs/COMMAND_REFERENCE.md +0 -76
  197. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  198. package/scripts/build-web.mjs +0 -46
  199. package/scripts/diagnose-nocturnal.mjs +0 -537
  200. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  201. package/src/commands/nocturnal-review.ts +0 -322
  202. package/src/commands/nocturnal-rollout.ts +0 -790
  203. package/src/commands/nocturnal-train.ts +0 -986
  204. package/src/commands/pd-reflect.ts +0 -88
  205. package/src/core/adaptive-thresholds.ts +0 -478
  206. package/src/core/diagnostician-task-store.ts +0 -192
  207. package/src/core/nocturnal-arbiter.ts +0 -715
  208. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  209. package/src/core/nocturnal-artificer.ts +0 -257
  210. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  211. package/src/core/nocturnal-compliance.ts +0 -1146
  212. package/src/core/nocturnal-dataset.ts +0 -763
  213. package/src/core/nocturnal-executability.ts +0 -428
  214. package/src/core/nocturnal-export.ts +0 -499
  215. package/src/core/nocturnal-paths.ts +0 -240
  216. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  217. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  218. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  219. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  220. package/src/core/nocturnal-trinity-types.ts +0 -218
  221. package/src/core/nocturnal-trinity.ts +0 -2680
  222. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  223. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  224. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  225. package/src/http/principles-console-route.ts +0 -709
  226. package/src/service/central-health-service.ts +0 -49
  227. package/src/service/central-overview-service.ts +0 -138
  228. package/src/service/control-ui-query-service.ts +0 -900
  229. package/src/service/cooldown-strategy.ts +0 -97
  230. package/src/service/evolution-pain-context.ts +0 -79
  231. package/src/service/evolution-query-service.ts +0 -407
  232. package/src/service/health-query-service.ts +0 -1038
  233. package/src/service/nocturnal-config.ts +0 -214
  234. package/src/service/nocturnal-runtime.ts +0 -734
  235. package/src/service/nocturnal-service.ts +0 -1605
  236. package/src/service/nocturnal-target-selector.ts +0 -545
  237. package/src/service/sleep-cycle.ts +0 -157
  238. package/src/service/startup-reconciler.ts +0 -112
  239. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  240. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  241. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  242. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  243. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  244. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  245. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  246. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  247. package/src/tools/write-pain-flag.ts +0 -215
  248. package/tests/commands/nocturnal-review.test.ts +0 -448
  249. package/tests/commands/nocturnal-train.test.ts +0 -97
  250. package/tests/commands/pd-reflect.test.ts +0 -49
  251. package/tests/core/adaptive-thresholds.test.ts +0 -261
  252. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  253. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  254. package/tests/core/nocturnal-artificer.test.ts +0 -241
  255. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  256. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  257. package/tests/core/nocturnal-compliance.test.ts +0 -646
  258. package/tests/core/nocturnal-dataset.test.ts +0 -892
  259. package/tests/core/nocturnal-e2e.test.ts +0 -234
  260. package/tests/core/nocturnal-executability.test.ts +0 -357
  261. package/tests/core/nocturnal-export.test.ts +0 -517
  262. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  263. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  264. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  265. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  266. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  267. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  268. package/tests/core/pain-auto-repair.test.ts +0 -96
  269. package/tests/core/pain-integration.test.ts +0 -510
  270. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  271. package/tests/http/principles-console-route.test.ts +0 -162
  272. package/tests/integration/chaos-resilience.test.ts +0 -348
  273. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  274. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  275. package/tests/service/control-ui-query-service.test.ts +0 -121
  276. package/tests/service/cooldown-strategy.test.ts +0 -164
  277. package/tests/service/data-endpoints-regression.test.ts +0 -834
  278. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  279. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  280. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  281. package/tests/service/nocturnal-runtime.test.ts +0 -473
  282. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  283. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  284. package/tests/service/startup-reconciler.test.ts +0 -148
  285. package/tests/tools/write-pain-flag.test.ts +0 -358
  286. package/ui/src/App.tsx +0 -45
  287. package/ui/src/api.ts +0 -220
  288. package/ui/src/charts.tsx +0 -955
  289. package/ui/src/components/ErrorState.tsx +0 -6
  290. package/ui/src/components/Loading.tsx +0 -13
  291. package/ui/src/components/ProtectedRoute.tsx +0 -12
  292. package/ui/src/components/Shell.tsx +0 -91
  293. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  294. package/ui/src/components/index.ts +0 -5
  295. package/ui/src/context/auth.tsx +0 -80
  296. package/ui/src/context/theme.tsx +0 -66
  297. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  298. package/ui/src/i18n/ui.ts +0 -473
  299. package/ui/src/main.tsx +0 -16
  300. package/ui/src/pages/EvolutionPage.tsx +0 -333
  301. package/ui/src/pages/FeedbackPage.tsx +0 -138
  302. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  303. package/ui/src/pages/LoginPage.tsx +0 -89
  304. package/ui/src/pages/OverviewPage.tsx +0 -599
  305. package/ui/src/pages/SamplesPage.tsx +0 -174
  306. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  307. package/ui/src/styles.css +0 -2020
  308. package/ui/src/types.ts +0 -384
  309. package/ui/src/utils/format.ts +0 -15
@@ -1,1038 +0,0 @@
1
-
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import { readPainFlagData } from '../core/pain.js';
5
- import { resolvePdPath } from '../core/paths.js';
6
- import { listSessions, type SessionState } from '../core/session-tracker.js';
7
- import { listDeployments } from '../core/model-deployment-registry.js';
8
- import { ControlUiDatabase } from '../core/control-ui-db.js';
9
- import { WorkspaceContext } from '../core/workspace-context.js';
10
- import type { EventLogEntry } from '../types/event-types.js';
11
-
12
- type HealthStage = 'healthy' | 'warning' | 'critical';
13
-
14
- interface QueueItem {
15
- status?: string;
16
- }
17
-
18
- interface AgentScorecard {
19
- trustStage?: number;
20
- trust_stage?: number;
21
- stage?: number;
22
- trustScore?: number;
23
- trust_score?: number;
24
- score?: number;
25
- }
26
-
27
- interface EvolutionScorecard {
28
- totalPoints?: number;
29
- total_points?: number;
30
- currentTier?: number | string;
31
- current_tier?: number | string;
32
- }
33
-
34
- interface EvolutionStreamRecord {
35
- ts?: string;
36
- type?: string;
37
- data?: Record<string, unknown>;
38
- }
39
-
40
- interface GateBlockRow {
41
- created_at: string;
42
- tool_name: string;
43
- file_path?: string | null;
44
- reason: string;
45
- gfi?: number | null;
46
- gfi_after?: number | null;
47
- trust_stage?: number | null;
48
- gate_type?: string | null;
49
- }
50
-
51
- interface NocturnalSampleRecord {
52
- artifactId?: string;
53
- status?: string;
54
- createdAt?: string;
55
- arbiter?: {
56
- passed?: boolean;
57
- };
58
- }
59
-
60
- interface RecentPrincipleChange {
61
- principleId: string;
62
- status: string;
63
- triggerPattern: string;
64
- action: string;
65
- fromStatus: string;
66
- toStatus: string;
67
- timestamp: string;
68
- }
69
-
70
- export class HealthQueryService {
71
- private readonly workspaceDir: string;
72
- private readonly stateDir: string;
73
- private readonly trajectory;
74
- private readonly config;
75
- private readonly eventLog;
76
- private readonly evolutionReducer;
77
- private readonly uiDb: ControlUiDatabase;
78
- private readonly tableColumnCache = new Map<string, Set<string>>();
79
-
80
- constructor(workspaceDir: string) {
81
- this.workspaceDir = workspaceDir;
82
- const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
83
- this.stateDir = wctx.stateDir;
84
- this.trajectory = wctx.trajectory;
85
- this.config = wctx.config;
86
- this.eventLog = wctx.eventLog;
87
- this.evolutionReducer = wctx.evolutionReducer;
88
- this.uiDb = new ControlUiDatabase({ workspaceDir });
89
- this.ensureTables();
90
- this.syncGfiFromSession();
91
- }
92
-
93
- dispose(): void {
94
- this.uiDb.dispose();
95
- }
96
-
97
- /**
98
- * Ensure all tables required by HealthQueryService exist.
99
- * This is a safety net for workspaces created before TrajectoryDatabase
100
- * had full schema initialization, or where initSchema() was never called.
101
- * Uses CREATE TABLE IF NOT EXISTS so it's idempotent.
102
- */
103
- private ensureTables(): void {
104
- try {
105
- this.uiDb.execute(`
106
- CREATE TABLE IF NOT EXISTS pain_events (
107
- id INTEGER PRIMARY KEY AUTOINCREMENT,
108
- session_id TEXT NOT NULL,
109
- source TEXT NOT NULL,
110
- score REAL NOT NULL DEFAULT 0,
111
- reason TEXT DEFAULT '',
112
- severity TEXT DEFAULT 'mild',
113
- origin TEXT DEFAULT '',
114
- confidence REAL DEFAULT 0,
115
- text TEXT DEFAULT '',
116
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
117
- )
118
- `);
119
- this.uiDb.execute(`
120
- CREATE INDEX IF NOT EXISTS idx_pain_events_created_at ON pain_events(created_at)
121
- `);
122
- this.uiDb.execute(`
123
- CREATE TABLE IF NOT EXISTS gate_blocks (
124
- id INTEGER PRIMARY KEY AUTOINCREMENT,
125
- session_id TEXT NOT NULL,
126
- tool_name TEXT NOT NULL,
127
- file_path TEXT,
128
- reason TEXT NOT NULL,
129
- gfi REAL,
130
- gfi_after REAL,
131
- trust_stage INTEGER,
132
- gate_type TEXT,
133
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
134
- )
135
- `);
136
- this.uiDb.execute(`
137
- CREATE INDEX IF NOT EXISTS idx_gate_blocks_created_at ON gate_blocks(created_at)
138
- `);
139
- } catch (err) {
140
- console.warn('[HealthQueryService] Table ensure failed (non-fatal):', err);
141
- }
142
- }
143
-
144
- getOverviewHealth(): {
145
- gfi: { current: number; peakToday: number; threshold: number; trend: { hour: string; value: number }[] };
146
- trust: { stage: number; stageLabel: string; score: number };
147
- evolution: { tier: string; points: number };
148
- painFlag: { active: boolean; source: string | null; score: number | null };
149
- principles: { candidate: number; probation: number; active: number; deprecated: number };
150
- queue: { pending: number; inProgress: number; completed: number };
151
- activeStage: HealthStage;
152
- } {
153
- const threshold = this.getGfiThreshold();
154
- const trust = this.readTrust();
155
- const evolution = this.readEvolutionScore();
156
- const reducerStats = this.evolutionReducer.getStats();
157
- const queue = this.readQueueStats();
158
- const painFlag = this.readPainFlag();
159
-
160
- // GFI: Re-sync from session JSON on every request for real-time data
161
- this.syncGfiFromSession();
162
- const gfiData = this.readGfiFromDb();
163
- const {currentGfi} = gfiData;
164
- const peakToday = gfiData.dailyGfiPeak;
165
-
166
- // GFI history trend: aggregate pain_events by hour
167
- const today = new Date().toISOString().slice(0, 10);
168
- const gfiTrend = this.readGfiTrend(today);
169
-
170
- return {
171
- gfi: {
172
- current: currentGfi,
173
- peakToday,
174
- threshold,
175
- trend: gfiTrend,
176
- },
177
- trust,
178
- evolution,
179
- painFlag,
180
- principles: {
181
- candidate: reducerStats.candidateCount,
182
- probation: reducerStats.probationCount,
183
- active: reducerStats.activeCount,
184
- deprecated: reducerStats.deprecatedCount,
185
- },
186
- queue,
187
- activeStage: HealthQueryService.computeHealthStage(currentGfi, threshold, painFlag.active),
188
- };
189
- }
190
-
191
- getEvolutionPrinciples(): {
192
- principles: {
193
- summary: { candidate: number; probation: number; active: number; deprecated: number };
194
- recent: {
195
- principleId: string;
196
- status: string;
197
- triggerPattern: string;
198
- action: string;
199
- fromStatus: string;
200
- toStatus: string;
201
- timestamp: string;
202
- }[];
203
- };
204
- nocturnalTraining: {
205
- queue: { pending: number; inProgress: number; completed: number };
206
- trinityRecords: { artifactId: string; status: string; createdAt: string }[];
207
- arbiterPassRate: number;
208
- orpoSampleCount: number;
209
- deployments: { modelId: string; status: string; checkpointPath: string | null }[];
210
- };
211
- painSourceDistribution: Record<string, number>;
212
- activeStage: string;
213
- } {
214
- const stats = this.evolutionReducer.getStats();
215
- const recent = this.readRecentPrincipleChanges(30);
216
- const nocturnal = this.readNocturnalTraining();
217
- const painSourceDistribution = this.readPainSourceDistribution();
218
- const activeStage = this.readEvolutionActiveStage(nocturnal.queue);
219
-
220
- return {
221
- principles: {
222
- summary: {
223
- candidate: stats.candidateCount,
224
- probation: stats.probationCount,
225
- active: stats.activeCount,
226
- deprecated: stats.deprecatedCount,
227
- },
228
- recent,
229
- },
230
- nocturnalTraining: nocturnal,
231
- painSourceDistribution,
232
- activeStage,
233
- };
234
- }
235
-
236
- /**
237
- * Read GFI trend for a specific day by aggregating pain_events by hour.
238
- * Used by getOverviewHealth() to provide historical GFI context.
239
- */
240
- private readGfiTrend(date: string): { hour: string; value: number }[] {
241
- try {
242
- const rows = this.uiDb.all<{ hour: string; value: number }>(`
243
- SELECT substr(created_at, 1, 13) || ':00:00Z' AS hour, ROUND(SUM(score), 2) AS value
244
- FROM pain_events
245
- WHERE substr(created_at, 1, 10) = ?
246
- GROUP BY substr(created_at, 1, 13)
247
- ORDER BY hour ASC
248
- `, date);
249
- return rows.map(row => ({ hour: row.hour, value: this.asNumber(row.value, 0) }));
250
- } catch {
251
- // pain_events table may not exist in this workspace — return empty trend
252
- return [];
253
- }
254
- }
255
-
256
- getFeedbackGfi(): {
257
- current: number;
258
- peakToday: number;
259
- threshold: number;
260
- trend: { hour: string; value: number }[];
261
- sources: Record<string, number>;
262
- } {
263
- const threshold = this.getGfiThreshold();
264
- const today = new Date().toISOString().slice(0, 10);
265
-
266
- const trendRows = this.uiDb.all<{ hour: string; value: number }>(`
267
- SELECT substr(created_at, 1, 13) || ':00:00Z' AS hour, ROUND(SUM(score), 2) AS value
268
- FROM pain_events
269
- WHERE substr(created_at, 1, 10) = ?
270
- GROUP BY substr(created_at, 1, 13)
271
- ORDER BY hour ASC
272
- `, today);
273
-
274
- const sourceRows = this.uiDb.all<{ source: string; total: number }>(`
275
- SELECT source, COUNT(*) AS total
276
- FROM pain_events
277
- WHERE substr(created_at, 1, 10) = ?
278
- GROUP BY source
279
- ORDER BY total DESC
280
- `, today);
281
-
282
- // GFI: Re-sync from session JSON for real-time data
283
- this.syncGfiFromSession();
284
- const gfiData = this.readGfiFromDb();
285
-
286
- return {
287
- current: gfiData.currentGfi,
288
- peakToday: gfiData.dailyGfiPeak,
289
- threshold,
290
- trend: trendRows.map((row) => ({ hour: row.hour, value: this.asNumber(row.value, 0) })),
291
- sources: Object.fromEntries(sourceRows.map((row) => [row.source, this.asNumber(row.total, 0)])),
292
- };
293
- }
294
-
295
- getFeedbackEmpathyEvents(limit = 50): {
296
- timestamp: string;
297
- severity: string;
298
- score: number;
299
- reason: string;
300
- origin: string;
301
- gfiAfter: number;
302
- }[] {
303
- const safeLimit = Math.max(1, Math.min(500, Math.floor(limit)));
304
- const events = this.readMergedEvents()
305
- .filter((entry) => entry.type === 'pain_signal' && String(entry.data?.source ?? '') === 'user_empathy')
306
- .sort((a, b) => (b.ts || '').localeCompare(a.ts || ''))
307
- .slice(0, safeLimit);
308
-
309
- return events.map((entry) => {
310
- const data = entry.data ?? {};
311
- return {
312
- timestamp: String(entry.ts ?? ''),
313
- severity: typeof data.severity === 'string' ? data.severity : 'mild',
314
- score: this.asNumber(data.score, 0),
315
- reason: typeof data.reason === 'string' ? data.reason : '',
316
- origin: typeof data.origin === 'string' ? data.origin : 'unknown',
317
- gfiAfter: this.asNumber(data.gfiAfter ?? data.gfi_after ?? data.gfi, 0),
318
- };
319
- });
320
- }
321
-
322
- getFeedbackGateBlocks(limit = 50): {
323
- timestamp: string;
324
- toolName: string;
325
- reason: string;
326
- gfi: number;
327
- trustStage: number;
328
- }[] {
329
- const trust = this.readTrust();
330
- const rows = this.readGateBlocksRaw(limit);
331
-
332
- return rows.map((row) => ({
333
- timestamp: row.created_at,
334
- toolName: row.tool_name,
335
- reason: row.reason,
336
- gfi: this.resolveGateBlockGfi(row),
337
- trustStage: this.resolveGateBlockTrustStage(row, trust.stage),
338
- }));
339
- }
340
-
341
-
342
- getGateStats(): {
343
- today: {
344
- gfiBlocks: number;
345
- stageBlocks: number;
346
- p03Blocks: number;
347
- bypassAttempts: number;
348
- p16Exemptions: number;
349
- };
350
- trust: { stage: number; score: number; status: string };
351
- evolution: { tier: string; points: number; status: string };
352
- } {
353
- const today = new Date().toISOString().slice(0, 10);
354
- const rows = this.uiDb.all<{ reason: string }>(`
355
- SELECT reason
356
- FROM gate_blocks
357
- WHERE substr(created_at, 1, 10) = ?
358
- `, today);
359
-
360
- let gfiBlocks = 0;
361
- let stageBlocks = 0;
362
- let p03Blocks = 0;
363
- let bypassAttempts = 0;
364
- let p16Exemptions = 0;
365
-
366
- for (const row of rows) {
367
- const reason = String(row.reason || '').toLowerCase();
368
- if (reason.includes('gfi')) gfiBlocks++;
369
- if (reason.includes('tier') || reason.includes('stage') || reason.includes('trust')) stageBlocks++;
370
- if (reason.includes('p-03') || reason.includes('edit verification') || reason.includes('oldtext')) p03Blocks++;
371
- if (reason.includes('bypass')) bypassAttempts++;
372
- if (reason.includes('p-16') || reason.includes('exemption')) p16Exemptions++;
373
- }
374
-
375
- const trust = this.readTrust();
376
- const evolution = this.readEvolutionScore();
377
-
378
- return {
379
- today: {
380
- gfiBlocks,
381
- stageBlocks,
382
- p03Blocks,
383
- bypassAttempts,
384
- p16Exemptions,
385
- },
386
- trust: {
387
- stage: trust.stage,
388
- score: trust.score,
389
- status: this.scoreToStatus(trust.score),
390
- },
391
- evolution: {
392
- tier: evolution.tier,
393
- points: evolution.points,
394
- status: this.evolutionToStatus(evolution.tier, evolution.points),
395
- },
396
- };
397
- }
398
-
399
- getGateBlocks(limit = 50): {
400
- timestamp: string;
401
- toolName: string;
402
- filePath: string | null;
403
- reason: string;
404
- gateType: string;
405
- gfi: number;
406
- trustStage: number;
407
- }[] {
408
- const trust = this.readTrust();
409
- const rows = this.readGateBlocksRaw(limit);
410
-
411
- return rows.map((row) => ({
412
- timestamp: row.created_at,
413
- toolName: row.tool_name,
414
- filePath: row.file_path ?? null,
415
- reason: row.reason,
416
- gateType: this.resolveGateType(row),
417
- gfi: this.resolveGateBlockGfi(row),
418
- trustStage: this.resolveGateBlockTrustStage(row, trust.stage),
419
- }));
420
- }
421
-
422
- private readTrust(): { stage: number; stageLabel: string; score: number } {
423
- const scorecardPath = resolvePdPath(this.workspaceDir, 'AGENT_SCORECARD');
424
- const scorecard = this.readJsonFile<AgentScorecard>(scorecardPath, {});
425
- const score = this.asNumber(
426
- scorecard.trustScore ?? scorecard.trust_score ?? scorecard.score,
427
- 0,
428
- );
429
- const rawStage = this.asNumber(
430
- scorecard.trustStage ?? scorecard.trust_stage ?? scorecard.stage,
431
- HealthQueryService.inferTrustStageFromScore(score),
432
- );
433
- const stage = Math.max(1, Math.min(4, Math.round(rawStage)));
434
-
435
- return {
436
- stage,
437
- stageLabel: HealthQueryService.getTrustStageLabel(stage),
438
- score,
439
- };
440
- }
441
-
442
- private readEvolutionScore(): { tier: string; points: number } {
443
- const scorecardPath = path.join(this.stateDir, 'evolution-scorecard.json');
444
- const scorecard = this.readJsonFile<EvolutionScorecard>(scorecardPath, {});
445
- const points = this.asNumber(scorecard.totalPoints ?? scorecard.total_points, 0);
446
- const tierRaw = scorecard.currentTier ?? scorecard.current_tier ?? 1;
447
- return {
448
- tier: this.normalizeTierName(tierRaw),
449
- points,
450
- };
451
- }
452
-
453
- private readQueueStats(): { pending: number; inProgress: number; completed: number } {
454
- const queuePath = resolvePdPath(this.workspaceDir, 'EVOLUTION_QUEUE');
455
- const queue = this.readJsonFile<QueueItem[]>(queuePath, []);
456
- const stats = { pending: 0, inProgress: 0, completed: 0 };
457
- for (const item of queue) {
458
- const status = String(item?.status ?? 'pending');
459
- if (status === 'completed') stats.completed++;
460
- else if (status === 'in_progress') stats.inProgress++;
461
- else stats.pending++;
462
- }
463
- return stats;
464
- }
465
-
466
- private readPainFlag(): { active: boolean; source: string | null; score: number | null } {
467
- const painFlagPath = resolvePdPath(this.workspaceDir, 'PAIN_FLAG');
468
- if (!fs.existsSync(painFlagPath)) {
469
- return { active: false, source: null, score: null };
470
- }
471
-
472
- const data = readPainFlagData(this.workspaceDir);
473
- return {
474
- active: true,
475
- source: typeof data.source === 'string' ? data.source : null,
476
- score: this.asNullableNumber(data.score),
477
- };
478
- }
479
-
480
- private getCurrentSession(): SessionState | null {
481
- // First, try in-memory sessions (live sessions from session-tracker)
482
- const sessions = listSessions(this.workspaceDir);
483
- if (sessions.length > 0) {
484
- const sorted = [...sessions].sort((a, b) => {
485
- const aTs = Number(a.lastControlActivityAt ?? a.lastActivityAt ?? 0);
486
- const bTs = Number(b.lastControlActivityAt ?? b.lastActivityAt ?? 0);
487
- return bTs - aTs;
488
- });
489
- return sorted[0] ?? null;
490
- }
491
-
492
- // Fallback: read session JSON files directly (handles process restarts where
493
- // loadAllSessions may not have reloaded all sessions, or workspaceDir mismatch)
494
- return this.readLatestSessionFromFile();
495
- }
496
-
497
- private getGfiThreshold(): number {
498
- const fromConfig = this.asNullableNumber(this.config.get('gfi_gate.thresholds.low_risk_block'));
499
- if (fromConfig !== null) return fromConfig;
500
- const fallbackPath = resolvePdPath(this.workspaceDir, 'PAIN_SETTINGS');
501
- const raw = this.readJsonFile<Record<string, unknown>>(fallbackPath, {});
502
- const fallback = this.asNullableNumber(
503
- ((raw.gfi_gate as { thresholds?: { low_risk_block?: unknown } } | undefined)?.thresholds?.low_risk_block),
504
- );
505
- return fallback ?? 70;
506
- }
507
-
508
- private static computeHealthStage(currentGfi: number, threshold: number, painFlagActive: boolean): HealthStage {
509
- if (painFlagActive || currentGfi >= threshold) {
510
- return 'critical';
511
- }
512
- if (currentGfi >= threshold * 0.7) {
513
- return 'warning';
514
- }
515
- return 'healthy';
516
- }
517
-
518
- private static getTrustStageLabel(stage: number): string {
519
- switch (stage) {
520
- case 1:
521
- return 'Observer';
522
- case 2:
523
- return 'Editor';
524
- case 3:
525
- return 'Developer';
526
- case 4:
527
- return 'Architect';
528
- default:
529
- return 'Observer';
530
- }
531
- }
532
-
533
- private static inferTrustStageFromScore(score: number): number {
534
- if (score >= 80) return 4;
535
- if (score >= 60) return 3;
536
- if (score >= 30) return 2;
537
- return 1;
538
- }
539
-
540
- private normalizeTierName(rawTier: number | string): string {
541
- if (typeof rawTier === 'string') {
542
- const t = rawTier.trim();
543
- if (t.length > 0) return t;
544
- }
545
- const n = this.asNumber(rawTier, 1);
546
- if (n === 1) return 'Seed';
547
- if (n === 2) return 'Sprout';
548
- if (n === 3) return 'Sapling';
549
- if (n === 4) return 'Tree';
550
- if (n === 5) return 'Forest';
551
- return `Tier-${n}`;
552
- }
553
-
554
- private readRecentPrincipleChanges(limit: number): RecentPrincipleChange[] {
555
- const streamPath = resolvePdPath(this.workspaceDir, 'EVOLUTION_STREAM');
556
- if (!fs.existsSync(streamPath)) return [];
557
-
558
-
559
- let lines: string[];
560
- try {
561
- const raw = fs.readFileSync(streamPath, 'utf8').trim();
562
- if (!raw) return [];
563
- lines = raw.split('\n');
564
- } catch {
565
- return [];
566
- }
567
-
568
- const records: RecentPrincipleChange[] = [];
569
- for (const line of lines) {
570
-
571
-
572
- let event: EvolutionStreamRecord | null;
573
- try {
574
- event = JSON.parse(line) as EvolutionStreamRecord;
575
- } catch {
576
- event = null;
577
- }
578
- if (!event?.type || !event.data) continue;
579
-
580
- const mapped = this.mapPrincipleEvent(event);
581
- if (mapped) records.push(mapped);
582
- }
583
-
584
- return records
585
- .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
586
- .slice(0, Math.max(1, Math.min(200, limit)));
587
- }
588
-
589
- private mapPrincipleEvent(event: EvolutionStreamRecord): RecentPrincipleChange | null {
590
- const data = event.data ?? {};
591
- const type = String(event.type);
592
- const principleId = typeof data.principleId === 'string' ? data.principleId : '';
593
- if (!principleId) return null;
594
-
595
- const principle = this.evolutionReducer.getPrincipleById(principleId);
596
- const triggerPattern =
597
- typeof data.trigger === 'string'
598
- ? data.trigger
599
- : typeof principle?.trigger === 'string'
600
- ? principle.trigger
601
- : '';
602
- const action =
603
- typeof data.action === 'string'
604
- ? data.action
605
- : typeof principle?.action === 'string'
606
- ? principle.action
607
- : '';
608
-
609
- if (type === 'candidate_created') {
610
- const toStatus = typeof data.status === 'string' ? data.status : 'candidate';
611
- return {
612
- principleId,
613
- status: toStatus,
614
- triggerPattern,
615
- action,
616
- fromStatus: 'none',
617
- toStatus,
618
- timestamp: String(event.ts ?? ''),
619
- };
620
- }
621
-
622
- if (type === 'principle_promoted') {
623
- const fromStatus = typeof data.from === 'string' ? data.from : 'candidate';
624
- const toStatus = typeof data.to === 'string' ? data.to : 'probation';
625
- return {
626
- principleId,
627
- status: toStatus,
628
- triggerPattern,
629
- action,
630
- fromStatus,
631
- toStatus,
632
- timestamp: String(event.ts ?? ''),
633
- };
634
- }
635
-
636
- if (type === 'principle_deprecated') {
637
- return {
638
- principleId,
639
- status: 'deprecated',
640
- triggerPattern,
641
- action,
642
- fromStatus: 'active',
643
- toStatus: 'deprecated',
644
- timestamp: String(event.ts ?? ''),
645
- };
646
- }
647
-
648
- if (type === 'principle_rolled_back') {
649
- return {
650
- principleId,
651
- status: 'deprecated',
652
- triggerPattern,
653
- action,
654
- fromStatus: 'probation',
655
- toStatus: 'deprecated',
656
- timestamp: String(event.ts ?? ''),
657
- };
658
- }
659
-
660
- return null;
661
- }
662
-
663
-
664
- private readNocturnalTraining(): {
665
- queue: { pending: number; inProgress: number; completed: number };
666
- trinityRecords: { artifactId: string; status: string; createdAt: string }[];
667
- arbiterPassRate: number;
668
- orpoSampleCount: number;
669
- deployments: { modelId: string; status: string; checkpointPath: string | null }[];
670
- } {
671
- const sampleDir = resolvePdPath(this.workspaceDir, 'NOCTURNAL_SAMPLES_DIR');
672
- const exportDir = resolvePdPath(this.workspaceDir, 'NOCTURNAL_EXPORTS_DIR');
673
- const reviewQueuePath = path.join(this.stateDir, 'nocturnal', 'review-queue.json');
674
-
675
- const sampleFiles = this.safeListFiles(sampleDir, (name) => name.endsWith('.json') && name !== 'lineage-index.json');
676
- const samples: NocturnalSampleRecord[] = sampleFiles.map((filePath) =>
677
- this.readJsonFile<NocturnalSampleRecord>(filePath, {}),
678
- );
679
-
680
- const queue = { pending: 0, inProgress: 0, completed: 0 };
681
- for (const sample of samples) {
682
- const status = String(sample.status ?? '').toLowerCase();
683
- if (status === 'in_progress' || status === 'running') queue.inProgress++;
684
- else if (status === 'pending' || status === 'pending_review') queue.pending++;
685
- else queue.completed++;
686
- }
687
-
688
- const reviewQueue = this.readJsonFile<unknown[]>(reviewQueuePath, []);
689
- queue.pending += reviewQueue.length;
690
-
691
- const trinityRecords = samples
692
- .map((sample) => ({
693
- artifactId: String(sample.artifactId ?? ''),
694
- status: String(sample.status ?? 'unknown'),
695
- createdAt: String(sample.createdAt ?? ''),
696
- }))
697
- .filter((row) => row.artifactId.length > 0)
698
- .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
699
- .slice(0, 20);
700
-
701
- let passed = 0;
702
- for (const sample of samples) {
703
- const status = String(sample.status ?? '').toLowerCase();
704
- const arbiterPassed = sample.arbiter?.passed === true;
705
- if (arbiterPassed || status === 'approved' || status === 'approved_for_training') {
706
- passed++;
707
- }
708
- }
709
- const total = samples.length;
710
- const arbiterPassRate = total > 0 ? Number((passed / total).toFixed(4)) : 0;
711
-
712
- const exportFiles = this.safeListFiles(exportDir, (name) => name.endsWith('.jsonl'));
713
- const orpoSampleCount = exportFiles.length;
714
-
715
- const deployments = listDeployments(this.stateDir).map((deployment) => ({
716
- modelId: deployment.workerProfile,
717
- status: deployment.routingEnabled ? 'active' : 'inactive',
718
- checkpointPath: deployment.activeCheckpointId,
719
- }));
720
-
721
- return {
722
- queue,
723
- trinityRecords,
724
- arbiterPassRate,
725
- orpoSampleCount,
726
- deployments,
727
- };
728
- }
729
-
730
- private readPainSourceDistribution(): Record<string, number> {
731
- const rows = this.uiDb.all<{ source: string; total: number }>(`
732
- SELECT source, COUNT(*) AS total
733
- FROM pain_events
734
- GROUP BY source
735
- ORDER BY total DESC
736
- `);
737
- return Object.fromEntries(rows.map((row) => [row.source, this.asNumber(row.total, 0)]));
738
- }
739
-
740
- private readEvolutionActiveStage(queue: { pending: number; inProgress: number; completed: number }): string {
741
- const events = this.trajectory.listEvolutionEvents(undefined, { limit: 1, offset: 0 });
742
- if (events.length > 0) {
743
- return events[0].stage;
744
- }
745
- if (queue.inProgress > 0) return 'in_progress';
746
- if (queue.pending > 0) return 'pending';
747
- if (queue.completed > 0) return 'completed';
748
- return 'idle';
749
- }
750
-
751
- private readMergedEvents(): EventLogEntry[] {
752
- const persistedEvents = this.readPersistedEvents();
753
- const bufferedEvents = this.getBufferedEvents();
754
- const merged = new Map<string, EventLogEntry>();
755
- for (const entry of [...persistedEvents, ...bufferedEvents]) {
756
- merged.set(this.getEventDedupKey(entry), entry);
757
- }
758
- return [...merged.values()].sort((a, b) => (a.ts || '').localeCompare(b.ts || ''));
759
- }
760
-
761
- private readPersistedEvents(): EventLogEntry[] {
762
- const eventsPath = path.join(this.stateDir, 'logs', 'events.jsonl');
763
- if (!fs.existsSync(eventsPath)) return [];
764
- try {
765
- const raw = fs.readFileSync(eventsPath, 'utf8').trim();
766
- if (!raw) return [];
767
- return raw
768
- .split('\n')
769
- .map((line) => {
770
- try {
771
- return JSON.parse(line) as EventLogEntry;
772
- } catch {
773
- return null;
774
- }
775
- })
776
- .filter((entry): entry is EventLogEntry => entry !== null);
777
- } catch {
778
- return [];
779
- }
780
- }
781
-
782
- private getBufferedEvents(): EventLogEntry[] {
783
- const candidate = this.eventLog as { getBufferedEvents?: () => EventLogEntry[] };
784
- if (typeof candidate.getBufferedEvents === 'function') {
785
- return candidate.getBufferedEvents();
786
- }
787
- return [];
788
- }
789
-
790
-
791
-
792
-
793
- private getEventDedupKey(entry: EventLogEntry): string {
794
- const eventId = typeof entry.data?.eventId === 'string' ? entry.data.eventId : null;
795
- if (eventId) {
796
- return `${entry.type}:${entry.sessionId ?? 'none'}:${eventId}`;
797
- }
798
-
799
- return [
800
- entry.ts ?? 'no-ts',
801
- entry.type ?? 'no-type',
802
- entry.category ?? 'no-category',
803
- entry.sessionId ?? 'no-session',
804
- typeof entry.data?.source === 'string' ? entry.data.source : 'no-source',
805
- typeof entry.data?.toolName === 'string' ? entry.data.toolName : 'no-tool',
806
- typeof entry.data?.reason === 'string' ? entry.data.reason : 'no-reason',
807
- ].join('::');
808
- }
809
-
810
- private readGateBlocksRaw(limit: number): GateBlockRow[] {
811
- const safeLimit = Math.max(1, Math.min(1000, Math.floor(limit)));
812
- const hasGfi = this.hasTableColumn('gate_blocks', 'gfi');
813
- const hasGfiAfter = this.hasTableColumn('gate_blocks', 'gfi_after');
814
- const hasTrustStage = this.hasTableColumn('gate_blocks', 'trust_stage');
815
- const hasGateType = this.hasTableColumn('gate_blocks', 'gate_type');
816
-
817
- const sql = `
818
- SELECT
819
- created_at,
820
- tool_name,
821
- file_path,
822
- reason,
823
- ${hasGfi ? 'gfi' : 'NULL'} AS gfi,
824
- ${hasGfiAfter ? 'gfi_after' : 'NULL'} AS gfi_after,
825
- ${hasTrustStage ? 'trust_stage' : 'NULL'} AS trust_stage,
826
- ${hasGateType ? 'gate_type' : 'NULL'} AS gate_type
827
- FROM gate_blocks
828
- ORDER BY created_at DESC
829
- LIMIT ?
830
- `;
831
-
832
- return this.uiDb.all<GateBlockRow>(sql, safeLimit);
833
- }
834
-
835
- private resolveGateBlockGfi(row: GateBlockRow): number {
836
- const direct = this.asNullableNumber(row.gfi ?? row.gfi_after);
837
- if (direct !== null) return direct;
838
-
839
- const reason = String(row.reason ?? '');
840
- const match = /gfi\s*[:=]\s*(-?\d+(?:\.\d+)?)/i.exec(reason);
841
- if (match) {
842
- return this.asNumber(Number(match[1]), 0);
843
- }
844
-
845
- const session = this.getCurrentSession();
846
- return this.asNumber(session?.currentGfi, 0);
847
- }
848
-
849
- private resolveGateBlockTrustStage(row: GateBlockRow, fallbackStage: number): number {
850
- const direct = this.asNullableNumber(row.trust_stage);
851
- if (direct !== null) return Math.max(1, Math.min(4, Math.round(direct)));
852
-
853
- const reason = String(row.reason ?? '').toLowerCase();
854
- const match = /stage\s*(\d+)/i.exec(reason);
855
- if (match) {
856
- return Math.max(1, Math.min(4, Math.round(this.asNumber(Number(match[1]), fallbackStage))));
857
- }
858
-
859
- return fallbackStage;
860
- }
861
-
862
-
863
-
864
-
865
- private resolveGateType(row: GateBlockRow): string {
866
- if (typeof row.gate_type === 'string' && row.gate_type.trim().length > 0) {
867
- return row.gate_type;
868
- }
869
-
870
- const reason = String(row.reason ?? '').toLowerCase();
871
- if (reason.includes('gfi')) return 'gfi';
872
- if (reason.includes('tier') || reason.includes('stage') || reason.includes('trust')) return 'stage';
873
- if (reason.includes('p-03') || reason.includes('edit verification') || reason.includes('oldtext')) return 'p03';
874
- if (reason.includes('p-16') || reason.includes('exemption')) return 'p16';
875
- return 'general';
876
- }
877
-
878
- private hasTableColumn(tableName: string, columnName: string): boolean {
879
- let cached = this.tableColumnCache.get(tableName);
880
- if (!cached) {
881
- const rows = this.uiDb.all<{ name: string }>(`PRAGMA table_info(${tableName})`);
882
- cached = new Set(rows.map((row) => row.name));
883
- this.tableColumnCache.set(tableName, cached);
884
- }
885
- return cached.has(columnName);
886
- }
887
-
888
-
889
-
890
- private scoreToStatus(score: number): string {
891
- if (score >= 70) return 'healthy';
892
- if (score >= 40) return 'warning';
893
- return 'critical';
894
- }
895
-
896
-
897
-
898
- private evolutionToStatus(tier: string, points: number): string {
899
- const lower = tier.toLowerCase();
900
- if (lower === 'forest' || lower === 'tree') return 'healthy';
901
- if (lower === 'sapling' || points >= 200) return 'warning';
902
- return 'critical';
903
- }
904
-
905
-
906
-
907
- private safeListFiles(dirPath: string, predicate: (_name: string) => boolean): string[] {
908
- if (!fs.existsSync(dirPath)) return [];
909
- try {
910
- return fs.readdirSync(dirPath)
911
- .filter((name) => predicate(name))
912
- .map((name) => path.join(dirPath, name));
913
- } catch {
914
- return [];
915
- }
916
- }
917
-
918
-
919
-
920
- private readJsonFile<T>(filePath: string, fallback: T): T {
921
- if (!fs.existsSync(filePath)) return fallback;
922
- try {
923
- return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T;
924
- } catch {
925
- return fallback;
926
- }
927
- }
928
-
929
-
930
-
931
- private asNumber(value: unknown, fallback: number): number {
932
- return Number.isFinite(value) ? Number(value) : fallback;
933
- }
934
-
935
-
936
-
937
- private asNullableNumber(value: unknown): number | null {
938
- if (Number.isFinite(value)) return Number(value);
939
- if (typeof value === 'string' && value.trim().length > 0) {
940
- const n = Number(value);
941
- return Number.isFinite(n) ? n : null;
942
- }
943
- return null;
944
- }
945
-
946
- /**
947
- * Sync GFI from the latest session JSON file into SQLite.
948
- * Called at construction time so all queries read from a single source (SQLite).
949
- */
950
- private syncGfiFromSession(): void {
951
- const session = this.readLatestSessionFromFile();
952
- const currentGfi = this.asNumber(session?.currentGfi, 0);
953
- const dailyGfiPeak = this.asNumber(session?.dailyGfiPeak, currentGfi);
954
- const today = new Date().toISOString().slice(0, 10);
955
-
956
- try {
957
- this.uiDb.execute(`
958
- CREATE TABLE IF NOT EXISTS gfi_state (
959
- id INTEGER PRIMARY KEY CHECK (id = 1),
960
- current_gfi REAL NOT NULL DEFAULT 0,
961
- daily_gfi_peak REAL NOT NULL DEFAULT 0,
962
- gfi_date TEXT NOT NULL DEFAULT ''
963
- )
964
- `);
965
- this.uiDb.run(
966
- 'INSERT OR REPLACE INTO gfi_state (id, current_gfi, daily_gfi_peak, gfi_date) VALUES (1, ?, ?, ?)',
967
- currentGfi,
968
- dailyGfiPeak,
969
- today,
970
- );
971
- } catch {
972
- // Non-critical: GFI sync failure should not block queries
973
- }
974
- }
975
-
976
- /**
977
- * Read current GFI state from SQLite.
978
- */
979
- private readGfiFromDb(): { currentGfi: number; dailyGfiPeak: number } {
980
- try {
981
- const row = this.uiDb.get<{ current_gfi: number; daily_gfi_peak: number }>(
982
- 'SELECT current_gfi, daily_gfi_peak FROM gfi_state WHERE id = 1',
983
- );
984
- if (!row) return { currentGfi: 0, dailyGfiPeak: 0 };
985
- return {
986
- currentGfi: row.current_gfi ?? 0,
987
- dailyGfiPeak: row.daily_gfi_peak ?? 0,
988
- };
989
- } catch {
990
- return { currentGfi: 0, dailyGfiPeak: 0 };
991
- }
992
- }
993
-
994
- /**
995
- * Read the most recent session JSON file from disk.
996
- * Used to sync GFI from session-tracker's persistence into SQLite.
997
- */
998
-
999
- private readLatestSessionFromFile(): SessionState | null {
1000
- const sessionsDir = path.join(this.stateDir, 'sessions');
1001
- if (!fs.existsSync(sessionsDir)) {
1002
- return null;
1003
- }
1004
-
1005
- try {
1006
- const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
1007
- if (files.length === 0) {
1008
- return null;
1009
- }
1010
-
1011
- let latest: SessionState | null = null;
1012
- let latestTs = 0;
1013
-
1014
- for (const file of files) {
1015
- try {
1016
- const content = fs.readFileSync(path.join(sessionsDir, file), 'utf-8');
1017
- const state = JSON.parse(content) as SessionState;
1018
- // Skip sessions from different workspaces
1019
- if (state.workspaceDir && state.workspaceDir !== this.workspaceDir) {
1020
- continue;
1021
- }
1022
- const ts = Number(state.lastControlActivityAt ?? state.lastActivityAt ?? 0);
1023
- if (ts > latestTs) {
1024
- latestTs = ts;
1025
- latest = state;
1026
- }
1027
- } catch {
1028
- // Skip corrupted files
1029
- }
1030
- }
1031
-
1032
- return latest;
1033
- } catch {
1034
- // Non-critical: failure to read session files should not crash the service
1035
- return null;
1036
- }
1037
- }
1038
- }