dual-brain 0.2.9 → 0.2.10

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/src/pipeline.mjs CHANGED
@@ -13,6 +13,15 @@ import { loadProfile } from './profile.mjs';
13
13
  import { mkdirSync, writeFileSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
15
 
16
+ // Lazy-load collaboration module
17
+ let _collab = null;
18
+ async function getCollab() {
19
+ if (!_collab) {
20
+ try { _collab = await import('./collaboration.mjs'); } catch { _collab = false; }
21
+ }
22
+ return _collab || null;
23
+ }
24
+
16
25
  // ─── PipelineRun factory ──────────────────────────────────────────────────────
17
26
 
18
27
  /**
@@ -87,6 +96,12 @@ export function createPipelineRun(trigger = '', prompt = '') {
87
96
  // Execution safety (populated in Phase 3 when risk is high/critical)
88
97
  checkpoint: null, // from checkpoint.mjs — { success, id, label, timestamp } or null
89
98
 
99
+ // HEAD cognitive judgment (populated in Phase 0 from head.mjs)
100
+ headJudgment: null, // from processTurn — situation, uncertainties, obligations, noticings, result
101
+
102
+ // Collaboration (populated when multi-agent patterns are used)
103
+ collaboration: null, // from collaboration.mjs — session object with blackboard, events, agents
104
+
90
105
  completedAt: null,
91
106
  };
92
107
  }
@@ -258,12 +273,12 @@ export function outcomeGate(run) {
258
273
  * @param {string} cwd
259
274
  * @returns {object}
260
275
  */
261
- async function buildContextPack(prompt, files = [], cwd = process.cwd(), sessionContext = null) {
276
+ async function buildContextPack(prompt, files = [], cwd = process.cwd(), sessionContext = null, headJudgment = null) {
262
277
  const profile = await _loadProfileSafe(cwd);
263
278
 
264
279
  const priorFailures = _getPriorFailures(prompt, cwd);
265
280
 
266
- const detection = detectTask({ prompt, files, priorFailures, sessionContext });
281
+ const detection = detectTask({ prompt, files, priorFailures, sessionContext, headJudgment });
267
282
 
268
283
  return {
269
284
  prompt,
@@ -667,170 +682,215 @@ export async function runPipeline(trigger, prompt, options = {}) {
667
682
  const run = createPipelineRun(trigger, prompt);
668
683
 
669
684
  try {
670
- // ── Phase 0: Situational awareness ───────────────────────────────────────
671
-
672
- // Session history context load first so Phase 0 modules (intelligence, formatBrief) can use it
673
- try {
674
- const session = await import('./session.mjs');
675
- if (session.getRoutingContext) {
676
- run.sessionContext = session.getRoutingContext(cwd, prompt);
677
- }
678
- } catch {} // session.mjs not available or getRoutingContext not exported — non-blocking
679
-
680
- try {
681
- const { deriveProjectState, deriveTaskContext, detectContradictions, formatBrief } = await import('./intelligence.mjs');
682
- run.projectBrief = await deriveProjectState(options.cwd || process.cwd());
683
- run.taskBrief = deriveTaskContext(prompt, options.recentEvents || []);
684
- run.situationBrief = formatBrief(run.projectBrief, run.taskBrief, run.sessionContext);
685
- } catch (e) {
686
- // intelligence module not available — continue without it (degraded)
687
- }
688
-
689
- // Doctor: discover capabilities (cached per process via discovery log)
690
- try {
691
- const { discover, verifyAll } = await import('./doctor.mjs');
692
- const doctorCwd = options.cwd || process.cwd();
693
- discover(doctorCwd); // writes to .dual-brain/discoveries.jsonl (idempotent)
694
- verifyAll(doctorCwd); // writes to .dual-brain/verifications.jsonl
695
- } catch (e) {
696
- // doctor not available — non-blocking
697
- }
698
-
699
- // Ledger: check open tasks + create task for this run
700
- try {
701
- const { getOpenTasks, createTask, reconcile } = await import('./ledger.mjs');
702
- const cwd = options.cwd || process.cwd();
703
-
704
- // Check for stale tasks on session start
705
- run.openTasks = getOpenTasks(cwd);
706
- const staleTasks = reconcile(cwd);
707
-
708
- // Create a ledger task for this pipeline run
709
- const task = createTask({
710
- intent: prompt,
711
- owner: 'head',
712
- priority: run.projectBrief?.recentFailures?.length > 0 ? 'high' : 'medium',
713
- files: options.files || []
714
- }, cwd);
715
- run.taskId = task.id;
716
- } catch (e) {
717
- // ledger not available — continue degraded
718
- }
719
-
720
- // Append open tasks to situation brief if any exist
721
- if (run.openTasks.length > 0) {
722
- const preview = run.openTasks.slice(0, 3).map(t => t.intent).join(', ');
723
- const pendingLine = `PENDING TASKS: ${run.openTasks.length} open (${preview})`;
724
- run.situationBrief = run.situationBrief
725
- ? `${run.situationBrief}\n${pendingLine}`
726
- : pendingLine;
727
- }
685
+ // ── Phase 0: HEAD Cognitive Judgment ─────────────────────────────────────
686
+ // HEAD perceives the situation FIRST. Its judgment gates everything else:
687
+ // - depth controls how much intelligence the pipeline loads
688
+ // - shouldAskUser can block dispatch and surface uncertainty
689
+ // - obligations flow into dispatched agent prompts
690
+ // - noticings inform the user of things they should know
728
691
 
729
- // Calibration: analyze user input and adapt
730
692
  try {
731
- const { analyzeInput, getAdaptation, detectCorrection, updateCalibration } = await import('./calibration.mjs');
732
- const { getProjectState, updateProject } = await import('./living-docs.mjs');
733
- const cwd = options.cwd || process.cwd();
734
-
735
- const projectState = getProjectState(cwd);
736
- const currentCal = projectState?.project?.userCalibration || { specificity: 3, corrections: 3, autonomy: 3, interactions: 0 };
737
- const isCorrection = detectCorrection(prompt);
693
+ const head = await import('./head.mjs');
694
+ const headState = head.loadState();
695
+ const headContext = {
696
+ files: files,
697
+ priorFailures: 0,
698
+ uncommittedFiles: [],
699
+ recentFiles: [],
700
+ patterns: [],
701
+ };
738
702
 
739
- run.calibration = updateCalibration(currentCal, prompt, isCorrection);
740
- run.adaptation = getAdaptation(run.calibration);
703
+ // Enrich head context from git state (best-effort)
704
+ try {
705
+ const gitStatus = execSync('git status --porcelain -u', { cwd, stdio: ['ignore', 'pipe', 'pipe'] }).toString();
706
+ headContext.uncommittedFiles = gitStatus.split('\n').map(l => l.slice(3).trim()).filter(Boolean);
707
+ } catch {}
708
+
709
+ run.headJudgment = head.processTurn(headState, prompt, headContext);
710
+
711
+ // HEAD says to ask the user — block pipeline with the uncertainty + noticings
712
+ if (run.headJudgment.shouldAskUser && !options.forceDispatch) {
713
+ const reasons = [];
714
+ if (run.headJudgment.result.confidence.level !== 'sufficient') {
715
+ reasons.push(`Confidence: ${run.headJudgment.result.confidence.level} (${run.headJudgment.result.confidence.score})`);
716
+ for (const gap of run.headJudgment.result.confidence.gaps || []) {
717
+ reasons.push(` Uncertain: ${gap}`);
718
+ }
719
+ }
720
+ for (const n of run.headJudgment.result.surfaceNoticings || []) {
721
+ reasons.push(` ${n.type}: ${n.observation}`);
722
+ }
723
+ if (run.headJudgment.result.action.type === 'clarify') {
724
+ reasons.push(`HEAD recommends clarifying before acting`);
725
+ }
741
726
 
742
- // Persist updated calibration
743
- updateProject({ userCalibration: run.calibration }, cwd);
744
- } catch (e) {
745
- // calibration not available — continue degraded
746
- }
727
+ run.completedAt = Date.now();
728
+ return {
729
+ success: false,
730
+ gateFailure: 'head-judgment',
731
+ reason: reasons.join('\n'),
732
+ headJudgment: run.headJudgment,
733
+ run,
734
+ };
735
+ }
747
736
 
748
- // Environment awareness
749
- try {
750
- const { scanEnvironment, getCapabilitySummary } = await import('./awareness.mjs');
751
- run.environment = scanEnvironment(cwd);
752
-
753
- // Add capabilities to situation brief
754
- if (run.situationBrief && run.environment) {
755
- const caps = getCapabilitySummary(run.environment);
756
- if (caps.length > 0) {
757
- run.situationBrief += '\nCAPABILITIES: ' + caps.join(', ');
737
+ if (verbose) {
738
+ log(`[pipeline] HEAD depth: ${run.headJudgment.depth}, action: ${run.headJudgment.action.type}/${run.headJudgment.action.mode}`);
739
+ if (run.headJudgment.result.surfaceNoticings.length > 0) {
740
+ for (const n of run.headJudgment.result.surfaceNoticings) {
741
+ log(`[pipeline] HEAD noticed: ${n.observation}`);
742
+ }
758
743
  }
759
744
  }
760
- } catch (e) {
761
- // awareness not available
745
+ } catch {
746
+ // head.mjs unavailable — continue degraded (no cognitive layer)
762
747
  }
763
748
 
764
- // Replit context enrichment augment run with Replit environment data
765
- try {
766
- const replit = await import('./replit.mjs');
767
- const replitEnv = replit.detectReplitEnvironment(cwd);
768
- if (replitEnv.isReplit) {
769
- run.replitEnvironment = replitEnv;
770
- run.replitTools = replit.inspectReplitTools(cwd);
771
- run.replitConfig = replit.getReplitToolsConfig(cwd);
772
- }
773
- } catch {} // replit.mjs not available — non-blocking
749
+ // ── Phase 0: Situational awareness ───────────────────────────────────────
750
+ // HEAD's depth assessment controls how much intelligence we load.
751
+ // reflexive = skip heavy modules, light = core only, full/deep = everything
752
+ const headDepth = run.headJudgment?.depth || 'full';
753
+ const loadFull = headDepth === 'full' || headDepth === 'deep';
754
+ const loadLight = loadFull || headDepth === 'light';
774
755
 
775
- // Knowledge preflightcheck if we already know the answer
756
+ // Session historyalways load (lightweight, index-only)
776
757
  try {
777
- const { lookupDecision, triageQuestion } = await import('./think-engine.mjs');
778
- const cwd = options.cwd || process.cwd();
779
-
780
- run.decisionPreflight = lookupDecision(prompt, options.tags || [], cwd);
758
+ const session = await import('./session.mjs');
759
+ if (session.getRoutingContext) {
760
+ run.sessionContext = session.getRoutingContext(cwd, prompt);
761
+ }
762
+ } catch {} // non-blocking
781
763
 
782
- // If exact reuse found, we can short-circuit
783
- if (run.decisionPreflight.recommendation === 'reuse' && run.decisionPreflight.candidates[0]) {
784
- // Add cached decision info to situation brief
785
- if (run.situationBrief) {
786
- run.situationBrief += '\nCACHED DECISION: Found prior decision with ' +
787
- Math.round(run.decisionPreflight.candidates[0].relevance * 100) + '% relevance';
788
- }
764
+ // Intelligence module skip for reflexive
765
+ if (loadLight) {
766
+ try {
767
+ const { deriveProjectState, deriveTaskContext, detectContradictions, formatBrief } = await import('./intelligence.mjs');
768
+ run.projectBrief = await deriveProjectState(options.cwd || process.cwd());
769
+ run.taskBrief = deriveTaskContext(prompt, options.recentEvents || []);
770
+ run.situationBrief = formatBrief(run.projectBrief, run.taskBrief, run.sessionContext);
771
+ } catch (e) {
772
+ // intelligence module not available — continue without it (degraded)
789
773
  }
774
+ }
790
775
 
791
- // Triage to determine thinking tier
792
- const triage = triageQuestion(prompt, run.projectBrief, run.decisionPreflight);
793
- run.thinkResult = { tier: triage.recommendedTier, estimatedTokens: triage.estimatedTokens, triage };
776
+ // Doctor, ledger, calibration, awareness, replit, think-engine, prompt-intel
777
+ // only load for light/full/deep depth
778
+ if (loadLight) {
779
+ // Doctor: discover capabilities (cached per process)
780
+ try {
781
+ const { discover, verifyAll } = await import('./doctor.mjs');
782
+ const doctorCwd = options.cwd || process.cwd();
783
+ discover(doctorCwd);
784
+ verifyAll(doctorCwd);
785
+ } catch (e) {}
794
786
 
795
- // Add to situation brief
796
- if (run.situationBrief) {
797
- run.situationBrief += '\nTHINK TIER: ' + triage.recommendedTier + ' (' + triage.estimatedTokens + ' tokens est.)';
787
+ // Ledger: check open tasks + create task
788
+ try {
789
+ const { getOpenTasks, createTask, reconcile } = await import('./ledger.mjs');
790
+ const cwd = options.cwd || process.cwd();
791
+ run.openTasks = getOpenTasks(cwd);
792
+ const staleTasks = reconcile(cwd);
793
+ const task = createTask({
794
+ intent: prompt,
795
+ owner: 'head',
796
+ priority: run.projectBrief?.recentFailures?.length > 0 ? 'high' : 'medium',
797
+ files: options.files || []
798
+ }, cwd);
799
+ run.taskId = task.id;
800
+ } catch (e) {}
801
+
802
+ if (run.openTasks.length > 0) {
803
+ const preview = run.openTasks.slice(0, 3).map(t => t.intent).join(', ');
804
+ const pendingLine = `PENDING TASKS: ${run.openTasks.length} open (${preview})`;
805
+ run.situationBrief = run.situationBrief
806
+ ? `${run.situationBrief}\n${pendingLine}`
807
+ : pendingLine;
798
808
  }
799
- } catch (e) {
800
- // think-engine not available
801
809
  }
802
810
 
803
- // Prompt intelligence
804
- try {
805
- const { analyzePrompt, enrichPrompt, shouldBlock, getBlockReason } = await import('./prompt-intel.mjs');
811
+ // Heavy intelligence modules — only for full/deep
812
+ if (loadFull) {
813
+ // Calibration
814
+ try {
815
+ const { analyzeInput, getAdaptation, detectCorrection, updateCalibration } = await import('./calibration.mjs');
816
+ const { getProjectState, updateProject } = await import('./living-docs.mjs');
817
+ const cwd = options.cwd || process.cwd();
818
+ const projectState = getProjectState(cwd);
819
+ const currentCal = projectState?.project?.userCalibration || { specificity: 3, corrections: 3, autonomy: 3, interactions: 0 };
820
+ const isCorrection = detectCorrection(prompt);
821
+ run.calibration = updateCalibration(currentCal, prompt, isCorrection);
822
+ run.adaptation = getAdaptation(run.calibration);
823
+ updateProject({ userCalibration: run.calibration }, cwd);
824
+ } catch (e) {}
825
+
826
+ // Environment awareness
827
+ try {
828
+ const { scanEnvironment, getCapabilitySummary } = await import('./awareness.mjs');
829
+ run.environment = scanEnvironment(cwd);
830
+ if (run.situationBrief && run.environment) {
831
+ const caps = getCapabilitySummary(run.environment);
832
+ if (caps.length > 0) {
833
+ run.situationBrief += '\nCAPABILITIES: ' + caps.join(', ');
834
+ }
835
+ }
836
+ } catch (e) {}
806
837
 
807
- run.promptAnalysis = analyzePrompt(prompt, run.projectBrief, run.calibration);
838
+ // Replit context
839
+ try {
840
+ const replit = await import('./replit.mjs');
841
+ const replitEnv = replit.detectReplitEnvironment(cwd);
842
+ if (replitEnv.isReplit) {
843
+ run.replitEnvironment = replitEnv;
844
+ run.replitTools = replit.inspectReplitTools(cwd);
845
+ run.replitConfig = replit.getReplitToolsConfig(cwd);
846
+ }
847
+ } catch {}
808
848
 
809
- // Hard block on dangerous intent
810
- if (shouldBlock(run.promptAnalysis)) {
811
- const reason = getBlockReason(run.promptAnalysis);
812
- if (run.taskId) {
813
- try {
814
- const { failTask } = await import('./ledger.mjs');
815
- failTask(run.taskId, 'Blocked by risk detection: ' + reason, cwd);
816
- } catch (e) {}
849
+ // Knowledge preflight
850
+ try {
851
+ const { lookupDecision, triageQuestion } = await import('./think-engine.mjs');
852
+ const cwd = options.cwd || process.cwd();
853
+ run.decisionPreflight = lookupDecision(prompt, options.tags || [], cwd);
854
+ if (run.decisionPreflight.recommendation === 'reuse' && run.decisionPreflight.candidates[0]) {
855
+ if (run.situationBrief) {
856
+ run.situationBrief += '\nCACHED DECISION: Found prior decision with ' +
857
+ Math.round(run.decisionPreflight.candidates[0].relevance * 100) + '% relevance';
858
+ }
817
859
  }
818
- run.completedAt = Date.now();
819
- return {
820
- success: false,
821
- gateFailure: 'risk',
822
- reason: 'Prompt blocked: ' + reason,
823
- promptAnalysis: run.promptAnalysis,
824
- run
825
- };
826
- }
860
+ const triage = triageQuestion(prompt, run.projectBrief, run.decisionPreflight);
861
+ run.thinkResult = { tier: triage.recommendedTier, estimatedTokens: triage.estimatedTokens, triage };
862
+ if (run.situationBrief) {
863
+ run.situationBrief += '\nTHINK TIER: ' + triage.recommendedTier + ' (' + triage.estimatedTokens + ' tokens est.)';
864
+ }
865
+ } catch (e) {}
827
866
 
828
- // Enrich prompt if intervention says so
829
- if (run.promptAnalysis.intervention === 'silent_enrich' || run.promptAnalysis.intervention === 'confirm_rewrite') {
830
- run.enrichedPrompt = enrichPrompt(prompt, run.projectBrief, run.promptAnalysis);
831
- }
832
- } catch (e) {
833
- // prompt-intel not available
867
+ // Prompt intelligence
868
+ try {
869
+ const { analyzePrompt, enrichPrompt, shouldBlock, getBlockReason } = await import('./prompt-intel.mjs');
870
+ run.promptAnalysis = analyzePrompt(prompt, run.projectBrief, run.calibration);
871
+
872
+ if (shouldBlock(run.promptAnalysis)) {
873
+ const reason = getBlockReason(run.promptAnalysis);
874
+ if (run.taskId) {
875
+ try {
876
+ const { failTask } = await import('./ledger.mjs');
877
+ failTask(run.taskId, 'Blocked by risk detection: ' + reason, cwd);
878
+ } catch (e) {}
879
+ }
880
+ run.completedAt = Date.now();
881
+ return {
882
+ success: false,
883
+ gateFailure: 'risk',
884
+ reason: 'Prompt blocked: ' + reason,
885
+ promptAnalysis: run.promptAnalysis,
886
+ run
887
+ };
888
+ }
889
+
890
+ if (run.promptAnalysis.intervention === 'silent_enrich' || run.promptAnalysis.intervention === 'confirm_rewrite') {
891
+ run.enrichedPrompt = enrichPrompt(prompt, run.projectBrief, run.promptAnalysis);
892
+ }
893
+ } catch (e) {}
834
894
  }
835
895
 
836
896
  // ── Phase 1: Context ──────────────────────────────────────────────────────
@@ -838,7 +898,7 @@ export async function runPipeline(trigger, prompt, options = {}) {
838
898
  const effectivePrompt = run.enrichedPrompt || prompt;
839
899
 
840
900
  // Build context pack (pass sessionContext so detect can use cross-session signals)
841
- run.context = await buildContextPack(effectivePrompt, files, cwd, run.sessionContext);
901
+ run.context = await buildContextPack(effectivePrompt, files, cwd, run.sessionContext, run.headJudgment);
842
902
 
843
903
  // Query failure history (must happen before context gate)
844
904
  try {
@@ -870,7 +930,14 @@ export async function runPipeline(trigger, prompt, options = {}) {
870
930
 
871
931
  // ── Phase 2: Plan ─────────────────────────────────────────────────────────
872
932
 
873
- run.plan = buildExecutionPlan(run.context, trigger, { forceDepth, forceChallenger, thinkResult: run.thinkResult });
933
+ // HEAD's depth assessment can influence the plan's reasoning depth
934
+ const headDepthMap = { reflexive: 'low', light: 'medium', full: 'high', deep: 'ultra' };
935
+ const headSuggestedDepth = run.headJudgment?.depth
936
+ ? headDepthMap[run.headJudgment.depth]
937
+ : undefined;
938
+ const effectiveForceDepth = forceDepth || headSuggestedDepth;
939
+
940
+ run.plan = buildExecutionPlan(run.context, trigger, { forceDepth: effectiveForceDepth, forceChallenger, thinkResult: run.thinkResult });
874
941
 
875
942
  // Model intelligence
876
943
  try {
@@ -1005,18 +1072,142 @@ export async function runPipeline(trigger, prompt, options = {}) {
1005
1072
 
1006
1073
  const decision = { ...run.plan._decision };
1007
1074
 
1008
- run.result = await dispatch({
1009
- decision,
1010
- prompt: effectivePrompt,
1011
- files,
1012
- cwd,
1013
- dryRun: false,
1014
- verbose,
1015
- profile: run.context.profile,
1016
- situationBrief: run.situationBrief,
1017
- adaptation: run.adaptation,
1018
- modelSuggestion: run.modelSuggestion,
1019
- });
1075
+ // ── HEAD judgment injection into agent prompts ─────────────────────────────
1076
+ // HEAD's obligations, noticings, and uncertainties flow to the work agent
1077
+ // so it knows what to be careful about, what HEAD was worried about, and
1078
+ // what to double-check.
1079
+ let headJudgmentBlock = '';
1080
+ if (run.headJudgment) {
1081
+ const hj = run.headJudgment;
1082
+ const hjLines = ['[HEAD JUDGMENT]'];
1083
+
1084
+ // Critical obligations the agent must respect
1085
+ const criticalObs = (hj.result?.obligations || []).filter(o => o.priority === 'critical' || o.priority === 'high');
1086
+ if (criticalObs.length > 0) {
1087
+ hjLines.push('Obligations:');
1088
+ for (const o of criticalObs) hjLines.push(`- ${o.description}`);
1089
+ }
1090
+
1091
+ // Uncertainties the agent should verify
1092
+ const gaps = (hj.uncertainties || []).filter(u => u.confidence < 0.6);
1093
+ if (gaps.length > 0) {
1094
+ hjLines.push('Verify these (HEAD is uncertain):');
1095
+ for (const g of gaps) hjLines.push(`- ${g.claim} (confidence: ${Math.round(g.confidence * 100)}%) — ${g.wouldChangeIf}`);
1096
+ }
1097
+
1098
+ // Noticings the agent should be aware of
1099
+ const surfaced = hj.result?.surfaceNoticings || [];
1100
+ if (surfaced.length > 0) {
1101
+ hjLines.push('HEAD noticed:');
1102
+ for (const n of surfaced) hjLines.push(`- ${n.observation}`);
1103
+ }
1104
+
1105
+ hjLines.push('[/HEAD JUDGMENT]');
1106
+
1107
+ if (hjLines.length > 2) {
1108
+ headJudgmentBlock = hjLines.join('\n');
1109
+ }
1110
+ }
1111
+
1112
+ // Collaborative dispatch: when challenger is active or cross-review is
1113
+ // warranted, wrap the dispatch in a collaboration session so agents share
1114
+ // context and results chain forward.
1115
+ const collab = await getCollab();
1116
+ const useCollaboration = collab && (
1117
+ run.plan.useChallenger ||
1118
+ detectedRisk === 'high' || detectedRisk === 'critical'
1119
+ );
1120
+
1121
+ if (useCollaboration) {
1122
+ const session = collab.createSession(run.id, effectivePrompt, {
1123
+ crossReview: run.plan.useChallenger,
1124
+ });
1125
+
1126
+ // Register primary agent
1127
+ const primaryId = `primary-${run.id.slice(0, 8)}`;
1128
+ collab.registerAgent(session, primaryId, 'implementer', decision.provider, decision.model);
1129
+ collab.startAgent(session, primaryId);
1130
+
1131
+ // Inject collaboration context + HEAD judgment into prompt
1132
+ const collabContext = collab.buildAgentContext(session, primaryId);
1133
+ const promptParts = [collabContext, headJudgmentBlock, effectivePrompt].filter(Boolean);
1134
+ const collabPrompt = promptParts.join('\n\n');
1135
+
1136
+ run.result = await dispatch({
1137
+ decision,
1138
+ prompt: collabPrompt,
1139
+ files,
1140
+ cwd,
1141
+ dryRun: false,
1142
+ verbose,
1143
+ profile: run.context.profile,
1144
+ situationBrief: run.situationBrief,
1145
+ adaptation: run.adaptation,
1146
+ modelSuggestion: run.modelSuggestion,
1147
+ });
1148
+
1149
+ // Record agent completion
1150
+ collab.completeAgent(session, primaryId, run.result, run.result?.summary);
1151
+
1152
+ // Extract findings from result
1153
+ if (run.result?.filesChanged?.length) {
1154
+ for (const f of run.result.filesChanged) collab.trackFile(session, f, primaryId);
1155
+ }
1156
+
1157
+ // Cross-review: symmetric — works Claude→OpenAI and OpenAI→Claude
1158
+ const availableProviders = [];
1159
+ if (run.context?.profile?.providers?.claude?.enabled !== false) availableProviders.push('claude');
1160
+ if (run.context?.profile?.providers?.openai?.enabled && run.context?.profile?.providers?.openai?.plan) availableProviders.push('openai');
1161
+
1162
+ if (run.plan.useChallenger && run.plan.challengerModel && run.result?.status === 'completed') {
1163
+ const reviewSpec = collab.buildCrossReviewPrompt(session, primaryId, availableProviders);
1164
+ if (reviewSpec) {
1165
+ const reviewId = `reviewer-${run.id.slice(0, 8)}`;
1166
+ collab.registerAgent(session, reviewId, 'cross-reviewer', reviewSpec.provider, reviewSpec.model || run.plan.challengerModel);
1167
+ collab.startAgent(session, reviewId);
1168
+
1169
+ try {
1170
+ const reviewResult = await dispatch({
1171
+ decision: { provider: reviewSpec.provider, model: reviewSpec.model || run.plan.challengerModel, tier: 'search' },
1172
+ prompt: reviewSpec.prompt,
1173
+ files,
1174
+ cwd,
1175
+ dryRun: false,
1176
+ verbose,
1177
+ profile: run.context.profile,
1178
+ situationBrief: run.situationBrief,
1179
+ });
1180
+ collab.completeAgent(session, reviewId, reviewResult, reviewResult?.summary);
1181
+ } catch {
1182
+ collab.completeAgent(session, reviewId, { error: 'review dispatch failed' });
1183
+ }
1184
+ }
1185
+ }
1186
+
1187
+ // Synthesize and attach to run
1188
+ run.collaboration = collab.synthesize(session);
1189
+
1190
+ // Persist collaboration session
1191
+ try { collab.saveSession(session, cwd); } catch {}
1192
+ try { collab.persistEvents(session, cwd); } catch {}
1193
+ } else {
1194
+ const directPrompt = headJudgmentBlock
1195
+ ? `${headJudgmentBlock}\n\n${effectivePrompt}`
1196
+ : effectivePrompt;
1197
+
1198
+ run.result = await dispatch({
1199
+ decision,
1200
+ prompt: directPrompt,
1201
+ files,
1202
+ cwd,
1203
+ dryRun: false,
1204
+ verbose,
1205
+ profile: run.context.profile,
1206
+ situationBrief: run.situationBrief,
1207
+ adaptation: run.adaptation,
1208
+ modelSuggestion: run.modelSuggestion,
1209
+ });
1210
+ }
1020
1211
 
1021
1212
  // Update ledger task with result
1022
1213
  if (run.taskId) {
@@ -1154,6 +1345,32 @@ export async function runPipeline(trigger, prompt, options = {}) {
1154
1345
  return { success: false, gateFailure: 'outcome', reason: run.gates.outcome.reason, run };
1155
1346
  }
1156
1347
 
1348
+ // Provider-aware compaction survival — adapts format for Claude vs Codex.
1349
+ // Claude: tagged blocks that survive automatic context compression.
1350
+ // Codex: compact header block at prompt start (no native compaction).
1351
+ try {
1352
+ const { buildSurvivalBlock } = await import('./provider-context.mjs');
1353
+ const effectiveProvider = run.result?.provider || run.plan?.primaryProvider || 'claude';
1354
+ const survivalKit = buildSurvivalBlock(effectiveProvider, {
1355
+ activeTask: prompt.slice(0, 120),
1356
+ provider: effectiveProvider,
1357
+ model: run.result?.model || run.plan?.primaryModel,
1358
+ tier: run.plan?.tier,
1359
+ risk: run.context?.detection?.risk,
1360
+ filesInProgress: run.result?.filesChanged || [],
1361
+ decisions: run.collaboration?.decisions?.map(d => d.decision) || [],
1362
+ warnings: run.contradictions?.map(c => c.message) || [],
1363
+ routingRules: [
1364
+ `provider=${effectiveProvider}`,
1365
+ `model=${run.result?.model || run.plan?.primaryModel}`,
1366
+ `tier=${run.plan?.tier}`,
1367
+ ],
1368
+ });
1369
+ if (run.situationBrief) {
1370
+ run.situationBrief = `${survivalKit}\n\n${run.situationBrief}`;
1371
+ }
1372
+ } catch { /* non-blocking */ }
1373
+
1157
1374
  // Post-session receipt — capture what happened and seed next session's context
1158
1375
  try {
1159
1376
  const { generateReceipt } = await import('./receipt.mjs');
@@ -1177,11 +1394,13 @@ export async function runPipeline(trigger, prompt, options = {}) {
1177
1394
  }
1178
1395
  }
1179
1396
 
1180
- // Continuity handoff — generate and persist a compact receipt so the next
1181
- // session can resume seamlessly (survives context limits and crashes).
1397
+ // Provider-aware continuity handoff — tracks which provider ran the task
1398
+ // so the next session (on either provider) gets appropriate context.
1182
1399
  try {
1183
1400
  const { generateHandoff, saveHandoff, pruneHandoffs } = await import('./continuity.mjs');
1401
+ const { generateProviderHandoff } = await import('./provider-context.mjs');
1184
1402
  const handoffCwd = options.cwd || process.cwd();
1403
+ const handoffProvider = run.result?.provider || run.plan?.primaryProvider || 'claude';
1185
1404
 
1186
1405
  const sessionState = {
1187
1406
  taskDescription: prompt.slice(0, 200),
@@ -1195,7 +1414,7 @@ export async function runPipeline(trigger, prompt, options = {}) {
1195
1414
  }] : [],
1196
1415
  unresolved: run.contradictions?.filter(c => c.severity !== 'block').map(c => c.message) || [],
1197
1416
  routingHistory: {
1198
- lastProvider: run.result?.provider || run.plan?.primaryProvider || null,
1417
+ lastProvider: handoffProvider,
1199
1418
  lastModel: run.result?.model || run.plan?.primaryModel || null,
1200
1419
  failedProviders: run.result?.error ? [run.plan?.primaryProvider].filter(Boolean) : [],
1201
1420
  },
@@ -1205,9 +1424,10 @@ export async function runPipeline(trigger, prompt, options = {}) {
1205
1424
  : `retry: ${prompt.slice(0, 100)}`,
1206
1425
  };
1207
1426
 
1208
- const handoff = generateHandoff(sessionState);
1427
+ // Save both standard + provider-aware handoff
1428
+ const handoff = generateProviderHandoff(sessionState, handoffProvider);
1209
1429
  saveHandoff(handoff, handoffCwd);
1210
- pruneHandoffs(handoffCwd, 10); // keep last 10 handoffs
1430
+ pruneHandoffs(handoffCwd, 10);
1211
1431
  } catch {
1212
1432
  // continuity is best-effort — never block pipeline completion
1213
1433
  }
@@ -1227,6 +1447,8 @@ export async function runPipeline(trigger, prompt, options = {}) {
1227
1447
  return {
1228
1448
  success: true,
1229
1449
  run,
1450
+ // HEAD cognitive judgment
1451
+ headJudgment: run.headJudgment,
1230
1452
  // Intelligence fields for callers to inspect
1231
1453
  projectBrief: run.projectBrief,
1232
1454
  contradictions: run.contradictions,
@@ -1237,6 +1459,8 @@ export async function runPipeline(trigger, prompt, options = {}) {
1237
1459
  decisionPreflight: run.decisionPreflight,
1238
1460
  // Execution safety
1239
1461
  checkpoint: run.checkpoint,
1462
+ // Collaboration
1463
+ collaboration: run.collaboration,
1240
1464
  // Legacy compatibility
1241
1465
  plan: run.plan,
1242
1466
  result: run.result,