principles-disciple 1.18.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.
@@ -307,6 +307,17 @@ If you cannot synthesize an artifact:
307
307
  */
308
308
 
309
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
+
310
321
  /**
311
322
  * Invoke the Dreamer stage.
312
323
  * @param snapshot Session trajectory snapshot
@@ -369,6 +380,29 @@ export interface TrinityRuntimeAdapter {
369
380
  * Uses api.runtime.agent.runEmbeddedPiAgent() which works in background contexts
370
381
  * (unlike api.runtime.subagent.* which requires gateway request scope).
371
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
+
372
406
  export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
373
407
 
374
408
  private readonly api: {
@@ -396,6 +430,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
396
430
  config?: unknown;
397
431
  logger?: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void };
398
432
  };
433
+ private lastFailureReason: string | null = null;
399
434
 
400
435
 
401
436
  private readonly stageTimeoutMs: number;
@@ -405,6 +440,13 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
405
440
  api: OpenClawTrinityRuntimeAdapter['api'],
406
441
  stageTimeoutMs = 300_000 // 5 min — increased from 3 min to accommodate slower LLM responses
407
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
+
408
450
  this.api = api;
409
451
  this.stageTimeoutMs = stageTimeoutMs;
410
452
  // Cross-platform temp directory for session files
@@ -413,6 +455,14 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
413
455
  this.cleanupStaleTempDirs();
414
456
  }
415
457
 
458
+ isRuntimeAvailable(): boolean {
459
+ return true;
460
+ }
461
+
462
+ getLastFailureReason(): string | null {
463
+ return this.lastFailureReason;
464
+ }
465
+
416
466
  /**
417
467
  * Clean up temp directories from previous crashed runs.
418
468
  * Matches pattern pd-trinity-* in the OS temp directory.
@@ -509,11 +559,17 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
509
559
  .join('\n');
510
560
  }
511
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';
565
+ }
566
+
512
567
  async invokeDreamer(
513
568
  snapshot: NocturnalSessionSnapshot,
514
569
  principleId: string,
515
570
  maxCandidates: number
516
571
  ): Promise<DreamerOutput> {
572
+ this.lastFailureReason = null;
517
573
  const runId = `dreamer-${randomUUID()}`;
518
574
  const sessionFile = this.createSessionFile('dreamer');
519
575
  const prompt = this.buildDreamerPrompt(snapshot, principleId, maxCandidates);
@@ -537,12 +593,10 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
537
593
 
538
594
  const outputText = this.extractPayloadText(result);
539
595
  if (!outputText) {
540
- return {
541
- valid: false,
542
- candidates: [],
543
- reason: 'Dreamer returned empty response',
544
- generatedAt: new Date().toISOString(),
545
- };
596
+ return this.buildRuntimeFailureDreamerOutput(
597
+ 'runtime_session_read_failed',
598
+ 'Dreamer returned empty response',
599
+ );
546
600
  }
547
601
 
548
602
  // DEBUG: Log Dreamer's actual output
@@ -550,12 +604,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
550
604
 
551
605
  return this.parseDreamerOutput(outputText);
552
606
  } catch (err) {
553
- return {
554
- valid: false,
555
- candidates: [],
556
- reason: `Dreamer failed: ${err instanceof Error ? err.message : String(err)}`,
557
- generatedAt: new Date().toISOString(),
558
- };
607
+ return this.buildRuntimeFailureDreamerOutput(this.classifyRuntimeError(err), err);
559
608
  } finally {
560
609
  try { fs.unlinkSync(sessionFile); } catch { /* ignore */ }
561
610
  }
@@ -566,6 +615,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
566
615
  principleId: string,
567
616
  snapshot: NocturnalSessionSnapshot
568
617
  ): Promise<PhilosopherOutput> {
618
+ this.lastFailureReason = null;
569
619
  const runId = `philosopher-${randomUUID()}`;
570
620
  const sessionFile = this.createSessionFile('philosopher');
571
621
  const prompt = this.buildPhilosopherPrompt(dreamerOutput, principleId, snapshot);
@@ -587,13 +637,10 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
587
637
 
588
638
  const outputText = this.extractPayloadText(result);
589
639
  if (!outputText) {
590
- return {
591
- valid: false,
592
- judgments: [],
593
- overallAssessment: '',
594
- reason: 'Philosopher returned empty response',
595
- generatedAt: new Date().toISOString(),
596
- };
640
+ return this.buildRuntimeFailurePhilosopherOutput(
641
+ 'runtime_session_read_failed',
642
+ 'Philosopher returned empty response',
643
+ );
597
644
  }
598
645
 
599
646
  // DEBUG: Log Philosopher's actual output
@@ -601,13 +648,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
601
648
 
602
649
  return this.parsePhilosopherOutput(outputText);
603
650
  } catch (err) {
604
- return {
605
- valid: false,
606
- judgments: [],
607
- overallAssessment: '',
608
- reason: `Philosopher failed: ${err instanceof Error ? err.message : String(err)}`,
609
- generatedAt: new Date().toISOString(),
610
- };
651
+ return this.buildRuntimeFailurePhilosopherOutput(this.classifyRuntimeError(err), err);
611
652
  } finally {
612
653
  try { fs.unlinkSync(sessionFile); } catch { /* ignore */ }
613
654
  }
@@ -623,6 +664,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
623
664
 
624
665
  _config: TrinityConfig
625
666
  ): Promise<TrinityDraftArtifact | null> {
667
+ this.lastFailureReason = null;
626
668
  const runId = `scribe-${randomUUID()}`;
627
669
  const sessionFile = this.createSessionFile('scribe');
628
670
  const prompt = this.buildScribePrompt(dreamerOutput, philosopherOutput, snapshot, principleId);
@@ -644,6 +686,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
644
686
 
645
687
  const outputText = this.extractPayloadText(result);
646
688
  if (!outputText) {
689
+ this.recordFailure('runtime_session_read_failed', 'Scribe returned empty response');
647
690
  return null;
648
691
  }
649
692
 
@@ -652,6 +695,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
652
695
 
653
696
  return this.parseScribeOutput(outputText, snapshot, principleId, telemetry);
654
697
  } catch (err) {
698
+ this.recordFailure(this.classifyRuntimeError(err), err);
655
699
  return null;
656
700
  } finally {
657
701
  try { fs.unlinkSync(sessionFile); } catch { /* ignore */ }
@@ -966,6 +1010,19 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
966
1010
  }
967
1011
  }
968
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
+
969
1026
  private parsePhilosopherOutput(text: string): PhilosopherOutput {
970
1027
  const json = this.extractJson(text);
971
1028
  if (!json) {
@@ -1016,22 +1073,47 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
1016
1073
  }
1017
1074
  }
1018
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
+
1019
1099
 
1020
1100
  private parseScribeOutput(
1021
1101
  text: string,
1022
1102
  snapshot: NocturnalSessionSnapshot,
1023
1103
  principleId: string,
1024
-
1104
+
1025
1105
  _telemetry: TrinityTelemetry
1026
1106
  ): TrinityDraftArtifact | null {
1027
1107
  const json = this.extractJson(text);
1028
1108
  if (!json) {
1109
+ this.recordFailure('runtime_run_failed', new Error('Scribe output contains no parseable JSON'));
1029
1110
  return null;
1030
1111
  }
1031
1112
 
1032
1113
  try {
1033
1114
  const parsed = JSON.parse(json);
1034
1115
  if (typeof parsed.selectedCandidateIndex !== 'number') {
1116
+ this.recordFailure('runtime_run_failed', new Error(`Scribe output missing "selectedCandidateIndex" field: ${text.slice(0, 200)}`));
1035
1117
  return null;
1036
1118
  }
1037
1119
 
@@ -1045,7 +1127,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
1045
1127
  sourceSnapshotRef: `snapshot-${snapshot.sessionId}-${Date.now()}`,
1046
1128
  telemetry: {
1047
1129
  chainMode: 'trinity',
1048
- usedStubs: false,
1130
+ usedStubs: _telemetry.usedStubs,
1049
1131
  dreamerPassed: true,
1050
1132
  philosopherPassed: true,
1051
1133
  scribePassed: true,
@@ -1055,6 +1137,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
1055
1137
  },
1056
1138
  };
1057
1139
  } catch {
1140
+ this.recordFailure('runtime_run_failed', new Error(`Scribe output JSON parse error: ${json.slice(0, 200)}`));
1058
1141
  return null;
1059
1142
  }
1060
1143
  }
@@ -277,6 +277,12 @@ export interface PromotionGateResult {
277
277
  threshold: number;
278
278
  passed: boolean;
279
279
  };
280
+
281
+ evidenceSummary: {
282
+ evidenceMode: 'shadow' | 'eval-proxy' | 'mixed';
283
+ shadowSampleCount: number;
284
+ deltaSource: 'eval';
285
+ };
280
286
  }
281
287
 
282
288
  /**
@@ -337,6 +343,11 @@ export function evaluatePromotionGate(
337
343
  blockers,
338
344
  constraintChecks: [],
339
345
  deltaCheck: { actual: 0, threshold: minDelta, passed: false },
346
+ evidenceSummary: {
347
+ evidenceMode: 'eval-proxy',
348
+ shadowSampleCount: 0,
349
+ deltaSource: 'eval',
350
+ },
340
351
  };
341
352
  }
342
353
 
@@ -351,6 +362,11 @@ export function evaluatePromotionGate(
351
362
  blockers,
352
363
  constraintChecks: [],
353
364
  deltaCheck: { actual: 0, threshold: minDelta, passed: false },
365
+ evidenceSummary: {
366
+ evidenceMode: 'eval-proxy',
367
+ shadowSampleCount: 0,
368
+ deltaSource: 'eval',
369
+ },
354
370
  };
355
371
  }
356
372
 
@@ -366,6 +382,11 @@ export function evaluatePromotionGate(
366
382
  blockers,
367
383
  constraintChecks: [],
368
384
  deltaCheck: { actual: 0, threshold: minDelta, passed: false },
385
+ evidenceSummary: {
386
+ evidenceMode: 'eval-proxy',
387
+ shadowSampleCount: 0,
388
+ deltaSource: 'eval',
389
+ },
369
390
  };
370
391
  }
371
392
 
@@ -496,12 +517,24 @@ export function evaluatePromotionGate(
496
517
  suggestedState = 'rejected';
497
518
  }
498
519
 
520
+ const evidenceMode =
521
+ arbiterRejectSource === 'shadow' && executabilityRejectSource === 'shadow'
522
+ ? 'shadow'
523
+ : arbiterRejectSource === 'eval-proxy' && executabilityRejectSource === 'eval-proxy'
524
+ ? 'eval-proxy'
525
+ : 'mixed';
526
+
499
527
  return {
500
528
  passes: allPassed,
501
529
  suggestedState,
502
530
  blockers,
503
531
  constraintChecks,
504
532
  deltaCheck,
533
+ evidenceSummary: {
534
+ evidenceMode,
535
+ shadowSampleCount: shadowStats?.totalCount ?? 0,
536
+ deltaSource: 'eval',
537
+ },
505
538
  };
506
539
  }
507
540
 
@@ -63,6 +63,15 @@ export interface ReplayReport {
63
63
  principleAnchor: ClassificationSummary;
64
64
  };
65
65
  blockers: string[];
66
+ evidenceSummary: {
67
+ evidenceStatus: 'observed' | 'empty';
68
+ totalSamples: number;
69
+ classifiedCounts: {
70
+ painNegative: number;
71
+ successPositive: number;
72
+ principleAnchor: number;
73
+ };
74
+ };
66
75
  generatedAt: string;
67
76
  implementationId: string;
68
77
  sampleFingerprints: string[];
@@ -432,6 +441,11 @@ export class ReplayEngine {
432
441
  const successSummary = toSummary(successPositive);
433
442
  const anchorSummary = toSummary(principleAnchor);
434
443
  const blockers: string[] = [];
444
+ const totalSamples = results.length;
445
+
446
+ if (totalSamples === 0) {
447
+ blockers.push('NO REPLAY EVIDENCE: No classified replay samples were available. Report cannot justify promotion-quality conclusions.');
448
+ }
435
449
 
436
450
  for (const leak of painSummary.details.filter((result) => !result.passed)) {
437
451
  blockers.push(
@@ -459,6 +473,15 @@ export class ReplayEngine {
459
473
  principleAnchor: anchorSummary,
460
474
  },
461
475
  blockers,
476
+ evidenceSummary: {
477
+ evidenceStatus: totalSamples > 0 ? 'observed' : 'empty',
478
+ totalSamples,
479
+ classifiedCounts: {
480
+ painNegative: painSummary.total,
481
+ successPositive: successSummary.total,
482
+ principleAnchor: anchorSummary.total,
483
+ },
484
+ },
462
485
  generatedAt: new Date().toISOString(),
463
486
  implementationId,
464
487
  sampleFingerprints: results.map((result) => result.sampleFingerprint),
@@ -471,6 +494,7 @@ export class ReplayEngine {
471
494
  success: ClassificationSummary,
472
495
  anchor: ClassificationSummary
473
496
  ): 'pass' | 'fail' | 'needs-review' {
497
+ if (pain.total + success.total + anchor.total === 0) return 'needs-review';
474
498
  if (pain.failed > 0) return 'fail';
475
499
  if (anchor.failed > 0) return 'fail';
476
500
  if (success.failed > 0) return 'needs-review';
@@ -526,6 +550,7 @@ export function formatReplayReport(report: ReplayReport): string {
526
550
  output += `Implementation: ${report.implementationId}\n`;
527
551
  output += `Generated At: ${report.generatedAt}\n`;
528
552
  output += `Overall Decision: [${decisionEmoji}]\n\n`;
553
+ output += `Evidence Status: ${report.evidenceSummary.evidenceStatus} (samples=${report.evidenceSummary.totalSamples})\n\n`;
529
554
 
530
555
  const formatSection = (
531
556
  label: string,
@@ -40,7 +40,6 @@ import type { NocturnalSessionSnapshot } from '../../core/nocturnal-trajectory-e
40
40
  import type { RecentPainContext } from '../evolution-worker.js';
41
41
  import * as fs from 'fs';
42
42
  import * as path from 'path';
43
- import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
44
43
  import { validateNocturnalSnapshotIngress } from '../../core/nocturnal-snapshot-contract.js';
45
44
 
46
45
  // ─────────────────────────────────────────────────────────────────────────────
@@ -173,11 +172,8 @@ export class NocturnalWorkflowManager implements WorkflowManager {
173
172
  metadata?: Record<string, unknown>;
174
173
  }
175
174
  ): Promise<WorkflowHandle> {
176
- // #179: Check subagent runtime availability before starting
177
- // Other workflow managers (empathy, deep-reflect) have this check
178
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: TrinityRuntimeAdapter interface doesn't expose api.runtime.subagent, but OpenClawTrinityRuntimeAdapter has it
179
- const subagent = (this.runtimeAdapter as any).api?.runtime?.subagent;
180
- if (!isSubagentRuntimeAvailable(subagent)) {
175
+ const runtimeAvailable = this.runtimeAdapter.isRuntimeAvailable();
176
+ if (!runtimeAvailable) {
181
177
  this.logger.warn(`[PD:NocturnalWorkflow] Subagent runtime unavailable, skipping workflow`);
182
178
  throw new Error(`NocturnalWorkflowManager: subagent runtime unavailable`);
183
179
  }