pi-background-tasks 0.9.0 → 1.0.3

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 (72) hide show
  1. package/BACKGROUND-TASKS-INSTRUCTIONS.md +63 -0
  2. package/PUBLISHING.md +43 -29
  3. package/README.md +233 -441
  4. package/TESTING.md +15 -9
  5. package/TEST_PLAN.md +43 -17
  6. package/docs/INDEX.md +157 -0
  7. package/docs/api/eventbus-v1.md +166 -0
  8. package/docs/assets/architecture.svg +78 -0
  9. package/docs/assets/footer-dock.svg +47 -0
  10. package/docs/assets/logo.svg +49 -0
  11. package/docs/attestations.json +189 -0
  12. package/docs/choose-a-workflow.md +98 -0
  13. package/docs/commands/bg-clear.md +70 -0
  14. package/docs/commands/bg-update.md +82 -0
  15. package/docs/commands/bg.md +90 -0
  16. package/docs/commands/fusion-models.md +70 -0
  17. package/docs/commands/fusion.md +69 -0
  18. package/docs/commands/jobs.md +74 -0
  19. package/docs/commands/kill.md +82 -0
  20. package/docs/commands/logs.md +90 -0
  21. package/docs/commands/task-manager.md +109 -0
  22. package/docs/concepts/completion-delivery.md +66 -0
  23. package/docs/concepts/context-projection-and-budgeting.md +79 -0
  24. package/docs/getting-started.md +122 -0
  25. package/docs/manifest.json +1825 -0
  26. package/docs/operations/configuration.md +110 -0
  27. package/docs/operations/releasing.md +67 -0
  28. package/docs/operations/testing.md +101 -0
  29. package/docs/operations/troubleshooting.md +38 -0
  30. package/docs/read-before-edit.md +94 -0
  31. package/docs/reference/runtime-contracts.md +213 -0
  32. package/docs/reference/shortcuts-and-dock.md +70 -0
  33. package/docs/subsystems/attested-pi-runs.md +141 -0
  34. package/docs/subsystems/background-task-runtime.md +85 -0
  35. package/docs/subsystems/child-launch-durability-and-safety.md +57 -0
  36. package/docs/subsystems/delegation.md +190 -0
  37. package/docs/subsystems/docs-freshness-gate.md +26 -0
  38. package/docs/subsystems/fusion.md +121 -0
  39. package/docs/subsystems/host-ui-and-telemetry.md +83 -0
  40. package/docs/tools/bg_delegate.md +193 -0
  41. package/docs/tools/bg_kill.md +114 -0
  42. package/docs/tools/bg_logs.md +133 -0
  43. package/docs/tools/bg_result.md +120 -0
  44. package/docs/tools/bg_run.md +168 -0
  45. package/docs/tools/bg_run_pi_attested.md +170 -0
  46. package/docs/tools/bg_status.md +111 -0
  47. package/docs/tools/fusion_investigate.md +116 -0
  48. package/docs/tools/fusion_reason.md +75 -0
  49. package/docs/tools/fusion_research.md +162 -0
  50. package/docs/tools/fusion_validate.md +206 -0
  51. package/logo.png +0 -0
  52. package/package.json +25 -7
  53. package/src/core/delegate/budget.ts +1 -1
  54. package/src/core/delegate/launch.ts +5 -0
  55. package/src/core/fusion/artifacts.ts +34 -4
  56. package/src/core/fusion/budget.ts +112 -20
  57. package/src/core/fusion/child-protocol.ts +82 -0
  58. package/src/core/fusion/clean-context.ts +91 -0
  59. package/src/core/fusion/config.ts +124 -35
  60. package/src/core/fusion/context.ts +29 -7
  61. package/src/core/fusion/evaluation.ts +392 -15
  62. package/src/core/fusion/orchestrator.ts +217 -23
  63. package/src/core/fusion/pi-child.ts +183 -23
  64. package/src/core/fusion/prompts.ts +39 -26
  65. package/src/core/fusion/source-policy.ts +257 -0
  66. package/src/core/fusion/types.ts +156 -11
  67. package/src/core/fusion/web-fetch.ts +104 -15
  68. package/src/core/fusion/workflows.ts +119 -65
  69. package/src/extension.ts +3 -3
  70. package/src/fusion-child-extension.ts +159 -120
  71. package/src/fusion-extension.ts +585 -240
  72. package/src/testing/normalize.ts +0 -22
@@ -1,4 +1,5 @@
1
1
  import { randomBytes as nodeRandomBytes } from 'node:crypto';
2
+ import { canonicalJson } from '../attested-pi-run.js';
2
3
  import { parseJsonText } from '../common.js';
3
4
  import { FUSION_BUDGET_POLICY, FusionBudget, assertChildOutputWithinContract } from './budget.js';
4
5
  import {
@@ -9,7 +10,10 @@ import {
9
10
  import {
10
11
  boundedEvaluationErrors,
11
12
  formatEvaluationErrors,
13
+ parseFusionValidationCandidateReport,
14
+ renderValidatedFusionValidationReport,
12
15
  validateFusionEvaluation,
16
+ validateFusionFindingAccounting,
13
17
  } from './evaluation.js';
14
18
  import { FusionChildRunError, runPiChild, type RunPiChildOptions } from './pi-child.js';
15
19
  import {
@@ -22,12 +26,14 @@ import {
22
26
  type AnonymousFusionCandidate,
23
27
  } from './prompts.js';
24
28
  import {
25
- FUSION_BRAINSTORM_WORKFLOW,
26
- resolveWorkflowCapability,
29
+ assertWorkflowCapability,
30
+ fusionWorkflowProfile,
27
31
  type FusionWorkflowProfile,
28
32
  } from './workflows.js';
33
+ import { buildFusionSourcePolicy, sourcePolicyCanonicalBytes } from './source-policy.js';
29
34
  import {
30
- FUSION_DEFAULT_CAPABILITY,
35
+ FUSION_INPUT_SCHEMA_VERSION,
36
+ FUSION_NO_TOOLS_CAPABILITY,
31
37
  FUSION_RESULT_SCHEMA_VERSION,
32
38
  FusionError,
33
39
  addFusionUsage,
@@ -46,6 +52,7 @@ import {
46
52
  type FusionSource,
47
53
  type FusionStage,
48
54
  type FusionUsage,
55
+ type FusionValidationFindingRecord,
49
56
  type ResolvedFusionModel,
50
57
  type ResolvedFusionModels,
51
58
  } from './types.js';
@@ -62,11 +69,11 @@ export interface FusionWorkflowInput {
62
69
  sessionId?: string | undefined;
63
70
  canonicalInput: FusionCanonicalInputV3;
64
71
  canonicalInputSerialized: string;
65
- contextLedger: FusionContextOmissionLedgerV2;
72
+ contextLedger?: FusionContextOmissionLedgerV2 | undefined;
66
73
  config: FusionModelConfigV1;
67
74
  models: ResolvedFusionModels;
68
75
  candidateCapability?: FusionCapability | undefined;
69
- /** Stage framing and capability policy. Defaults to the brainstorm workflow. */
76
+ /** Mandatory v5 workflow profile. */
70
77
  profile?: FusionWorkflowProfile | undefined;
71
78
  signal?: AbortSignal | undefined;
72
79
  onProgress?: FusionProgressSink | undefined;
@@ -100,6 +107,33 @@ function errorText(error: unknown): string {
100
107
  return error instanceof Error ? error.message : String(error);
101
108
  }
102
109
 
110
+ function isRecord(value: unknown): value is Record<string, unknown> {
111
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
112
+ }
113
+
114
+ function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
115
+ const allowedSet = new Set(allowed);
116
+ return Object.keys(value).every((key) => allowedSet.has(key));
117
+ }
118
+
119
+ function isStrictCleanCanonicalInput(value: unknown): boolean {
120
+ if (!isRecord(value)) return false;
121
+ if (!hasOnlyKeys(value, ['schema_version', 'workflow', 'cwd', 'request', 'context'])) return false;
122
+ const request = value['request'];
123
+ if (!isRecord(request)) return false;
124
+ if (!hasOnlyKeys(request, ['source', 'authority', 'text', 'sha256'])) return false;
125
+ const context = value['context'];
126
+ if (!isRecord(context)) return false;
127
+ if (!hasOnlyKeys(context, ['kind', 'policy_id', 'declared_sources'])) return false;
128
+ if (context['kind'] !== 'clean_task') return false;
129
+ const declaredSources = context['declared_sources'];
130
+ if (!Array.isArray(declaredSources)) return false;
131
+ for (const source of declaredSources) {
132
+ if (!isRecord(source) || !hasOnlyKeys(source, ['url', 'canonical_url', 'purpose', 'sha256'])) return false;
133
+ }
134
+ return true;
135
+ }
136
+
103
137
  function asFusionError(error: unknown, artifactDir: string, messageOverride?: string): FusionError {
104
138
  if (error instanceof FusionError) {
105
139
  const details: FusionErrorDetails = {
@@ -191,6 +225,7 @@ function childOptions(
191
225
  signal: AbortSignal,
192
226
  slot?: CandidateSlot,
193
227
  toolCallLogPath?: string,
228
+ sourcePolicy?: { path: string; sha256: string },
194
229
  ): RunPiChildOptions {
195
230
  const out: RunPiChildOptions = {
196
231
  stage,
@@ -204,10 +239,14 @@ function childOptions(
204
239
  };
205
240
  if (slot !== undefined) out.slot = slot;
206
241
  if (toolCallLogPath !== undefined) out.toolCallLogPath = toolCallLogPath;
242
+ if (sourcePolicy !== undefined) out.sourcePolicy = sourcePolicy;
207
243
  return out;
208
244
  }
209
245
 
210
- function parseEvaluationAttempt(text: string): {
246
+ function parseEvaluationAttempt(
247
+ text: string,
248
+ expectedValidationFindings: readonly FusionValidationFindingRecord[] | undefined,
249
+ ): {
211
250
  evaluation: FusionEvaluationV1 | undefined;
212
251
  errors: readonly string[];
213
252
  } {
@@ -221,8 +260,21 @@ function parseEvaluationAttempt(text: string): {
221
260
  };
222
261
  }
223
262
  const result = validateFusionEvaluation(parsed);
224
- if (result.ok) return { evaluation: result.value, errors: [] };
225
- return { evaluation: undefined, errors: result.errors };
263
+ if (!result.ok) return { evaluation: undefined, errors: result.errors };
264
+ if (
265
+ expectedValidationFindings === undefined &&
266
+ result.value.validation_accounting !== undefined
267
+ ) {
268
+ return {
269
+ evaluation: undefined,
270
+ errors: ['evaluation.validation_accounting is permitted only for fusion_validate'],
271
+ };
272
+ }
273
+ if (expectedValidationFindings !== undefined) {
274
+ const accountingErrors = validateEvaluationAccountsForSourceFindings(result.value, expectedValidationFindings);
275
+ if (accountingErrors.length > 0) return { evaluation: undefined, errors: accountingErrors };
276
+ }
277
+ return { evaluation: result.value, errors: [] };
226
278
  }
227
279
 
228
280
  function randomIndex(limit: number, randomBytes: FusionRandomBytes): number {
@@ -318,6 +370,63 @@ function anonymousCandidates(
318
370
  };
319
371
  }
320
372
 
373
+ interface ValidationSourceData {
374
+ findings: readonly FusionValidationFindingRecord[];
375
+ verified: readonly string[];
376
+ limitations: readonly string[];
377
+ }
378
+
379
+ function validationSourceData(candidates: readonly [AnonymousFusionCandidate, AnonymousFusionCandidate, AnonymousFusionCandidate]): ValidationSourceData {
380
+ const findings: FusionValidationFindingRecord[] = [];
381
+ const verified: string[] = [];
382
+ const limitations: string[] = [];
383
+ for (const candidate of candidates) {
384
+ const report = parseFusionValidationCandidateReport(candidate.response, candidate.candidate_id);
385
+ findings.push(...report.findings);
386
+ verified.push(...report.verified);
387
+ limitations.push(...report.limitations);
388
+ }
389
+ return { findings, verified, limitations };
390
+ }
391
+
392
+ function validateEvaluationAccountsForSourceFindings(
393
+ evaluation: FusionEvaluationV1,
394
+ sourceFindings: readonly FusionValidationFindingRecord[],
395
+ ): readonly string[] {
396
+ const errors: string[] = [];
397
+ const accounting = evaluation.validation_accounting;
398
+ if (accounting === undefined) {
399
+ return ['validation evaluator output must include validation_accounting'];
400
+ }
401
+ const expected = sourceFindings.map((finding) => canonicalJson(finding)).sort();
402
+ const actual = accounting.findings.map((finding) => canonicalJson(finding)).sort();
403
+ if (expected.length !== actual.length || expected.some((value, index) => value !== actual[index])) {
404
+ errors.push('validation evaluator validation_accounting.findings must exactly equal host-assigned source findings');
405
+ }
406
+ errors.push(...validateFusionFindingAccounting(accounting));
407
+ return errors;
408
+ }
409
+
410
+
411
+ function resolveRunProfile(input: FusionWorkflowInput): FusionWorkflowProfile {
412
+ if (input.profile !== undefined) return fusionWorkflowProfile(input.profile.id);
413
+ const workflow = input.canonicalInput.workflow;
414
+ const contextKind = input.canonicalInput.context?.kind;
415
+ if (workflow !== undefined && workflow !== 'reason') {
416
+ throw new FusionError(`fusion workflow profile is required for ${workflow} runs`, {
417
+ code: 'orchestration_failed',
418
+ childCreated: false,
419
+ });
420
+ }
421
+ if (contextKind === 'clean_task') {
422
+ throw new FusionError('fusion workflow profile is required for clean-task runs', {
423
+ code: 'orchestration_failed',
424
+ childCreated: false,
425
+ });
426
+ }
427
+ return fusionWorkflowProfile('reason');
428
+ }
429
+
321
430
  export class FusionOrchestrator {
322
431
  private readonly childRunner: FusionChildRunner;
323
432
  private readonly randomBytes: FusionRandomBytes;
@@ -334,11 +443,28 @@ export class FusionOrchestrator {
334
443
  }
335
444
 
336
445
  async run(input: FusionWorkflowInput): Promise<FusionRunResult> {
337
- const profile = input.profile ?? FUSION_BRAINSTORM_WORKFLOW;
338
- // Workflow policy, not caller input: a fixed-capability workflow rejects each
339
- // other capability here, before a single child exists, rather than silently
340
- // substituting its own and running a review that never read the code.
341
- const candidateCapability = resolveWorkflowCapability(profile, input.candidateCapability);
446
+ if (input.canonicalInput.schema_version !== FUSION_INPUT_SCHEMA_VERSION) {
447
+ throw new FusionError('fusion orchestrator accepts only v5 canonical input', {
448
+ code: 'orchestration_failed',
449
+ childCreated: false,
450
+ });
451
+ }
452
+ const profile = resolveRunProfile(input);
453
+ const inputWorkflow = input.canonicalInput.workflow ?? profile.id;
454
+ const inputContextKind = input.canonicalInput.context?.kind ?? 'session_projection';
455
+ if (inputWorkflow !== profile.id || inputContextKind !== profile.contextKind) {
456
+ throw new FusionError(
457
+ `fusion workflow profile ${profile.id} is incompatible with canonical input workflow=${String(inputWorkflow)} context=${String(inputContextKind)}`,
458
+ { code: 'orchestration_failed', childCreated: false },
459
+ );
460
+ }
461
+ if (profile.contextKind === 'clean_task' && !isStrictCleanCanonicalInput(input.canonicalInput)) {
462
+ throw new FusionError('clean-task fusion input must not carry parent context fields and must match the strict clean canonical shape', {
463
+ code: 'orchestration_failed',
464
+ childCreated: false,
465
+ });
466
+ }
467
+ const candidateCapability = assertWorkflowCapability(profile, input.candidateCapability);
342
468
  const storeOptions: CreateFusionArtifactStoreOptions = {
343
469
  cwd: input.cwd,
344
470
  profile,
@@ -347,24 +473,63 @@ export class FusionOrchestrator {
347
473
  models: input.models,
348
474
  capabilities: {
349
475
  candidate: candidateCapability,
350
- evaluation: FUSION_DEFAULT_CAPABILITY,
351
- merge: FUSION_DEFAULT_CAPABILITY,
476
+ evaluation: FUSION_NO_TOOLS_CAPABILITY,
477
+ merge: FUSION_NO_TOOLS_CAPABILITY,
352
478
  },
353
479
  };
354
480
  if (input.sessionId !== undefined) storeOptions.sessionId = input.sessionId;
355
481
  if (this.now !== undefined) storeOptions.now = this.now;
482
+ let serializedParsed: unknown;
483
+ try {
484
+ serializedParsed = parseJsonText(input.canonicalInputSerialized);
485
+ } catch (error) {
486
+ throw new FusionError(`fusion canonical input artifact is not valid JSON: ${errorText(error)}`, {
487
+ code: 'orchestration_failed',
488
+ childCreated: false,
489
+ });
490
+ }
491
+ if (canonicalJson(serializedParsed) !== canonicalJson(input.canonicalInput)) {
492
+ throw new FusionError('fusion canonical input serialized bytes do not match canonical input object', {
493
+ code: 'orchestration_failed',
494
+ childCreated: false,
495
+ });
496
+ }
356
497
  const store = await this.createArtifactStore(storeOptions);
357
498
  input.onProgress?.({ type: 'state', state: 'initializing' });
358
499
  const usage = createEmptyFusionUsage();
359
500
  const calibrationWarnings: FusionCalibrationViolation[] = [];
360
501
  try {
361
502
  await store.writeCanonicalInput(input.canonicalInputSerialized);
362
- await store.writeContextLedger(input.contextLedger);
503
+ if (inputContextKind === 'session_projection') {
504
+ if (input.contextLedger === undefined) {
505
+ throw new FusionError('session-projection fusion input requires an omission ledger artifact', {
506
+ code: 'orchestration_failed',
507
+ childCreated: false,
508
+ });
509
+ }
510
+ await store.writeContextLedger(input.contextLedger);
511
+ } else if (input.contextLedger !== undefined) {
512
+ throw new FusionError('clean-task fusion input must not carry a parent omission ledger', {
513
+ code: 'orchestration_failed',
514
+ childCreated: false,
515
+ });
516
+ }
517
+ if (profile.id === 'research') {
518
+ const cleanContext = input.canonicalInput.context;
519
+ if (cleanContext?.kind !== 'clean_task') {
520
+ throw new FusionError('research workflow requires a clean-task canonical input', {
521
+ code: 'orchestration_failed',
522
+ childCreated: false,
523
+ });
524
+ }
525
+ const policy = buildFusionSourcePolicy(input.cwd, cleanContext.declared_sources);
526
+ await store.writeSourcePolicy(sourcePolicyCanonicalBytes(policy));
527
+ }
363
528
  // Deterministic size accounting for the whole workflow, performed before
364
529
  // a single child process exists. A rejection here launches zero children.
365
530
  const budget = new FusionBudget(
366
531
  input.models,
367
- input.canonicalInput.conversation_projection.policy.id,
532
+ input.canonicalInput.context?.policy_id ?? 'fusion-session-projection-v1',
368
533
  candidateCapability,
369
534
  profile,
370
535
  );
@@ -393,8 +558,13 @@ export class FusionOrchestrator {
393
558
  input.onProgress?.({ type: 'state', state: 'candidates_complete' });
394
559
 
395
560
  const shuffled = anonymousCandidates(candidateResults, shuffledSlots(this.randomBytes));
561
+ const validationData = profile.id === 'validate' ? validationSourceData(shuffled.candidates) : undefined;
396
562
  await store.setAnonymousMap(shuffled.map);
397
- const blindInput = buildBlindEvaluationInput(input.canonicalInput, shuffled.candidates);
563
+ const blindInput = buildBlindEvaluationInput(
564
+ input.canonicalInput,
565
+ shuffled.candidates,
566
+ validationData?.findings,
567
+ );
398
568
  await store.writeBlindCandidates(buildEvaluationPrompt(blindInput));
399
569
 
400
570
  await store.transition('evaluating');
@@ -407,6 +577,7 @@ export class FusionOrchestrator {
407
577
  budget,
408
578
  calibrationWarnings,
409
579
  profile,
580
+ validationData?.findings,
410
581
  );
411
582
  await store.writeEvaluationJson(evaluation);
412
583
  await store.transition('evaluation_complete');
@@ -428,7 +599,7 @@ export class FusionOrchestrator {
428
599
  mergePrompt,
429
600
  input.signal ?? new AbortController().signal,
430
601
  // Stage policy, not caller input: evaluator and merger are always reasoning-only.
431
- FUSION_DEFAULT_CAPABILITY,
602
+ FUSION_NO_TOOLS_CAPABILITY,
432
603
  undefined,
433
604
  'md',
434
605
  );
@@ -445,12 +616,24 @@ export class FusionOrchestrator {
445
616
  merged,
446
617
  );
447
618
  assertChildOutputWithinContract('merge', merged.text);
448
- await store.writeMerged(merged.text);
619
+ let finalMergedText = merged.text;
620
+ if (profile.id === 'validate') {
621
+ const accounting = evaluation.validation_accounting;
622
+ if (accounting === undefined) {
623
+ throw new FusionError('fusion_validate evaluation completed without validation accounting', {
624
+ code: 'evaluation_invalid',
625
+ stage: 'merge',
626
+ });
627
+ }
628
+ finalMergedText = renderValidatedFusionValidationReport(accounting, validationData);
629
+ }
630
+ if (finalMergedText !== merged.text) assertChildOutputWithinContract('merge', finalMergedText);
631
+ await store.writeMerged(finalMergedText);
449
632
  await store.setUsage(usage);
450
633
  await store.transition('completed');
451
634
  input.onProgress?.({ type: 'completed', runId: store.runId, artifactDir: store.artifactDir });
452
635
  return {
453
- mergedText: merged.text,
636
+ mergedText: finalMergedText,
454
637
  details: {
455
638
  schema_version: FUSION_RESULT_SCHEMA_VERSION,
456
639
  run_id: store.runId,
@@ -458,6 +641,8 @@ export class FusionOrchestrator {
458
641
  source: input.source,
459
642
  status: 'completed',
460
643
  artifact_dir: store.artifactDir,
644
+ context: { kind: inputContextKind, policy_id: input.canonicalInput.context?.policy_id ?? 'fusion-session-projection-v1' },
645
+ tool_policy: { candidate_tools: profile.candidateTools, evaluation_tools: [], merge_tools: [] },
461
646
  models: store.snapshot().models,
462
647
  evaluator_attempts: store
463
648
  .snapshot()
@@ -596,6 +781,7 @@ export class FusionOrchestrator {
596
781
  budget: FusionBudget,
597
782
  calibrationWarnings: FusionCalibrationViolation[],
598
783
  profile: FusionWorkflowProfile,
784
+ expectedValidationFindings: readonly FusionValidationFindingRecord[] | undefined,
599
785
  ): Promise<FusionEvaluationV1> {
600
786
  const firstPrompt = buildEvaluationPrompt(blindInput);
601
787
  budget.assertStagePrompt('evaluation', profile.evaluatorSystemPrompt, firstPrompt);
@@ -609,6 +795,7 @@ export class FusionOrchestrator {
609
795
  1,
610
796
  false,
611
797
  profile,
798
+ expectedValidationFindings,
612
799
  );
613
800
  if (first.evaluation !== undefined) return first.evaluation;
614
801
  const errors = boundedEvaluationErrors(first.errors);
@@ -634,6 +821,7 @@ export class FusionOrchestrator {
634
821
  2,
635
822
  true,
636
823
  profile,
824
+ expectedValidationFindings,
637
825
  );
638
826
  if (second.evaluation !== undefined) return second.evaluation;
639
827
  throw new FusionError(
@@ -656,6 +844,7 @@ export class FusionOrchestrator {
656
844
  attempt: 1 | 2,
657
845
  repair: boolean,
658
846
  profile: FusionWorkflowProfile,
847
+ expectedValidationFindings: readonly FusionValidationFindingRecord[] | undefined,
659
848
  ): Promise<EvaluationAttemptResult> {
660
849
  input.onProgress?.({ type: 'evaluation_started', attempt, repair });
661
850
  const systemPrompt = repair
@@ -671,7 +860,7 @@ export class FusionOrchestrator {
671
860
  prompt,
672
861
  input.signal ?? new AbortController().signal,
673
862
  // Stage policy, not caller input: evaluator and merger are always reasoning-only.
674
- FUSION_DEFAULT_CAPABILITY,
863
+ FUSION_NO_TOOLS_CAPABILITY,
675
864
  undefined,
676
865
  'txt',
677
866
  attempt,
@@ -691,7 +880,7 @@ export class FusionOrchestrator {
691
880
  await store.setUsage(usage);
692
881
  // Bound the evaluator output before it can be embedded in a repair prompt.
693
882
  assertChildOutputWithinContract('evaluation', result.text);
694
- const parsed = parseEvaluationAttempt(result.text);
883
+ const parsed = parseEvaluationAttempt(result.text, expectedValidationFindings);
695
884
  return { result, evaluation: parsed.evaluation, errors: parsed.errors };
696
885
  }
697
886
 
@@ -753,6 +942,10 @@ export class FusionOrchestrator {
753
942
  capability !== 'reason'
754
943
  ? store.childToolCallLogPath(stage, slot, logicalAttempt)
755
944
  : undefined;
945
+ const sourcePolicy =
946
+ capability === 'research'
947
+ ? store.sourcePolicyLaunchReference()
948
+ : undefined;
756
949
  try {
757
950
  return await this.childRunner(
758
951
  childOptions(
@@ -766,6 +959,7 @@ export class FusionOrchestrator {
766
959
  signal,
767
960
  slot,
768
961
  toolCallLogPath,
962
+ sourcePolicy,
769
963
  ),
770
964
  );
771
965
  } catch (error) {