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.
- package/dist/commands/evolution-status.js +4 -2
- package/dist/commands/focus.js +30 -155
- package/dist/constants/diagnostician.d.ts +16 -0
- package/dist/constants/diagnostician.js +60 -0
- package/dist/constants/tools.d.ts +2 -2
- package/dist/constants/tools.js +1 -1
- package/dist/core/config.d.ts +23 -0
- package/dist/core/config.js +26 -1
- package/dist/core/evolution-engine.js +1 -1
- package/dist/core/evolution-logger.d.ts +137 -0
- package/dist/core/evolution-logger.js +256 -0
- package/dist/core/evolution-reducer.d.ts +23 -0
- package/dist/core/evolution-reducer.js +73 -29
- package/dist/core/evolution-types.d.ts +6 -0
- package/dist/core/focus-history.d.ts +145 -0
- package/dist/core/focus-history.js +919 -0
- package/dist/core/init.js +24 -0
- package/dist/core/profile.js +1 -1
- package/dist/core/risk-calculator.d.ts +15 -0
- package/dist/core/risk-calculator.js +48 -0
- package/dist/core/trajectory.d.ts +73 -0
- package/dist/core/trajectory.js +206 -0
- package/dist/hooks/gate.js +130 -20
- package/dist/hooks/lifecycle.js +104 -0
- package/dist/hooks/pain.js +31 -0
- package/dist/hooks/prompt.js +136 -38
- package/dist/hooks/subagent.d.ts +1 -0
- package/dist/hooks/subagent.js +200 -18
- package/dist/http/principles-console-route.d.ts +7 -0
- package/dist/http/principles-console-route.js +301 -1
- package/dist/index.js +0 -2
- package/dist/service/central-database.d.ts +104 -0
- package/dist/service/central-database.js +648 -0
- package/dist/service/control-ui-query-service.d.ts +2 -0
- package/dist/service/control-ui-query-service.js +4 -0
- package/dist/service/empathy-observer-manager.d.ts +8 -0
- package/dist/service/empathy-observer-manager.js +40 -0
- package/dist/service/evolution-query-service.d.ts +155 -0
- package/dist/service/evolution-query-service.js +258 -0
- package/dist/service/evolution-worker.d.ts +4 -0
- package/dist/service/evolution-worker.js +185 -63
- package/dist/service/phase3-input-filter.d.ts +37 -0
- package/dist/service/phase3-input-filter.js +106 -0
- package/dist/service/runtime-summary-service.d.ts +15 -0
- package/dist/service/runtime-summary-service.js +111 -23
- package/dist/tools/deep-reflect.js +8 -2
- package/dist/utils/subagent-probe.d.ts +34 -0
- package/dist/utils/subagent-probe.js +81 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -4
- package/templates/langs/en/core/AGENTS.md +15 -3
- package/templates/langs/en/core/BOOTSTRAP.md +24 -1
- package/templates/langs/en/core/TOOLS.md +9 -0
- package/templates/langs/zh/core/AGENTS.md +15 -3
- package/templates/langs/zh/core/BOOTSTRAP.md +24 -1
- package/templates/langs/zh/core/TOOLS.md +9 -0
- package/templates/langs/zh/skills/pd-auditor/SKILL.md +61 -0
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +287 -0
- package/templates/langs/zh/skills/pd-explorer/SKILL.md +65 -0
- package/templates/langs/zh/skills/pd-implementer/SKILL.md +68 -0
- package/templates/langs/zh/skills/pd-planner/SKILL.md +65 -0
- package/templates/langs/zh/skills/pd-reporter/SKILL.md +78 -0
- package/templates/langs/zh/skills/pd-reviewer/SKILL.md +66 -0
- package/dist/core/agent-loader.d.ts +0 -44
- package/dist/core/agent-loader.js +0 -147
- package/dist/tools/agent-spawn.d.ts +0 -54
- 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:
|
|
184
|
-
enqueued_at:
|
|
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
|
-
|
|
212
|
-
|
|
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') || (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
task.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
}
|