principles-disciple 1.6.0 → 1.7.1

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 (75) hide show
  1. package/dist/commands/context.js +7 -3
  2. package/dist/commands/evolution-status.d.ts +4 -0
  3. package/dist/commands/evolution-status.js +134 -0
  4. package/dist/commands/export.d.ts +2 -0
  5. package/dist/commands/export.js +45 -0
  6. package/dist/commands/focus.js +9 -6
  7. package/dist/commands/pain.js +8 -0
  8. package/dist/commands/principle-rollback.d.ts +4 -0
  9. package/dist/commands/principle-rollback.js +22 -0
  10. package/dist/commands/rollback.js +9 -3
  11. package/dist/commands/samples.d.ts +2 -0
  12. package/dist/commands/samples.js +55 -0
  13. package/dist/commands/trust.js +64 -81
  14. package/dist/core/config.d.ts +5 -0
  15. package/dist/core/control-ui-db.d.ts +68 -0
  16. package/dist/core/control-ui-db.js +274 -0
  17. package/dist/core/detection-funnel.d.ts +1 -1
  18. package/dist/core/detection-funnel.js +4 -0
  19. package/dist/core/dictionary.d.ts +2 -0
  20. package/dist/core/dictionary.js +13 -0
  21. package/dist/core/event-log.d.ts +7 -1
  22. package/dist/core/event-log.js +10 -0
  23. package/dist/core/evolution-engine.d.ts +5 -5
  24. package/dist/core/evolution-engine.js +18 -18
  25. package/dist/core/evolution-migration.d.ts +5 -0
  26. package/dist/core/evolution-migration.js +65 -0
  27. package/dist/core/evolution-reducer.d.ts +69 -0
  28. package/dist/core/evolution-reducer.js +369 -0
  29. package/dist/core/evolution-types.d.ts +103 -0
  30. package/dist/core/path-resolver.js +75 -36
  31. package/dist/core/paths.d.ts +7 -8
  32. package/dist/core/paths.js +48 -40
  33. package/dist/core/profile.js +1 -1
  34. package/dist/core/session-tracker.d.ts +14 -2
  35. package/dist/core/session-tracker.js +75 -9
  36. package/dist/core/thinking-models.d.ts +38 -0
  37. package/dist/core/thinking-models.js +170 -0
  38. package/dist/core/trajectory.d.ts +184 -0
  39. package/dist/core/trajectory.js +817 -0
  40. package/dist/core/trust-engine.d.ts +6 -0
  41. package/dist/core/trust-engine.js +50 -29
  42. package/dist/core/workspace-context.d.ts +13 -0
  43. package/dist/core/workspace-context.js +50 -7
  44. package/dist/hooks/gate.js +171 -87
  45. package/dist/hooks/llm.js +119 -71
  46. package/dist/hooks/pain.js +105 -5
  47. package/dist/hooks/prompt.d.ts +11 -14
  48. package/dist/hooks/prompt.js +283 -57
  49. package/dist/hooks/subagent.js +69 -28
  50. package/dist/hooks/trajectory-collector.d.ts +32 -0
  51. package/dist/hooks/trajectory-collector.js +256 -0
  52. package/dist/http/principles-console-route.d.ts +2 -0
  53. package/dist/http/principles-console-route.js +257 -0
  54. package/dist/i18n/commands.js +16 -0
  55. package/dist/index.js +105 -4
  56. package/dist/service/control-ui-query-service.d.ts +217 -0
  57. package/dist/service/control-ui-query-service.js +537 -0
  58. package/dist/service/empathy-observer-manager.d.ts +2 -0
  59. package/dist/service/empathy-observer-manager.js +43 -1
  60. package/dist/service/evolution-worker.d.ts +27 -0
  61. package/dist/service/evolution-worker.js +256 -41
  62. package/dist/service/runtime-summary-service.d.ts +79 -0
  63. package/dist/service/runtime-summary-service.js +319 -0
  64. package/dist/service/trajectory-service.d.ts +2 -0
  65. package/dist/service/trajectory-service.js +15 -0
  66. package/dist/tools/agent-spawn.d.ts +27 -6
  67. package/dist/tools/agent-spawn.js +339 -87
  68. package/dist/tools/deep-reflect.d.ts +27 -7
  69. package/dist/tools/deep-reflect.js +210 -121
  70. package/dist/types/event-types.d.ts +10 -2
  71. package/dist/types.d.ts +10 -0
  72. package/dist/types.js +5 -0
  73. package/openclaw.plugin.json +43 -11
  74. package/package.json +14 -4
  75. package/templates/langs/zh/skills/pd-daily/SKILL.md +97 -13
@@ -8,6 +8,151 @@ import { SystemLogger } from '../core/system-logger.js';
8
8
  import { WorkspaceContext } from '../core/workspace-context.js';
9
9
  import { initPersistence, flushAllSessions } from '../core/session-tracker.js';
10
10
  let intervalId = null;
11
+ const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * 60 * 1000;
12
+ // P0 fix: File lock constants and helper for queue operations (prevents TOCTOU race)
13
+ export const EVOLUTION_QUEUE_LOCK_SUFFIX = '.lock';
14
+ export const PAIN_CANDIDATES_LOCK_SUFFIX = '.candidates.lock';
15
+ export const LOCK_MAX_RETRIES = 50;
16
+ export const LOCK_RETRY_DELAY_MS = 50;
17
+ export const LOCK_STALE_MS = 30_000;
18
+ const PAIN_CANDIDATE_MAX_SAMPLES = 5;
19
+ const PAIN_CANDIDATE_SAMPLE_LEN = 1000;
20
+ const PAIN_CANDIDATE_FINGERPRINT_HEAD_LEN = 160;
21
+ const PAIN_CANDIDATE_FINGERPRINT_TAIL_LEN = 80;
22
+ export function createEvolutionTaskId(source, score, preview, reason, now) {
23
+ // Keep ids short for prompt injection, but include enough entropy to avoid
24
+ // collisions between different pain events that share the same source/score/preview.
25
+ return createHash('md5')
26
+ .update(`${source}:${score}:${preview}:${reason}:${now}`)
27
+ .digest('hex')
28
+ .substring(0, 8);
29
+ }
30
+ function normalizePainCandidateText(text) {
31
+ return text.replace(/\s+/g, ' ').trim();
32
+ }
33
+ export function shouldTrackPainCandidate(text) {
34
+ const normalized = normalizePainCandidateText(text);
35
+ if (!normalized)
36
+ return false;
37
+ if (normalized === 'NO_REPLY')
38
+ return false;
39
+ // Skip empathy observer payloads: they are classifier telemetry, not user/system pain patterns.
40
+ if (normalized.startsWith('{')
41
+ && normalized.endsWith('}')
42
+ && normalized.includes('"damageDetected"')
43
+ && normalized.includes('"severity"')
44
+ && normalized.includes('"confidence"')) {
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ export function createPainCandidateFingerprint(text) {
50
+ const normalized = normalizePainCandidateText(text);
51
+ const head = normalized.substring(0, PAIN_CANDIDATE_FINGERPRINT_HEAD_LEN);
52
+ const tail = normalized.slice(-PAIN_CANDIDATE_FINGERPRINT_TAIL_LEN);
53
+ return createHash('md5')
54
+ .update(`${normalized.length}:${head}:${tail}`)
55
+ .digest('hex')
56
+ .substring(0, 8);
57
+ }
58
+ export function summarizePainCandidateSample(text) {
59
+ return normalizePainCandidateText(text).substring(0, PAIN_CANDIDATE_SAMPLE_LEN);
60
+ }
61
+ function isPendingPainCandidate(status) {
62
+ return status === undefined || status === 'pending';
63
+ }
64
+ /**
65
+ * Acquire an exclusive file lock for the given resource.
66
+ * Returns a release function. Uses 'wx' flag for atomic exclusive create.
67
+ * Detects stale locks by checking PID and mtime.
68
+ */
69
+ export function acquireQueueLock(lockPath, logger) {
70
+ let retries = 0;
71
+ while (retries < LOCK_MAX_RETRIES) {
72
+ try {
73
+ const fd = fs.openSync(lockPath, 'wx');
74
+ fs.writeSync(fd, `${process.pid}\n${Date.now()}`);
75
+ fs.closeSync(fd);
76
+ return () => {
77
+ try {
78
+ fs.unlinkSync(lockPath);
79
+ }
80
+ catch { /* ignore */ }
81
+ };
82
+ }
83
+ catch (err) {
84
+ if (err.code === 'EEXIST') {
85
+ // Check if lock is stale
86
+ try {
87
+ const stat = fs.statSync(lockPath);
88
+ const content = fs.readFileSync(lockPath, 'utf8').trim();
89
+ const pid = parseInt(content.split('\n')[0] || '0', 10);
90
+ let isStale = false;
91
+ if (pid > 0) {
92
+ try {
93
+ process.kill(pid, 0);
94
+ }
95
+ catch (e) {
96
+ if (e.code === 'ESRCH')
97
+ isStale = true;
98
+ }
99
+ if (!isStale && Date.now() - stat.mtimeMs > LOCK_STALE_MS)
100
+ isStale = true;
101
+ }
102
+ else if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
103
+ isStale = true;
104
+ }
105
+ if (isStale) {
106
+ fs.unlinkSync(lockPath);
107
+ retries++;
108
+ const start = Date.now();
109
+ while (Date.now() - start < LOCK_RETRY_DELAY_MS) { /* spin */ }
110
+ continue;
111
+ }
112
+ }
113
+ catch { /* stat/read failed, treat as busy */ }
114
+ retries++;
115
+ const start = Date.now();
116
+ while (Date.now() - start < LOCK_RETRY_DELAY_MS) { /* spin */ }
117
+ continue;
118
+ }
119
+ throw err;
120
+ }
121
+ }
122
+ logger?.warn?.(`[PD:EvolutionWorker] Failed to acquire lock after ${LOCK_MAX_RETRIES} retries: ${lockPath}`);
123
+ return null;
124
+ }
125
+ function normalizePainDedupKey(source, preview, reason) {
126
+ // Include reason in dedup key to match createEvolutionTaskId() behavior
127
+ // Different reasons for the same source/preview should create different tasks
128
+ const normalizedReason = (reason || '').trim().toLowerCase();
129
+ return `${source.trim().toLowerCase()}::${preview.trim().toLowerCase()}::${normalizedReason}`;
130
+ }
131
+ export function hasRecentDuplicateTask(queue, source, preview, now, reason) {
132
+ const key = normalizePainDedupKey(source, preview, reason);
133
+ return queue.some((task) => {
134
+ if (task.status === 'completed')
135
+ return false;
136
+ const taskTime = new Date(task.timestamp).getTime();
137
+ if (!Number.isFinite(taskTime) || (now - taskTime) > PAIN_QUEUE_DEDUP_WINDOW_MS)
138
+ return false;
139
+ return normalizePainDedupKey(task.source, task.trigger_text_preview || '', task.reason) === key;
140
+ });
141
+ }
142
+ export function hasEquivalentPromotedRule(dictionary, phrase) {
143
+ const normalizedPhrase = phrase.trim().toLowerCase();
144
+ return Object.values(dictionary.getAllRules()).some((rule) => {
145
+ if (rule.status !== 'active')
146
+ return false;
147
+ if (rule.type === 'exact_match' && Array.isArray(rule.phrases)) {
148
+ return rule.phrases.some((candidate) => candidate.trim().toLowerCase() === normalizedPhrase);
149
+ }
150
+ if (rule.type === 'regex' && typeof rule.pattern === 'string') {
151
+ return rule.pattern.trim().toLowerCase() === normalizedPhrase;
152
+ }
153
+ return false;
154
+ });
155
+ }
11
156
  function checkPainFlag(wctx, logger) {
12
157
  try {
13
158
  const painFlagPath = wctx.resolve('PAIN_FLAG');
@@ -37,28 +182,43 @@ function checkPainFlag(wctx, logger) {
37
182
  if (logger)
38
183
  logger.info(`[PD:EvolutionWorker] Detected pain flag (score: ${score}, source: ${source}). Enqueueing evolution task.`);
39
184
  const queuePath = wctx.resolve('EVOLUTION_QUEUE');
40
- let queue = [];
41
- if (fs.existsSync(queuePath)) {
42
- try {
43
- queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
185
+ const lockPath = queuePath + EVOLUTION_QUEUE_LOCK_SUFFIX;
186
+ const releaseLock = acquireQueueLock(lockPath, logger);
187
+ if (!releaseLock)
188
+ return; // Could not acquire lock
189
+ try {
190
+ let queue = [];
191
+ if (fs.existsSync(queuePath)) {
192
+ try {
193
+ queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
194
+ }
195
+ catch (e) {
196
+ if (logger)
197
+ logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
198
+ }
44
199
  }
45
- catch (e) {
46
- if (logger)
47
- logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
200
+ const now = Date.now();
201
+ if (hasRecentDuplicateTask(queue, source, preview, now, reason)) {
202
+ logger?.info?.(`[PD:EvolutionWorker] Duplicate pain task skipped for source=${source} preview=${preview || 'N/A'}`);
203
+ fs.appendFileSync(painFlagPath, `\nstatus: queued\n`, 'utf8');
204
+ return;
48
205
  }
206
+ const taskId = createEvolutionTaskId(source, score, preview, reason, now);
207
+ queue.push({
208
+ id: taskId,
209
+ score,
210
+ source,
211
+ reason,
212
+ trigger_text_preview: preview,
213
+ timestamp: new Date(now).toISOString(),
214
+ status: 'pending'
215
+ });
216
+ fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
217
+ fs.appendFileSync(painFlagPath, '\nstatus: queued\n', 'utf8');
218
+ }
219
+ finally {
220
+ releaseLock();
49
221
  }
50
- const taskId = createHash('md5').update(`${source}:${score}:${new Date().toISOString()}`).digest('hex').substring(0, 8);
51
- queue.push({
52
- id: taskId,
53
- score,
54
- source,
55
- reason,
56
- trigger_text_preview: preview,
57
- timestamp: new Date().toISOString(),
58
- status: 'pending'
59
- });
60
- fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
61
- fs.appendFileSync(painFlagPath, '\nstatus: queued\n', 'utf8');
62
222
  }
63
223
  catch (err) {
64
224
  if (logger)
@@ -66,11 +226,23 @@ function checkPainFlag(wctx, logger) {
66
226
  }
67
227
  }
68
228
  function processEvolutionQueue(wctx, logger, eventLog) {
229
+ const queuePath = wctx.resolve('EVOLUTION_QUEUE');
230
+ if (!fs.existsSync(queuePath))
231
+ return;
232
+ const lockPath = queuePath + EVOLUTION_QUEUE_LOCK_SUFFIX;
233
+ const releaseLock = acquireQueueLock(lockPath, logger);
234
+ if (!releaseLock)
235
+ return; // Could not acquire lock
69
236
  try {
70
- const queuePath = wctx.resolve('EVOLUTION_QUEUE');
71
- if (!fs.existsSync(queuePath))
237
+ let queue = [];
238
+ try {
239
+ queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
240
+ }
241
+ catch (e) {
242
+ if (logger)
243
+ logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
72
244
  return;
73
- const queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
245
+ }
74
246
  let queueChanged = false;
75
247
  const config = wctx.config;
76
248
  const timeout = config.get('intervals.task_timeout_ms') || (30 * 60 * 1000);
@@ -116,6 +288,9 @@ function processEvolutionQueue(wctx, logger, eventLog) {
116
288
  if (logger)
117
289
  logger.warn(`[PD:EvolutionWorker] Error processing evolution queue: ${String(err)}`);
118
290
  }
291
+ finally {
292
+ releaseLock();
293
+ }
119
294
  }
120
295
  async function processDetectionQueue(wctx, api, eventLog) {
121
296
  const logger = api.logger;
@@ -177,44 +352,80 @@ async function processDetectionQueue(wctx, api, eventLog) {
177
352
  logger.warn(`[PD:EvolutionWorker] Detection queue failed: ${String(err)}`);
178
353
  }
179
354
  }
180
- function trackPainCandidate(text, wctx) {
355
+ export function trackPainCandidate(text, wctx) {
356
+ if (!shouldTrackPainCandidate(text))
357
+ return;
181
358
  const candidatePath = wctx.resolve('PAIN_CANDIDATES');
182
- let data = { candidates: {} };
183
- if (fs.existsSync(candidatePath)) {
184
- try {
185
- data = JSON.parse(fs.readFileSync(candidatePath, 'utf8'));
359
+ const lockPath = candidatePath + PAIN_CANDIDATES_LOCK_SUFFIX;
360
+ const releaseLock = acquireQueueLock(lockPath, console);
361
+ if (!releaseLock) {
362
+ console.warn('[PD:EvolutionWorker] Failed to acquire pain candidates lock, skipping track');
363
+ return;
364
+ }
365
+ try {
366
+ let data = { candidates: {} };
367
+ if (fs.existsSync(candidatePath)) {
368
+ try {
369
+ data = JSON.parse(fs.readFileSync(candidatePath, 'utf8'));
370
+ }
371
+ catch (e) {
372
+ // Keep going with empty data if parse fails, but log it
373
+ console.error(`[PD:EvolutionWorker] Failed to parse pain candidates: ${String(e)}`);
374
+ }
186
375
  }
187
- catch (e) {
188
- // Keep going with empty data if parse fails, but log it
189
- console.error(`[PD:EvolutionWorker] Failed to parse pain candidates: ${String(e)}`);
376
+ const fingerprint = createPainCandidateFingerprint(text);
377
+ const now = new Date().toISOString();
378
+ if (!data.candidates[fingerprint]) {
379
+ data.candidates[fingerprint] = { count: 0, status: 'pending', firstSeen: now, lastSeen: now, samples: [] };
190
380
  }
381
+ const cand = data.candidates[fingerprint];
382
+ cand.status = cand.status || 'pending';
383
+ cand.count++;
384
+ cand.lastSeen = now;
385
+ const sample = summarizePainCandidateSample(text);
386
+ if (cand.samples.length < PAIN_CANDIDATE_MAX_SAMPLES && !cand.samples.includes(sample)) {
387
+ cand.samples.push(sample);
388
+ }
389
+ fs.writeFileSync(candidatePath, JSON.stringify(data, null, 2), 'utf8');
191
390
  }
192
- const fingerprint = createHash('md5').update(text.substring(0, 50)).digest('hex').substring(0, 8);
193
- if (!data.candidates[fingerprint]) {
194
- data.candidates[fingerprint] = { count: 0, firstSeen: new Date().toISOString(), samples: [] };
391
+ finally {
392
+ releaseLock();
195
393
  }
196
- const cand = data.candidates[fingerprint];
197
- cand.count++;
198
- if (cand.samples.length < 5)
199
- cand.samples.push(text.substring(0, 200));
200
- fs.writeFileSync(candidatePath, JSON.stringify(data, null, 2), 'utf8');
201
394
  }
202
- function processPromotion(wctx, logger, eventLog) {
395
+ export function processPromotion(wctx, logger, eventLog) {
203
396
  const candidatePath = wctx.resolve('PAIN_CANDIDATES');
204
397
  if (!fs.existsSync(candidatePath))
205
398
  return;
399
+ const lockPath = candidatePath + PAIN_CANDIDATES_LOCK_SUFFIX;
400
+ const releaseLock = acquireQueueLock(lockPath, logger);
401
+ if (!releaseLock) {
402
+ logger?.warn?.('[PD:EvolutionWorker] Failed to acquire pain candidates lock, skipping promotion');
403
+ return;
404
+ }
206
405
  try {
207
406
  const config = wctx.config;
208
407
  const dictionary = wctx.dictionary;
209
408
  const data = JSON.parse(fs.readFileSync(candidatePath, 'utf8'));
210
409
  const countThreshold = config.get('thresholds.promotion_count_threshold') || 3;
211
410
  let promotedCount = 0;
411
+ let changed = false;
212
412
  for (const [fingerprint, cand] of Object.entries(data.candidates)) {
213
- if (cand.status === 'pending' && cand.count >= countThreshold) {
413
+ if (isPendingPainCandidate(cand.status) && cand.count >= countThreshold) {
414
+ // Normalize undefined status to 'pending'
415
+ if (cand.status !== 'pending') {
416
+ cand.status = 'pending';
417
+ changed = true;
418
+ }
214
419
  const commonPhrases = extractCommonSubstring(cand.samples);
215
420
  if (commonPhrases.length > 0) {
216
421
  const phrase = commonPhrases[0];
217
422
  const ruleId = `P_PROMOTED_${fingerprint.toUpperCase()}`;
423
+ if (hasEquivalentPromotedRule(dictionary, phrase)) {
424
+ cand.status = 'duplicate';
425
+ changed = true;
426
+ logger?.info?.(`[PD:EvolutionWorker] Skipping duplicate promoted rule for candidate ${fingerprint}: ${phrase}`);
427
+ continue;
428
+ }
218
429
  if (logger)
219
430
  logger.info(`[PD:EvolutionWorker] Promoting candidate ${fingerprint} to formal rule: ${ruleId}`);
220
431
  SystemLogger.log(wctx.workspaceDir, 'RULE_PROMOTED', `Candidate ${fingerprint} promoted to rule ${ruleId}`);
@@ -226,10 +437,11 @@ function processPromotion(wctx, logger, eventLog) {
226
437
  });
227
438
  cand.status = 'promoted';
228
439
  promotedCount++;
440
+ changed = true;
229
441
  }
230
442
  }
231
443
  }
232
- if (promotedCount > 0) {
444
+ if (changed) {
233
445
  fs.writeFileSync(candidatePath, JSON.stringify(data, null, 2), 'utf8');
234
446
  }
235
447
  }
@@ -237,6 +449,9 @@ function processPromotion(wctx, logger, eventLog) {
237
449
  if (logger)
238
450
  logger.warn(`[PD:EvolutionWorker] Error during rule promotion: ${String(err)}`);
239
451
  }
452
+ finally {
453
+ releaseLock();
454
+ }
240
455
  }
241
456
  export const EvolutionWorkerService = {
242
457
  id: 'principles-evolution-worker',
@@ -0,0 +1,79 @@
1
+ export type RuntimeDataQuality = 'authoritative' | 'partial';
2
+ export type RuntimeRewardPolicy = 'frozen_all_positive' | 'frozen_atomic_positive_keep_plan_ready';
3
+ interface RuntimeSummarySource {
4
+ source: string;
5
+ score?: number;
6
+ ts?: string;
7
+ confidence?: number;
8
+ origin?: string;
9
+ }
10
+ interface RuntimePainSignal {
11
+ source: string;
12
+ ts: string | null;
13
+ reason: string | null;
14
+ }
15
+ export interface RuntimeSummary {
16
+ gfi: {
17
+ current: number | null;
18
+ peak: number | null;
19
+ sources: RuntimeSummarySource[];
20
+ dataQuality: RuntimeDataQuality;
21
+ };
22
+ legacyTrust: {
23
+ score: number | null;
24
+ stage: 1 | 2 | 3 | 4 | null;
25
+ frozen: true;
26
+ lastUpdated: string | null;
27
+ rewardPolicy: RuntimeRewardPolicy;
28
+ };
29
+ evolution: {
30
+ queue: {
31
+ pending: number;
32
+ inProgress: number;
33
+ completed: number;
34
+ };
35
+ directive: {
36
+ exists: boolean;
37
+ active: boolean | null;
38
+ ageSeconds: number | null;
39
+ taskPreview: string | null;
40
+ };
41
+ dataQuality: RuntimeDataQuality;
42
+ };
43
+ pain: {
44
+ activeFlag: boolean;
45
+ activeFlagSource: string | null;
46
+ candidates: number | null;
47
+ lastSignal: RuntimePainSignal | null;
48
+ };
49
+ gate: {
50
+ recentBlocks: number | null;
51
+ recentBypasses: number | null;
52
+ dataQuality: RuntimeDataQuality;
53
+ };
54
+ metadata: {
55
+ generatedAt: string;
56
+ workspaceDir: string;
57
+ sessionId: string | null;
58
+ selectedSessionReason: 'explicit' | 'latest_active' | 'none';
59
+ warnings: string[];
60
+ };
61
+ }
62
+ export declare class RuntimeSummaryService {
63
+ static getSummary(workspaceDir: string, options?: {
64
+ sessionId?: string | null;
65
+ }): RuntimeSummary;
66
+ private static readSessions;
67
+ private static selectSession;
68
+ private static mergeSessionSnapshots;
69
+ private static buildQueueStats;
70
+ private static buildDirectiveSummary;
71
+ private static readLegacyTrust;
72
+ private static readEvents;
73
+ private static buildGfiSources;
74
+ private static findLastPainSignal;
75
+ private static buildGateStats;
76
+ private static readJsonFile;
77
+ private static asFiniteNumber;
78
+ }
79
+ export {};