principles-disciple 1.72.0 → 1.74.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 (319) hide show
  1. package/INSTALL.md +1 -3
  2. package/openclaw.plugin.json +10 -5
  3. package/package.json +17 -19
  4. package/scripts/acceptance-test.mjs +16 -73
  5. package/scripts/sync-plugin.mjs +382 -77
  6. package/src/commands/archive-impl.ts +2 -1
  7. package/src/commands/capabilities.ts +2 -2
  8. package/src/commands/context.ts +2 -2
  9. package/src/commands/disable-impl.ts +2 -1
  10. package/src/commands/evolution-status.ts +16 -16
  11. package/src/commands/export.ts +12 -67
  12. package/src/commands/pain.ts +91 -1
  13. package/src/commands/principle-rollback.ts +2 -1
  14. package/src/commands/promote-impl.ts +7 -43
  15. package/src/commands/rollback-impl.ts +2 -1
  16. package/src/commands/rollback.ts +2 -1
  17. package/src/commands/samples.ts +2 -1
  18. package/src/commands/thinking-os.ts +2 -1
  19. package/src/config/errors.ts +18 -2
  20. package/src/constants/diagnostician.ts +2 -2
  21. package/src/constants/tools.ts +2 -1
  22. package/src/core/__tests__/focus-history.test.ts +210 -0
  23. package/src/core/config.ts +1 -1
  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 +29 -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/migration.ts +0 -1
  37. package/src/core/pain-diagnostic-gate.ts +154 -0
  38. package/src/core/pain-signal.ts +21 -138
  39. package/src/core/pain.ts +15 -88
  40. package/src/core/path-resolver.ts +0 -1
  41. package/src/core/paths.ts +0 -1
  42. package/src/core/pd-task-reconciler.ts +26 -115
  43. package/src/core/pd-task-service.ts +9 -9
  44. package/src/core/pd-task-types.ts +23 -127
  45. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  46. package/src/core/principle-compiler/code-validator.ts +15 -42
  47. package/src/core/principle-compiler/compiler.ts +100 -15
  48. package/src/core/principle-compiler/index.ts +5 -2
  49. package/src/core/principle-compiler/template-generator.ts +4 -104
  50. package/src/core/principle-injection.ts +10 -202
  51. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  52. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  53. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  54. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  55. package/src/core/principle-tree-ledger.ts +8 -6
  56. package/src/core/reflection/reflection-context.ts +14 -109
  57. package/src/core/replay-engine.ts +8 -500
  58. package/src/core/rule-host-helpers.ts +5 -35
  59. package/src/core/rule-host-types.ts +10 -82
  60. package/src/core/rule-host.ts +6 -63
  61. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  62. package/src/core/session-tracker.ts +87 -101
  63. package/src/core/shadow-observation-registry.ts +19 -48
  64. package/src/core/trajectory.ts +3 -1
  65. package/src/core/workflow-funnel-loader.ts +62 -68
  66. package/src/core/workspace-context.ts +46 -0
  67. package/src/core/workspace-dir-service.ts +1 -1
  68. package/src/core/workspace-dir-validation.ts +18 -9
  69. package/src/hooks/AGENTS.md +1 -1
  70. package/src/hooks/gate-block-helper.ts +71 -64
  71. package/src/hooks/gate.ts +183 -31
  72. package/src/hooks/lifecycle.ts +30 -32
  73. package/src/hooks/llm.ts +60 -32
  74. package/src/hooks/pain.ts +297 -103
  75. package/src/hooks/prompt.ts +400 -440
  76. package/src/hooks/subagent.ts +2 -29
  77. package/src/i18n/commands.ts +2 -10
  78. package/src/index.ts +95 -85
  79. package/src/openclaw-sdk.ts +311 -0
  80. package/src/service/central-database.ts +8 -4
  81. package/src/service/evolution-queue-migration.ts +2 -1
  82. package/src/service/evolution-worker.ts +163 -1786
  83. package/src/service/internalization-trigger-adapter.ts +302 -0
  84. package/src/service/keyword-optimization-service.ts +4 -4
  85. package/src/service/monitoring-query-service.ts +1 -215
  86. package/src/service/queue-io.ts +60 -331
  87. package/src/service/runtime-summary-service.ts +59 -16
  88. package/src/service/subagent-workflow/index.ts +0 -41
  89. package/src/service/subagent-workflow/types.ts +9 -120
  90. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  91. package/src/service/workflow-watchdog.ts +0 -43
  92. package/src/types/event-payload.ts +16 -74
  93. package/src/types/event-types.ts +38 -547
  94. package/src/types/hygiene-types.ts +7 -30
  95. package/src/types/principle-tree-schema.ts +20 -222
  96. package/src/types/queue.ts +15 -70
  97. package/src/types/runtime-summary.ts +5 -49
  98. package/src/utils/io.ts +8 -20
  99. package/src/utils/retry.ts +1 -1
  100. package/src/utils/shadow-fingerprint.ts +2 -2
  101. package/src/utils/workspace-resolver.ts +50 -0
  102. package/templates/langs/en/core/AGENTS.md +7 -7
  103. package/templates/langs/en/core/BOOT.md +1 -1
  104. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  105. package/templates/langs/en/principles/THINKING_OS.md +3 -2
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  107. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  108. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  109. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  110. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  111. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  112. package/templates/langs/en/skills/evolve-task/SKILL.md +3 -3
  113. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  114. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  115. package/templates/langs/en/skills/pd-mentor/SKILL.md +2 -3
  116. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  117. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  118. package/templates/langs/zh/core/AGENTS.md +7 -7
  119. package/templates/langs/zh/core/BOOT.md +1 -1
  120. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  121. package/templates/langs/zh/principles/THINKING_OS.md +3 -2
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  127. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  128. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  129. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  130. package/templates/langs/zh/skills/evolve-task/SKILL.md +4 -4
  131. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  132. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  133. package/templates/langs/zh/skills/pd-mentor/SKILL.md +2 -3
  134. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  135. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  136. package/tests/build-artifacts.test.ts +1 -3
  137. package/tests/commands/evolution-status.test.ts +0 -118
  138. package/tests/core/bootstrap-rules.test.ts +1 -1
  139. package/tests/core/config.test.ts +1 -1
  140. package/tests/core/event-log.test.ts +35 -0
  141. package/tests/core/evolution-engine.test.ts +610 -0
  142. package/tests/core/file-store.test.ts +102 -0
  143. package/tests/core/focus-history.test.ts +203 -11
  144. package/tests/core/merge-gate-audit.test.ts +2 -169
  145. package/tests/core/migration.test.ts +7 -7
  146. package/tests/core/model-deployment-registry.test.ts +7 -1
  147. package/tests/core/model-training-registry.test.ts +19 -0
  148. package/tests/core/observability.test.ts +0 -1
  149. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  150. package/tests/core/pain.test.ts +0 -1
  151. package/tests/core/path-resolver.test.ts +1 -1
  152. package/tests/core/paths-refactor.test.ts +0 -22
  153. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  154. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  155. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  156. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  157. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  158. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  159. package/tests/core/reflection-context.test.ts +0 -14
  160. package/tests/core/replay-engine.test.ts +127 -215
  161. package/tests/core/rule-host-helpers.test.ts +2 -2
  162. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  163. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  164. package/tests/core/workspace-context.test.ts +2 -2
  165. package/tests/core/workspace-dir-validation.test.ts +8 -1
  166. package/tests/core-anti-growth.test.ts +191 -0
  167. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  168. package/tests/hooks/confirm-first-removal.test.ts +188 -0
  169. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  170. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  171. package/tests/hooks/gate-no-path-write-tool.test.ts +172 -0
  172. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  173. package/tests/hooks/pain.test.ts +269 -12
  174. package/tests/hooks/prompt-characterization.test.ts +500 -0
  175. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  176. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  177. package/tests/index.test.ts +94 -1
  178. package/tests/integration/auto-entry-gate.test.ts +248 -0
  179. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  180. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  181. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  182. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  183. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  184. package/tests/service/central-database.test.ts +457 -0
  185. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  186. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  187. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  188. package/tests/service/monitoring-query-service.test.ts +1 -47
  189. package/tests/service/queue-io.test.ts +1 -62
  190. package/tests/service/runtime-summary-service.test.ts +3 -1
  191. package/tests/service/workflow-watchdog.test.ts +0 -91
  192. package/tests/utils/file-lock.test.ts +5 -3
  193. package/tests/utils/session-key.test.ts +52 -0
  194. package/tests/utils/subagent-probe.test.ts +48 -1
  195. package/vitest.config.ts +4 -11
  196. package/.planning/codebase/ARCHITECTURE.md +0 -157
  197. package/.planning/codebase/CONCERNS.md +0 -145
  198. package/.planning/codebase/CONVENTIONS.md +0 -148
  199. package/.planning/codebase/INTEGRATIONS.md +0 -81
  200. package/.planning/codebase/STACK.md +0 -87
  201. package/.planning/codebase/STRUCTURE.md +0 -193
  202. package/.planning/codebase/TESTING.md +0 -243
  203. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  204. package/docs/COMMAND_REFERENCE.md +0 -76
  205. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  206. package/scripts/build-web.mjs +0 -46
  207. package/scripts/diagnose-nocturnal.mjs +0 -537
  208. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  209. package/src/commands/nocturnal-review.ts +0 -322
  210. package/src/commands/nocturnal-rollout.ts +0 -790
  211. package/src/commands/nocturnal-train.ts +0 -986
  212. package/src/commands/pd-reflect.ts +0 -88
  213. package/src/core/adaptive-thresholds.ts +0 -478
  214. package/src/core/diagnostician-task-store.ts +0 -192
  215. package/src/core/nocturnal-arbiter.ts +0 -715
  216. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  217. package/src/core/nocturnal-artificer.ts +0 -257
  218. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  219. package/src/core/nocturnal-compliance.ts +0 -1146
  220. package/src/core/nocturnal-dataset.ts +0 -763
  221. package/src/core/nocturnal-executability.ts +0 -428
  222. package/src/core/nocturnal-export.ts +0 -499
  223. package/src/core/nocturnal-paths.ts +0 -240
  224. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  225. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  226. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  227. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  228. package/src/core/nocturnal-trinity-types.ts +0 -218
  229. package/src/core/nocturnal-trinity.ts +0 -2680
  230. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  231. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  232. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  233. package/src/http/principles-console-route.ts +0 -709
  234. package/src/service/central-health-service.ts +0 -49
  235. package/src/service/central-overview-service.ts +0 -138
  236. package/src/service/control-ui-query-service.ts +0 -900
  237. package/src/service/cooldown-strategy.ts +0 -97
  238. package/src/service/evolution-pain-context.ts +0 -79
  239. package/src/service/evolution-query-service.ts +0 -407
  240. package/src/service/health-query-service.ts +0 -1038
  241. package/src/service/nocturnal-config.ts +0 -214
  242. package/src/service/nocturnal-runtime.ts +0 -734
  243. package/src/service/nocturnal-service.ts +0 -1605
  244. package/src/service/nocturnal-target-selector.ts +0 -545
  245. package/src/service/sleep-cycle.ts +0 -157
  246. package/src/service/startup-reconciler.ts +0 -112
  247. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  248. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  249. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  250. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  251. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  252. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  253. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  254. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  255. package/src/tools/write-pain-flag.ts +0 -215
  256. package/templates/langs/en/skills/plan-script/SKILL.md +0 -32
  257. package/templates/langs/zh/skills/plan-script/SKILL.md +0 -32
  258. package/tests/commands/nocturnal-review.test.ts +0 -448
  259. package/tests/commands/nocturnal-train.test.ts +0 -97
  260. package/tests/commands/pd-reflect.test.ts +0 -49
  261. package/tests/core/adaptive-thresholds.test.ts +0 -261
  262. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  263. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  264. package/tests/core/nocturnal-artificer.test.ts +0 -241
  265. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  266. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  267. package/tests/core/nocturnal-compliance.test.ts +0 -646
  268. package/tests/core/nocturnal-dataset.test.ts +0 -892
  269. package/tests/core/nocturnal-e2e.test.ts +0 -234
  270. package/tests/core/nocturnal-executability.test.ts +0 -357
  271. package/tests/core/nocturnal-export.test.ts +0 -517
  272. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  273. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  274. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  275. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  276. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  277. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  278. package/tests/core/pain-auto-repair.test.ts +0 -96
  279. package/tests/core/pain-integration.test.ts +0 -510
  280. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  281. package/tests/http/principles-console-route.test.ts +0 -162
  282. package/tests/integration/chaos-resilience.test.ts +0 -348
  283. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  284. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  285. package/tests/service/control-ui-query-service.test.ts +0 -121
  286. package/tests/service/cooldown-strategy.test.ts +0 -164
  287. package/tests/service/data-endpoints-regression.test.ts +0 -834
  288. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  289. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  290. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  291. package/tests/service/nocturnal-runtime.test.ts +0 -473
  292. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  293. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  294. package/tests/service/startup-reconciler.test.ts +0 -148
  295. package/tests/tools/write-pain-flag.test.ts +0 -358
  296. package/ui/src/App.tsx +0 -45
  297. package/ui/src/api.ts +0 -220
  298. package/ui/src/charts.tsx +0 -955
  299. package/ui/src/components/ErrorState.tsx +0 -6
  300. package/ui/src/components/Loading.tsx +0 -13
  301. package/ui/src/components/ProtectedRoute.tsx +0 -12
  302. package/ui/src/components/Shell.tsx +0 -91
  303. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  304. package/ui/src/components/index.ts +0 -5
  305. package/ui/src/context/auth.tsx +0 -80
  306. package/ui/src/context/theme.tsx +0 -66
  307. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  308. package/ui/src/i18n/ui.ts +0 -473
  309. package/ui/src/main.tsx +0 -16
  310. package/ui/src/pages/EvolutionPage.tsx +0 -333
  311. package/ui/src/pages/FeedbackPage.tsx +0 -138
  312. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  313. package/ui/src/pages/LoginPage.tsx +0 -89
  314. package/ui/src/pages/OverviewPage.tsx +0 -599
  315. package/ui/src/pages/SamplesPage.tsx +0 -174
  316. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  317. package/ui/src/styles.css +0 -2020
  318. package/ui/src/types.ts +0 -384
  319. 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
- }