principles-disciple 1.16.0 → 1.18.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.
Files changed (132) hide show
  1. package/README.md +13 -5
  2. package/openclaw.plugin.json +4 -4
  3. package/package.json +1 -1
  4. package/src/commands/archive-impl.ts +3 -3
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +3 -3
  7. package/src/commands/disable-impl.ts +1 -1
  8. package/src/commands/evolution-status.ts +2 -2
  9. package/src/commands/focus.ts +2 -2
  10. package/src/commands/nocturnal-train.ts +6 -6
  11. package/src/commands/pain.ts +4 -4
  12. package/src/commands/pd-reflect.ts +87 -0
  13. package/src/commands/rollback-impl.ts +4 -4
  14. package/src/commands/rollback.ts +2 -2
  15. package/src/commands/samples.ts +2 -2
  16. package/src/commands/workflow-debug.ts +1 -1
  17. package/src/config/errors.ts +1 -1
  18. package/src/core/adaptive-thresholds.ts +1 -1
  19. package/src/core/code-implementation-storage.ts +2 -2
  20. package/src/core/config.ts +1 -1
  21. package/src/core/diagnostician-task-store.ts +2 -2
  22. package/src/core/empathy-keyword-matcher.ts +3 -3
  23. package/src/core/event-log.ts +5 -5
  24. package/src/core/evolution-engine.ts +4 -4
  25. package/src/core/evolution-logger.ts +1 -1
  26. package/src/core/evolution-reducer.ts +3 -3
  27. package/src/core/evolution-types.ts +5 -5
  28. package/src/core/external-training-contract.ts +1 -1
  29. package/src/core/focus-history.ts +14 -14
  30. package/src/core/hygiene/tracker.ts +1 -1
  31. package/src/core/init.ts +2 -2
  32. package/src/core/model-deployment-registry.ts +2 -2
  33. package/src/core/model-training-registry.ts +2 -2
  34. package/src/core/nocturnal-arbiter.ts +1 -1
  35. package/src/core/nocturnal-artificer.ts +2 -2
  36. package/src/core/nocturnal-candidate-scoring.ts +2 -2
  37. package/src/core/nocturnal-compliance.ts +4 -3
  38. package/src/core/nocturnal-dataset.ts +3 -3
  39. package/src/core/nocturnal-export.ts +4 -4
  40. package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
  41. package/src/core/nocturnal-snapshot-contract.ts +112 -0
  42. package/src/core/nocturnal-trajectory-extractor.ts +7 -5
  43. package/src/core/nocturnal-trinity.ts +480 -158
  44. package/src/core/pain-context-extractor.ts +3 -3
  45. package/src/core/pain.ts +124 -11
  46. package/src/core/path-resolver.ts +4 -4
  47. package/src/core/pd-task-reconciler.ts +10 -10
  48. package/src/core/pd-task-service.ts +1 -1
  49. package/src/core/pd-task-store.ts +1 -1
  50. package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
  51. package/src/core/principle-training-state.ts +2 -2
  52. package/src/core/principle-tree-ledger.ts +7 -7
  53. package/src/core/promotion-gate.ts +9 -9
  54. package/src/core/replay-engine.ts +12 -12
  55. package/src/core/risk-calculator.ts +1 -1
  56. package/src/core/rule-host-types.ts +2 -2
  57. package/src/core/rule-host.ts +5 -5
  58. package/src/core/schema/db-types.ts +1 -1
  59. package/src/core/schema/schema-definitions.ts +1 -1
  60. package/src/core/session-tracker.ts +96 -4
  61. package/src/core/shadow-observation-registry.ts +3 -3
  62. package/src/core/system-logger.ts +2 -2
  63. package/src/core/thinking-os-parser.ts +1 -1
  64. package/src/core/training-program.ts +2 -2
  65. package/src/core/trajectory.ts +8 -8
  66. package/src/core/workspace-context.ts +2 -2
  67. package/src/core/workspace-dir-service.ts +85 -0
  68. package/src/core/workspace-dir-validation.ts +30 -107
  69. package/src/hooks/bash-risk.ts +3 -3
  70. package/src/hooks/edit-verification.ts +4 -4
  71. package/src/hooks/gate-block-helper.ts +4 -4
  72. package/src/hooks/gate.ts +10 -10
  73. package/src/hooks/gfi-gate.ts +7 -7
  74. package/src/hooks/lifecycle.ts +2 -2
  75. package/src/hooks/llm.ts +1 -1
  76. package/src/hooks/pain.ts +25 -5
  77. package/src/hooks/progressive-trust-gate.ts +7 -7
  78. package/src/hooks/prompt.ts +24 -5
  79. package/src/hooks/subagent.ts +2 -2
  80. package/src/hooks/thinking-checkpoint.ts +2 -2
  81. package/src/hooks/trajectory-collector.ts +1 -1
  82. package/src/http/principles-console-route.ts +14 -6
  83. package/src/i18n/commands.ts +4 -0
  84. package/src/index.ts +181 -185
  85. package/src/service/central-health-service.ts +1 -1
  86. package/src/service/central-overview-service.ts +3 -3
  87. package/src/service/evolution-query-service.ts +1 -1
  88. package/src/service/evolution-worker.ts +221 -109
  89. package/src/service/health-query-service.ts +27 -17
  90. package/src/service/monitoring-query-service.ts +3 -3
  91. package/src/service/nocturnal-runtime.ts +4 -4
  92. package/src/service/nocturnal-service.ts +40 -23
  93. package/src/service/nocturnal-target-selector.ts +11 -4
  94. package/src/service/runtime-summary-service.ts +1 -1
  95. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
  96. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
  97. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
  98. package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
  99. package/src/service/subagent-workflow/types.ts +4 -4
  100. package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
  101. package/src/service/subagent-workflow/workflow-store.ts +2 -2
  102. package/src/tools/critique-prompt.ts +2 -3
  103. package/src/tools/deep-reflect.ts +17 -16
  104. package/src/tools/model-index.ts +1 -1
  105. package/src/utils/file-lock.ts +1 -1
  106. package/src/utils/io.ts +7 -2
  107. package/src/utils/nlp.ts +1 -1
  108. package/src/utils/plugin-logger.ts +2 -2
  109. package/src/utils/retry.ts +3 -2
  110. package/src/utils/subagent-probe.ts +20 -33
  111. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
  112. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +111 -0
  113. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +1 -1
  114. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +1 -1
  115. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
  116. package/templates/pain_settings.json +1 -1
  117. package/tests/build-artifacts.test.ts +4 -58
  118. package/tests/commands/pd-reflect.test.ts +49 -0
  119. package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
  120. package/tests/core/pain-auto-repair.test.ts +96 -0
  121. package/tests/core/pain-integration.test.ts +483 -0
  122. package/tests/core/pain.test.ts +5 -4
  123. package/tests/core/workspace-dir-service.test.ts +68 -0
  124. package/tests/core/workspace-dir-validation.test.ts +56 -192
  125. package/tests/hooks/pain.test.ts +20 -0
  126. package/tests/http/principles-console-route.test.ts +42 -20
  127. package/tests/integration/empathy-workflow-integration.test.ts +1 -2
  128. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
  129. package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
  130. package/tests/service/evolution-worker.nocturnal.test.ts +118 -109
  131. package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
  132. package/tests/utils/subagent-probe.test.ts +32 -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 ONLY public plugin runtime APIs (api.runtime.subagent.*)
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,12 +48,18 @@ 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
  // ---------------------------------------------------------------------------
48
- // These prompts are embedded at build time to eliminate file system dependency.
49
- // Previously loaded from src/agents/*.md at runtime fragile because esbuild
50
- // did not copy the agents/ directory into the bundle.
61
+ // These prompts are embedded at build time. The agents/ directory was removed
62
+ // to eliminate fragile runtime file dependencies on the file system.
51
63
 
52
64
  const NOCTURNAL_DREAMER_PROMPT = `# Nocturnal Dreamer — Candidate Generation
53
65
 
@@ -107,6 +119,13 @@ You MUST respond with ONLY a valid JSON object. No markdown, no explanation, no
107
119
  - Provide a principle-grounded rationale (explicitly references the principle)
108
120
  - Include a confidence score (0.0-1.0, higher = more confident)
109
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
+
110
129
  ### Candidates should DIFFER from each other:
111
130
  - Different candidates should represent genuinely different approaches
112
131
  - Do not generate candidates with identical betterDecisions
@@ -178,13 +197,23 @@ You MUST respond with ONLY a valid JSON object. No markdown, no explanation, no
178
197
  ## Evaluation Criteria
179
198
 
180
199
  ### Score Components (0-1 scale each):
181
- 1. **Principle Alignment** (weight: 0.4) — Does the betterDecision properly reflect the target principle?
182
- 2. **Specificity** (weight: 0.3) — Is badDecision specific? Is betterDecision actionable?
183
- 3. **Actionability** (weight: 0.3) — Does betterDecision describe a specific next step?
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)
184
212
 
185
213
  ### Ranking Rules:
186
214
  - Candidates are ranked by score (highest = rank 1)
187
- - 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
188
217
 
189
218
  ## Validation
190
219
 
@@ -276,7 +305,7 @@ If you cannot synthesize an artifact:
276
305
  * Interface for Trinity stage invocation.
277
306
  * Implementations can use real subagent runtimes or stubs.
278
307
  */
279
- /* eslint-disable no-unused-vars -- Reason: interface method params are type signatures, not implementations */
308
+
280
309
  export interface TrinityRuntimeAdapter {
281
310
  /**
282
311
  * Invoke the Dreamer stage.
@@ -295,11 +324,13 @@ export interface TrinityRuntimeAdapter {
295
324
  * Invoke the Philosopher stage.
296
325
  * @param dreamerOutput Dreamer's output
297
326
  * @param principleId Target principle ID
327
+ * @param snapshot Session snapshot (for violation evidence)
298
328
  * @returns Philosopher output JSON
299
329
  */
300
330
  invokePhilosopher(
301
331
  _dreamerOutput: DreamerOutput,
302
- _principleId: string
332
+ _principleId: string,
333
+ _snapshot: NocturnalSessionSnapshot
303
334
  ): Promise<PhilosopherOutput>;
304
335
 
305
336
  /**
@@ -327,7 +358,7 @@ export interface TrinityRuntimeAdapter {
327
358
  */
328
359
  close?(): Promise<void>;
329
360
  }
330
- /* eslint-enable no-unused-vars */
361
+
331
362
 
332
363
  // ---------------------------------------------------------------------------
333
364
  // OpenClaw Runtime Adapter
@@ -335,40 +366,40 @@ export interface TrinityRuntimeAdapter {
335
366
 
336
367
  /**
337
368
  * OpenClaw-backed Trinity runtime adapter.
338
- * Uses ONLY public plugin runtime APIs (api.runtime.subagent.*).
339
- * Does NOT depend on OpenClaw internals.
369
+ * Uses api.runtime.agent.runEmbeddedPiAgent() which works in background contexts
370
+ * (unlike api.runtime.subagent.* which requires gateway request scope).
340
371
  */
341
372
  export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
342
- /* eslint-disable no-unused-vars -- Reason: type-level function parameter names are documentation */
373
+
343
374
  private readonly api: {
344
375
  runtime: {
345
- subagent: {
346
- run: (_opts: {
347
- sessionKey: string;
348
- message: string;
376
+ agent: {
377
+ runEmbeddedPiAgent: (_opts: {
378
+ sessionId: string;
379
+ sessionFile: string;
380
+ prompt: string;
349
381
  extraSystemPrompt?: string;
350
- deliver?: boolean;
351
- }) => Promise<{ runId: string }>;
352
- waitForRun: (_opts: { runId: string; timeoutMs: number }) => Promise<{
353
- status: string;
354
- error?: string;
355
- }>;
356
- getSessionMessages: (_opts: {
357
- sessionKey: string;
358
- limit: number;
382
+ config?: unknown;
383
+ provider?: string;
384
+ model?: string;
385
+ timeoutMs: number;
386
+ runId: string;
387
+ disableTools?: boolean;
359
388
  }) => Promise<{
360
- messages: unknown[];
389
+ payloads?: { isError?: boolean; text?: string }[];
361
390
  }>;
362
- deleteSession: (_opts: {
363
- sessionKey: string;
364
- deleteTranscript?: boolean;
365
- }) => Promise<void>;
391
+ };
392
+ config?: {
393
+ loadConfig?: () => unknown;
366
394
  };
367
395
  };
396
+ config?: unknown;
397
+ logger?: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void };
368
398
  };
369
- /* eslint-enable no-unused-vars */
399
+
370
400
 
371
401
  private readonly stageTimeoutMs: number;
402
+ private readonly tempDir: string;
372
403
 
373
404
  constructor(
374
405
  api: OpenClawTrinityRuntimeAdapter['api'],
@@ -376,6 +407,106 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
376
407
  ) {
377
408
  this.api = api;
378
409
  this.stageTimeoutMs = stageTimeoutMs;
410
+ // Cross-platform temp directory for session files
411
+ this.tempDir = path.join(os.tmpdir(), `pd-trinity-${process.pid}`);
412
+ // Clean up any stale temp files from previous crashed runs
413
+ this.cleanupStaleTempDirs();
414
+ }
415
+
416
+ /**
417
+ * Clean up temp directories from previous crashed runs.
418
+ * Matches pattern pd-trinity-* in the OS temp directory.
419
+ */
420
+ private cleanupStaleTempDirs(): void {
421
+ try {
422
+ const osTempDir = os.tmpdir();
423
+ if (!fs.existsSync(osTempDir)) return;
424
+ const entries = fs.readdirSync(osTempDir);
425
+ for (const entry of entries) {
426
+ if (entry.startsWith('pd-trinity-') && entry !== path.basename(this.tempDir)) {
427
+ const fullPath = path.join(osTempDir, entry);
428
+ fs.rmSync(fullPath, { recursive: true, force: true });
429
+ }
430
+ }
431
+ } catch {
432
+ // Non-fatal: stale temp files will be cleaned up eventually
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Load the full OpenClaw config (including models.providers).
438
+ *
439
+ * Why: `this.api.config` is the plugin config, not the full OpenClaw config.
440
+ * It does NOT contain `models.providers`, which is needed to resolve provider
441
+ * model definitions. `api.runtime.config.loadConfig()` returns the full config.
442
+ *
443
+ * Fallback: If loadConfig() is unavailable, we return the plugin config.
444
+ * The caller (resolveModel) handles this with a minimax-portal fallback.
445
+ */
446
+ private loadFullConfig(): Record<string, unknown> | undefined {
447
+ // Try runtime.config.loadConfig() first (available in native plugin context)
448
+ const loadConfig = this.api.runtime?.config?.loadConfig;
449
+ if (loadConfig && typeof loadConfig === 'function') {
450
+ try {
451
+ return loadConfig() as Record<string, unknown> | undefined;
452
+ } catch (err) {
453
+ this.api.logger?.warn?.(`[Trinity] loadConfig() failed, falling back to plugin config: ${err instanceof Error ? err.message : String(err)}`);
454
+ }
455
+ }
456
+ // Fallback: plugin config (limited — won't have models.providers)
457
+ // resolveModel() handles this with a minimax-portal/MiniMax-M2.7 fallback
458
+ return this.api.config as Record<string, unknown> | undefined;
459
+ }
460
+
461
+ /**
462
+ * Resolve the provider and model from the OpenClaw config.
463
+ * runEmbeddedPiAgent does NOT read config.agents.defaults.model —
464
+ * it requires explicit params.provider and params.model.
465
+ */
466
+ private resolveModel(): { provider: string; model: string } {
467
+ const config = this.loadFullConfig();
468
+ const agents = config?.agents as Record<string, unknown> | undefined;
469
+ const defaults = agents?.defaults as Record<string, unknown> | undefined;
470
+ const modelConfig = defaults?.model;
471
+
472
+ if (typeof modelConfig === 'string' && modelConfig.includes('/')) {
473
+ const parts = modelConfig.split('/');
474
+ return { provider: parts[0], model: parts.slice(1).join('/') };
475
+ }
476
+
477
+ if (modelConfig && typeof modelConfig === 'object') {
478
+ const mc = modelConfig as Record<string, unknown>;
479
+ const primary = mc.primary as string | undefined;
480
+ if (primary && primary.includes('/')) {
481
+ const parts = primary.split('/');
482
+ return { provider: parts[0], model: parts.slice(1).join('/') };
483
+ }
484
+ }
485
+
486
+ // Last resort fallback — read from env vars to avoid hardcoded strings
487
+ this.api.logger?.warn?.(`[Trinity] Could not resolve model from config, using fallback: ${FALLBACK_PROVIDER}/${FALLBACK_MODEL}`);
488
+ return { provider: FALLBACK_PROVIDER, model: FALLBACK_MODEL };
489
+ }
490
+
491
+ /**
492
+ * Create a valid JSONL session file for runEmbeddedPiAgent.
493
+ */
494
+ private createSessionFile(stage: string): string {
495
+ if (!fs.existsSync(this.tempDir)) {
496
+ fs.mkdirSync(this.tempDir, { recursive: true });
497
+ }
498
+ return path.join(this.tempDir, `${stage}-${randomUUID()}.jsonl`);
499
+ }
500
+
501
+ /**
502
+ * Extract text from runEmbeddedPiAgent result.
503
+ */
504
+ private extractPayloadText(result: { payloads?: { isError?: boolean; text?: string }[] }): string {
505
+ return (result.payloads ?? [])
506
+ .filter(p => !p.isError)
507
+ .map(p => p.text?.trim() ?? '')
508
+ .filter(Boolean)
509
+ .join('\n');
379
510
  }
380
511
 
381
512
  async invokeDreamer(
@@ -383,200 +514,333 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
383
514
  principleId: string,
384
515
  maxCandidates: number
385
516
  ): Promise<DreamerOutput> {
386
- const sessionKey = `agent:main:subagent:ne-dreamer-${randomUUID()}`;
387
- const systemPrompt = NOCTURNAL_DREAMER_PROMPT;
388
-
517
+ const runId = `dreamer-${randomUUID()}`;
518
+ const sessionFile = this.createSessionFile('dreamer');
389
519
  const prompt = this.buildDreamerPrompt(snapshot, principleId, maxCandidates);
520
+ const model = this.resolveModel();
390
521
 
391
- try {
392
- const { runId } = await this.api.runtime.subagent.run({
393
- sessionKey,
394
- message: prompt,
395
- extraSystemPrompt: systemPrompt,
396
- deliver: false,
397
- });
522
+ this.api.logger?.info(`[Trinity:Dreamer] Using model: ${model.provider}/${model.model}`);
398
523
 
399
- const result = await this.api.runtime.subagent.waitForRun({
400
- runId,
524
+ try {
525
+ const result = await this.api.runtime.agent.runEmbeddedPiAgent({
526
+ sessionId: runId,
527
+ sessionFile,
528
+ prompt,
529
+ extraSystemPrompt: NOCTURNAL_DREAMER_PROMPT,
530
+ config: this.loadFullConfig(),
531
+ provider: model.provider,
532
+ model: model.model,
401
533
  timeoutMs: this.stageTimeoutMs,
534
+ runId,
535
+ disableTools: true,
402
536
  });
403
537
 
404
- if (result.status !== 'ok') {
538
+ const outputText = this.extractPayloadText(result);
539
+ if (!outputText) {
405
540
  return {
406
541
  valid: false,
407
542
  candidates: [],
408
- reason: `Dreamer subagent failed: ${result.error ?? result.status}`,
543
+ reason: 'Dreamer returned empty response',
409
544
  generatedAt: new Date().toISOString(),
410
545
  };
411
546
  }
412
547
 
413
- const messages = await this.api.runtime.subagent.getSessionMessages({
414
- sessionKey,
415
- limit: 5,
416
- });
548
+ // DEBUG: Log Dreamer's actual output
549
+ this.api.logger?.info(`[Trinity:Dreamer] Output preview: ${outputText.slice(0, 500)}`);
417
550
 
418
- const outputText = this.extractAssistantText(messages.messages as { role: string; text?: string; content?: string }[]);
419
551
  return this.parseDreamerOutput(outputText);
552
+ } 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
+ };
420
559
  } finally {
421
- await this.api.runtime.subagent.deleteSession({
422
- sessionKey,
423
- deleteTranscript: true,
424
- }).catch(() => { /* intentionally empty - fire-and-forget session cleanup */ });
560
+ try { fs.unlinkSync(sessionFile); } catch { /* ignore */ }
425
561
  }
426
562
  }
427
563
 
428
564
  async invokePhilosopher(
429
565
  dreamerOutput: DreamerOutput,
430
- principleId: string
566
+ principleId: string,
567
+ snapshot: NocturnalSessionSnapshot
431
568
  ): Promise<PhilosopherOutput> {
432
- const sessionKey = `agent:main:subagent:ne-philosopher-${randomUUID()}`;
433
- const systemPrompt = NOCTURNAL_PHILOSOPHER_PROMPT;
434
-
435
- const prompt = this.buildPhilosopherPrompt(dreamerOutput, principleId);
569
+ const runId = `philosopher-${randomUUID()}`;
570
+ const sessionFile = this.createSessionFile('philosopher');
571
+ const prompt = this.buildPhilosopherPrompt(dreamerOutput, principleId, snapshot);
572
+ const model = this.resolveModel();
436
573
 
437
574
  try {
438
- const { runId } = await this.api.runtime.subagent.run({
439
- sessionKey,
440
- message: prompt,
441
- extraSystemPrompt: systemPrompt,
442
- deliver: false,
443
- });
444
-
445
- const result = await this.api.runtime.subagent.waitForRun({
446
- runId,
575
+ const result = await this.api.runtime.agent.runEmbeddedPiAgent({
576
+ sessionId: runId,
577
+ sessionFile,
578
+ prompt,
579
+ extraSystemPrompt: NOCTURNAL_PHILOSOPHER_PROMPT,
580
+ config: this.loadFullConfig(),
581
+ provider: model.provider,
582
+ model: model.model,
447
583
  timeoutMs: this.stageTimeoutMs,
584
+ runId,
585
+ disableTools: true,
448
586
  });
449
587
 
450
- if (result.status !== 'ok') {
588
+ const outputText = this.extractPayloadText(result);
589
+ if (!outputText) {
451
590
  return {
452
591
  valid: false,
453
592
  judgments: [],
454
593
  overallAssessment: '',
455
- reason: `Philosopher subagent failed: ${result.error ?? result.status}`,
594
+ reason: 'Philosopher returned empty response',
456
595
  generatedAt: new Date().toISOString(),
457
596
  };
458
597
  }
459
598
 
460
- const messages = await this.api.runtime.subagent.getSessionMessages({
461
- sessionKey,
462
- limit: 5,
463
- });
599
+ // DEBUG: Log Philosopher's actual output
600
+ this.api.logger?.info(`[Trinity:Philosopher] Output preview: ${outputText.slice(0, 500)}`);
464
601
 
465
- const outputText = this.extractAssistantText(messages.messages as { role: string; text?: string; content?: string }[]);
466
602
  return this.parsePhilosopherOutput(outputText);
603
+ } 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
+ };
467
611
  } finally {
468
- await this.api.runtime.subagent.deleteSession({
469
- sessionKey,
470
- deleteTranscript: true,
471
- }).catch(() => { /* intentionally empty - fire-and-forget session cleanup */ });
612
+ try { fs.unlinkSync(sessionFile); } catch { /* ignore */ }
472
613
  }
473
614
  }
474
615
 
475
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: scribe invocation requires all context parameters - refactoring would break API
616
+
476
617
  async invokeScribe(
477
618
  dreamerOutput: DreamerOutput,
478
619
  philosopherOutput: PhilosopherOutput,
479
620
  snapshot: NocturnalSessionSnapshot,
480
621
  principleId: string,
481
622
  telemetry: TrinityTelemetry,
482
- // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: interface requires this param but implementation doesn't use it
623
+
483
624
  _config: TrinityConfig
484
625
  ): Promise<TrinityDraftArtifact | null> {
485
- const sessionKey = `agent:main:subagent:ne-scribe-${randomUUID()}`;
486
- const systemPrompt = NOCTURNAL_SCRIBE_PROMPT;
487
-
626
+ const runId = `scribe-${randomUUID()}`;
627
+ const sessionFile = this.createSessionFile('scribe');
488
628
  const prompt = this.buildScribePrompt(dreamerOutput, philosopherOutput, snapshot, principleId);
629
+ const model = this.resolveModel();
489
630
 
490
631
  try {
491
- const { runId } = await this.api.runtime.subagent.run({
492
- sessionKey,
493
- message: prompt,
494
- extraSystemPrompt: systemPrompt,
495
- deliver: false,
496
- });
497
-
498
- const result = await this.api.runtime.subagent.waitForRun({
499
- runId,
632
+ const result = await this.api.runtime.agent.runEmbeddedPiAgent({
633
+ sessionId: runId,
634
+ sessionFile,
635
+ prompt,
636
+ extraSystemPrompt: NOCTURNAL_SCRIBE_PROMPT,
637
+ config: this.loadFullConfig(),
638
+ provider: model.provider,
639
+ model: model.model,
500
640
  timeoutMs: this.stageTimeoutMs,
641
+ runId,
642
+ disableTools: true,
501
643
  });
502
644
 
503
- if (result.status !== 'ok') {
645
+ const outputText = this.extractPayloadText(result);
646
+ if (!outputText) {
504
647
  return null;
505
648
  }
506
649
 
507
- const messages = await this.api.runtime.subagent.getSessionMessages({
508
- sessionKey,
509
- limit: 5,
510
- });
650
+ // DEBUG: Log Scribe's actual output
651
+ this.api.logger?.info(`[Trinity:Scribe] Output preview: ${outputText.slice(0, 800)}`);
511
652
 
512
- const outputText = this.extractAssistantText(messages.messages as { role: string; text?: string; content?: string }[]);
513
653
  return this.parseScribeOutput(outputText, snapshot, principleId, telemetry);
654
+ } catch (err) {
655
+ return null;
514
656
  } finally {
515
- await this.api.runtime.subagent.deleteSession({
516
- sessionKey,
517
- deleteTranscript: true,
518
- }).catch(() => { /* intentionally empty - fire-and-forget session cleanup */ });
657
+ try { fs.unlinkSync(sessionFile); } catch { /* ignore */ }
519
658
  }
520
659
  }
521
660
 
522
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: interface-required close() with no-op implementation
661
+
523
662
  async close(): Promise<void> {
524
- // Nothing to clean up in this implementation
663
+ // Clean up temp directory
664
+ try {
665
+ if (fs.existsSync(this.tempDir)) {
666
+ const files = fs.readdirSync(this.tempDir);
667
+ for (const file of files) {
668
+ fs.unlinkSync(path.join(this.tempDir, file));
669
+ }
670
+ fs.rmSync(this.tempDir, { recursive: true, force: true });
671
+ }
672
+ } catch { /* ignore cleanup errors */ }
525
673
  }
526
674
 
527
675
  // ---------------------------------------------------------------------------
528
676
  // Private Helper Methods
529
677
  // ---------------------------------------------------------------------------
530
678
 
531
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: pure utility function that doesn't need instance state
532
- private extractAssistantText(
533
- messages: { role: string; text?: string; content?: string }[]
534
- ): string {
535
- for (let i = messages.length - 1; i >= 0; i--) {
536
- const msg = messages[i] as { role: string; text?: string; content?: string };
537
- if (msg.role === 'assistant') {
538
- return msg.text ?? msg.content ?? '';
539
- }
540
- }
541
- return '';
542
- }
543
-
544
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: pure utility function that doesn't need instance state
679
+
545
680
  private buildDreamerPrompt(
546
681
  snapshot: NocturnalSessionSnapshot,
547
682
  principleId: string,
548
683
  maxCandidates: number
549
684
  ): string {
550
- return `Target Principle: ${principleId}
685
+ // Build detailed tool failure list
686
+ const failures = snapshot.toolCalls
687
+ .filter(tc => tc.outcome === 'failure')
688
+ .map(tc => {
689
+ let desc = `- ${tc.toolName}`;
690
+ if (tc.filePath) desc += ` on ${tc.filePath}`;
691
+ desc += ` → FAILED: ${tc.errorMessage || 'unknown error'}`;
692
+ return desc;
693
+ });
694
+
695
+ // Build detailed pain event list
696
+ const pains = snapshot.painEvents
697
+ .filter(pe => pe.score >= 50)
698
+ .map(pe => `- Pain (score: ${pe.score}): ${pe.reason || 'no reason'} [source: ${pe.source}]`);
699
+
700
+ // Build gate block list
701
+ const blocks = snapshot.gateBlocks
702
+ .map(gb => `- Gate blocked ${gb.toolName}: ${gb.reason}`);
703
+
704
+ // Build assistant decision context (last 3 turns max)
705
+ const recentTurns = snapshot.assistantTurns
706
+ .slice(-3)
707
+ .map((t, i) => `[Turn ${i+1}] ${t.sanitizedText.slice(0, 300)}`)
708
+ .join('\n');
709
+
710
+ // Build user correction cues (if any)
711
+ const userCues = snapshot.userTurns
712
+ .filter(ut => ut.correctionDetected)
713
+ .map(ut => `- User correction: ${ut.correctionCue || 'detected'}`)
714
+ .join('\n');
715
+
716
+ const sections = [
717
+ `## Target Principle`,
718
+ `**Principle ID**: ${principleId}`,
719
+ ``,
720
+ `## Session Context`,
721
+ `**Session ID**: ${snapshot.sessionId}`,
722
+ ``,
723
+ ];
724
+
725
+ if (failures.length > 0) {
726
+ sections.push(`## Tool Failures (${failures.length})`);
727
+ sections.push(failures.join('\n'));
728
+ sections.push('');
729
+ }
730
+
731
+ if (pains.length > 0) {
732
+ sections.push(`## Pain Signals (${pains.length})`);
733
+ sections.push(pains.join('\n'));
734
+ sections.push('');
735
+ }
736
+
737
+ if (blocks.length > 0) {
738
+ sections.push(`## Gate Blocks (${blocks.length})`);
739
+ sections.push(blocks.join('\n'));
740
+ sections.push('');
741
+ }
551
742
 
552
- Session Snapshot:
553
- - Session ID: ${snapshot.sessionId}
554
- - Assistant Turns: ${snapshot.stats.totalAssistantTurns}
555
- - Tool Calls: ${snapshot.stats.totalToolCalls}
556
- - Failures: ${snapshot.stats.failureCount}
557
- - Pain Events: ${snapshot.stats.totalPainEvents}
558
- - Gate Blocks: ${snapshot.stats.totalGateBlocks}
743
+ if (recentTurns) {
744
+ sections.push(`## Assistant Decision Context`);
745
+ sections.push(recentTurns);
746
+ sections.push('');
747
+ }
559
748
 
560
- Please analyze this session and generate ${maxCandidates} candidate corrections. Each candidate should identify a bad decision and propose a better alternative grounded in the target principle.
749
+ if (userCues) {
750
+ sections.push(`## User Corrections`);
751
+ sections.push(userCues);
752
+ sections.push('');
753
+ }
561
754
 
562
- Respond with ONLY a valid JSON object matching the DreamerOutput contract.`;
755
+ sections.push(`## Task`,
756
+ `Analyze the above session and generate ${maxCandidates} candidate corrections.`,
757
+ `Each candidate must:`,
758
+ `1. Identify a specific bad decision from the session`,
759
+ `2. Propose a concrete better decision grounded in principle ${principleId}`,
760
+ `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`,
761
+ `4. Explain the rationale referencing the principle`,
762
+ ``,
763
+ `Respond with ONLY a valid JSON object matching the DreamerOutput contract.`
764
+ );
765
+
766
+ return sections.join('\n');
563
767
  }
564
768
 
565
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: utility method doesn't require this - pure prompt building function
769
+
566
770
  private buildPhilosopherPrompt(
567
771
  dreamerOutput: DreamerOutput,
568
- principleId: string
772
+ principleId: string,
773
+ snapshot: NocturnalSessionSnapshot
569
774
  ): string {
570
775
  const candidatesJson = JSON.stringify(dreamerOutput.candidates, null, 2);
571
- return `Target Principle: ${principleId}
572
776
 
573
- Dreamer's Candidates:
574
- ${candidatesJson}
777
+ // Build violation summary from snapshot for Philosopher to validate candidates
778
+ const failures = snapshot.toolCalls
779
+ .filter(tc => tc.outcome === 'failure')
780
+ .map(tc => `- ${tc.toolName}${tc.filePath ? ` on ${tc.filePath}` : ''} → FAILED: ${tc.errorMessage || 'unknown error'}`);
781
+
782
+ const pains = snapshot.painEvents
783
+ .filter(pe => pe.score >= 50)
784
+ .map(pe => `- Pain (score: ${pe.score}, severity: ${pe.severity || 'N/A'}): ${pe.reason || 'no reason'} [source: ${pe.source}]`);
785
+
786
+ const blocks = snapshot.gateBlocks
787
+ .map(gb => `- Gate blocked ${gb.toolName}: ${gb.reason}`);
788
+
789
+ const userCues = snapshot.userTurns
790
+ .filter(ut => ut.correctionDetected)
791
+ .map(ut => `- User correction: ${ut.correctionCue || 'detected'}`);
792
+
793
+ const sections = [
794
+ `## Target Principle`,
795
+ `**Principle ID**: ${principleId}`,
796
+ ``,
797
+ `## Session Violation Summary`,
798
+ `**Session ID**: ${snapshot.sessionId}`,
799
+ ];
800
+
801
+ if (failures.length > 0) {
802
+ sections.push(`\n### Tool Failures (${failures.length})`);
803
+ sections.push(failures.join('\n'));
804
+ }
575
805
 
576
- Please evaluate each candidate and rank them by principle alignment, specificity, and actionability. Respond with ONLY a valid JSON object matching the PhilosopherOutput contract.`;
806
+ if (pains.length > 0) {
807
+ sections.push(`\n### Pain Signals (${pains.length})`);
808
+ sections.push(pains.join('\n'));
809
+ }
810
+
811
+ if (blocks.length > 0) {
812
+ sections.push(`\n### Gate Blocks (${blocks.length})`);
813
+ sections.push(blocks.join('\n'));
814
+ }
815
+
816
+ if (userCues.length > 0) {
817
+ sections.push(`\n### User Corrections (${userCues.length})`);
818
+ sections.push(userCues.join('\n'));
819
+ }
820
+
821
+ sections.push(
822
+ ``,
823
+ `## Dreamer's Candidates`,
824
+ candidatesJson,
825
+ ``,
826
+ `## Task`,
827
+ `Evaluate each candidate against the violation summary above.`,
828
+ `For each candidate:`,
829
+ `1. Is the badDecision accurate — does it match the actual violations in the session?`,
830
+ `2. Is the betterDecision specific and actionable?`,
831
+ `3. Does the betterDecision START with a bounded verb (read, check, verify, edit, write, etc.)?`,
832
+ `4. Does the rationale correctly reference principle ${principleId}?`,
833
+ `5. Is the confidence score justified?`,
834
+ ``,
835
+ `**Penalize executability**: If betterDecision does NOT start with a bounded verb, reduce score by 0.2.`,
836
+ ``,
837
+ `Respond with ONLY a valid JSON object matching the PhilosopherOutput contract.`
838
+ );
839
+
840
+ return sections.join('\n');
577
841
  }
578
842
 
579
- /* eslint-disable @typescript-eslint/class-methods-use-this, @typescript-eslint/max-params -- Reason: helper function needs all 4 context params and doesn't use instance state */
843
+
580
844
  private buildScribePrompt(
581
845
  dreamerOutput: DreamerOutput,
582
846
  philosopherOutput: PhilosopherOutput,
@@ -585,18 +849,76 @@ Please evaluate each candidate and rank them by principle alignment, specificity
585
849
  ): string {
586
850
  const candidatesJson = JSON.stringify(dreamerOutput.candidates, null, 2);
587
851
  const judgmentsJson = JSON.stringify(philosopherOutput.judgments, null, 2);
588
- return `Target Principle: ${principleId}
589
- Session ID: ${snapshot.sessionId}
590
852
 
591
- Dreamer's Candidates:
592
- ${candidatesJson}
853
+ // Build violation evidence for Scribe to ground the final artifact
854
+ const violations: string[] = [];
855
+
856
+ const failures = snapshot.toolCalls.filter(tc => tc.outcome === 'failure');
857
+ for (const tc of failures) {
858
+ violations.push(`- Tool failure: ${tc.toolName}${tc.filePath ? ` on ${tc.filePath}` : ''} → ${tc.errorMessage || 'unknown error'}`);
859
+ }
860
+
861
+ const pains = snapshot.painEvents.filter(pe => pe.score >= 50);
862
+ for (const pe of pains) {
863
+ violations.push(`- Pain signal (score: ${pe.score}): ${pe.reason || 'no reason'} [source: ${pe.source}]`);
864
+ }
865
+
866
+ const blocks = snapshot.gateBlocks;
867
+ for (const gb of blocks) {
868
+ violations.push(`- Gate blocked: ${gb.toolName} → ${gb.reason}`);
869
+ }
593
870
 
594
- Philosopher's Judgments:
595
- ${judgmentsJson}
871
+ const sections = [
872
+ `## Target Principle`,
873
+ `**Principle ID**: ${principleId}`,
874
+ ``,
875
+ `## Original Violation Evidence`,
876
+ `**Session ID**: ${snapshot.sessionId}`,
877
+ ];
878
+
879
+ if (violations.length > 0) {
880
+ sections.push(violations.join('\n'));
881
+ } else {
882
+ sections.push(`(No specific violations found in snapshot)`);
883
+ }
884
+
885
+ sections.push(
886
+ ``,
887
+ `## Dreamer's Candidates`,
888
+ candidatesJson,
889
+ ``,
890
+ `## Philosopher's Judgments`,
891
+ judgmentsJson,
892
+ ``,
893
+ `## Task`,
894
+ `Select the best candidate (Philosopher's rank 1) and synthesize it into a final TrinityDraftArtifact.`,
895
+ `Use the Original Violation Evidence above to ensure your final badDecision and betterDecision`,
896
+ `are grounded in the actual session events, not just Dreamer's interpretation.`,
897
+ ``,
898
+ `## CRITICAL: betterDecision Format Requirements`,
899
+ `Your betterDecision MUST pass executability validation. It MUST:`,
900
+ `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`,
901
+ `2. Reference a SPECIFIC, concrete target (file path, command name, config key, etc.)`,
902
+ `3. Describe a BOUNDED, executable action — not a vague principle or process`,
903
+ ``,
904
+ `**Examples that PASS executability check**:`,
905
+ `- "Read the file before editing to verify current content"`,
906
+ `- "Check user permissions before executing privileged commands"`,
907
+ `- "Verify the routing infrastructure is operational before analyzing system state"`,
908
+ `- "Edit the config file to set timeout=30000ms"`,
909
+ ``,
910
+ `**Examples that FAIL executability check**:`,
911
+ `- "Per T-01, pause all analysis tasks..." (starts with "Per", not a bounded verb)`,
912
+ `- "The agent should have first checked..." (starts with "The", not the action verb)`,
913
+ `- "Be more careful with routing tools" (vague verb "be")`,
914
+ `- "Ensure proper authorization" (vague verb "ensure")`,
915
+ ``,
916
+ `Respond with ONLY a valid JSON object.`
917
+ );
596
918
 
597
- Select the best candidate (Philosopher's rank 1) and synthesize it into a final TrinityDraftArtifact. Respond with ONLY a valid JSON object.`;
919
+ return sections.join('\n');
598
920
  }
599
- /* eslint-enable @typescript-eslint/class-methods-use-this, @typescript-eslint/max-params */
921
+
600
922
 
601
923
  private parseDreamerOutput(text: string): DreamerOutput {
602
924
  const json = this.extractJson(text);
@@ -694,12 +1016,12 @@ Select the best candidate (Philosopher's rank 1) and synthesize it into a final
694
1016
  }
695
1017
  }
696
1018
 
697
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: output parsing requires text + snapshot + principleId + telemetry - refactoring would break API
1019
+
698
1020
  private parseScribeOutput(
699
1021
  text: string,
700
1022
  snapshot: NocturnalSessionSnapshot,
701
1023
  principleId: string,
702
- // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: interface requires this param but implementation doesn't use it
1024
+
703
1025
  _telemetry: TrinityTelemetry
704
1026
  ): TrinityDraftArtifact | null {
705
1027
  const json = this.extractJson(text);
@@ -740,7 +1062,7 @@ Select the best candidate (Philosopher's rank 1) and synthesize it into a final
740
1062
  /**
741
1063
  * Extract JSON object from text that may contain markdown code blocks.
742
1064
  */
743
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: pure utility function that doesn't need instance state
1065
+
744
1066
  private extractJson(text: string): string | null {
745
1067
  // Try direct parse first
746
1068
  try {
@@ -996,9 +1318,9 @@ export function invokeStubDreamer(
996
1318
  principleId: string,
997
1319
  maxCandidates: number
998
1320
  ): DreamerOutput {
999
- const hasFailures = snapshot.stats.failureCount > 0;
1321
+ const hasFailures = (snapshot.stats.failureCount ?? 0) > 0;
1000
1322
  const hasPain = snapshot.stats.totalPainEvents > 0;
1001
- const hasGateBlocks = snapshot.stats.totalGateBlocks > 0;
1323
+ const hasGateBlocks = (snapshot.stats.totalGateBlocks ?? 0) > 0;
1002
1324
 
1003
1325
  // #219: Detect fallback data source - stats may be incomplete
1004
1326
  const isFallback = snapshot._dataSource === 'pain_context_fallback';
@@ -1126,8 +1448,8 @@ export function invokeStubDreamer(
1126
1448
  */
1127
1449
  export function invokeStubPhilosopher(
1128
1450
  dreamerOutput: DreamerOutput,
1129
- // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: interface requires this param but stub implementation doesn't use it
1130
- _principleId: string
1451
+ _principleId: string,
1452
+ _snapshot: NocturnalSessionSnapshot
1131
1453
  ): PhilosopherOutput {
1132
1454
  if (!dreamerOutput.valid || dreamerOutput.candidates.length === 0) {
1133
1455
  return {
@@ -1201,7 +1523,7 @@ export function invokeStubPhilosopher(
1201
1523
  * In production, this would call the actual Scribe subagent.
1202
1524
  * The stub uses tournament selection (scoring + thresholds) to pick the winner.
1203
1525
  */
1204
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: stub scribe requires all context parameters - refactoring would break API
1526
+
1205
1527
  export function invokeStubScribe(
1206
1528
  dreamerOutput: DreamerOutput,
1207
1529
  philosopherOutput: PhilosopherOutput,
@@ -1279,7 +1601,7 @@ export function runTrinity(options: RunTrinityOptions): TrinityResult {
1279
1601
 
1280
1602
  // Stub path: use synchronous stub implementations
1281
1603
  if (config.useStubs) {
1282
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later in file but callback order makes this necessary
1604
+
1283
1605
  return runTrinityWithStubs(snapshot, principleId, config);
1284
1606
  }
1285
1607
 
@@ -1318,7 +1640,7 @@ export async function runTrinityAsync(options: RunTrinityOptions): Promise<Trini
1318
1640
 
1319
1641
  if (config.useStubs) {
1320
1642
  // Stub path: use synchronous stubs
1321
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later in file but callback order makes this necessary
1643
+
1322
1644
  return runTrinityWithStubs(snapshot, principleId, config);
1323
1645
  }
1324
1646
 
@@ -1375,7 +1697,7 @@ export async function runTrinityAsync(options: RunTrinityOptions): Promise<Trini
1375
1697
  telemetry.candidateCount = dreamerOutput.candidates.length;
1376
1698
 
1377
1699
  // Step 2: Philosopher — rank candidates via real subagent
1378
- const philosopherOutput = await adapter.invokePhilosopher(dreamerOutput, principleId);
1700
+ const philosopherOutput = await adapter.invokePhilosopher(dreamerOutput, principleId, snapshot);
1379
1701
 
1380
1702
  if (!philosopherOutput.valid || philosopherOutput.judgments.length === 0) {
1381
1703
  failures.push({
@@ -1471,7 +1793,7 @@ function runTrinityWithStubs(
1471
1793
  telemetry.candidateCount = dreamerOutput.candidates.length;
1472
1794
 
1473
1795
  // Step 2: Philosopher — rank candidates (stub)
1474
- const philosopherOutput = invokeStubPhilosopher(dreamerOutput, principleId);
1796
+ const philosopherOutput = invokeStubPhilosopher(dreamerOutput, principleId, snapshot);
1475
1797
 
1476
1798
  if (!philosopherOutput.valid || philosopherOutput.judgments.length === 0) {
1477
1799
  failures.push({