principles-disciple 1.17.0 → 1.19.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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands/nocturnal-rollout.ts +2 -0
- package/src/core/merge-gate-audit.ts +506 -0
- package/src/core/nocturnal-compliance.ts +1 -0
- package/src/core/nocturnal-export.ts +106 -6
- package/src/core/nocturnal-trinity.ts +559 -153
- package/src/core/promotion-gate.ts +33 -0
- package/src/core/replay-engine.ts +25 -0
- package/src/service/evolution-worker.ts +13 -6
- package/src/service/nocturnal-target-selector.ts +9 -2
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +2 -6
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +111 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +1 -1
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +1 -1
- package/tests/core/merge-gate-audit.test.ts +284 -0
- package/tests/core/nocturnal-export.test.ts +55 -0
- package/tests/core/nocturnal-trinity.test.ts +77 -4
- package/tests/core/pain-integration.test.ts +27 -0
- package/tests/core/promotion-gate.test.ts +5 -0
- package/tests/core/replay-engine.test.ts +19 -0
- package/tests/service/evolution-worker.nocturnal.test.ts +0 -547
- package/tests/service/nocturnal-workflow-manager.test.ts +2 -0
|
@@ -23,10 +23,16 @@
|
|
|
23
23
|
* RUNTIME ADAPTER:
|
|
24
24
|
* - useStubs=true: uses synchronous stub implementations (no external calls)
|
|
25
25
|
* - useStubs=false: requires a TrinityRuntimeAdapter for real subagent execution
|
|
26
|
-
* - Adapter uses
|
|
26
|
+
* - Adapter uses api.runtime.agent.runEmbeddedPiAgent() which works in background contexts
|
|
27
|
+
* (unlike api.runtime.subagent.* which requires gateway request scope)
|
|
28
|
+
* - IMPORTANT: provider and model must be passed explicitly — runEmbeddedPiAgent does NOT
|
|
29
|
+
* read config.agents.defaults.model and falls back to openai/gpt-5.4 if not specified
|
|
27
30
|
*/
|
|
28
31
|
|
|
29
32
|
import { randomUUID } from 'crypto';
|
|
33
|
+
import * as fs from 'fs';
|
|
34
|
+
import * as os from 'os';
|
|
35
|
+
import * as path from 'path';
|
|
30
36
|
import type { NocturnalSessionSnapshot } from './nocturnal-trajectory-extractor.js';
|
|
31
37
|
import { computeThinkingModelDelta } from './nocturnal-trajectory-extractor.js';
|
|
32
38
|
import type { TrinityArtificerContext } from './nocturnal-artificer.js';
|
|
@@ -42,6 +48,13 @@ import {
|
|
|
42
48
|
type ThresholdValues,
|
|
43
49
|
} from './adaptive-thresholds.js';
|
|
44
50
|
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Configurable Model Fallback (avoid hardcoded strings deep in adapters)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
const FALLBACK_PROVIDER = process.env.OPENCLAW_DEFAULT_PROVIDER || 'minimax-portal';
|
|
56
|
+
const FALLBACK_MODEL = process.env.OPENCLAW_DEFAULT_MODEL || 'MiniMax-M2.7';
|
|
57
|
+
|
|
45
58
|
// ---------------------------------------------------------------------------
|
|
46
59
|
// Embedded Role Prompts
|
|
47
60
|
// ---------------------------------------------------------------------------
|
|
@@ -106,6 +119,13 @@ You MUST respond with ONLY a valid JSON object. No markdown, no explanation, no
|
|
|
106
119
|
- Provide a principle-grounded rationale (explicitly references the principle)
|
|
107
120
|
- Include a confidence score (0.0-1.0, higher = more confident)
|
|
108
121
|
|
|
122
|
+
### betterDecision FORMAT — Must be executable:
|
|
123
|
+
- MUST start with a concrete action verb: read, check, verify, edit, write, create, delete, search, grep, find, list, review, examine, inspect, test, run, execute, analyze, diagnose, debug
|
|
124
|
+
- MUST reference a specific, concrete target (file, command, config, etc.)
|
|
125
|
+
- MUST describe a bounded, executable action — not a vague principle
|
|
126
|
+
- Examples: "Read the file before editing to verify current content", "Check user permissions before executing privileged commands"
|
|
127
|
+
- Anti-examples: "Per T-01, pause all tasks..." (starts with "Per"), "Be more careful" (vague verb "be")
|
|
128
|
+
|
|
109
129
|
### Candidates should DIFFER from each other:
|
|
110
130
|
- Different candidates should represent genuinely different approaches
|
|
111
131
|
- Do not generate candidates with identical betterDecisions
|
|
@@ -177,13 +197,23 @@ You MUST respond with ONLY a valid JSON object. No markdown, no explanation, no
|
|
|
177
197
|
## Evaluation Criteria
|
|
178
198
|
|
|
179
199
|
### Score Components (0-1 scale each):
|
|
180
|
-
1. **Principle Alignment** (weight: 0.
|
|
181
|
-
2. **Specificity** (weight: 0.
|
|
182
|
-
3. **Actionability** (weight: 0.
|
|
200
|
+
1. **Principle Alignment** (weight: 0.35) — Does the betterDecision properly reflect the target principle?
|
|
201
|
+
2. **Specificity** (weight: 0.25) — Is badDecision specific? Is betterDecision actionable?
|
|
202
|
+
3. **Actionability** (weight: 0.25) — Does betterDecision describe a specific next step?
|
|
203
|
+
4. **Executability** (weight: 0.15) — Does betterDecision start with a bounded verb (read, check, verify, edit, write, etc.) and reference a concrete target?
|
|
204
|
+
|
|
205
|
+
### Executability Check:
|
|
206
|
+
A betterDecision is executable if it:
|
|
207
|
+
- STARTS with a concrete action verb: read, check, verify, edit, write, create, delete, search, grep, find, list, review, examine, inspect, test, run, execute, analyze, diagnose, debug
|
|
208
|
+
- References a specific, concrete target (file, command, config, etc.)
|
|
209
|
+
- Describes a bounded, executable action — not a vague principle
|
|
210
|
+
- Examples that PASS: "Read the file before editing", "Check user permissions before executing"
|
|
211
|
+
- Examples that FAIL: "Per T-01, pause all tasks..." (starts with "Per"), "Be more careful" (vague)
|
|
183
212
|
|
|
184
213
|
### Ranking Rules:
|
|
185
214
|
- Candidates are ranked by score (highest = rank 1)
|
|
186
|
-
- Ties broken by: higher principle alignment, then lower candidateIndex
|
|
215
|
+
- Ties broken by: higher executability, then higher principle alignment, then lower candidateIndex
|
|
216
|
+
- If a candidate's betterDecision is NOT executable, penalize its score by 0.2
|
|
187
217
|
|
|
188
218
|
## Validation
|
|
189
219
|
|
|
@@ -277,6 +307,17 @@ If you cannot synthesize an artifact:
|
|
|
277
307
|
*/
|
|
278
308
|
|
|
279
309
|
export interface TrinityRuntimeAdapter {
|
|
310
|
+
/**
|
|
311
|
+
* Check if the runtime surface is available for Trinity stage execution.
|
|
312
|
+
* @returns true if the adapter can invoke stages
|
|
313
|
+
*/
|
|
314
|
+
isRuntimeAvailable(): boolean;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get the reason for the last runtime failure, or null if no failure.
|
|
318
|
+
*/
|
|
319
|
+
getLastFailureReason(): string | null;
|
|
320
|
+
|
|
280
321
|
/**
|
|
281
322
|
* Invoke the Dreamer stage.
|
|
282
323
|
* @param snapshot Session trajectory snapshot
|
|
@@ -294,11 +335,13 @@ export interface TrinityRuntimeAdapter {
|
|
|
294
335
|
* Invoke the Philosopher stage.
|
|
295
336
|
* @param dreamerOutput Dreamer's output
|
|
296
337
|
* @param principleId Target principle ID
|
|
338
|
+
* @param snapshot Session snapshot (for violation evidence)
|
|
297
339
|
* @returns Philosopher output JSON
|
|
298
340
|
*/
|
|
299
341
|
invokePhilosopher(
|
|
300
342
|
_dreamerOutput: DreamerOutput,
|
|
301
|
-
_principleId: string
|
|
343
|
+
_principleId: string,
|
|
344
|
+
_snapshot: NocturnalSessionSnapshot
|
|
302
345
|
): Promise<PhilosopherOutput>;
|
|
303
346
|
|
|
304
347
|
/**
|
|
@@ -334,47 +377,191 @@ export interface TrinityRuntimeAdapter {
|
|
|
334
377
|
|
|
335
378
|
/**
|
|
336
379
|
* OpenClaw-backed Trinity runtime adapter.
|
|
337
|
-
* Uses
|
|
338
|
-
*
|
|
380
|
+
* Uses api.runtime.agent.runEmbeddedPiAgent() which works in background contexts
|
|
381
|
+
* (unlike api.runtime.subagent.* which requires gateway request scope).
|
|
339
382
|
*/
|
|
383
|
+
export type TrinityRuntimeFailureCode =
|
|
384
|
+
| 'runtime_unavailable'
|
|
385
|
+
| 'invalid_runtime_request'
|
|
386
|
+
| 'runtime_run_failed'
|
|
387
|
+
| 'runtime_timeout'
|
|
388
|
+
| 'runtime_session_read_failed';
|
|
389
|
+
|
|
390
|
+
export class TrinityRuntimeContractError extends Error {
|
|
391
|
+
readonly code: TrinityRuntimeFailureCode;
|
|
392
|
+
readonly diagnostics?: Record<string, unknown>;
|
|
393
|
+
|
|
394
|
+
constructor(
|
|
395
|
+
code: TrinityRuntimeFailureCode,
|
|
396
|
+
message: string,
|
|
397
|
+
diagnostics?: Record<string, unknown>
|
|
398
|
+
) {
|
|
399
|
+
super(`${code}: ${message}`);
|
|
400
|
+
this.name = 'TrinityRuntimeContractError';
|
|
401
|
+
this.code = code;
|
|
402
|
+
this.diagnostics = diagnostics;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
340
406
|
export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
|
|
341
|
-
|
|
407
|
+
|
|
342
408
|
private readonly api: {
|
|
343
409
|
runtime: {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
410
|
+
agent: {
|
|
411
|
+
runEmbeddedPiAgent: (_opts: {
|
|
412
|
+
sessionId: string;
|
|
413
|
+
sessionFile: string;
|
|
414
|
+
prompt: string;
|
|
348
415
|
extraSystemPrompt?: string;
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
getSessionMessages: (_opts: {
|
|
356
|
-
sessionKey: string;
|
|
357
|
-
limit: number;
|
|
416
|
+
config?: unknown;
|
|
417
|
+
provider?: string;
|
|
418
|
+
model?: string;
|
|
419
|
+
timeoutMs: number;
|
|
420
|
+
runId: string;
|
|
421
|
+
disableTools?: boolean;
|
|
358
422
|
}) => Promise<{
|
|
359
|
-
|
|
423
|
+
payloads?: { isError?: boolean; text?: string }[];
|
|
360
424
|
}>;
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}) => Promise<void>;
|
|
425
|
+
};
|
|
426
|
+
config?: {
|
|
427
|
+
loadConfig?: () => unknown;
|
|
365
428
|
};
|
|
366
429
|
};
|
|
430
|
+
config?: unknown;
|
|
431
|
+
logger?: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void };
|
|
367
432
|
};
|
|
433
|
+
private lastFailureReason: string | null = null;
|
|
368
434
|
|
|
369
435
|
|
|
370
436
|
private readonly stageTimeoutMs: number;
|
|
437
|
+
private readonly tempDir: string;
|
|
371
438
|
|
|
372
439
|
constructor(
|
|
373
440
|
api: OpenClawTrinityRuntimeAdapter['api'],
|
|
374
441
|
stageTimeoutMs = 300_000 // 5 min — increased from 3 min to accommodate slower LLM responses
|
|
375
442
|
) {
|
|
443
|
+
if (typeof api?.runtime?.agent?.runEmbeddedPiAgent !== 'function') {
|
|
444
|
+
throw new TrinityRuntimeContractError(
|
|
445
|
+
'runtime_unavailable',
|
|
446
|
+
'embedded runtime unavailable (missing runtime.agent.runEmbeddedPiAgent)',
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
376
450
|
this.api = api;
|
|
377
451
|
this.stageTimeoutMs = stageTimeoutMs;
|
|
452
|
+
// Cross-platform temp directory for session files
|
|
453
|
+
this.tempDir = path.join(os.tmpdir(), `pd-trinity-${process.pid}`);
|
|
454
|
+
// Clean up any stale temp files from previous crashed runs
|
|
455
|
+
this.cleanupStaleTempDirs();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
isRuntimeAvailable(): boolean {
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
getLastFailureReason(): string | null {
|
|
463
|
+
return this.lastFailureReason;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Clean up temp directories from previous crashed runs.
|
|
468
|
+
* Matches pattern pd-trinity-* in the OS temp directory.
|
|
469
|
+
*/
|
|
470
|
+
private cleanupStaleTempDirs(): void {
|
|
471
|
+
try {
|
|
472
|
+
const osTempDir = os.tmpdir();
|
|
473
|
+
if (!fs.existsSync(osTempDir)) return;
|
|
474
|
+
const entries = fs.readdirSync(osTempDir);
|
|
475
|
+
for (const entry of entries) {
|
|
476
|
+
if (entry.startsWith('pd-trinity-') && entry !== path.basename(this.tempDir)) {
|
|
477
|
+
const fullPath = path.join(osTempDir, entry);
|
|
478
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
// Non-fatal: stale temp files will be cleaned up eventually
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Load the full OpenClaw config (including models.providers).
|
|
488
|
+
*
|
|
489
|
+
* Why: `this.api.config` is the plugin config, not the full OpenClaw config.
|
|
490
|
+
* It does NOT contain `models.providers`, which is needed to resolve provider
|
|
491
|
+
* model definitions. `api.runtime.config.loadConfig()` returns the full config.
|
|
492
|
+
*
|
|
493
|
+
* Fallback: If loadConfig() is unavailable, we return the plugin config.
|
|
494
|
+
* The caller (resolveModel) handles this with a minimax-portal fallback.
|
|
495
|
+
*/
|
|
496
|
+
private loadFullConfig(): Record<string, unknown> | undefined {
|
|
497
|
+
// Try runtime.config.loadConfig() first (available in native plugin context)
|
|
498
|
+
const loadConfig = this.api.runtime?.config?.loadConfig;
|
|
499
|
+
if (loadConfig && typeof loadConfig === 'function') {
|
|
500
|
+
try {
|
|
501
|
+
return loadConfig() as Record<string, unknown> | undefined;
|
|
502
|
+
} catch (err) {
|
|
503
|
+
this.api.logger?.warn?.(`[Trinity] loadConfig() failed, falling back to plugin config: ${err instanceof Error ? err.message : String(err)}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Fallback: plugin config (limited — won't have models.providers)
|
|
507
|
+
// resolveModel() handles this with a minimax-portal/MiniMax-M2.7 fallback
|
|
508
|
+
return this.api.config as Record<string, unknown> | undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Resolve the provider and model from the OpenClaw config.
|
|
513
|
+
* runEmbeddedPiAgent does NOT read config.agents.defaults.model —
|
|
514
|
+
* it requires explicit params.provider and params.model.
|
|
515
|
+
*/
|
|
516
|
+
private resolveModel(): { provider: string; model: string } {
|
|
517
|
+
const config = this.loadFullConfig();
|
|
518
|
+
const agents = config?.agents as Record<string, unknown> | undefined;
|
|
519
|
+
const defaults = agents?.defaults as Record<string, unknown> | undefined;
|
|
520
|
+
const modelConfig = defaults?.model;
|
|
521
|
+
|
|
522
|
+
if (typeof modelConfig === 'string' && modelConfig.includes('/')) {
|
|
523
|
+
const parts = modelConfig.split('/');
|
|
524
|
+
return { provider: parts[0], model: parts.slice(1).join('/') };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (modelConfig && typeof modelConfig === 'object') {
|
|
528
|
+
const mc = modelConfig as Record<string, unknown>;
|
|
529
|
+
const primary = mc.primary as string | undefined;
|
|
530
|
+
if (primary && primary.includes('/')) {
|
|
531
|
+
const parts = primary.split('/');
|
|
532
|
+
return { provider: parts[0], model: parts.slice(1).join('/') };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Last resort fallback — read from env vars to avoid hardcoded strings
|
|
537
|
+
this.api.logger?.warn?.(`[Trinity] Could not resolve model from config, using fallback: ${FALLBACK_PROVIDER}/${FALLBACK_MODEL}`);
|
|
538
|
+
return { provider: FALLBACK_PROVIDER, model: FALLBACK_MODEL };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Create a valid JSONL session file for runEmbeddedPiAgent.
|
|
543
|
+
*/
|
|
544
|
+
private createSessionFile(stage: string): string {
|
|
545
|
+
if (!fs.existsSync(this.tempDir)) {
|
|
546
|
+
fs.mkdirSync(this.tempDir, { recursive: true });
|
|
547
|
+
}
|
|
548
|
+
return path.join(this.tempDir, `${stage}-${randomUUID()}.jsonl`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Extract text from runEmbeddedPiAgent result.
|
|
553
|
+
*/
|
|
554
|
+
private extractPayloadText(result: { payloads?: { isError?: boolean; text?: string }[] }): string {
|
|
555
|
+
return (result.payloads ?? [])
|
|
556
|
+
.filter(p => !p.isError)
|
|
557
|
+
.map(p => p.text?.trim() ?? '')
|
|
558
|
+
.filter(Boolean)
|
|
559
|
+
.join('\n');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private classifyRuntimeError(error: unknown): TrinityRuntimeFailureCode {
|
|
563
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
564
|
+
return /timeout/i.test(detail) ? 'runtime_timeout' : 'runtime_run_failed';
|
|
378
565
|
}
|
|
379
566
|
|
|
380
567
|
async invokeDreamer(
|
|
@@ -382,145 +569,151 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
|
|
|
382
569
|
principleId: string,
|
|
383
570
|
maxCandidates: number
|
|
384
571
|
): Promise<DreamerOutput> {
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
572
|
+
this.lastFailureReason = null;
|
|
573
|
+
const runId = `dreamer-${randomUUID()}`;
|
|
574
|
+
const sessionFile = this.createSessionFile('dreamer');
|
|
388
575
|
const prompt = this.buildDreamerPrompt(snapshot, principleId, maxCandidates);
|
|
576
|
+
const model = this.resolveModel();
|
|
389
577
|
|
|
390
|
-
|
|
391
|
-
const { runId } = await this.api.runtime.subagent.run({
|
|
392
|
-
sessionKey,
|
|
393
|
-
message: prompt,
|
|
394
|
-
extraSystemPrompt: systemPrompt,
|
|
395
|
-
deliver: false,
|
|
396
|
-
});
|
|
578
|
+
this.api.logger?.info(`[Trinity:Dreamer] Using model: ${model.provider}/${model.model}`);
|
|
397
579
|
|
|
398
|
-
|
|
399
|
-
|
|
580
|
+
try {
|
|
581
|
+
const result = await this.api.runtime.agent.runEmbeddedPiAgent({
|
|
582
|
+
sessionId: runId,
|
|
583
|
+
sessionFile,
|
|
584
|
+
prompt,
|
|
585
|
+
extraSystemPrompt: NOCTURNAL_DREAMER_PROMPT,
|
|
586
|
+
config: this.loadFullConfig(),
|
|
587
|
+
provider: model.provider,
|
|
588
|
+
model: model.model,
|
|
400
589
|
timeoutMs: this.stageTimeoutMs,
|
|
590
|
+
runId,
|
|
591
|
+
disableTools: true,
|
|
401
592
|
});
|
|
402
593
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
};
|
|
594
|
+
const outputText = this.extractPayloadText(result);
|
|
595
|
+
if (!outputText) {
|
|
596
|
+
return this.buildRuntimeFailureDreamerOutput(
|
|
597
|
+
'runtime_session_read_failed',
|
|
598
|
+
'Dreamer returned empty response',
|
|
599
|
+
);
|
|
410
600
|
}
|
|
411
601
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
limit: 5,
|
|
415
|
-
});
|
|
602
|
+
// DEBUG: Log Dreamer's actual output
|
|
603
|
+
this.api.logger?.info(`[Trinity:Dreamer] Output preview: ${outputText.slice(0, 500)}`);
|
|
416
604
|
|
|
417
|
-
const outputText = this.extractAssistantText(messages.messages as { role: string; text?: string; content?: string }[]);
|
|
418
605
|
return this.parseDreamerOutput(outputText);
|
|
606
|
+
} catch (err) {
|
|
607
|
+
return this.buildRuntimeFailureDreamerOutput(this.classifyRuntimeError(err), err);
|
|
419
608
|
} finally {
|
|
420
|
-
|
|
421
|
-
sessionKey,
|
|
422
|
-
deleteTranscript: true,
|
|
423
|
-
}).catch(() => { /* intentionally empty - fire-and-forget session cleanup */ });
|
|
609
|
+
try { fs.unlinkSync(sessionFile); } catch { /* ignore */ }
|
|
424
610
|
}
|
|
425
611
|
}
|
|
426
612
|
|
|
427
613
|
async invokePhilosopher(
|
|
428
614
|
dreamerOutput: DreamerOutput,
|
|
429
|
-
principleId: string
|
|
615
|
+
principleId: string,
|
|
616
|
+
snapshot: NocturnalSessionSnapshot
|
|
430
617
|
): Promise<PhilosopherOutput> {
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
const prompt = this.buildPhilosopherPrompt(dreamerOutput, principleId);
|
|
618
|
+
this.lastFailureReason = null;
|
|
619
|
+
const runId = `philosopher-${randomUUID()}`;
|
|
620
|
+
const sessionFile = this.createSessionFile('philosopher');
|
|
621
|
+
const prompt = this.buildPhilosopherPrompt(dreamerOutput, principleId, snapshot);
|
|
622
|
+
const model = this.resolveModel();
|
|
435
623
|
|
|
436
624
|
try {
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
runId,
|
|
625
|
+
const result = await this.api.runtime.agent.runEmbeddedPiAgent({
|
|
626
|
+
sessionId: runId,
|
|
627
|
+
sessionFile,
|
|
628
|
+
prompt,
|
|
629
|
+
extraSystemPrompt: NOCTURNAL_PHILOSOPHER_PROMPT,
|
|
630
|
+
config: this.loadFullConfig(),
|
|
631
|
+
provider: model.provider,
|
|
632
|
+
model: model.model,
|
|
446
633
|
timeoutMs: this.stageTimeoutMs,
|
|
634
|
+
runId,
|
|
635
|
+
disableTools: true,
|
|
447
636
|
});
|
|
448
637
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
generatedAt: new Date().toISOString(),
|
|
456
|
-
};
|
|
638
|
+
const outputText = this.extractPayloadText(result);
|
|
639
|
+
if (!outputText) {
|
|
640
|
+
return this.buildRuntimeFailurePhilosopherOutput(
|
|
641
|
+
'runtime_session_read_failed',
|
|
642
|
+
'Philosopher returned empty response',
|
|
643
|
+
);
|
|
457
644
|
}
|
|
458
645
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
limit: 5,
|
|
462
|
-
});
|
|
646
|
+
// DEBUG: Log Philosopher's actual output
|
|
647
|
+
this.api.logger?.info(`[Trinity:Philosopher] Output preview: ${outputText.slice(0, 500)}`);
|
|
463
648
|
|
|
464
|
-
const outputText = this.extractAssistantText(messages.messages as { role: string; text?: string; content?: string }[]);
|
|
465
649
|
return this.parsePhilosopherOutput(outputText);
|
|
650
|
+
} catch (err) {
|
|
651
|
+
return this.buildRuntimeFailurePhilosopherOutput(this.classifyRuntimeError(err), err);
|
|
466
652
|
} finally {
|
|
467
|
-
|
|
468
|
-
sessionKey,
|
|
469
|
-
deleteTranscript: true,
|
|
470
|
-
}).catch(() => { /* intentionally empty - fire-and-forget session cleanup */ });
|
|
653
|
+
try { fs.unlinkSync(sessionFile); } catch { /* ignore */ }
|
|
471
654
|
}
|
|
472
655
|
}
|
|
473
656
|
|
|
474
|
-
|
|
657
|
+
|
|
475
658
|
async invokeScribe(
|
|
476
659
|
dreamerOutput: DreamerOutput,
|
|
477
660
|
philosopherOutput: PhilosopherOutput,
|
|
478
661
|
snapshot: NocturnalSessionSnapshot,
|
|
479
662
|
principleId: string,
|
|
480
663
|
telemetry: TrinityTelemetry,
|
|
481
|
-
|
|
664
|
+
|
|
482
665
|
_config: TrinityConfig
|
|
483
666
|
): Promise<TrinityDraftArtifact | null> {
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
667
|
+
this.lastFailureReason = null;
|
|
668
|
+
const runId = `scribe-${randomUUID()}`;
|
|
669
|
+
const sessionFile = this.createSessionFile('scribe');
|
|
487
670
|
const prompt = this.buildScribePrompt(dreamerOutput, philosopherOutput, snapshot, principleId);
|
|
671
|
+
const model = this.resolveModel();
|
|
488
672
|
|
|
489
673
|
try {
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
runId,
|
|
674
|
+
const result = await this.api.runtime.agent.runEmbeddedPiAgent({
|
|
675
|
+
sessionId: runId,
|
|
676
|
+
sessionFile,
|
|
677
|
+
prompt,
|
|
678
|
+
extraSystemPrompt: NOCTURNAL_SCRIBE_PROMPT,
|
|
679
|
+
config: this.loadFullConfig(),
|
|
680
|
+
provider: model.provider,
|
|
681
|
+
model: model.model,
|
|
499
682
|
timeoutMs: this.stageTimeoutMs,
|
|
683
|
+
runId,
|
|
684
|
+
disableTools: true,
|
|
500
685
|
});
|
|
501
686
|
|
|
502
|
-
|
|
687
|
+
const outputText = this.extractPayloadText(result);
|
|
688
|
+
if (!outputText) {
|
|
689
|
+
this.recordFailure('runtime_session_read_failed', 'Scribe returned empty response');
|
|
503
690
|
return null;
|
|
504
691
|
}
|
|
505
692
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
limit: 5,
|
|
509
|
-
});
|
|
693
|
+
// DEBUG: Log Scribe's actual output
|
|
694
|
+
this.api.logger?.info(`[Trinity:Scribe] Output preview: ${outputText.slice(0, 800)}`);
|
|
510
695
|
|
|
511
|
-
const outputText = this.extractAssistantText(messages.messages as { role: string; text?: string; content?: string }[]);
|
|
512
696
|
return this.parseScribeOutput(outputText, snapshot, principleId, telemetry);
|
|
697
|
+
} catch (err) {
|
|
698
|
+
this.recordFailure(this.classifyRuntimeError(err), err);
|
|
699
|
+
return null;
|
|
513
700
|
} finally {
|
|
514
|
-
|
|
515
|
-
sessionKey,
|
|
516
|
-
deleteTranscript: true,
|
|
517
|
-
}).catch(() => { /* intentionally empty - fire-and-forget session cleanup */ });
|
|
701
|
+
try { fs.unlinkSync(sessionFile); } catch { /* ignore */ }
|
|
518
702
|
}
|
|
519
703
|
}
|
|
520
704
|
|
|
521
|
-
|
|
705
|
+
|
|
522
706
|
async close(): Promise<void> {
|
|
523
|
-
//
|
|
707
|
+
// Clean up temp directory
|
|
708
|
+
try {
|
|
709
|
+
if (fs.existsSync(this.tempDir)) {
|
|
710
|
+
const files = fs.readdirSync(this.tempDir);
|
|
711
|
+
for (const file of files) {
|
|
712
|
+
fs.unlinkSync(path.join(this.tempDir, file));
|
|
713
|
+
}
|
|
714
|
+
fs.rmSync(this.tempDir, { recursive: true, force: true });
|
|
715
|
+
}
|
|
716
|
+
} catch { /* ignore cleanup errors */ }
|
|
524
717
|
}
|
|
525
718
|
|
|
526
719
|
// ---------------------------------------------------------------------------
|
|
@@ -528,51 +721,167 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
|
|
|
528
721
|
// ---------------------------------------------------------------------------
|
|
529
722
|
|
|
530
723
|
|
|
531
|
-
private extractAssistantText(
|
|
532
|
-
messages: { role: string; text?: string; content?: string }[]
|
|
533
|
-
): string {
|
|
534
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
535
|
-
const msg = messages[i] as { role: string; text?: string; content?: string };
|
|
536
|
-
if (msg.role === 'assistant') {
|
|
537
|
-
return msg.text ?? msg.content ?? '';
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
return '';
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
|
|
544
724
|
private buildDreamerPrompt(
|
|
545
725
|
snapshot: NocturnalSessionSnapshot,
|
|
546
726
|
principleId: string,
|
|
547
727
|
maxCandidates: number
|
|
548
728
|
): string {
|
|
549
|
-
|
|
729
|
+
// Build detailed tool failure list
|
|
730
|
+
const failures = snapshot.toolCalls
|
|
731
|
+
.filter(tc => tc.outcome === 'failure')
|
|
732
|
+
.map(tc => {
|
|
733
|
+
let desc = `- ${tc.toolName}`;
|
|
734
|
+
if (tc.filePath) desc += ` on ${tc.filePath}`;
|
|
735
|
+
desc += ` → FAILED: ${tc.errorMessage || 'unknown error'}`;
|
|
736
|
+
return desc;
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// Build detailed pain event list
|
|
740
|
+
const pains = snapshot.painEvents
|
|
741
|
+
.filter(pe => pe.score >= 50)
|
|
742
|
+
.map(pe => `- Pain (score: ${pe.score}): ${pe.reason || 'no reason'} [source: ${pe.source}]`);
|
|
743
|
+
|
|
744
|
+
// Build gate block list
|
|
745
|
+
const blocks = snapshot.gateBlocks
|
|
746
|
+
.map(gb => `- Gate blocked ${gb.toolName}: ${gb.reason}`);
|
|
747
|
+
|
|
748
|
+
// Build assistant decision context (last 3 turns max)
|
|
749
|
+
const recentTurns = snapshot.assistantTurns
|
|
750
|
+
.slice(-3)
|
|
751
|
+
.map((t, i) => `[Turn ${i+1}] ${t.sanitizedText.slice(0, 300)}`)
|
|
752
|
+
.join('\n');
|
|
753
|
+
|
|
754
|
+
// Build user correction cues (if any)
|
|
755
|
+
const userCues = snapshot.userTurns
|
|
756
|
+
.filter(ut => ut.correctionDetected)
|
|
757
|
+
.map(ut => `- User correction: ${ut.correctionCue || 'detected'}`)
|
|
758
|
+
.join('\n');
|
|
759
|
+
|
|
760
|
+
const sections = [
|
|
761
|
+
`## Target Principle`,
|
|
762
|
+
`**Principle ID**: ${principleId}`,
|
|
763
|
+
``,
|
|
764
|
+
`## Session Context`,
|
|
765
|
+
`**Session ID**: ${snapshot.sessionId}`,
|
|
766
|
+
``,
|
|
767
|
+
];
|
|
768
|
+
|
|
769
|
+
if (failures.length > 0) {
|
|
770
|
+
sections.push(`## Tool Failures (${failures.length})`);
|
|
771
|
+
sections.push(failures.join('\n'));
|
|
772
|
+
sections.push('');
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (pains.length > 0) {
|
|
776
|
+
sections.push(`## Pain Signals (${pains.length})`);
|
|
777
|
+
sections.push(pains.join('\n'));
|
|
778
|
+
sections.push('');
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (blocks.length > 0) {
|
|
782
|
+
sections.push(`## Gate Blocks (${blocks.length})`);
|
|
783
|
+
sections.push(blocks.join('\n'));
|
|
784
|
+
sections.push('');
|
|
785
|
+
}
|
|
550
786
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
- Pain Events: ${snapshot.stats.totalPainEvents}
|
|
557
|
-
- Gate Blocks: ${snapshot.stats.totalGateBlocks ?? 'N/A (data unavailable)'}
|
|
787
|
+
if (recentTurns) {
|
|
788
|
+
sections.push(`## Assistant Decision Context`);
|
|
789
|
+
sections.push(recentTurns);
|
|
790
|
+
sections.push('');
|
|
791
|
+
}
|
|
558
792
|
|
|
559
|
-
|
|
793
|
+
if (userCues) {
|
|
794
|
+
sections.push(`## User Corrections`);
|
|
795
|
+
sections.push(userCues);
|
|
796
|
+
sections.push('');
|
|
797
|
+
}
|
|
560
798
|
|
|
561
|
-
|
|
799
|
+
sections.push(`## Task`,
|
|
800
|
+
`Analyze the above session and generate ${maxCandidates} candidate corrections.`,
|
|
801
|
+
`Each candidate must:`,
|
|
802
|
+
`1. Identify a specific bad decision from the session`,
|
|
803
|
+
`2. Propose a concrete better decision grounded in principle ${principleId}`,
|
|
804
|
+
`3. The betterDecision MUST START with a bounded verb: read, check, verify, edit, write, create, delete, search, grep, find, list, review, examine, inspect, test, run, execute, analyze, diagnose, debug`,
|
|
805
|
+
`4. Explain the rationale referencing the principle`,
|
|
806
|
+
``,
|
|
807
|
+
`Respond with ONLY a valid JSON object matching the DreamerOutput contract.`
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
return sections.join('\n');
|
|
562
811
|
}
|
|
563
812
|
|
|
564
813
|
|
|
565
814
|
private buildPhilosopherPrompt(
|
|
566
815
|
dreamerOutput: DreamerOutput,
|
|
567
|
-
principleId: string
|
|
816
|
+
principleId: string,
|
|
817
|
+
snapshot: NocturnalSessionSnapshot
|
|
568
818
|
): string {
|
|
569
819
|
const candidatesJson = JSON.stringify(dreamerOutput.candidates, null, 2);
|
|
570
|
-
return `Target Principle: ${principleId}
|
|
571
820
|
|
|
572
|
-
|
|
573
|
-
|
|
821
|
+
// Build violation summary from snapshot for Philosopher to validate candidates
|
|
822
|
+
const failures = snapshot.toolCalls
|
|
823
|
+
.filter(tc => tc.outcome === 'failure')
|
|
824
|
+
.map(tc => `- ${tc.toolName}${tc.filePath ? ` on ${tc.filePath}` : ''} → FAILED: ${tc.errorMessage || 'unknown error'}`);
|
|
825
|
+
|
|
826
|
+
const pains = snapshot.painEvents
|
|
827
|
+
.filter(pe => pe.score >= 50)
|
|
828
|
+
.map(pe => `- Pain (score: ${pe.score}, severity: ${pe.severity || 'N/A'}): ${pe.reason || 'no reason'} [source: ${pe.source}]`);
|
|
829
|
+
|
|
830
|
+
const blocks = snapshot.gateBlocks
|
|
831
|
+
.map(gb => `- Gate blocked ${gb.toolName}: ${gb.reason}`);
|
|
832
|
+
|
|
833
|
+
const userCues = snapshot.userTurns
|
|
834
|
+
.filter(ut => ut.correctionDetected)
|
|
835
|
+
.map(ut => `- User correction: ${ut.correctionCue || 'detected'}`);
|
|
836
|
+
|
|
837
|
+
const sections = [
|
|
838
|
+
`## Target Principle`,
|
|
839
|
+
`**Principle ID**: ${principleId}`,
|
|
840
|
+
``,
|
|
841
|
+
`## Session Violation Summary`,
|
|
842
|
+
`**Session ID**: ${snapshot.sessionId}`,
|
|
843
|
+
];
|
|
844
|
+
|
|
845
|
+
if (failures.length > 0) {
|
|
846
|
+
sections.push(`\n### Tool Failures (${failures.length})`);
|
|
847
|
+
sections.push(failures.join('\n'));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (pains.length > 0) {
|
|
851
|
+
sections.push(`\n### Pain Signals (${pains.length})`);
|
|
852
|
+
sections.push(pains.join('\n'));
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (blocks.length > 0) {
|
|
856
|
+
sections.push(`\n### Gate Blocks (${blocks.length})`);
|
|
857
|
+
sections.push(blocks.join('\n'));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (userCues.length > 0) {
|
|
861
|
+
sections.push(`\n### User Corrections (${userCues.length})`);
|
|
862
|
+
sections.push(userCues.join('\n'));
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
sections.push(
|
|
866
|
+
``,
|
|
867
|
+
`## Dreamer's Candidates`,
|
|
868
|
+
candidatesJson,
|
|
869
|
+
``,
|
|
870
|
+
`## Task`,
|
|
871
|
+
`Evaluate each candidate against the violation summary above.`,
|
|
872
|
+
`For each candidate:`,
|
|
873
|
+
`1. Is the badDecision accurate — does it match the actual violations in the session?`,
|
|
874
|
+
`2. Is the betterDecision specific and actionable?`,
|
|
875
|
+
`3. Does the betterDecision START with a bounded verb (read, check, verify, edit, write, etc.)?`,
|
|
876
|
+
`4. Does the rationale correctly reference principle ${principleId}?`,
|
|
877
|
+
`5. Is the confidence score justified?`,
|
|
878
|
+
``,
|
|
879
|
+
`**Penalize executability**: If betterDecision does NOT start with a bounded verb, reduce score by 0.2.`,
|
|
880
|
+
``,
|
|
881
|
+
`Respond with ONLY a valid JSON object matching the PhilosopherOutput contract.`
|
|
882
|
+
);
|
|
574
883
|
|
|
575
|
-
|
|
884
|
+
return sections.join('\n');
|
|
576
885
|
}
|
|
577
886
|
|
|
578
887
|
|
|
@@ -584,16 +893,74 @@ Please evaluate each candidate and rank them by principle alignment, specificity
|
|
|
584
893
|
): string {
|
|
585
894
|
const candidatesJson = JSON.stringify(dreamerOutput.candidates, null, 2);
|
|
586
895
|
const judgmentsJson = JSON.stringify(philosopherOutput.judgments, null, 2);
|
|
587
|
-
return `Target Principle: ${principleId}
|
|
588
|
-
Session ID: ${snapshot.sessionId}
|
|
589
896
|
|
|
590
|
-
|
|
591
|
-
|
|
897
|
+
// Build violation evidence for Scribe to ground the final artifact
|
|
898
|
+
const violations: string[] = [];
|
|
592
899
|
|
|
593
|
-
|
|
594
|
-
|
|
900
|
+
const failures = snapshot.toolCalls.filter(tc => tc.outcome === 'failure');
|
|
901
|
+
for (const tc of failures) {
|
|
902
|
+
violations.push(`- Tool failure: ${tc.toolName}${tc.filePath ? ` on ${tc.filePath}` : ''} → ${tc.errorMessage || 'unknown error'}`);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const pains = snapshot.painEvents.filter(pe => pe.score >= 50);
|
|
906
|
+
for (const pe of pains) {
|
|
907
|
+
violations.push(`- Pain signal (score: ${pe.score}): ${pe.reason || 'no reason'} [source: ${pe.source}]`);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const blocks = snapshot.gateBlocks;
|
|
911
|
+
for (const gb of blocks) {
|
|
912
|
+
violations.push(`- Gate blocked: ${gb.toolName} → ${gb.reason}`);
|
|
913
|
+
}
|
|
595
914
|
|
|
596
|
-
|
|
915
|
+
const sections = [
|
|
916
|
+
`## Target Principle`,
|
|
917
|
+
`**Principle ID**: ${principleId}`,
|
|
918
|
+
``,
|
|
919
|
+
`## Original Violation Evidence`,
|
|
920
|
+
`**Session ID**: ${snapshot.sessionId}`,
|
|
921
|
+
];
|
|
922
|
+
|
|
923
|
+
if (violations.length > 0) {
|
|
924
|
+
sections.push(violations.join('\n'));
|
|
925
|
+
} else {
|
|
926
|
+
sections.push(`(No specific violations found in snapshot)`);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
sections.push(
|
|
930
|
+
``,
|
|
931
|
+
`## Dreamer's Candidates`,
|
|
932
|
+
candidatesJson,
|
|
933
|
+
``,
|
|
934
|
+
`## Philosopher's Judgments`,
|
|
935
|
+
judgmentsJson,
|
|
936
|
+
``,
|
|
937
|
+
`## Task`,
|
|
938
|
+
`Select the best candidate (Philosopher's rank 1) and synthesize it into a final TrinityDraftArtifact.`,
|
|
939
|
+
`Use the Original Violation Evidence above to ensure your final badDecision and betterDecision`,
|
|
940
|
+
`are grounded in the actual session events, not just Dreamer's interpretation.`,
|
|
941
|
+
``,
|
|
942
|
+
`## CRITICAL: betterDecision Format Requirements`,
|
|
943
|
+
`Your betterDecision MUST pass executability validation. It MUST:`,
|
|
944
|
+
`1. START with a concrete action verb from this list: read, check, verify, edit, write, create, delete, search, grep, find, list, review, examine, inspect, test, run, execute, analyze, diagnose, debug`,
|
|
945
|
+
`2. Reference a SPECIFIC, concrete target (file path, command name, config key, etc.)`,
|
|
946
|
+
`3. Describe a BOUNDED, executable action — not a vague principle or process`,
|
|
947
|
+
``,
|
|
948
|
+
`**Examples that PASS executability check**:`,
|
|
949
|
+
`- "Read the file before editing to verify current content"`,
|
|
950
|
+
`- "Check user permissions before executing privileged commands"`,
|
|
951
|
+
`- "Verify the routing infrastructure is operational before analyzing system state"`,
|
|
952
|
+
`- "Edit the config file to set timeout=30000ms"`,
|
|
953
|
+
``,
|
|
954
|
+
`**Examples that FAIL executability check**:`,
|
|
955
|
+
`- "Per T-01, pause all analysis tasks..." (starts with "Per", not a bounded verb)`,
|
|
956
|
+
`- "The agent should have first checked..." (starts with "The", not the action verb)`,
|
|
957
|
+
`- "Be more careful with routing tools" (vague verb "be")`,
|
|
958
|
+
`- "Ensure proper authorization" (vague verb "ensure")`,
|
|
959
|
+
``,
|
|
960
|
+
`Respond with ONLY a valid JSON object.`
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
return sections.join('\n');
|
|
597
964
|
}
|
|
598
965
|
|
|
599
966
|
|
|
@@ -643,6 +1010,19 @@ Select the best candidate (Philosopher's rank 1) and synthesize it into a final
|
|
|
643
1010
|
}
|
|
644
1011
|
}
|
|
645
1012
|
|
|
1013
|
+
private buildRuntimeFailureDreamerOutput(
|
|
1014
|
+
code: TrinityRuntimeFailureCode,
|
|
1015
|
+
error: unknown
|
|
1016
|
+
): DreamerOutput {
|
|
1017
|
+
const reason = this.recordFailure(code, error);
|
|
1018
|
+
return {
|
|
1019
|
+
valid: false,
|
|
1020
|
+
candidates: [],
|
|
1021
|
+
reason,
|
|
1022
|
+
generatedAt: new Date().toISOString(),
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
|
|
646
1026
|
private parsePhilosopherOutput(text: string): PhilosopherOutput {
|
|
647
1027
|
const json = this.extractJson(text);
|
|
648
1028
|
if (!json) {
|
|
@@ -693,22 +1073,47 @@ Select the best candidate (Philosopher's rank 1) and synthesize it into a final
|
|
|
693
1073
|
}
|
|
694
1074
|
}
|
|
695
1075
|
|
|
1076
|
+
private buildRuntimeFailurePhilosopherOutput(
|
|
1077
|
+
code: TrinityRuntimeFailureCode,
|
|
1078
|
+
error: unknown
|
|
1079
|
+
): PhilosopherOutput {
|
|
1080
|
+
const reason = this.recordFailure(code, error);
|
|
1081
|
+
return {
|
|
1082
|
+
valid: false,
|
|
1083
|
+
judgments: [],
|
|
1084
|
+
overallAssessment: '',
|
|
1085
|
+
reason,
|
|
1086
|
+
generatedAt: new Date().toISOString(),
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
private recordFailure(
|
|
1091
|
+
code: TrinityRuntimeFailureCode,
|
|
1092
|
+
error: unknown
|
|
1093
|
+
): string {
|
|
1094
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1095
|
+
this.lastFailureReason = `${code}: ${detail}`;
|
|
1096
|
+
return this.lastFailureReason;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
696
1099
|
|
|
697
1100
|
private parseScribeOutput(
|
|
698
1101
|
text: string,
|
|
699
1102
|
snapshot: NocturnalSessionSnapshot,
|
|
700
1103
|
principleId: string,
|
|
701
|
-
|
|
1104
|
+
|
|
702
1105
|
_telemetry: TrinityTelemetry
|
|
703
1106
|
): TrinityDraftArtifact | null {
|
|
704
1107
|
const json = this.extractJson(text);
|
|
705
1108
|
if (!json) {
|
|
1109
|
+
this.recordFailure('runtime_run_failed', new Error('Scribe output contains no parseable JSON'));
|
|
706
1110
|
return null;
|
|
707
1111
|
}
|
|
708
1112
|
|
|
709
1113
|
try {
|
|
710
1114
|
const parsed = JSON.parse(json);
|
|
711
1115
|
if (typeof parsed.selectedCandidateIndex !== 'number') {
|
|
1116
|
+
this.recordFailure('runtime_run_failed', new Error(`Scribe output missing "selectedCandidateIndex" field: ${text.slice(0, 200)}`));
|
|
712
1117
|
return null;
|
|
713
1118
|
}
|
|
714
1119
|
|
|
@@ -722,7 +1127,7 @@ Select the best candidate (Philosopher's rank 1) and synthesize it into a final
|
|
|
722
1127
|
sourceSnapshotRef: `snapshot-${snapshot.sessionId}-${Date.now()}`,
|
|
723
1128
|
telemetry: {
|
|
724
1129
|
chainMode: 'trinity',
|
|
725
|
-
usedStubs:
|
|
1130
|
+
usedStubs: _telemetry.usedStubs,
|
|
726
1131
|
dreamerPassed: true,
|
|
727
1132
|
philosopherPassed: true,
|
|
728
1133
|
scribePassed: true,
|
|
@@ -732,6 +1137,7 @@ Select the best candidate (Philosopher's rank 1) and synthesize it into a final
|
|
|
732
1137
|
},
|
|
733
1138
|
};
|
|
734
1139
|
} catch {
|
|
1140
|
+
this.recordFailure('runtime_run_failed', new Error(`Scribe output JSON parse error: ${json.slice(0, 200)}`));
|
|
735
1141
|
return null;
|
|
736
1142
|
}
|
|
737
1143
|
}
|
|
@@ -1125,8 +1531,8 @@ export function invokeStubDreamer(
|
|
|
1125
1531
|
*/
|
|
1126
1532
|
export function invokeStubPhilosopher(
|
|
1127
1533
|
dreamerOutput: DreamerOutput,
|
|
1128
|
-
|
|
1129
|
-
|
|
1534
|
+
_principleId: string,
|
|
1535
|
+
_snapshot: NocturnalSessionSnapshot
|
|
1130
1536
|
): PhilosopherOutput {
|
|
1131
1537
|
if (!dreamerOutput.valid || dreamerOutput.candidates.length === 0) {
|
|
1132
1538
|
return {
|
|
@@ -1374,7 +1780,7 @@ export async function runTrinityAsync(options: RunTrinityOptions): Promise<Trini
|
|
|
1374
1780
|
telemetry.candidateCount = dreamerOutput.candidates.length;
|
|
1375
1781
|
|
|
1376
1782
|
// Step 2: Philosopher — rank candidates via real subagent
|
|
1377
|
-
const philosopherOutput = await adapter.invokePhilosopher(dreamerOutput, principleId);
|
|
1783
|
+
const philosopherOutput = await adapter.invokePhilosopher(dreamerOutput, principleId, snapshot);
|
|
1378
1784
|
|
|
1379
1785
|
if (!philosopherOutput.valid || philosopherOutput.judgments.length === 0) {
|
|
1380
1786
|
failures.push({
|
|
@@ -1470,7 +1876,7 @@ function runTrinityWithStubs(
|
|
|
1470
1876
|
telemetry.candidateCount = dreamerOutput.candidates.length;
|
|
1471
1877
|
|
|
1472
1878
|
// Step 2: Philosopher — rank candidates (stub)
|
|
1473
|
-
const philosopherOutput = invokeStubPhilosopher(dreamerOutput, principleId);
|
|
1879
|
+
const philosopherOutput = invokeStubPhilosopher(dreamerOutput, principleId, snapshot);
|
|
1474
1880
|
|
|
1475
1881
|
if (!philosopherOutput.valid || philosopherOutput.judgments.length === 0) {
|
|
1476
1882
|
failures.push({
|