principles-disciple 1.11.0 → 1.13.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 (233) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +66 -0
  3. package/esbuild.config.js +1 -1
  4. package/openclaw.plugin.json +4 -4
  5. package/package.json +2 -3
  6. package/run-nocturnal.mjs +30 -0
  7. package/scripts/db-migrate.mjs +170 -0
  8. package/scripts/sync-plugin.mjs +250 -6
  9. package/src/commands/archive-impl.ts +136 -0
  10. package/src/commands/capabilities.ts +4 -2
  11. package/src/commands/context.ts +5 -1
  12. package/src/commands/disable-impl.ts +151 -0
  13. package/src/commands/evolution-status.ts +64 -19
  14. package/src/commands/export.ts +8 -6
  15. package/src/commands/focus.ts +8 -20
  16. package/src/commands/nocturnal-review.ts +5 -7
  17. package/src/commands/nocturnal-rollout.ts +1 -12
  18. package/src/commands/nocturnal-train.ts +21 -47
  19. package/src/commands/pain.ts +10 -5
  20. package/src/commands/principle-rollback.ts +4 -2
  21. package/src/commands/promote-impl.ts +274 -0
  22. package/src/commands/rollback-impl.ts +234 -0
  23. package/src/commands/rollback.ts +6 -3
  24. package/src/commands/samples.ts +2 -0
  25. package/src/commands/thinking-os.ts +3 -4
  26. package/src/commands/workflow-debug.ts +2 -1
  27. package/src/config/errors.ts +1 -0
  28. package/src/core/AGENTS.md +34 -0
  29. package/src/core/adaptive-thresholds.ts +4 -3
  30. package/src/core/code-implementation-storage.ts +241 -0
  31. package/src/core/config.ts +5 -2
  32. package/src/core/control-ui-db.ts +29 -10
  33. package/src/core/detection-funnel.ts +12 -7
  34. package/src/core/diagnostician-task-store.ts +156 -0
  35. package/src/core/dictionary.ts +4 -4
  36. package/src/core/empathy-keyword-matcher.ts +7 -3
  37. package/src/core/empathy-types.ts +13 -2
  38. package/src/core/event-log.ts +14 -6
  39. package/src/core/evolution-engine.ts +27 -31
  40. package/src/core/evolution-logger.ts +3 -2
  41. package/src/core/evolution-reducer.ts +110 -31
  42. package/src/core/evolution-types.ts +10 -0
  43. package/src/core/external-training-contract.ts +1 -0
  44. package/src/core/focus-history.ts +38 -24
  45. package/src/core/hygiene/tracker.ts +10 -6
  46. package/src/core/init.ts +5 -2
  47. package/src/core/migration.ts +3 -3
  48. package/src/core/model-deployment-registry.ts +6 -4
  49. package/src/core/model-training-registry.ts +5 -3
  50. package/src/core/nocturnal-arbiter.ts +13 -14
  51. package/src/core/nocturnal-artifact-lineage.ts +117 -0
  52. package/src/core/nocturnal-artificer.ts +257 -0
  53. package/src/core/nocturnal-candidate-scoring.ts +4 -2
  54. package/src/core/nocturnal-compliance.ts +67 -19
  55. package/src/core/nocturnal-dataset.ts +95 -2
  56. package/src/core/nocturnal-executability.ts +2 -3
  57. package/src/core/nocturnal-export.ts +6 -3
  58. package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
  59. package/src/core/nocturnal-trajectory-extractor.ts +10 -3
  60. package/src/core/nocturnal-trinity.ts +319 -61
  61. package/src/core/pain-context-extractor.ts +29 -15
  62. package/src/core/pain.ts +7 -5
  63. package/src/core/path-resolver.ts +16 -15
  64. package/src/core/paths.ts +2 -1
  65. package/src/core/pd-task-reconciler.ts +463 -0
  66. package/src/core/pd-task-service.ts +42 -0
  67. package/src/core/pd-task-store.ts +77 -0
  68. package/src/core/pd-task-types.ts +128 -0
  69. package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
  70. package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
  71. package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
  72. package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
  73. package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
  74. package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
  75. package/src/core/principle-training-state.ts +95 -370
  76. package/src/core/principle-tree-ledger.ts +733 -0
  77. package/src/core/principle-tree-migration.ts +195 -0
  78. package/src/core/profile.ts +3 -1
  79. package/src/core/promotion-gate.ts +14 -18
  80. package/src/core/replay-engine.ts +562 -0
  81. package/src/core/risk-calculator.ts +6 -4
  82. package/src/core/rule-host-helpers.ts +39 -0
  83. package/src/core/rule-host-types.ts +82 -0
  84. package/src/core/rule-host.ts +245 -0
  85. package/src/core/rule-implementation-runtime.ts +38 -0
  86. package/src/core/schema/db-types.ts +16 -0
  87. package/src/core/schema/index.ts +26 -0
  88. package/src/core/schema/migration-runner.ts +207 -0
  89. package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
  90. package/src/core/schema/migrations/002-init-central.ts +122 -0
  91. package/src/core/schema/migrations/003-init-workflow.ts +55 -0
  92. package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
  93. package/src/core/schema/migrations/index.ts +31 -0
  94. package/src/core/schema/schema-definitions.ts +650 -0
  95. package/src/core/session-tracker.ts +6 -4
  96. package/src/core/shadow-observation-registry.ts +6 -3
  97. package/src/core/system-logger.ts +2 -2
  98. package/src/core/thinking-models.ts +182 -46
  99. package/src/core/thinking-os-parser.ts +156 -0
  100. package/src/core/training-program.ts +7 -7
  101. package/src/core/trajectory.ts +42 -36
  102. package/src/core/workspace-context.ts +77 -11
  103. package/src/core/workspace-dir-validation.ts +152 -0
  104. package/src/hooks/AGENTS.md +31 -0
  105. package/src/hooks/bash-risk.ts +3 -1
  106. package/src/hooks/edit-verification.ts +9 -5
  107. package/src/hooks/gate-block-helper.ts +5 -1
  108. package/src/hooks/gate.ts +152 -5
  109. package/src/hooks/gfi-gate.ts +9 -2
  110. package/src/hooks/lifecycle-routing.ts +124 -0
  111. package/src/hooks/lifecycle.ts +12 -12
  112. package/src/hooks/llm.ts +17 -109
  113. package/src/hooks/message-sanitize.ts +5 -3
  114. package/src/hooks/pain.ts +19 -15
  115. package/src/hooks/progressive-trust-gate.ts +7 -1
  116. package/src/hooks/prompt.ts +169 -60
  117. package/src/hooks/subagent.ts +5 -4
  118. package/src/hooks/thinking-checkpoint.ts +2 -0
  119. package/src/hooks/trajectory-collector.ts +15 -12
  120. package/src/http/principles-console-route.ts +31 -68
  121. package/src/i18n/commands.ts +2 -2
  122. package/src/index.ts +130 -40
  123. package/src/service/central-database.ts +131 -43
  124. package/src/service/central-health-service.ts +47 -0
  125. package/src/service/central-overview-service.ts +135 -0
  126. package/src/service/central-sync-service.ts +87 -0
  127. package/src/service/control-ui-query-service.ts +46 -36
  128. package/src/service/event-log-auditor.ts +261 -0
  129. package/src/service/evolution-query-service.ts +23 -22
  130. package/src/service/evolution-worker.ts +565 -261
  131. package/src/service/health-query-service.ts +213 -36
  132. package/src/service/nocturnal-runtime.ts +8 -4
  133. package/src/service/nocturnal-service.ts +503 -59
  134. package/src/service/nocturnal-target-selector.ts +5 -7
  135. package/src/service/runtime-summary-service.ts +2 -1
  136. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
  137. package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
  138. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
  139. package/src/service/subagent-workflow/index.ts +2 -0
  140. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
  141. package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
  142. package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
  143. package/src/service/subagent-workflow/types.ts +9 -4
  144. package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
  145. package/src/service/subagent-workflow/workflow-store.ts +71 -11
  146. package/src/service/trajectory-service.ts +2 -1
  147. package/src/tools/critique-prompt.ts +1 -1
  148. package/src/tools/deep-reflect.ts +175 -209
  149. package/src/tools/model-index.ts +2 -1
  150. package/src/types/event-types.ts +2 -2
  151. package/src/types/principle-tree-schema.ts +29 -23
  152. package/src/utils/file-lock.ts +5 -3
  153. package/src/utils/io.ts +5 -2
  154. package/src/utils/nlp.ts +5 -46
  155. package/src/utils/node-vm-polyfill.ts +11 -0
  156. package/src/utils/plugin-logger.ts +2 -0
  157. package/src/utils/retry.ts +572 -0
  158. package/src/utils/subagent-probe.ts +1 -1
  159. package/templates/langs/en/core/AGENTS.md +0 -13
  160. package/templates/langs/en/core/SOUL.md +1 -31
  161. package/templates/langs/en/core/TOOLS.md +0 -4
  162. package/templates/langs/en/principles/THINKING_OS.md +77 -0
  163. package/templates/langs/en/skills/admin/SKILL.md +0 -1
  164. package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
  165. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
  166. package/templates/langs/zh/core/AGENTS.md +0 -22
  167. package/templates/langs/zh/core/SOUL.md +1 -31
  168. package/templates/langs/zh/core/TOOLS.md +0 -4
  169. package/templates/langs/zh/principles/THINKING_OS.md +77 -0
  170. package/templates/langs/zh/skills/admin/SKILL.md +0 -1
  171. package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
  172. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
  173. package/tests/commands/evolution-status.test.ts +119 -0
  174. package/tests/commands/implementation-lifecycle.test.ts +362 -0
  175. package/tests/core/code-implementation-storage.test.ts +398 -0
  176. package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
  177. package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
  178. package/tests/core/nocturnal-artificer.test.ts +241 -0
  179. package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
  180. package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
  181. package/tests/core/pd-task-store.test.ts +126 -0
  182. package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
  183. package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
  184. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
  185. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
  186. package/tests/core/principle-training-state.test.ts +228 -1
  187. package/tests/core/principle-tree-ledger.test.ts +423 -0
  188. package/tests/core/regression-v1-9-1.test.ts +265 -0
  189. package/tests/core/replay-engine.test.ts +234 -0
  190. package/tests/core/rule-host-helpers.test.ts +120 -0
  191. package/tests/core/rule-host.test.ts +389 -0
  192. package/tests/core/rule-implementation-runtime.test.ts +64 -0
  193. package/tests/core/workspace-context.test.ts +53 -0
  194. package/tests/core/workspace-dir-validation.test.ts +272 -0
  195. package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
  196. package/tests/hooks/pain.test.ts +74 -10
  197. package/tests/hooks/prompt.test.ts +63 -1
  198. package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
  199. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
  200. package/tests/service/data-endpoints-regression.test.ts +834 -0
  201. package/tests/service/evolution-worker.test.ts +0 -123
  202. package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
  203. package/tests/utils/nlp.test.ts +1 -19
  204. package/tests/utils/retry.test.ts +327 -0
  205. package/ui/src/App.tsx +1 -1
  206. package/ui/src/api.ts +4 -0
  207. package/ui/src/charts.tsx +366 -0
  208. package/ui/src/components/WorkspaceConfig.tsx +107 -75
  209. package/ui/src/i18n/ui.ts +89 -31
  210. package/ui/src/pages/EvolutionPage.tsx +1 -1
  211. package/ui/src/pages/OverviewPage.tsx +441 -81
  212. package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
  213. package/ui/src/styles.css +43 -0
  214. package/ui/src/types.ts +17 -1
  215. package/src/agents/nocturnal-dreamer.md +0 -152
  216. package/src/agents/nocturnal-philosopher.md +0 -138
  217. package/src/agents/nocturnal-reflector.md +0 -126
  218. package/src/agents/nocturnal-scribe.md +0 -164
  219. package/templates/workspace/.principles/00-kernel.md +0 -51
  220. package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
  221. package/templates/workspace/.principles/PRINCIPLES.md +0 -20
  222. package/templates/workspace/.principles/PROFILE.json +0 -54
  223. package/templates/workspace/.principles/PROFILE.schema.json +0 -56
  224. package/templates/workspace/.principles/THINKING_OS.md +0 -64
  225. package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
  226. package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
  227. package/templates/workspace/.principles/models/_INDEX.md +0 -27
  228. package/templates/workspace/.principles/models/first_principles.md +0 -62
  229. package/templates/workspace/.principles/models/marketing_4p.md +0 -52
  230. package/templates/workspace/.principles/models/porter_five.md +0 -63
  231. package/templates/workspace/.principles/models/swot.md +0 -60
  232. package/templates/workspace/.principles/models/user_story_map.md +0 -63
  233. package/templates/workspace/.state/WORKBOARD.json +0 -4
@@ -85,14 +85,63 @@ export class HealthQueryService {
85
85
  this.eventLog = wctx.eventLog;
86
86
  this.evolutionReducer = wctx.evolutionReducer;
87
87
  this.uiDb = new ControlUiDatabase({ workspaceDir });
88
+ this.ensureTables();
89
+ this.syncGfiFromSession();
88
90
  }
89
91
 
90
92
  dispose(): void {
91
93
  this.uiDb.dispose();
92
94
  }
93
95
 
96
+ /**
97
+ * Ensure all tables required by HealthQueryService exist.
98
+ * This is a safety net for workspaces created before TrajectoryDatabase
99
+ * had full schema initialization, or where initSchema() was never called.
100
+ * Uses CREATE TABLE IF NOT EXISTS so it's idempotent.
101
+ */
102
+ private ensureTables(): void {
103
+ try {
104
+ this.uiDb.execute(`
105
+ CREATE TABLE IF NOT EXISTS pain_events (
106
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
107
+ session_id TEXT NOT NULL,
108
+ source TEXT NOT NULL,
109
+ score REAL NOT NULL DEFAULT 0,
110
+ reason TEXT DEFAULT '',
111
+ severity TEXT DEFAULT 'mild',
112
+ origin TEXT DEFAULT '',
113
+ confidence REAL DEFAULT 0,
114
+ text TEXT DEFAULT '',
115
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
116
+ )
117
+ `);
118
+ this.uiDb.execute(`
119
+ CREATE INDEX IF NOT EXISTS idx_pain_events_created_at ON pain_events(created_at)
120
+ `);
121
+ this.uiDb.execute(`
122
+ CREATE TABLE IF NOT EXISTS gate_blocks (
123
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
124
+ session_id TEXT NOT NULL,
125
+ tool_name TEXT NOT NULL,
126
+ file_path TEXT,
127
+ reason TEXT NOT NULL,
128
+ gfi REAL,
129
+ gfi_after REAL,
130
+ trust_stage INTEGER,
131
+ gate_type TEXT,
132
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
133
+ )
134
+ `);
135
+ this.uiDb.execute(`
136
+ CREATE INDEX IF NOT EXISTS idx_gate_blocks_created_at ON gate_blocks(created_at)
137
+ `);
138
+ } catch (err) {
139
+ console.warn('[HealthQueryService] Table ensure failed (non-fatal):', err);
140
+ }
141
+ }
142
+
94
143
  getOverviewHealth(): {
95
- gfi: { current: number; peakToday: number; threshold: number };
144
+ gfi: { current: number; peakToday: number; threshold: number; trend: { hour: string; value: number }[] };
96
145
  trust: { stage: number; stageLabel: string; score: number };
97
146
  evolution: { tier: string; points: number };
98
147
  painFlag: { active: boolean; source: string | null; score: number | null };
@@ -100,7 +149,6 @@ export class HealthQueryService {
100
149
  queue: { pending: number; inProgress: number; completed: number };
101
150
  activeStage: HealthStage;
102
151
  } {
103
- const session = this.getCurrentSession();
104
152
  const threshold = this.getGfiThreshold();
105
153
  const trust = this.readTrust();
106
154
  const evolution = this.readEvolutionScore();
@@ -108,14 +156,21 @@ export class HealthQueryService {
108
156
  const queue = this.readQueueStats();
109
157
  const painFlag = this.readPainFlag();
110
158
 
111
- const currentGfi = this.asNumber(session?.currentGfi, 0);
112
- const peakToday = this.asNumber(session?.dailyGfiPeak, currentGfi);
159
+ // GFI: always read from SQLite (synced from session JSON at construction time)
160
+ const gfiData = this.readGfiFromDb();
161
+ const {currentGfi} = gfiData;
162
+ const peakToday = gfiData.dailyGfiPeak;
163
+
164
+ // GFI history trend: aggregate pain_events by hour
165
+ const today = new Date().toISOString().slice(0, 10);
166
+ const gfiTrend = this.readGfiTrend(today);
113
167
 
114
168
  return {
115
169
  gfi: {
116
170
  current: currentGfi,
117
171
  peakToday,
118
172
  threshold,
173
+ trend: gfiTrend,
119
174
  },
120
175
  trust,
121
176
  evolution,
@@ -127,14 +182,14 @@ export class HealthQueryService {
127
182
  deprecated: reducerStats.deprecatedCount,
128
183
  },
129
184
  queue,
130
- activeStage: this.computeHealthStage(currentGfi, threshold, painFlag.active),
185
+ activeStage: HealthQueryService.computeHealthStage(currentGfi, threshold, painFlag.active),
131
186
  };
132
187
  }
133
188
 
134
189
  getEvolutionPrinciples(): {
135
190
  principles: {
136
191
  summary: { candidate: number; probation: number; active: number; deprecated: number };
137
- recent: Array<{
192
+ recent: {
138
193
  principleId: string;
139
194
  status: string;
140
195
  triggerPattern: string;
@@ -142,14 +197,14 @@ export class HealthQueryService {
142
197
  fromStatus: string;
143
198
  toStatus: string;
144
199
  timestamp: string;
145
- }>;
200
+ }[];
146
201
  };
147
202
  nocturnalTraining: {
148
203
  queue: { pending: number; inProgress: number; completed: number };
149
- trinityRecords: Array<{ artifactId: string; status: string; createdAt: string }>;
204
+ trinityRecords: { artifactId: string; status: string; createdAt: string }[];
150
205
  arbiterPassRate: number;
151
206
  orpoSampleCount: number;
152
- deployments: Array<{ modelId: string; status: string; checkpointPath: string | null }>;
207
+ deployments: { modelId: string; status: string; checkpointPath: string | null }[];
153
208
  };
154
209
  painSourceDistribution: Record<string, number>;
155
210
  activeStage: string;
@@ -176,14 +231,33 @@ export class HealthQueryService {
176
231
  };
177
232
  }
178
233
 
234
+ /**
235
+ * Read GFI trend for a specific day by aggregating pain_events by hour.
236
+ * Used by getOverviewHealth() to provide historical GFI context.
237
+ */
238
+ private readGfiTrend(date: string): { hour: string; value: number }[] {
239
+ try {
240
+ const rows = this.uiDb.all<{ hour: string; value: number }>(`
241
+ SELECT substr(created_at, 1, 13) || ':00:00Z' AS hour, ROUND(SUM(score), 2) AS value
242
+ FROM pain_events
243
+ WHERE substr(created_at, 1, 10) = ?
244
+ GROUP BY substr(created_at, 1, 13)
245
+ ORDER BY hour ASC
246
+ `, date);
247
+ return rows.map(row => ({ hour: row.hour, value: this.asNumber(row.value, 0) }));
248
+ } catch {
249
+ // pain_events table may not exist in this workspace — return empty trend
250
+ return [];
251
+ }
252
+ }
253
+
179
254
  getFeedbackGfi(): {
180
255
  current: number;
181
256
  peakToday: number;
182
257
  threshold: number;
183
- trend: Array<{ hour: string; value: number }>;
258
+ trend: { hour: string; value: number }[];
184
259
  sources: Record<string, number>;
185
260
  } {
186
- const session = this.getCurrentSession();
187
261
  const threshold = this.getGfiThreshold();
188
262
  const today = new Date().toISOString().slice(0, 10);
189
263
 
@@ -203,23 +277,26 @@ export class HealthQueryService {
203
277
  ORDER BY total DESC
204
278
  `, today);
205
279
 
280
+ // GFI: read from SQLite (synced from session JSON at construction time)
281
+ const gfiData = this.readGfiFromDb();
282
+
206
283
  return {
207
- current: this.asNumber(session?.currentGfi, 0),
208
- peakToday: this.asNumber(session?.dailyGfiPeak, this.asNumber(session?.currentGfi, 0)),
284
+ current: gfiData.currentGfi,
285
+ peakToday: gfiData.dailyGfiPeak,
209
286
  threshold,
210
287
  trend: trendRows.map((row) => ({ hour: row.hour, value: this.asNumber(row.value, 0) })),
211
288
  sources: Object.fromEntries(sourceRows.map((row) => [row.source, this.asNumber(row.total, 0)])),
212
289
  };
213
290
  }
214
291
 
215
- getFeedbackEmpathyEvents(limit: number = 50): Array<{
292
+ getFeedbackEmpathyEvents(limit = 50): {
216
293
  timestamp: string;
217
294
  severity: string;
218
295
  score: number;
219
296
  reason: string;
220
297
  origin: string;
221
298
  gfiAfter: number;
222
- }> {
299
+ }[] {
223
300
  const safeLimit = Math.max(1, Math.min(500, Math.floor(limit)));
224
301
  const events = this.readMergedEvents()
225
302
  .filter((entry) => entry.type === 'pain_signal' && String(entry.data?.source ?? '') === 'user_empathy')
@@ -239,13 +316,13 @@ export class HealthQueryService {
239
316
  });
240
317
  }
241
318
 
242
- getFeedbackGateBlocks(limit: number = 50): Array<{
319
+ getFeedbackGateBlocks(limit = 50): {
243
320
  timestamp: string;
244
321
  toolName: string;
245
322
  reason: string;
246
323
  gfi: number;
247
324
  trustStage: number;
248
- }> {
325
+ }[] {
249
326
  const trust = this.readTrust();
250
327
  const rows = this.readGateBlocksRaw(limit);
251
328
 
@@ -315,7 +392,7 @@ export class HealthQueryService {
315
392
  };
316
393
  }
317
394
 
318
- getGateBlocks(limit: number = 50): Array<{
395
+ getGateBlocks(limit = 50): {
319
396
  timestamp: string;
320
397
  toolName: string;
321
398
  filePath: string | null;
@@ -323,7 +400,7 @@ export class HealthQueryService {
323
400
  gateType: string;
324
401
  gfi: number;
325
402
  trustStage: number;
326
- }> {
403
+ }[] {
327
404
  const trust = this.readTrust();
328
405
  const rows = this.readGateBlocksRaw(limit);
329
406
 
@@ -347,13 +424,13 @@ export class HealthQueryService {
347
424
  );
348
425
  const rawStage = this.asNumber(
349
426
  scorecard.trustStage ?? scorecard.trust_stage ?? scorecard.stage,
350
- this.inferTrustStageFromScore(score),
427
+ HealthQueryService.inferTrustStageFromScore(score),
351
428
  );
352
429
  const stage = Math.max(1, Math.min(4, Math.round(rawStage)));
353
430
 
354
431
  return {
355
432
  stage,
356
- stageLabel: this.getTrustStageLabel(stage),
433
+ stageLabel: HealthQueryService.getTrustStageLabel(stage),
357
434
  score,
358
435
  };
359
436
  }
@@ -397,14 +474,20 @@ export class HealthQueryService {
397
474
  }
398
475
 
399
476
  private getCurrentSession(): SessionState | null {
477
+ // First, try in-memory sessions (live sessions from session-tracker)
400
478
  const sessions = listSessions(this.workspaceDir);
401
- if (sessions.length === 0) return null;
402
- const sorted = [...sessions].sort((a, b) => {
403
- const aTs = Number(a.lastControlActivityAt ?? a.lastActivityAt ?? 0);
404
- const bTs = Number(b.lastControlActivityAt ?? b.lastActivityAt ?? 0);
405
- return bTs - aTs;
406
- });
407
- return sorted[0] ?? null;
479
+ if (sessions.length > 0) {
480
+ const sorted = [...sessions].sort((a, b) => {
481
+ const aTs = Number(a.lastControlActivityAt ?? a.lastActivityAt ?? 0);
482
+ const bTs = Number(b.lastControlActivityAt ?? b.lastActivityAt ?? 0);
483
+ return bTs - aTs;
484
+ });
485
+ return sorted[0] ?? null;
486
+ }
487
+
488
+ // Fallback: read session JSON files directly (handles process restarts where
489
+ // loadAllSessions may not have reloaded all sessions, or workspaceDir mismatch)
490
+ return this.readLatestSessionFromFile();
408
491
  }
409
492
 
410
493
  private getGfiThreshold(): number {
@@ -418,7 +501,7 @@ export class HealthQueryService {
418
501
  return fallback ?? 70;
419
502
  }
420
503
 
421
- private computeHealthStage(currentGfi: number, threshold: number, painFlagActive: boolean): HealthStage {
504
+ private static computeHealthStage(currentGfi: number, threshold: number, painFlagActive: boolean): HealthStage {
422
505
  if (painFlagActive || currentGfi >= threshold) {
423
506
  return 'critical';
424
507
  }
@@ -428,7 +511,7 @@ export class HealthQueryService {
428
511
  return 'healthy';
429
512
  }
430
513
 
431
- private getTrustStageLabel(stage: number): string {
514
+ private static getTrustStageLabel(stage: number): string {
432
515
  switch (stage) {
433
516
  case 1:
434
517
  return 'Observer';
@@ -443,7 +526,7 @@ export class HealthQueryService {
443
526
  }
444
527
  }
445
528
 
446
- private inferTrustStageFromScore(score: number): number {
529
+ private static inferTrustStageFromScore(score: number): number {
447
530
  if (score >= 80) return 4;
448
531
  if (score >= 60) return 3;
449
532
  if (score >= 30) return 2;
@@ -468,6 +551,7 @@ export class HealthQueryService {
468
551
  const streamPath = resolvePdPath(this.workspaceDir, 'EVOLUTION_STREAM');
469
552
  if (!fs.existsSync(streamPath)) return [];
470
553
 
554
+ // eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment
471
555
  let lines: string[] = [];
472
556
  try {
473
557
  const raw = fs.readFileSync(streamPath, 'utf8').trim();
@@ -479,6 +563,7 @@ export class HealthQueryService {
479
563
 
480
564
  const records: RecentPrincipleChange[] = [];
481
565
  for (const line of lines) {
566
+ // eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment in try/catch
482
567
  let event: EvolutionStreamRecord | null = null;
483
568
  try {
484
569
  event = JSON.parse(line) as EvolutionStreamRecord;
@@ -572,10 +657,10 @@ export class HealthQueryService {
572
657
 
573
658
  private readNocturnalTraining(): {
574
659
  queue: { pending: number; inProgress: number; completed: number };
575
- trinityRecords: Array<{ artifactId: string; status: string; createdAt: string }>;
660
+ trinityRecords: { artifactId: string; status: string; createdAt: string }[];
576
661
  arbiterPassRate: number;
577
662
  orpoSampleCount: number;
578
- deployments: Array<{ modelId: string; status: string; checkpointPath: string | null }>;
663
+ deployments: { modelId: string; status: string; checkpointPath: string | null }[];
579
664
  } {
580
665
  const sampleDir = resolvePdPath(this.workspaceDir, 'NOCTURNAL_SAMPLES_DIR');
581
666
  const exportDir = resolvePdPath(this.workspaceDir, 'NOCTURNAL_EXPORTS_DIR');
@@ -696,6 +781,7 @@ export class HealthQueryService {
696
781
  return [];
697
782
  }
698
783
 
784
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
699
785
  private getEventDedupKey(entry: EventLogEntry): string {
700
786
  const eventId = typeof entry.data?.eventId === 'string' ? entry.data.eventId : null;
701
787
  if (eventId) {
@@ -743,7 +829,7 @@ export class HealthQueryService {
743
829
  if (direct !== null) return direct;
744
830
 
745
831
  const reason = String(row.reason ?? '');
746
- const match = reason.match(/gfi\s*[:=]\s*(-?\d+(?:\.\d+)?)/i);
832
+ const match = /gfi\s*[:=]\s*(-?\d+(?:\.\d+)?)/i.exec(reason);
747
833
  if (match) {
748
834
  return this.asNumber(Number(match[1]), 0);
749
835
  }
@@ -757,7 +843,7 @@ export class HealthQueryService {
757
843
  if (direct !== null) return Math.max(1, Math.min(4, Math.round(direct)));
758
844
 
759
845
  const reason = String(row.reason ?? '').toLowerCase();
760
- const match = reason.match(/stage\s*(\d+)/i);
846
+ const match = /stage\s*(\d+)/i.exec(reason);
761
847
  if (match) {
762
848
  return Math.max(1, Math.min(4, Math.round(this.asNumber(Number(match[1]), fallbackStage))));
763
849
  }
@@ -765,6 +851,7 @@ export class HealthQueryService {
765
851
  return fallbackStage;
766
852
  }
767
853
 
854
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
768
855
  private resolveGateType(row: GateBlockRow): string {
769
856
  if (typeof row.gate_type === 'string' && row.gate_type.trim().length > 0) {
770
857
  return row.gate_type;
@@ -788,12 +875,14 @@ export class HealthQueryService {
788
875
  return cached.has(columnName);
789
876
  }
790
877
 
878
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
791
879
  private scoreToStatus(score: number): string {
792
880
  if (score >= 70) return 'healthy';
793
881
  if (score >= 40) return 'warning';
794
882
  return 'critical';
795
883
  }
796
884
 
885
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
797
886
  private evolutionToStatus(tier: string, points: number): string {
798
887
  const lower = tier.toLowerCase();
799
888
  if (lower === 'forest' || lower === 'tree') return 'healthy';
@@ -801,7 +890,8 @@ export class HealthQueryService {
801
890
  return 'critical';
802
891
  }
803
892
 
804
- private safeListFiles(dirPath: string, predicate: (name: string) => boolean): string[] {
893
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this, no-unused-vars -- Reason: Private helper doesn't use instance state
894
+ private safeListFiles(dirPath: string, predicate: (_name: string) => boolean): string[] {
805
895
  if (!fs.existsSync(dirPath)) return [];
806
896
  try {
807
897
  return fs.readdirSync(dirPath)
@@ -812,6 +902,7 @@ export class HealthQueryService {
812
902
  }
813
903
  }
814
904
 
905
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
815
906
  private readJsonFile<T>(filePath: string, fallback: T): T {
816
907
  if (!fs.existsSync(filePath)) return fallback;
817
908
  try {
@@ -821,10 +912,12 @@ export class HealthQueryService {
821
912
  }
822
913
  }
823
914
 
915
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
824
916
  private asNumber(value: unknown, fallback: number): number {
825
917
  return Number.isFinite(value) ? Number(value) : fallback;
826
918
  }
827
919
 
920
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: Private helper doesn't use instance state
828
921
  private asNullableNumber(value: unknown): number | null {
829
922
  if (Number.isFinite(value)) return Number(value);
830
923
  if (typeof value === 'string' && value.trim().length > 0) {
@@ -833,4 +926,88 @@ export class HealthQueryService {
833
926
  }
834
927
  return null;
835
928
  }
929
+
930
+ /**
931
+ * Sync GFI from the latest session JSON file into SQLite.
932
+ * Called at construction time so all queries read from a single source (SQLite).
933
+ */
934
+ private syncGfiFromSession(): void {
935
+ const session = this.readLatestSessionFromFile();
936
+ const currentGfi = this.asNumber(session?.currentGfi, 0);
937
+ const dailyGfiPeak = this.asNumber(session?.dailyGfiPeak, currentGfi);
938
+ const today = new Date().toISOString().slice(0, 10);
939
+
940
+ try {
941
+ this.uiDb.execute(`
942
+ CREATE TABLE IF NOT EXISTS gfi_state (
943
+ id INTEGER PRIMARY KEY CHECK (id = 1),
944
+ current_gfi REAL NOT NULL DEFAULT 0,
945
+ daily_gfi_peak REAL NOT NULL DEFAULT 0,
946
+ gfi_date TEXT NOT NULL DEFAULT ''
947
+ )
948
+ `);
949
+ this.uiDb.run(
950
+ 'INSERT OR REPLACE INTO gfi_state (id, current_gfi, daily_gfi_peak, gfi_date) VALUES (1, ?, ?, ?)',
951
+ currentGfi,
952
+ dailyGfiPeak,
953
+ today,
954
+ );
955
+ } catch (err) {
956
+ console.warn('[HealthQueryService] Failed to sync GFI from session:', err);
957
+ }
958
+ }
959
+
960
+ /**
961
+ * Read current GFI state from SQLite.
962
+ */
963
+ private readGfiFromDb(): { currentGfi: number; dailyGfiPeak: number } {
964
+ try {
965
+ const row = this.uiDb.get<{ current_gfi: number; daily_gfi_peak: number }>(
966
+ 'SELECT current_gfi, daily_gfi_peak FROM gfi_state WHERE id = 1',
967
+ );
968
+ if (!row) return { currentGfi: 0, dailyGfiPeak: 0 };
969
+ return {
970
+ currentGfi: row.current_gfi ?? 0,
971
+ dailyGfiPeak: row.daily_gfi_peak ?? 0,
972
+ };
973
+ } catch {
974
+ return { currentGfi: 0, dailyGfiPeak: 0 };
975
+ }
976
+ }
977
+
978
+ /**
979
+ * Read the most recent session JSON file from disk.
980
+ * Used to sync GFI from session-tracker's persistence into SQLite.
981
+ */
982
+ private readLatestSessionFromFile(): SessionState | null {
983
+ const sessionsDir = path.join(this.stateDir, 'sessions');
984
+ if (!fs.existsSync(sessionsDir)) return null;
985
+
986
+ try {
987
+ const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
988
+ if (files.length === 0) return null;
989
+
990
+ let latest: SessionState | null = null;
991
+ let latestTs = 0;
992
+
993
+ for (const file of files) {
994
+ try {
995
+ const content = fs.readFileSync(path.join(sessionsDir, file), 'utf-8');
996
+ const state = JSON.parse(content) as SessionState;
997
+ if (state.workspaceDir && state.workspaceDir !== this.workspaceDir) continue;
998
+ const ts = Number(state.lastControlActivityAt ?? state.lastActivityAt ?? 0);
999
+ if (ts > latestTs) {
1000
+ latestTs = ts;
1001
+ latest = state;
1002
+ }
1003
+ } catch {
1004
+ // Skip corrupted files
1005
+ }
1006
+ }
1007
+
1008
+ return latest;
1009
+ } catch {
1010
+ return null;
1011
+ }
1012
+ }
836
1013
  }
@@ -20,7 +20,8 @@
20
20
 
21
21
  import * as fs from 'fs';
22
22
  import * as path from 'path';
23
- import { listSessions, SessionState } from '../core/session-tracker.js';
23
+ import type { SessionState } from '../core/session-tracker.js';
24
+ import { listSessions } from '../core/session-tracker.js';
24
25
  import { withLockAsync } from '../utils/file-lock.js';
25
26
 
26
27
  // ---------------------------------------------------------------------------
@@ -303,6 +304,7 @@ export function checkWorkspaceIdle(
303
304
  trajectoryGuardrailConfirmsIdle = trajectoryIdleFor > idleThresholdMs * 0.8;
304
305
  }
305
306
 
307
+ // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all if/else branches
306
308
  let reason: string;
307
309
  if (mostRecentActivityAt === 0) {
308
310
  reason = 'No active sessions found — workspace is idle';
@@ -350,8 +352,9 @@ export function checkCooldown(
350
352
  } = {}
351
353
  ): CooldownCheckResult {
352
354
  const {
353
- globalCooldownMs = DEFAULT_GLOBAL_COOLDOWN_MS,
354
- principleCooldownMs = DEFAULT_PRINCIPLE_COOLDOWN_MS,
355
+ /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: Cooldown parameters reserved for future quota enforcement */
356
+ globalCooldownMs: _globalCooldownMs = DEFAULT_GLOBAL_COOLDOWN_MS,
357
+ principleCooldownMs: _principleCooldownMs = DEFAULT_PRINCIPLE_COOLDOWN_MS,
355
358
  maxRunsPerWindow = DEFAULT_MAX_RUNS_PER_WINDOW,
356
359
  quotaWindowMs = DEFAULT_QUOTA_WINDOW_MS,
357
360
  } = options;
@@ -369,7 +372,7 @@ export function checkCooldown(
369
372
  if (cooldownEnd > now) {
370
373
  globalCooldownActive = true;
371
374
  globalCooldownRemainingMs = cooldownEnd - now;
372
- globalCooldownUntil = state.globalCooldownUntil;
375
+ globalCooldownUntil = state.globalCooldownUntil; // eslint-disable-line @typescript-eslint/prefer-destructuring -- Reason: globalCooldownUntil is reassignable outer let - destructuring would shadow
373
376
  }
374
377
  }
375
378
 
@@ -537,6 +540,7 @@ export interface PreflightCheckResult {
537
540
  * @param trajectoryLastActivityAt - Optional trajectory timestamp as secondary guardrail
538
541
  * @param idleCheckOverride - Optional override for idle check result (for testing)
539
542
  */
543
+ /* eslint-disable @typescript-eslint/max-params -- Reason: Preflight check requires workspace, state, principleId, trajectory timestamp, and idle override */
540
544
  export function checkPreflight(
541
545
  workspaceDir: string,
542
546
  stateDir: string,