dual-brain 7.1.22 → 7.1.24

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.
@@ -1483,9 +1483,9 @@ function detectInterruptedWork(sessions, cwd) {
1483
1483
  * Shows: "● Claude ● OpenAI ⚖️ Balanced"
1484
1484
  * Uses ANSI color codes for the dots — no dollar amounts or usage bars.
1485
1485
  */
1486
- function buildProviderStatusLine(profile, auth, maxWidth = 54) {
1487
- const GREEN = '●';
1488
- const RED = '●';
1486
+ function buildProviderStatusLine(profile, auth) {
1487
+ const GREEN = '\x1b[32m●\x1b[0m';
1488
+ const RED = '\x1b[31m●\x1b[0m';
1489
1489
 
1490
1490
  const claudeDot = auth.claude.found ? GREEN : RED;
1491
1491
  const openaiDot = auth.openai.found ? GREEN : RED;
@@ -1498,30 +1498,11 @@ function buildProviderStatusLine(profile, auth, maxWidth = 54) {
1498
1498
  'solo-claude': '⚡ Fast',
1499
1499
  'solo-openai': '⚡ Fast',
1500
1500
  };
1501
- const WORK_STYLE_TIPS = {
1502
- 'auto': 'adapts routing by task risk',
1503
- 'cost-saver': 'single model, minimal reviews',
1504
- 'balanced': 'smart routing, reviews when needed',
1505
- 'quality-first': 'dual-brain on everything important',
1506
- 'solo-claude': 'Claude only, no GPT dispatch',
1507
- 'solo-openai': 'OpenAI only, no Claude dispatch',
1508
- };
1509
1501
  const bias = profile?.bias || profile?.mode || 'balanced';
1510
1502
  const label = WORK_STYLE_LABELS[bias] || '⚖️ Balanced';
1511
- const fullTip = WORK_STYLE_TIPS[bias] || 'smart routing, reviews when needed';
1512
-
1513
- // Trim tip to fit within box width (measure visible chars: strip ANSI + variation selectors)
1514
- const labelPlain = label.replace(/[︀-️]/g, '').replace(/[[0-9;]*m/g, '');
1515
- const prefixLen = ('● Claude ● OpenAI ' + labelPlain + ' — ').length;
1516
- const tipMax = maxWidth - prefixLen;
1517
- const tip = tipMax >= 6
1518
- ? (fullTip.length > tipMax ? fullTip.slice(0, tipMax - 1) + '…' : fullTip)
1519
- : '';
1520
1503
 
1521
- const suffix = tip ? ` — ${tip}` : '';
1522
- return `${claudeDot} Claude ${openaiDot} OpenAI ${label}${suffix}`;
1504
+ return `${claudeDot} Claude ${openaiDot} OpenAI ${label}`;
1523
1505
  }
1524
-
1525
1506
  /**
1526
1507
  * Render a box row padded to inner width W (stripping ANSI for length calculation).
1527
1508
  * Returns a string like: "│ content padded to W │"
@@ -1609,25 +1590,6 @@ async function mainScreen(rl, ask) {
1609
1590
 
1610
1591
  const row = (content) => makeBoxRow(content, W);
1611
1592
 
1612
- // ── Header: one line above the box ────────────────────────────────────────
1613
- process.stdout.write(`\n🧠 dual-brain v${version}\n`);
1614
- {
1615
- let gitName = '';
1616
- try {
1617
- const { execSync } = await import('node:child_process');
1618
- gitName = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
1619
- } catch { /* ignore */ }
1620
- if (gitName) {
1621
- const hour = new Date().getHours();
1622
- let greet;
1623
- if (hour >= 5 && hour <= 11) greet = 'Good morning';
1624
- else if (hour >= 12 && hour <= 16) greet = 'Good afternoon';
1625
- else if (hour >= 17 && hour <= 21) greet = 'Good evening';
1626
- else greet = 'Late night';
1627
- process.stdout.write(`\x1b[2m${greet}, ${gitName}\x1b[0m\n`);
1628
- }
1629
- }
1630
-
1631
1593
  // ── Continuation card (interrupted work) ─────────────────────────────────
1632
1594
  if (interrupted) {
1633
1595
  const ctop = `┌${'─'.repeat(boxW - 2)}┐`;
@@ -1706,15 +1668,74 @@ async function mainScreen(rl, ask) {
1706
1668
  // 's' → fall through to normal dashboard
1707
1669
  }
1708
1670
 
1709
- // ── Status section ────────────────────────────────────────────────────────
1710
- const providerLine = buildProviderStatusLine(profile, auth, W);
1671
+ // ── Box 1 — Header row data ─────────────────────────────────────────────
1672
+ const providerLine = buildProviderStatusLine(profile, auth);
1711
1673
 
1712
- const statusRows = [row(providerLine)];
1713
- if (dtVersion) {
1714
- statusRows.push(row(`\x1b[2m📦 replit-tools v${dtVersion}\x1b[0m`));
1674
+ // ── Box 2 — Workspace: gather git data ───────────────────────────────────
1675
+ let gitBranch = 'unknown';
1676
+ let gitUncommitted = 0;
1677
+ let gitAheadCount = 0;
1678
+ let gitLastMsg = '';
1679
+ let gitLastAgo = '';
1680
+
1681
+ try {
1682
+ gitBranch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', {
1683
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1684
+ }).trim() || 'unknown';
1685
+ } catch {}
1686
+
1687
+ try {
1688
+ const status = execSync('git status --porcelain 2>/dev/null', {
1689
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1690
+ });
1691
+ gitUncommitted = status.trim().split('\n').filter(Boolean).length;
1692
+ } catch {}
1693
+
1694
+ try {
1695
+ const aheadOut = execSync('git rev-list @{u}..HEAD 2>/dev/null | wc -l', {
1696
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1697
+ });
1698
+ gitAheadCount = parseInt(aheadOut.trim(), 10) || 0;
1699
+ } catch {}
1700
+
1701
+ try {
1702
+ const logOut = execSync('git log -1 --format="%s|%ct" 2>/dev/null', {
1703
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1704
+ }).trim();
1705
+ if (logOut) {
1706
+ const [msg, ts] = logOut.split('|');
1707
+ gitLastMsg = (msg || '').slice(0, 38);
1708
+ const ageMs = Date.now() - (parseInt(ts, 10) * 1000);
1709
+ const ageMin = Math.floor(ageMs / 60000);
1710
+ if (ageMin < 60) gitLastAgo = `${ageMin}m ago`;
1711
+ else if (ageMin < 1440) gitLastAgo = `${Math.floor(ageMin / 60)}h ago`;
1712
+ else gitLastAgo = `${Math.floor(ageMin / 1440)}d ago`;
1713
+ }
1714
+ } catch {}
1715
+
1716
+ // ── Box 2 rows ────────────────────────────────────────────────────────────
1717
+ const uncommittedPart = gitUncommitted > 0 ? ` · ${gitUncommitted} uncommitted` : '';
1718
+ const aheadPart = gitAheadCount > 0 ? ` · ${gitAheadCount} ahead` : '';
1719
+ const workspaceLine1 = `${gitBranch}${uncommittedPart}${aheadPart}`;
1720
+ const workspaceLine2 = gitLastMsg
1721
+ ? `Last: ${gitLastMsg} (${gitLastAgo})`
1722
+ : '';
1723
+
1724
+ // Open PRs
1725
+ const repoState = detectRepoState(cwd);
1726
+ const openPRs = await detectOpenPRs(cwd);
1727
+
1728
+ const workspaceRows = [row(workspaceLine1)];
1729
+ if (workspaceLine2) workspaceRows.push(row(workspaceLine2));
1730
+ if (openPRs.length > 0) {
1731
+ workspaceRows.push(row(`${openPRs.length} open PR${openPRs.length === 1 ? '' : 's'}`));
1715
1732
  }
1716
1733
 
1717
- // ── Observer observations (top 2, high priority first) ───────────────────
1734
+ // ── Box 3 Awareness: observer + roadmap + risk ──────────────────────────
1735
+ let awarenessLine1 = '\x1b[2m💡\x1b[0m Ready to work';
1736
+ let awarenessLine2 = '\x1b[2m📋 No roadmap yet\x1b[0m';
1737
+ let awarenessLine3 = '\x1b[32m✓\x1b[0m No risk flags';
1738
+
1718
1739
  let quickObservations = [];
1719
1740
  try {
1720
1741
  const observerMod = await import('../src/observer.mjs');
@@ -1724,64 +1745,39 @@ async function mainScreen(rl, ask) {
1724
1745
  const sorted = [...quickState.observations].sort(
1725
1746
  (a, b) => (PRIO[a.priority] ?? 2) - (PRIO[b.priority] ?? 2)
1726
1747
  );
1727
- quickObservations = sorted.slice(0, 2);
1728
- for (const obs of quickObservations) {
1729
- let prefix;
1730
- if (obs.priority === 'high') prefix = '🔴';
1731
- else if (obs.priority === 'medium') prefix = '🟡';
1732
- else prefix = '\x1b[2m💡\x1b[0m';
1733
- statusRows.push(row(`${prefix} ${obs.message}`));
1748
+ quickObservations = sorted.slice(0, 3);
1749
+ const top = quickObservations[0];
1750
+ if (top) {
1751
+ const prefix = top.priority === 'high' ? '🔴' : top.priority === 'medium' ? '🟡' : '\x1b[2m💡\x1b[0m';
1752
+ awarenessLine1 = `${prefix} ${top.message}`;
1753
+ }
1754
+ const hasHighRisk = quickObservations.some(o => o.priority === 'high');
1755
+ if (hasHighRisk) {
1756
+ awarenessLine3 = '\x1b[31m⚠\x1b[0m Risk flags detected — run: dual-brain review';
1734
1757
  }
1735
1758
  }
1736
- } catch { /* non-fatal — module may not exist yet */ }
1737
-
1738
- // ── Action cards (git state + open PRs) ──────────────────────────────────
1739
- const repoState = detectRepoState(cwd);
1740
- const openPRs = await detectOpenPRs(cwd);
1741
- const actionRows = buildActionRows(repoState, row, openPRs);
1742
-
1743
- // ── High-priority observer action cards ───────────────────────────────────
1744
- if (quickObservations.some(o => o.priority === 'high')) {
1745
- const DIM = '\x1b[2m';
1746
- const RESET = '\x1b[0m';
1747
- actionRows.push(row(`${DIM}[r] Security review [t] Run tests [c] Commit${RESET}`));
1748
- }
1759
+ } catch { /* non-fatal — observer may not exist */ }
1749
1760
 
1750
- // ── Related sessions hint (only when no continuation card is showing) ─────
1751
- if (!interrupted && recentSessions.length > 0) {
1752
- try {
1753
- const { findRelatedSessions } = await import('../src/session.mjs');
1754
- const mostRecent = recentSessions[0];
1755
- // Build a pseudo-prompt from the most recent session's name/objective
1756
- const recentPrompt = mostRecent.name || '';
1757
- // Load session index to get files for the most recent session
1758
- const indexPath = join(cwd, '.dualbrain', 'session-index.json');
1759
- let recentFiles = [];
1760
- try {
1761
- const idx = JSON.parse(readFileSync(indexPath, 'utf8'));
1762
- recentFiles = idx[mostRecent.id]?.files || [];
1763
- } catch {}
1764
- const related = findRelatedSessions(recentPrompt, recentFiles, cwd);
1765
- if (related.length > 0) {
1766
- const relAgeLabel = (isoDate) => {
1767
- if (!isoDate) return '';
1768
- const diff = Date.now() - Date.parse(isoDate);
1769
- const days = Math.floor(diff / 86400000);
1770
- const hours = Math.floor(diff / 3600000);
1771
- if (days >= 1) return `${days}d`;
1772
- return `${hours}h ago`;
1773
- };
1774
- const relatedParts = related.slice(0, 2).map(r => {
1775
- const age = relAgeLabel(r.date);
1776
- return age ? `${r.smartName} (${age})` : r.smartName;
1777
- });
1778
- const DIM = '\x1b[2m';
1779
- const RESET = '\x1b[0m';
1780
- actionRows.push(row(`${DIM}📎 Related: ${relatedParts.join(', ')}${RESET}`));
1761
+ // Try roadmap file
1762
+ try {
1763
+ const roadmapPath = join(cwd, '.dual-brain', 'roadmap.md');
1764
+ if (existsSync(roadmapPath)) {
1765
+ const roadmapText = readFileSync(roadmapPath, 'utf8');
1766
+ const lines = roadmapText.split('\n').filter(Boolean);
1767
+ // Skip heading lines, grab first non-heading line
1768
+ const firstItem = lines.find(l => !l.startsWith('#') && l.trim().length > 0);
1769
+ if (firstItem) {
1770
+ const clean = firstItem.replace(/^[-*>]+\s*/, '').trim().slice(0, 45);
1771
+ awarenessLine2 = `\x1b[2m📋\x1b[0m ${clean}`;
1781
1772
  }
1782
- } catch { /* non-fatal */ }
1783
- }
1784
- // ── End related sessions hint ─────────────────────────────────────────────
1773
+ }
1774
+ } catch { /* non-fatal */ }
1775
+
1776
+ const awarenessRows = [
1777
+ row(awarenessLine1),
1778
+ row(awarenessLine2),
1779
+ row(awarenessLine3),
1780
+ ];
1785
1781
 
1786
1782
  // ── Sessions section ──────────────────────────────────────────────────────
1787
1783
  const sessionRows = [];
@@ -1837,23 +1833,28 @@ async function mainScreen(rl, ask) {
1837
1833
  });
1838
1834
  }
1839
1835
 
1840
- // ── Actions barnavigation only (pipeline verbs are internal stages, not menu items) ─
1841
- const actionsContent = 'n New session / Search q Quit';
1836
+ // ── Box 5Input bar ──────────────────────────────────────────────────
1837
+ const actionsContent = '> type anything... [s] settings [/] search [q] quit';
1842
1838
  const actionsRow = row(actionsContent);
1843
1839
 
1844
- // ── Print the full box ────────────────────────────────────────────────────
1845
- // Include action cards between status and sessions (with separators only when non-empty)
1846
- const poweredByRow = row('\x1b[2mPowered by dual-brain\x1b[0m');
1840
+ // ── Print the full 5-box layout ───────────────────────────────────────────
1841
+ // Box 1: header (title + provider dots + work style)
1842
+ // Box 2: workspace (branch · uncommitted · ahead, last commit, open PRs)
1843
+ // Box 3: awareness (observer, roadmap, risk)
1844
+ // Box 4: sessions
1845
+ // Box 5: input bar
1847
1846
  const lines = [
1848
1847
  top,
1849
- ...statusRows,
1850
- ...(actionRows.length > 0 ? [sep, ...actionRows] : []),
1848
+ row(`🧠 dual-brain v${version}`),
1849
+ row(providerLine),
1850
+ sep,
1851
+ ...workspaceRows,
1852
+ sep,
1853
+ ...awarenessRows,
1851
1854
  sep,
1852
1855
  ...sessionRows,
1853
1856
  sep,
1854
1857
  actionsRow,
1855
- sep,
1856
- poweredByRow,
1857
1858
  bot,
1858
1859
  ];
1859
1860
  // ── Stale session hint ──────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "7.1.22",
3
+ "version": "7.1.24",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,8 @@
19
19
  "./session": "./src/session.mjs",
20
20
  "./decompose": "./src/decompose.mjs",
21
21
  "./brief": "./src/brief.mjs",
22
- "./redact": "./src/redact.mjs"
22
+ "./redact": "./src/redact.mjs",
23
+ "./calibration": "./src/calibration.mjs"
23
24
  },
24
25
  "keywords": [
25
26
  "claude-code",
@@ -58,6 +59,7 @@
58
59
  "src/decompose.mjs",
59
60
  "src/brief.mjs",
60
61
  "src/redact.mjs",
62
+ "src/calibration.mjs",
61
63
  "src/pipeline.mjs",
62
64
  "src/context.mjs",
63
65
  "src/outcome.mjs",
@@ -67,7 +69,10 @@
67
69
  "src/receipt.mjs",
68
70
  "src/failure-memory.mjs",
69
71
  "src/index.mjs",
72
+ "src/ledger.mjs",
73
+ "src/intelligence.mjs",
70
74
  "src/tui.mjs",
75
+ "src/living-docs.mjs",
71
76
  "src/install-hooks.mjs",
72
77
  "src/update-check.mjs",
73
78
  "bin/*.mjs",
@@ -0,0 +1,148 @@
1
+ // User calibration module — tracks specificity, corrections, and autonomy signals
2
+ // to adapt dual-brain behavior to any user from vague vibe-coder to precise expert.
3
+
4
+ const FILE_REF_RE = /(?:src\/|\.mjs|\.tsx?|\.jsx?|\.json|\.ya?ml|\.sh|line\s+\d+|\bL\d+\b)/i;
5
+ const TECH_TERM_RE = /\b(?:regex|middleware|api|endpoint|refactor|migration|schema|auth(?:entication)?|jwt|token|hook|dispatch|pipeline|module|function|class|interface|type|import|export|async|await|promise|callback|handler|router|controller|service|repository|factory|singleton|decorator|mixin|proxy|guard|interceptor|serializer|validator|transformer|adapter|facade|strategy|observer|subscriber|emitter|stream|buffer|cache|queue|worker|thread|mutex|semaphore|socket|websocket|http|grpc|graphql|rest|orm|sql|nosql|index|query|transaction|migration|seed|fixture|mock|stub|spy|assertion|coverage|lint|typecheck|bundle|compile|transpile|minify|tree.shake|dead.code|chunk|lazy.load|ssr|csr|hydrat)\b/i;
6
+ const VAGUE_RE = /\b(?:idk|just|make\s+it|fix\s+it|whatever|vibes?|better|nicer|faster|cleaner|improve|help|do\s+it|yeah|sure|ok|okay|hmm|uh|er|um)\b/i;
7
+ const AUTONOMY_HIGH_RE = /\b(?:just\s+do\s+it|go(?:\s+ahead)?|build\s+it|ship\s+it|run\s+it|execute|do\s+it|proceed|continue|carry\s+on|handle\s+it|take\s+care|make\s+it\s+happen|yolo)\b/i;
8
+ const QUESTION_RE = /\?|^(?:how|what|why|when|where|which|should|can|could|would|is|are|do|does|did|will|won't|don't)\b/i;
9
+ const CORRECTION_RE = /^(?:no[,.]?|not\s|wrong|stop|don't|that'?s?\s+not|i\s+said|i\s+meant)\b|(?:\binstead\b|\brather\b|\bactually\b)/i;
10
+
11
+ export function analyzeInput(input) {
12
+ const text = (input || '').trim();
13
+ const lower = text.toLowerCase();
14
+ const words = text.split(/\s+/).filter(Boolean);
15
+ const wordCount = words.length;
16
+
17
+ const hasFileRefs = FILE_REF_RE.test(text);
18
+ const hasTechTerms = TECH_TERM_RE.test(text);
19
+ const hasExactInstructions = /\b(?:step\s+\d|first[,\s]|then[,\s]|finally[,\s]|specifically|exactly|must|should\s+(?:use|call|return|handle)|(?:use|call|return|throw|emit|dispatch)\s+\w)/i.test(text);
20
+ const isVague = VAGUE_RE.test(lower) || wordCount <= 3;
21
+
22
+ let specificity;
23
+ if (hasFileRefs || /\bline\s*\d+\b|\bL\d+\b/i.test(text) || (hasTechTerms && hasExactInstructions)) {
24
+ specificity = 5;
25
+ } else if (hasTechTerms && wordCount >= 6) {
26
+ specificity = 4;
27
+ } else if (!isVague && wordCount >= 8) {
28
+ specificity = 3;
29
+ } else if (wordCount >= 4 && !isVague) {
30
+ specificity = 2;
31
+ } else {
32
+ specificity = 1;
33
+ }
34
+
35
+ return {
36
+ specificity,
37
+ signals: {
38
+ hasFileRefs,
39
+ hasTechTerms,
40
+ hasExactInstructions,
41
+ isVague,
42
+ wordCount
43
+ }
44
+ };
45
+ }
46
+
47
+ export function detectCorrection(input) {
48
+ return CORRECTION_RE.test((input || '').trim());
49
+ }
50
+
51
+ function clamp(value, min = 1, max = 5) {
52
+ return Math.min(max, Math.max(min, value));
53
+ }
54
+
55
+ function round1(value) {
56
+ return Math.round(value * 10) / 10;
57
+ }
58
+
59
+ function detectAutonomySignal(input) {
60
+ const text = (input || '').trim();
61
+ if (AUTONOMY_HIGH_RE.test(text)) return 5;
62
+ if (QUESTION_RE.test(text)) return 2;
63
+ return null;
64
+ }
65
+
66
+ export function updateCalibration(calibration, input, correction = false) {
67
+ const { specificity: newSpec } = analyzeInput(input);
68
+ const autonomySignal = detectAutonomySignal(input);
69
+
70
+ const prev = {
71
+ specificity: calibration.specificity ?? 3,
72
+ corrections: calibration.corrections ?? 3,
73
+ autonomy: calibration.autonomy ?? 3,
74
+ interactions: calibration.interactions ?? 0
75
+ };
76
+
77
+ const specificity = round1(clamp(prev.specificity * 0.7 + newSpec * 0.3));
78
+
79
+ const corrections = round1(clamp(
80
+ correction ? prev.corrections - 0.3 : prev.corrections + 0.1
81
+ ));
82
+
83
+ let autonomy = prev.autonomy;
84
+ if (autonomySignal !== null) {
85
+ autonomy = round1(clamp(prev.autonomy * 0.7 + autonomySignal * 0.3));
86
+ }
87
+
88
+ return {
89
+ specificity,
90
+ corrections,
91
+ autonomy,
92
+ interactions: prev.interactions + 1,
93
+ lastUpdated: new Date().toISOString()
94
+ };
95
+ }
96
+
97
+ export function getAdaptation(calibration) {
98
+ const s = calibration.specificity ?? 3;
99
+ const c = calibration.corrections ?? 3;
100
+ const a = calibration.autonomy ?? 3;
101
+
102
+ const clarifyBeforeActing = s < 2.5 || c > 3;
103
+ const explainReasoning = a < 3;
104
+ const suggestNextSteps = a < 4;
105
+ const askForApproval = c < 2.5;
106
+ const autoExecute = a > 4 && c > 3.5;
107
+
108
+ let responseStyle;
109
+ const combined = (s + c + a) / 3;
110
+ if (combined >= 4) {
111
+ responseStyle = 'terse';
112
+ } else if (combined >= 2.5) {
113
+ responseStyle = 'normal';
114
+ } else {
115
+ responseStyle = 'detailed';
116
+ }
117
+
118
+ let userLevel;
119
+ if (s >= 4) {
120
+ userLevel = 'advanced';
121
+ } else if (s >= 2.5) {
122
+ userLevel = 'intermediate';
123
+ } else {
124
+ userLevel = 'beginner';
125
+ }
126
+
127
+ return {
128
+ clarifyBeforeActing,
129
+ explainReasoning,
130
+ suggestNextSteps,
131
+ askForApproval,
132
+ autoExecute,
133
+ responseStyle,
134
+ userLevel
135
+ };
136
+ }
137
+
138
+ export function formatCalibration(calibration) {
139
+ const { userLevel, responseStyle } = getAdaptation(calibration);
140
+ const s = calibration.specificity ?? 3;
141
+ const c = calibration.corrections ?? 3;
142
+ const a = calibration.autonomy ?? 3;
143
+
144
+ const autonomyLabel = a >= 4 ? 'high autonomy' : a >= 2.5 ? 'normal autonomy' : 'low autonomy';
145
+ const trustLabel = c >= 4 ? 'high trust' : c >= 2.5 ? 'good trust' : 'low trust';
146
+
147
+ return `User: ${userLevel} · ${autonomyLabel} · ${trustLabel}\n (specificity: ${s}, corrections: ${c}, autonomy: ${a})`;
148
+ }
package/src/dispatch.mjs CHANGED
@@ -702,6 +702,17 @@ async function dispatch(input = {}) {
702
702
  // that this agent call came through the official pipeline.
703
703
  prompt = _prependDispatchMarker(prompt);
704
704
 
705
+ // ── Situation brief injection ────────────────────────────────────────────────
706
+ // Prepend a compact project-state summary when provided by the pipeline.
707
+ // This gives every dispatched agent immediate context about the project reality.
708
+ const situationBrief = typeof input.situationBrief === 'string' && input.situationBrief.trim()
709
+ ? input.situationBrief.trim()
710
+ : null;
711
+ if (situationBrief) {
712
+ prompt = `--- SITUATION BRIEF ---\n${situationBrief}\n--- END BRIEF ---\n\n${prompt}`;
713
+ }
714
+ // ── End situation brief ──────────────────────────────────────────────────────
715
+
705
716
  // ── Specialist prompt injection ──────────────────────────────────────────────
706
717
  const specialist = decision.specialist && decision.specialist !== 'generic'
707
718
  ? decision.specialist
@@ -1018,6 +1029,15 @@ async function dispatchDualBrain(input = {}) {
1018
1029
  // Stamp with dispatch marker so enforce-tier.mjs allows this Agent call
1019
1030
  prompt = _prependDispatchMarker(prompt);
1020
1031
 
1032
+ // ── Situation brief injection ────────────────────────────────────────────────
1033
+ const _dualBrainBrief = typeof input.situationBrief === 'string' && input.situationBrief.trim()
1034
+ ? input.situationBrief.trim()
1035
+ : null;
1036
+ if (_dualBrainBrief) {
1037
+ prompt = `--- SITUATION BRIEF ---\n${_dualBrainBrief}\n--- END BRIEF ---\n\n${prompt}`;
1038
+ }
1039
+ // ── End situation brief ──────────────────────────────────────────────────────
1040
+
1021
1041
  // Feature 1: Validate both sub-decisions before spawning anything
1022
1042
  const rt = await detectRuntime();
1023
1043
  const tier = decision.tier ?? 'execute';