dual-brain 0.1.22 → 0.2.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.
package/src/pipeline.mjs CHANGED
@@ -28,6 +28,12 @@ export function createPipelineRun(trigger = '', prompt = '') {
28
28
  trigger,
29
29
  prompt,
30
30
 
31
+ // Phase 0: Intelligence
32
+ projectBrief: null, // from deriveProjectState
33
+ taskBrief: null, // from deriveTaskContext
34
+ contradictions: [], // from detectContradictions
35
+ situationBrief: null, // formatted string from formatBrief
36
+
31
37
  // Phase 1: Context
32
38
  context: null,
33
39
  failureHistory: null, // result of checkFailureHistory — even empty counts as "queried"
@@ -54,6 +60,22 @@ export function createPipelineRun(trigger = '', prompt = '') {
54
60
  // Phase 5: Outcome
55
61
  outcome: null,
56
62
 
63
+ // Ledger + calibration
64
+ taskId: null, // ledger task ID for this run
65
+ openTasks: [], // pending tasks from ledger
66
+ calibration: null, // user calibration state
67
+ adaptation: null, // behavior adaptation from calibration
68
+
69
+ // Prompt intelligence + environment
70
+ promptAnalysis: null, // from analyzePrompt
71
+ enrichedPrompt: null, // from enrichPrompt
72
+ environment: null, // from scanEnvironment
73
+ modelSuggestion: null, // from suggestModel
74
+
75
+ // Think-engine fields
76
+ thinkResult: null, // from think-engine
77
+ decisionPreflight: null, // from lookupDecision
78
+
57
79
  completedAt: null,
58
80
  };
59
81
  }
@@ -367,7 +389,7 @@ export function buildExecutionPlan(contextPack, trigger, options = {}) {
367
389
  effort: depthToEffort[reasoningDepth] ?? detection.effort,
368
390
  };
369
391
 
370
- const decision = decideRoute({ profile, detection: detectionWithDepth, cwd: contextPack.cwd });
392
+ const decision = decideRoute({ profile, detection: detectionWithDepth, cwd: contextPack.cwd, thinkResult: options.thinkResult });
371
393
 
372
394
  // Resolve full model ID for display (mirrors dispatch.mjs CLAUDE_MODEL_IDS)
373
395
  const CLAUDE_MODEL_IDS = { opus: 'claude-opus-4-6', sonnet: 'claude-sonnet-4-6', haiku: 'claude-haiku-4-5-20251001' };
@@ -633,15 +655,164 @@ export async function runPipeline(trigger, prompt, options = {}) {
633
655
  const run = createPipelineRun(trigger, prompt);
634
656
 
635
657
  try {
658
+ // ── Phase 0: Situational awareness ───────────────────────────────────────
659
+
660
+ try {
661
+ const { deriveProjectState, deriveTaskContext, detectContradictions, formatBrief } = await import('./intelligence.mjs');
662
+ run.projectBrief = await deriveProjectState(options.cwd || process.cwd());
663
+ run.taskBrief = deriveTaskContext(prompt, options.recentEvents || []);
664
+ run.situationBrief = formatBrief(run.projectBrief, run.taskBrief);
665
+ } catch (e) {
666
+ // intelligence module not available — continue without it (degraded)
667
+ }
668
+
669
+ // Doctor: discover capabilities (cached per process via discovery log)
670
+ try {
671
+ const { discover, verifyAll } = await import('./doctor.mjs');
672
+ const doctorCwd = options.cwd || process.cwd();
673
+ discover(doctorCwd); // writes to .dual-brain/discoveries.jsonl (idempotent)
674
+ verifyAll(doctorCwd); // writes to .dual-brain/verifications.jsonl
675
+ } catch (e) {
676
+ // doctor not available — non-blocking
677
+ }
678
+
679
+ // Ledger: check open tasks + create task for this run
680
+ try {
681
+ const { getOpenTasks, createTask, reconcile } = await import('./ledger.mjs');
682
+ const cwd = options.cwd || process.cwd();
683
+
684
+ // Check for stale tasks on session start
685
+ run.openTasks = getOpenTasks(cwd);
686
+ const staleTasks = reconcile(cwd);
687
+
688
+ // Create a ledger task for this pipeline run
689
+ const task = createTask({
690
+ intent: prompt,
691
+ owner: 'head',
692
+ priority: run.projectBrief?.recentFailures?.length > 0 ? 'high' : 'medium',
693
+ files: options.files || []
694
+ }, cwd);
695
+ run.taskId = task.id;
696
+ } catch (e) {
697
+ // ledger not available — continue degraded
698
+ }
699
+
700
+ // Append open tasks to situation brief if any exist
701
+ if (run.openTasks.length > 0) {
702
+ const preview = run.openTasks.slice(0, 3).map(t => t.intent).join(', ');
703
+ const pendingLine = `PENDING TASKS: ${run.openTasks.length} open (${preview})`;
704
+ run.situationBrief = run.situationBrief
705
+ ? `${run.situationBrief}\n${pendingLine}`
706
+ : pendingLine;
707
+ }
708
+
709
+ // Calibration: analyze user input and adapt
710
+ try {
711
+ const { analyzeInput, getAdaptation, detectCorrection, updateCalibration } = await import('./calibration.mjs');
712
+ const { getProjectState, updateProject } = await import('./living-docs.mjs');
713
+ const cwd = options.cwd || process.cwd();
714
+
715
+ const projectState = getProjectState(cwd);
716
+ const currentCal = projectState?.project?.userCalibration || { specificity: 3, corrections: 3, autonomy: 3, interactions: 0 };
717
+ const isCorrection = detectCorrection(prompt);
718
+
719
+ run.calibration = updateCalibration(currentCal, prompt, isCorrection);
720
+ run.adaptation = getAdaptation(run.calibration);
721
+
722
+ // Persist updated calibration
723
+ updateProject({ userCalibration: run.calibration }, cwd);
724
+ } catch (e) {
725
+ // calibration not available — continue degraded
726
+ }
727
+
728
+ // Environment awareness
729
+ try {
730
+ const { scanEnvironment, getCapabilitySummary } = await import('./awareness.mjs');
731
+ run.environment = scanEnvironment(cwd);
732
+
733
+ // Add capabilities to situation brief
734
+ if (run.situationBrief && run.environment) {
735
+ const caps = getCapabilitySummary(run.environment);
736
+ if (caps.length > 0) {
737
+ run.situationBrief += '\nCAPABILITIES: ' + caps.join(', ');
738
+ }
739
+ }
740
+ } catch (e) {
741
+ // awareness not available
742
+ }
743
+
744
+ // Knowledge preflight — check if we already know the answer
745
+ try {
746
+ const { lookupDecision, triageQuestion } = await import('./think-engine.mjs');
747
+ const cwd = options.cwd || process.cwd();
748
+
749
+ run.decisionPreflight = lookupDecision(prompt, options.tags || [], cwd);
750
+
751
+ // If exact reuse found, we can short-circuit
752
+ if (run.decisionPreflight.recommendation === 'reuse' && run.decisionPreflight.candidates[0]) {
753
+ // Add cached decision info to situation brief
754
+ if (run.situationBrief) {
755
+ run.situationBrief += '\nCACHED DECISION: Found prior decision with ' +
756
+ Math.round(run.decisionPreflight.candidates[0].relevance * 100) + '% relevance';
757
+ }
758
+ }
759
+
760
+ // Triage to determine thinking tier
761
+ const triage = triageQuestion(prompt, run.projectBrief, run.decisionPreflight);
762
+ run.thinkResult = { tier: triage.recommendedTier, estimatedTokens: triage.estimatedTokens, triage };
763
+
764
+ // Add to situation brief
765
+ if (run.situationBrief) {
766
+ run.situationBrief += '\nTHINK TIER: ' + triage.recommendedTier + ' (' + triage.estimatedTokens + ' tokens est.)';
767
+ }
768
+ } catch (e) {
769
+ // think-engine not available
770
+ }
771
+
772
+ // Prompt intelligence
773
+ try {
774
+ const { analyzePrompt, enrichPrompt, shouldBlock, getBlockReason } = await import('./prompt-intel.mjs');
775
+
776
+ run.promptAnalysis = analyzePrompt(prompt, run.projectBrief, run.calibration);
777
+
778
+ // Hard block on dangerous intent
779
+ if (shouldBlock(run.promptAnalysis)) {
780
+ const reason = getBlockReason(run.promptAnalysis);
781
+ if (run.taskId) {
782
+ try {
783
+ const { failTask } = await import('./ledger.mjs');
784
+ failTask(run.taskId, 'Blocked by risk detection: ' + reason, cwd);
785
+ } catch (e) {}
786
+ }
787
+ run.completedAt = Date.now();
788
+ return {
789
+ success: false,
790
+ gateFailure: 'risk',
791
+ reason: 'Prompt blocked: ' + reason,
792
+ promptAnalysis: run.promptAnalysis,
793
+ run
794
+ };
795
+ }
796
+
797
+ // Enrich prompt if intervention says so
798
+ if (run.promptAnalysis.intervention === 'silent_enrich' || run.promptAnalysis.intervention === 'confirm_rewrite') {
799
+ run.enrichedPrompt = enrichPrompt(prompt, run.projectBrief, run.promptAnalysis);
800
+ }
801
+ } catch (e) {
802
+ // prompt-intel not available
803
+ }
804
+
636
805
  // ── Phase 1: Context ──────────────────────────────────────────────────────
637
806
 
807
+ const effectivePrompt = run.enrichedPrompt || prompt;
808
+
638
809
  // Build context pack
639
- run.context = await buildContextPack(prompt, files, cwd);
810
+ run.context = await buildContextPack(effectivePrompt, files, cwd);
640
811
 
641
812
  // Query failure history (must happen before context gate)
642
813
  try {
643
814
  const { checkFailureHistory } = await import('./failure-memory.mjs');
644
- run.failureHistory = await checkFailureHistory(prompt, files, cwd);
815
+ run.failureHistory = await checkFailureHistory(effectivePrompt, files, cwd);
645
816
  } catch {
646
817
  // failure-memory.mjs unavailable — set to empty result so gate still passes
647
818
  run.failureHistory = { hasPriorFailures: false, failureCount: 0, lastFailure: null, escalation: { recommended: false } };
@@ -650,7 +821,7 @@ export async function runPipeline(trigger, prompt, options = {}) {
650
821
  // Query relevant outcomes (must happen before context gate)
651
822
  try {
652
823
  const { getRelevantOutcomes } = await import('./outcome.mjs');
653
- run.priorOutcomes = await getRelevantOutcomes(prompt, files, cwd);
824
+ run.priorOutcomes = await getRelevantOutcomes(effectivePrompt, files, cwd);
654
825
  } catch {
655
826
  // outcome.mjs unavailable — set to empty array so gate still passes
656
827
  run.priorOutcomes = [];
@@ -664,12 +835,62 @@ export async function runPipeline(trigger, prompt, options = {}) {
664
835
 
665
836
  // ── Phase 2: Plan ─────────────────────────────────────────────────────────
666
837
 
667
- run.plan = buildExecutionPlan(run.context, trigger, { forceDepth, forceChallenger });
838
+ run.plan = buildExecutionPlan(run.context, trigger, { forceDepth, forceChallenger, thinkResult: run.thinkResult });
839
+
840
+ // Model intelligence
841
+ try {
842
+ const { suggestModel, getRegistryAge } = await import('./models.mjs');
843
+ const availableProviders = [];
844
+ if (run.environment?.secrets?.ANTHROPIC_API_KEY || run.environment?.claudeCode?.isInsideClaude) availableProviders.push('anthropic');
845
+ if (run.environment?.secrets?.OPENAI_API_KEY) availableProviders.push('openai');
846
+
847
+ const intent = run.promptAnalysis?.intent?.type || 'execute';
848
+ const risk = run.plan?.risk || 'medium';
849
+ const complexity = run.plan?.complexity || 'medium';
850
+
851
+ run.modelSuggestion = suggestModel(intent, risk, complexity, availableProviders);
852
+
853
+ // Warn if model registry is stale
854
+ const age = getRegistryAge();
855
+ if (age > 30 && run.situationBrief) {
856
+ run.situationBrief += '\nWARNING: Model registry is ' + age + ' days old';
857
+ }
858
+ } catch (e) {
859
+ // models not available
860
+ }
668
861
 
669
862
  if (verbose || dryRun) {
670
863
  log(formatExecutionPlan(run.plan));
671
864
  }
672
865
 
866
+ // Contradiction detection
867
+ if (run.projectBrief && run.plan) {
868
+ try {
869
+ const { detectContradictions } = await import('./intelligence.mjs');
870
+ const planForCheck = {
871
+ description: run.plan.description || prompt,
872
+ targetFiles: run.plan.targetFiles || run.plan.files || [],
873
+ assumptions: run.plan.assumptions || {}
874
+ };
875
+ run.contradictions = detectContradictions(run.projectBrief, run.taskBrief, planForCheck);
876
+
877
+ // Any blocking contradiction fails the pipeline
878
+ const blockers = run.contradictions.filter(c => c.severity === 'block');
879
+ if (blockers.length > 0) {
880
+ run.completedAt = Date.now();
881
+ return {
882
+ success: false,
883
+ gateFailure: 'contradiction',
884
+ reason: blockers.map(b => b.message).join('; '),
885
+ contradictions: blockers,
886
+ run
887
+ };
888
+ }
889
+ } catch (e) {
890
+ // contradiction detection failed — continue (degraded)
891
+ }
892
+ }
893
+
673
894
  // Gate 2: Planning gate
674
895
  if (!runGate(run, 'planning', planningGate)) {
675
896
  run.completedAt = Date.now();
@@ -684,8 +905,21 @@ export async function runPipeline(trigger, prompt, options = {}) {
684
905
 
685
906
  if (dryRun) {
686
907
  run.completedAt = Date.now();
687
- // Return legacy-compatible shape for dry-run callers
688
- return { plan: run.plan, result: null, verification: null, run };
908
+ // Return legacy-compatible shape plus intelligence fields for dry-run callers
909
+ return {
910
+ plan: run.plan,
911
+ result: null,
912
+ verification: null,
913
+ run,
914
+ // Intelligence fields (mirrors full execution return)
915
+ projectBrief: run.projectBrief,
916
+ contradictions: run.contradictions,
917
+ promptAnalysis: run.promptAnalysis,
918
+ environment: run.environment,
919
+ modelSuggestion: run.modelSuggestion,
920
+ thinkResult: run.thinkResult,
921
+ decisionPreflight: run.decisionPreflight,
922
+ };
689
923
  }
690
924
 
691
925
  // Gate 4: Execution gate (cleared to work?)
@@ -705,14 +939,58 @@ export async function runPipeline(trigger, prompt, options = {}) {
705
939
 
706
940
  run.result = await dispatch({
707
941
  decision,
708
- prompt,
942
+ prompt: effectivePrompt,
709
943
  files,
710
944
  cwd,
711
945
  dryRun: false,
712
946
  verbose,
713
947
  profile: run.context.profile,
948
+ situationBrief: run.situationBrief,
949
+ adaptation: run.adaptation,
950
+ modelSuggestion: run.modelSuggestion,
714
951
  });
715
952
 
953
+ // Update ledger task with result
954
+ if (run.taskId) {
955
+ const { updateTask, failTask } = await import('./ledger.mjs');
956
+ const ledgerCwd = options.cwd || process.cwd();
957
+
958
+ if (run.result && !run.result.error) {
959
+ // updateTask throws if proof/result is missing — let that propagate so
960
+ // the outcome gate catches it rather than silently succeeding.
961
+ updateTask(run.taskId, {
962
+ status: 'done',
963
+ result: typeof run.result === 'string' ? run.result : JSON.stringify(run.result).slice(0, 500),
964
+ proof: run.verification ? 'Pipeline verification passed' : 'Execution completed',
965
+ files: run.result.filesChanged || run.plan?.targetFiles || []
966
+ }, ledgerCwd);
967
+ } else {
968
+ try {
969
+ failTask(run.taskId, run.result?.error || 'Pipeline execution failed', ledgerCwd);
970
+ } catch (e) {
971
+ // failTask failure is non-blocking
972
+ }
973
+ }
974
+ }
975
+
976
+ // Record action in living docs
977
+ try {
978
+ const { appendAction } = await import('./living-docs.mjs');
979
+ const cwd = options.cwd || process.cwd();
980
+
981
+ appendAction({
982
+ type: trigger || 'task',
983
+ intent: prompt,
984
+ status: (run.result && !run.result.error) ? 'done' : 'failed',
985
+ owner: 'head',
986
+ files: run.result?.filesChanged || run.plan?.targetFiles || [],
987
+ proof: run.verification ? JSON.stringify(run.verification).slice(0, 200) : null,
988
+ result: typeof run.result === 'string' ? run.result.slice(0, 300) : null
989
+ }, cwd);
990
+ } catch (e) {
991
+ // living docs not available — non-blocking
992
+ }
993
+
716
994
  // ── Phase 4: Verification ─────────────────────────────────────────────────
717
995
 
718
996
  run.verification = await verify(run.result, run.plan, cwd);
@@ -726,6 +1004,62 @@ export async function runPipeline(trigger, prompt, options = {}) {
726
1004
  _incrementFailureCache(prompt);
727
1005
  }
728
1006
 
1007
+ // Track cost after verification (fail-silent — advisory only)
1008
+ try {
1009
+ const { trackCost } = await import('./cost-tracker.mjs');
1010
+ const tokensEstimated =
1011
+ (run.result?.usage?.inputTokens ?? run.result?.tokensUsed?.input ?? 0) +
1012
+ (run.result?.usage?.outputTokens ?? run.result?.tokensUsed?.output ?? 0);
1013
+ trackCost({
1014
+ action: trigger || 'execute',
1015
+ model: run.result?.model ?? run.plan?._decision?.model ?? 'default',
1016
+ tier: run.plan?.tier ?? 'standard',
1017
+ tokensEstimated,
1018
+ wasCacheHit: false,
1019
+ tokensSaved: 0,
1020
+ }, cwd);
1021
+ } catch (e) {
1022
+ // cost-tracker not available — non-blocking
1023
+ }
1024
+
1025
+ // Living docs: update state after significant execution (fail-silent — advisory only)
1026
+ try {
1027
+ const { updateState } = await import('./living-docs.mjs');
1028
+ const docsCwd = options.cwd || process.cwd();
1029
+ const successFlag = run.result && !run.result.error && run.verification.ok;
1030
+ const stateEntry =
1031
+ `# Current State\n\nLast run: ${new Date().toISOString()}\n` +
1032
+ `Task: ${prompt.slice(0, 120)}\n` +
1033
+ `Status: ${successFlag ? 'completed' : 'failed'}\n` +
1034
+ `Tier: ${run.plan?.tier ?? 'unknown'}\n` +
1035
+ `Model: ${run.plan?.primaryModel ?? 'unknown'}\n`;
1036
+ updateState(stateEntry, docsCwd);
1037
+ } catch (e) {
1038
+ // living-docs not available — non-blocking
1039
+ }
1040
+
1041
+ // Doctor: record learning from this execution outcome (fail-silent)
1042
+ try {
1043
+ const { recordLearning } = await import('./doctor.mjs');
1044
+ const doctorCwd = options.cwd || process.cwd();
1045
+ const successFlag = run.result && !run.result.error && run.verification.ok;
1046
+ recordLearning({
1047
+ taskType: run.context?.detection?.intent ?? 'unknown',
1048
+ prompt,
1049
+ model: run.result?.model ?? run.plan?._decision?.model ?? '',
1050
+ provider: run.result?.provider ?? run.plan?.primaryProvider ?? '',
1051
+ tier: run.plan?.tier ?? '',
1052
+ reasoningDepth: run.plan?.reasoningDepth ?? 'low',
1053
+ wasEnriched: !!run.enrichedPrompt,
1054
+ wasDualBrain: !!(run.plan?.useChallenger && run.plan?.challengerModel),
1055
+ success: successFlag,
1056
+ duration: run.completedAt ? (Date.now() - run.startedAt) : 0,
1057
+ filesChanged: (run.result?.filesChanged ?? []).length,
1058
+ }, doctorCwd);
1059
+ } catch (e) {
1060
+ // doctor not available — non-blocking
1061
+ }
1062
+
729
1063
  // ── Phase 5: Outcome ──────────────────────────────────────────────────────
730
1064
 
731
1065
  await recordOutcomeSafe(run);
@@ -736,6 +1070,23 @@ export async function runPipeline(trigger, prompt, options = {}) {
736
1070
  return { success: false, gateFailure: 'outcome', reason: run.gates.outcome.reason, run };
737
1071
  }
738
1072
 
1073
+ // Persist decision for future recall
1074
+ if (run.result && !run.result?.error) {
1075
+ try {
1076
+ const { persistDecision } = await import('./think-engine.mjs');
1077
+ const cwd = options.cwd || process.cwd();
1078
+ persistDecision(
1079
+ prompt,
1080
+ typeof run.result === 'string' ? run.result : JSON.stringify(run.result).slice(0, 1000),
1081
+ run.thinkResult?.tier || 'standard',
1082
+ { tags: options.tags || [], projectBrief: run.projectBrief },
1083
+ cwd
1084
+ );
1085
+ } catch (e) {
1086
+ // persist failed — non-blocking
1087
+ }
1088
+ }
1089
+
739
1090
  } catch (err) {
740
1091
  log(`[pipeline] error in pipeline step: ${err.message}`);
741
1092
  run.result = { status: 'error', error: err.message };
@@ -751,6 +1102,14 @@ export async function runPipeline(trigger, prompt, options = {}) {
751
1102
  return {
752
1103
  success: true,
753
1104
  run,
1105
+ // Intelligence fields for callers to inspect
1106
+ projectBrief: run.projectBrief,
1107
+ contradictions: run.contradictions,
1108
+ promptAnalysis: run.promptAnalysis,
1109
+ environment: run.environment,
1110
+ modelSuggestion: run.modelSuggestion,
1111
+ thinkResult: run.thinkResult,
1112
+ decisionPreflight: run.decisionPreflight,
754
1113
  // Legacy compatibility
755
1114
  plan: run.plan,
756
1115
  result: run.result,