dual-brain 0.2.8 → 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/bin/dual-brain.mjs +208 -42
- package/package.json +9 -2
- package/src/agents/registry.mjs +405 -0
- package/src/collaboration.mjs +545 -0
- package/src/detect.mjs +73 -1
- package/src/dispatch.mjs +47 -5
- package/src/head.mjs +705 -263
- package/src/pipeline.mjs +387 -163
- package/src/profile.mjs +82 -1
- package/src/provider-context.mjs +257 -0
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:
|
|
671
|
-
|
|
672
|
-
//
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
|
732
|
-
const
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
740
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
|
761
|
-
//
|
|
745
|
+
} catch {
|
|
746
|
+
// head.mjs unavailable — continue degraded (no cognitive layer)
|
|
762
747
|
}
|
|
763
748
|
|
|
764
|
-
//
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
//
|
|
756
|
+
// Session history — always load (lightweight, index-only)
|
|
776
757
|
try {
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
//
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
//
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
//
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
-
//
|
|
1181
|
-
//
|
|
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:
|
|
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
|
-
|
|
1427
|
+
// Save both standard + provider-aware handoff
|
|
1428
|
+
const handoff = generateProviderHandoff(sessionState, handoffProvider);
|
|
1209
1429
|
saveHandoff(handoff, handoffCwd);
|
|
1210
|
-
pruneHandoffs(handoffCwd, 10);
|
|
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,
|