principles-disciple 1.7.3 → 1.7.5

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 (67) hide show
  1. package/dist/commands/evolution-status.js +4 -2
  2. package/dist/commands/focus.js +30 -155
  3. package/dist/constants/diagnostician.d.ts +16 -0
  4. package/dist/constants/diagnostician.js +60 -0
  5. package/dist/constants/tools.d.ts +2 -2
  6. package/dist/constants/tools.js +1 -1
  7. package/dist/core/config.d.ts +23 -0
  8. package/dist/core/config.js +26 -1
  9. package/dist/core/evolution-engine.js +1 -1
  10. package/dist/core/evolution-logger.d.ts +137 -0
  11. package/dist/core/evolution-logger.js +256 -0
  12. package/dist/core/evolution-reducer.d.ts +23 -0
  13. package/dist/core/evolution-reducer.js +73 -29
  14. package/dist/core/evolution-types.d.ts +6 -0
  15. package/dist/core/focus-history.d.ts +145 -0
  16. package/dist/core/focus-history.js +919 -0
  17. package/dist/core/init.js +24 -0
  18. package/dist/core/profile.js +1 -1
  19. package/dist/core/risk-calculator.d.ts +15 -0
  20. package/dist/core/risk-calculator.js +48 -0
  21. package/dist/core/trajectory.d.ts +73 -0
  22. package/dist/core/trajectory.js +206 -0
  23. package/dist/hooks/gate.js +130 -20
  24. package/dist/hooks/lifecycle.js +104 -0
  25. package/dist/hooks/pain.js +31 -0
  26. package/dist/hooks/prompt.js +136 -38
  27. package/dist/hooks/subagent.d.ts +1 -0
  28. package/dist/hooks/subagent.js +200 -18
  29. package/dist/http/principles-console-route.d.ts +7 -0
  30. package/dist/http/principles-console-route.js +301 -1
  31. package/dist/index.js +0 -2
  32. package/dist/service/central-database.d.ts +104 -0
  33. package/dist/service/central-database.js +648 -0
  34. package/dist/service/control-ui-query-service.d.ts +2 -0
  35. package/dist/service/control-ui-query-service.js +4 -0
  36. package/dist/service/empathy-observer-manager.d.ts +8 -0
  37. package/dist/service/empathy-observer-manager.js +40 -0
  38. package/dist/service/evolution-query-service.d.ts +155 -0
  39. package/dist/service/evolution-query-service.js +258 -0
  40. package/dist/service/evolution-worker.d.ts +4 -0
  41. package/dist/service/evolution-worker.js +185 -63
  42. package/dist/service/phase3-input-filter.d.ts +37 -0
  43. package/dist/service/phase3-input-filter.js +106 -0
  44. package/dist/service/runtime-summary-service.d.ts +15 -0
  45. package/dist/service/runtime-summary-service.js +111 -23
  46. package/dist/tools/deep-reflect.js +8 -2
  47. package/dist/utils/subagent-probe.d.ts +34 -0
  48. package/dist/utils/subagent-probe.js +81 -0
  49. package/openclaw.plugin.json +1 -1
  50. package/package.json +6 -4
  51. package/templates/langs/en/core/AGENTS.md +15 -3
  52. package/templates/langs/en/core/BOOTSTRAP.md +24 -1
  53. package/templates/langs/en/core/TOOLS.md +9 -0
  54. package/templates/langs/zh/core/AGENTS.md +15 -3
  55. package/templates/langs/zh/core/BOOTSTRAP.md +24 -1
  56. package/templates/langs/zh/core/TOOLS.md +9 -0
  57. package/templates/langs/zh/skills/pd-auditor/SKILL.md +61 -0
  58. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +287 -0
  59. package/templates/langs/zh/skills/pd-explorer/SKILL.md +65 -0
  60. package/templates/langs/zh/skills/pd-implementer/SKILL.md +68 -0
  61. package/templates/langs/zh/skills/pd-planner/SKILL.md +65 -0
  62. package/templates/langs/zh/skills/pd-reporter/SKILL.md +78 -0
  63. package/templates/langs/zh/skills/pd-reviewer/SKILL.md +66 -0
  64. package/dist/core/agent-loader.d.ts +0 -44
  65. package/dist/core/agent-loader.js +0 -147
  66. package/dist/tools/agent-spawn.d.ts +0 -54
  67. package/dist/tools/agent-spawn.js +0 -445
@@ -1,4 +1,5 @@
1
1
  import * as fs from 'fs';
2
+ import * as path from 'path';
2
3
  import { createHash } from 'crypto';
3
4
  import { DictionaryService } from '../core/dictionary-service.js';
4
5
  import { DetectionService } from '../core/detection-service.js';
@@ -8,6 +9,8 @@ import { SystemLogger } from '../core/system-logger.js';
8
9
  import { WorkspaceContext } from '../core/workspace-context.js';
9
10
  import { initPersistence, flushAllSessions } from '../core/session-tracker.js';
10
11
  import { acquireLockAsync, releaseLock } from '../utils/file-lock.js';
12
+ import { getEvolutionLogger } from '../core/evolution-logger.js';
13
+ import { DIAGNOSTICIAN_PROTOCOL_SUMMARY } from '../constants/diagnostician.js';
11
14
  let intervalId = null;
12
15
  const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * 60 * 1000;
13
16
  // P0 fix: File lock constants and helper for queue operations (prevents TOCTOU race)
@@ -137,6 +140,9 @@ async function checkPainFlag(wctx, logger) {
137
140
  let reason = 'Systemic pain detected';
138
141
  let preview = '';
139
142
  let isQueued = false;
143
+ let traceId = '';
144
+ let sessionId = '';
145
+ let agentId = '';
140
146
  for (const line of lines) {
141
147
  if (line.startsWith('score:'))
142
148
  score = parseInt(line.split(':', 2)[1].trim(), 10) || 0;
@@ -148,6 +154,12 @@ async function checkPainFlag(wctx, logger) {
148
154
  preview = line.slice('trigger_text_preview:'.length).trim();
149
155
  if (line.startsWith('status: queued'))
150
156
  isQueued = true;
157
+ if (line.startsWith('trace_id:'))
158
+ traceId = line.split(':', 2)[1].trim();
159
+ if (line.startsWith('session_id:'))
160
+ sessionId = line.slice('session_id:'.length).trim();
161
+ if (line.startsWith('agent_id:'))
162
+ agentId = line.slice('agent_id:'.length).trim();
151
163
  }
152
164
  if (isQueued || score < 30)
153
165
  return;
@@ -174,18 +186,42 @@ async function checkPainFlag(wctx, logger) {
174
186
  return;
175
187
  }
176
188
  const taskId = createEvolutionTaskId(source, score, preview, reason, now);
189
+ const nowIso = new Date(now).toISOString();
190
+ const effectiveTraceId = traceId || taskId;
177
191
  queue.push({
178
192
  id: taskId,
179
193
  score,
180
194
  source,
181
195
  reason,
182
196
  trigger_text_preview: preview,
183
- timestamp: new Date(now).toISOString(),
184
- enqueued_at: new Date(now).toISOString(),
185
- status: 'pending'
197
+ timestamp: nowIso,
198
+ enqueued_at: nowIso,
199
+ status: 'pending',
200
+ session_id: sessionId || undefined,
201
+ agent_id: agentId || undefined,
202
+ traceId: effectiveTraceId,
186
203
  });
187
204
  fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
188
205
  fs.appendFileSync(painFlagPath, `\nstatus: queued\ntask_id: ${taskId}\n`, 'utf8');
206
+ // Log to EvolutionLogger
207
+ const evoLogger = getEvolutionLogger(wctx.workspaceDir, wctx.trajectory);
208
+ evoLogger.logQueued({
209
+ traceId: traceId || taskId,
210
+ taskId,
211
+ score,
212
+ source,
213
+ reason,
214
+ });
215
+ // Record to evolution_tasks table
216
+ wctx.trajectory?.recordEvolutionTask?.({
217
+ taskId,
218
+ traceId: traceId || taskId,
219
+ source,
220
+ reason,
221
+ score,
222
+ status: 'pending',
223
+ enqueuedAt: nowIso,
224
+ });
189
225
  }
190
226
  finally {
191
227
  releaseLock();
@@ -200,33 +236,104 @@ async function processEvolutionQueue(wctx, logger, eventLog) {
200
236
  const queuePath = wctx.resolve('EVOLUTION_QUEUE');
201
237
  if (!fs.existsSync(queuePath))
202
238
  return;
203
- const directivePath = wctx.resolve('EVOLUTION_DIRECTIVE');
204
239
  const releaseLock = await requireQueueLock(queuePath, logger, 'processEvolutionQueue');
240
+ const evoLogger = getEvolutionLogger(wctx.workspaceDir, wctx.trajectory);
205
241
  try {
206
242
  let queue = [];
207
243
  try {
208
244
  queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
209
245
  }
210
246
  catch (e) {
211
- if (logger)
212
- logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
247
+ // Backup corrupted file instead of silently discarding
248
+ const backupPath = `${queuePath}.corrupted.${Date.now()}`;
249
+ try {
250
+ fs.renameSync(queuePath, backupPath);
251
+ if (logger) {
252
+ logger.error(`[PD:EvolutionWorker] Evolution queue corrupted and backed up to ${backupPath}. All pending tasks have been preserved in the backup file. Parse error: ${String(e)}`);
253
+ }
254
+ SystemLogger.log(wctx.workspaceDir, 'QUEUE_CORRUPTED', `Queue file backed up to ${backupPath}. Error: ${String(e)}`);
255
+ }
256
+ catch (backupErr) {
257
+ if (logger) {
258
+ logger.error(`[PD:EvolutionWorker] Failed to backup corrupted queue: ${String(backupErr)}`);
259
+ }
260
+ }
213
261
  return;
214
262
  }
215
263
  let queueChanged = false;
216
264
  const config = wctx.config;
217
- const timeout = config.get('intervals.task_timeout_ms') || (30 * 60 * 1000);
218
- for (const task of queue) {
219
- if (task.status === 'in_progress' && task.timestamp) {
220
- const startedAt = task.started_at || task.timestamp;
221
- const age = Date.now() - new Date(startedAt).getTime();
222
- if (age > timeout) {
223
- if (logger)
224
- logger.info(`[PD:EvolutionWorker] Resetting timed-out task: ${task.id}`);
225
- task.status = 'pending';
226
- delete task.started_at;
227
- delete task.assigned_session_key;
228
- queueChanged = true;
265
+ const timeout = config.get('intervals.task_timeout_ms') || (60 * 60 * 1000); // Default 1 hour
266
+ // Check in_progress tasks for completion
267
+ for (const task of queue.filter(t => t.status === 'in_progress')) {
268
+ const startedAt = new Date(task.started_at || task.timestamp);
269
+ // Condition 1: Check for marker file (created by diagnostician on completion)
270
+ const completeMarker = path.join(wctx.stateDir, `.evolution_complete_${task.id}`);
271
+ if (fs.existsSync(completeMarker)) {
272
+ if (logger)
273
+ logger.info(`[PD:EvolutionWorker] Task ${task.id} completed - marker file detected`);
274
+ task.status = 'completed';
275
+ task.completed_at = new Date().toISOString();
276
+ task.resolution = 'marker_detected';
277
+ try {
278
+ fs.unlinkSync(completeMarker); // Clean up marker file
279
+ }
280
+ catch {
281
+ // Best effort cleanup
229
282
  }
283
+ // Log to EvolutionLogger
284
+ const durationMs = task.started_at
285
+ ? Date.now() - new Date(task.started_at).getTime()
286
+ : undefined;
287
+ evoLogger.logCompleted({
288
+ traceId: task.traceId || task.id,
289
+ taskId: task.id,
290
+ resolution: 'marker_detected',
291
+ durationMs,
292
+ });
293
+ // Update evolution_tasks table
294
+ wctx.trajectory?.updateEvolutionTask?.(task.id, {
295
+ status: 'completed',
296
+ completedAt: task.completed_at,
297
+ resolution: 'marker_detected',
298
+ });
299
+ wctx.trajectory?.recordTaskOutcome({
300
+ sessionId: task.assigned_session_key || 'heartbeat:diagnostician',
301
+ taskId: task.id,
302
+ outcome: 'ok',
303
+ summary: `Task ${task.id} completed - marker file detected.`
304
+ });
305
+ queueChanged = true;
306
+ continue;
307
+ }
308
+ // Condition 2: Timeout auto-complete
309
+ const age = Date.now() - startedAt.getTime();
310
+ if (age > timeout) {
311
+ const timeoutMinutes = Math.round(timeout / 60000);
312
+ if (logger)
313
+ logger.info(`[PD:EvolutionWorker] Task ${task.id} auto-completed after ${timeoutMinutes} minute timeout`);
314
+ task.status = 'completed';
315
+ task.completed_at = new Date().toISOString();
316
+ task.resolution = 'auto_completed_timeout';
317
+ // Log to EvolutionLogger
318
+ evoLogger.logCompleted({
319
+ traceId: task.traceId || task.id,
320
+ taskId: task.id,
321
+ resolution: 'auto_completed_timeout',
322
+ durationMs: age,
323
+ });
324
+ // Update evolution_tasks table
325
+ wctx.trajectory?.updateEvolutionTask?.(task.id, {
326
+ status: 'completed',
327
+ completedAt: task.completed_at,
328
+ resolution: 'auto_completed_timeout',
329
+ });
330
+ wctx.trajectory?.recordTaskOutcome({
331
+ sessionId: task.assigned_session_key || 'heartbeat:diagnostician',
332
+ taskId: task.id,
333
+ outcome: 'timeout',
334
+ summary: `Task ${task.id} auto-completed after ${timeoutMinutes} minute timeout.`
335
+ });
336
+ queueChanged = true;
230
337
  }
231
338
  }
232
339
  const pendingTasks = queue.filter(t => t.status === 'pending');
@@ -235,55 +342,70 @@ async function processEvolutionQueue(wctx, logger, eventLog) {
235
342
  const nowIso = new Date().toISOString();
236
343
  const taskDescription = `Diagnose systemic pain [ID: ${highestScoreTask.id}]. Source: ${highestScoreTask.source}. Reason: ${highestScoreTask.reason}. ` +
237
344
  `Trigger text: "${highestScoreTask.trigger_text_preview || 'N/A'}"`;
238
- highestScoreTask.task = taskDescription;
239
- highestScoreTask.status = 'in_progress';
240
- highestScoreTask.started_at = nowIso;
241
- delete highestScoreTask.completed_at;
242
- delete highestScoreTask.assigned_session_key;
243
- queueChanged = true;
244
- if (eventLog) {
245
- eventLog.recordEvolutionTask({
345
+ // Prepare HEARTBEAT content first
346
+ // Use shared diagnostician protocol (consistent with pd-diagnostician skill)
347
+ const heartbeatPath = wctx.resolve('HEARTBEAT');
348
+ const markerFilePath = path.join(wctx.stateDir, `.evolution_complete_${highestScoreTask.id}`);
349
+ const heartbeatContent = [
350
+ `## Evolution Task [ID: ${highestScoreTask.id}]`,
351
+ ``,
352
+ `**Pain Score**: ${highestScoreTask.score}`,
353
+ `**Source**: ${highestScoreTask.source}`,
354
+ `**Reason**: ${highestScoreTask.reason}`,
355
+ `**Trigger**: "${highestScoreTask.trigger_text_preview || 'N/A'}"`,
356
+ `**Queued At**: ${highestScoreTask.enqueued_at || nowIso}`,
357
+ `**Session ID**: ${highestScoreTask.session_id || 'N/A'}`,
358
+ `**Agent ID**: ${highestScoreTask.agent_id || 'main'}`,
359
+ ``,
360
+ `---`,
361
+ ``,
362
+ DIAGNOSTICIAN_PROTOCOL_SUMMARY,
363
+ ``,
364
+ `---`,
365
+ ``,
366
+ `After completing the analysis:`,
367
+ `1. Write the resulting principle(s) to PRINCIPLES.md`,
368
+ `2. Mark the task complete by creating an empty file: ${markerFilePath}`,
369
+ `3. Replace this HEARTBEAT.md content with "HEARTBEAT_OK"`,
370
+ ].join('\n');
371
+ // Try to write HEARTBEAT.md FIRST
372
+ // Only mark task as in_progress after successful write to avoid stuck tasks
373
+ try {
374
+ fs.writeFileSync(heartbeatPath, heartbeatContent, 'utf8');
375
+ if (logger)
376
+ logger.info(`[PD:EvolutionWorker] Wrote diagnostician task to HEARTBEAT.md for task ${highestScoreTask.id}`);
377
+ // HEARTBEAT write succeeded, now mark task as in_progress
378
+ highestScoreTask.task = taskDescription;
379
+ highestScoreTask.status = 'in_progress';
380
+ highestScoreTask.started_at = nowIso;
381
+ delete highestScoreTask.completed_at;
382
+ // Use placeholder instead of deleting - allows subagent_ended hook to match
383
+ // This fixes task_outcomes being empty for HEARTBEAT-triggered diagnostician runs
384
+ highestScoreTask.assigned_session_key = `heartbeat:diagnostician:${highestScoreTask.id}`;
385
+ queueChanged = true;
386
+ // Log to EvolutionLogger
387
+ evoLogger.logStarted({
388
+ traceId: highestScoreTask.traceId || highestScoreTask.id,
246
389
  taskId: highestScoreTask.id,
247
- taskType: highestScoreTask.source,
248
- reason: highestScoreTask.reason
249
390
  });
250
- }
251
- fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
252
- queueChanged = false;
253
- const directive = {
254
- active: true,
255
- taskId: highestScoreTask.id,
256
- task: taskDescription,
257
- timestamp: nowIso
258
- };
259
- try {
260
- fs.writeFileSync(directivePath, JSON.stringify(directive, null, 2), 'utf8');
261
- }
262
- catch (directiveError) {
263
- highestScoreTask.status = 'pending';
264
- delete highestScoreTask.started_at;
265
- delete highestScoreTask.task;
266
- delete highestScoreTask.assigned_session_key;
267
- try {
268
- fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
269
- }
270
- catch (rollbackError) {
271
- throw new Error(`[PD:EvolutionWorker] Failed to persist directive and failed to roll back queue: ${String(directiveError)}; rollback=${String(rollbackError)}`);
391
+ // Update evolution_tasks table
392
+ wctx.trajectory?.updateEvolutionTask?.(highestScoreTask.id, {
393
+ status: 'in_progress',
394
+ startedAt: nowIso,
395
+ });
396
+ if (eventLog) {
397
+ eventLog.recordEvolutionTask({
398
+ taskId: highestScoreTask.id,
399
+ taskType: highestScoreTask.source,
400
+ reason: highestScoreTask.reason
401
+ });
272
402
  }
273
- throw directiveError;
274
403
  }
275
- }
276
- else {
277
- const hasInProgressTask = queue.some((task) => task.status === 'in_progress');
278
- if (!hasInProgressTask && fs.existsSync(directivePath)) {
279
- const clearedAt = new Date().toISOString();
280
- fs.writeFileSync(directivePath, JSON.stringify({
281
- active: false,
282
- task: null,
283
- taskId: null,
284
- timestamp: clearedAt,
285
- clearedAt,
286
- }, null, 2), 'utf8');
404
+ catch (heartbeatErr) {
405
+ // HEARTBEAT write failed - keep task as pending for next cycle retry
406
+ if (logger)
407
+ logger.error(`[PD:EvolutionWorker] Failed to write HEARTBEAT.md for task ${highestScoreTask.id}: ${String(heartbeatErr)}. Task will remain pending for next cycle.`);
408
+ SystemLogger.log(wctx.workspaceDir, 'HEARTBEAT_WRITE_FAILED', `Task ${highestScoreTask.id} HEARTBEAT write failed: ${String(heartbeatErr)}`);
287
409
  }
288
410
  }
289
411
  if (queueChanged) {
@@ -0,0 +1,37 @@
1
+ export interface Phase3EvolutionInput {
2
+ id?: string | null;
3
+ status?: string | null;
4
+ started_at?: string | null;
5
+ completed_at?: string | null;
6
+ }
7
+ export interface Phase3TrustInput {
8
+ score?: number | null;
9
+ frozen?: boolean | null;
10
+ lastUpdated?: string | null;
11
+ }
12
+ export interface Phase3EvolutionSample {
13
+ taskId: string;
14
+ status: 'pending' | 'in_progress' | 'completed';
15
+ startedAt: string | null;
16
+ completedAt: string | null;
17
+ }
18
+ export interface Phase3RejectedEvolutionSample {
19
+ taskId: string | null;
20
+ status: string | null;
21
+ reasons: string[];
22
+ }
23
+ export interface Phase3TrustResult {
24
+ eligible: boolean;
25
+ rejectedReasons: string[];
26
+ }
27
+ export interface Phase3InputFilterResult {
28
+ queueTruthReady: boolean;
29
+ trustInputReady: boolean;
30
+ phase3ShadowEligible: boolean;
31
+ evolution: {
32
+ eligible: Phase3EvolutionSample[];
33
+ rejected: Phase3RejectedEvolutionSample[];
34
+ };
35
+ trust: Phase3TrustResult;
36
+ }
37
+ export declare function evaluatePhase3Inputs(queue: Phase3EvolutionInput[], trust: Phase3TrustInput): Phase3InputFilterResult;
@@ -0,0 +1,106 @@
1
+ function normalizeTaskId(value) {
2
+ if (typeof value !== 'string')
3
+ return null;
4
+ const normalized = value.trim();
5
+ return normalized ? normalized : null;
6
+ }
7
+ function normalizeStatus(value) {
8
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
9
+ if (normalized === 'in_progress' || normalized === 'completed')
10
+ return normalized;
11
+ if (normalized === 'pending')
12
+ return 'pending';
13
+ return null;
14
+ }
15
+ function normalizeTimestamp(value) {
16
+ if (typeof value !== 'string')
17
+ return null;
18
+ const normalized = value.trim();
19
+ if (!normalized)
20
+ return null;
21
+ const timestamp = Date.parse(normalized);
22
+ return Number.isNaN(timestamp) ? null : normalized;
23
+ }
24
+ function dedupe(values) {
25
+ return [...new Set(values)];
26
+ }
27
+ export function evaluatePhase3Inputs(queue, trust) {
28
+ const eligible = [];
29
+ const rejected = [];
30
+ const taskIdCounts = new Map();
31
+ for (const item of queue) {
32
+ const taskId = normalizeTaskId(item?.id);
33
+ if (!taskId)
34
+ continue;
35
+ taskIdCounts.set(taskId, (taskIdCounts.get(taskId) ?? 0) + 1);
36
+ }
37
+ for (const item of queue) {
38
+ const taskId = normalizeTaskId(item?.id);
39
+ const status = normalizeStatus(item?.status);
40
+ const reasons = [];
41
+ const startedAt = normalizeTimestamp(item?.started_at);
42
+ const completedAt = normalizeTimestamp(item?.completed_at);
43
+ if (!taskId) {
44
+ reasons.push('missing_task_id');
45
+ }
46
+ else if ((taskIdCounts.get(taskId) ?? 0) > 1) {
47
+ reasons.push('reused_task_id');
48
+ }
49
+ if (!status) {
50
+ reasons.push('invalid_status');
51
+ }
52
+ if (typeof item?.started_at === 'string' && item.started_at.trim() && !startedAt) {
53
+ reasons.push('invalid_started_at');
54
+ }
55
+ if (typeof item?.completed_at === 'string' && item.completed_at.trim() && !completedAt) {
56
+ reasons.push('invalid_completed_at');
57
+ }
58
+ if (status === 'in_progress' && !startedAt) {
59
+ reasons.push('missing_started_at');
60
+ }
61
+ if (status === 'completed' && !completedAt) {
62
+ reasons.push('missing_completed_at');
63
+ }
64
+ if (reasons.length > 0) {
65
+ rejected.push({
66
+ taskId,
67
+ status: typeof item?.status === 'string' ? item.status : null,
68
+ reasons: dedupe(reasons),
69
+ });
70
+ continue;
71
+ }
72
+ if (!taskId || !status) {
73
+ continue;
74
+ }
75
+ eligible.push({
76
+ taskId,
77
+ status,
78
+ startedAt,
79
+ completedAt,
80
+ });
81
+ }
82
+ const trustRejectedReasons = [];
83
+ const score = typeof trust.score === 'number' && Number.isFinite(trust.score) ? trust.score : null;
84
+ if (trust.frozen !== true) {
85
+ trustRejectedReasons.push('legacy_or_unfrozen_trust_schema');
86
+ }
87
+ if (score === null) {
88
+ trustRejectedReasons.push('missing_trust_score');
89
+ }
90
+ const trustInputReady = trustRejectedReasons.length === 0;
91
+ const queueTruthReady = queue.length > 0 && rejected.length === 0 && eligible.length > 0;
92
+ const phase3ShadowEligible = queueTruthReady && trustInputReady;
93
+ return {
94
+ queueTruthReady,
95
+ trustInputReady,
96
+ phase3ShadowEligible,
97
+ evolution: {
98
+ eligible,
99
+ rejected,
100
+ },
101
+ trust: {
102
+ eligible: trustInputReady,
103
+ rejectedReasons: trustRejectedReasons,
104
+ },
105
+ };
106
+ }
@@ -40,6 +40,15 @@ export interface RuntimeSummary {
40
40
  };
41
41
  dataQuality: RuntimeDataQuality;
42
42
  };
43
+ phase3: {
44
+ queueTruthReady: boolean;
45
+ trustInputReady: boolean;
46
+ phase3ShadowEligible: boolean;
47
+ evolutionEligible: number;
48
+ evolutionRejected: number;
49
+ evolutionRejectedReasons: string[];
50
+ trustRejectedReasons: string[];
51
+ };
43
52
  pain: {
44
53
  activeFlag: boolean;
45
54
  activeFlagSource: string | null;
@@ -77,6 +86,12 @@ export declare class RuntimeSummaryService {
77
86
  private static mergeEvents;
78
87
  private static getEventDedupKey;
79
88
  private static resolveEvolutionDataQuality;
89
+ private static selectInProgressTask;
90
+ private static getQueuePriority;
91
+ private static isResolvableEvolutionTask;
92
+ private static resolveDirectiveTimestamp;
93
+ private static buildDirectiveTaskPreview;
94
+ private static warnOnLegacyDirectiveMismatch;
80
95
  private static readJsonFile;
81
96
  private static asFiniteNumber;
82
97
  }