dual-brain 0.2.7 → 0.2.8
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/CLAUDE.md +29 -143
- package/bin/dual-brain.mjs +80 -44
- package/package.json +10 -2
- package/src/dispatch.mjs +87 -2
- package/src/head.mjs +353 -0
- package/src/health.mjs +156 -0
- package/src/integrity.mjs +245 -0
- package/src/prompt-audit.mjs +231 -0
- package/src/templates.mjs +223 -0
- package/src/tui.mjs +79 -0
package/CLAUDE.md
CHANGED
|
@@ -2,146 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
This project uses dual-provider orchestration. Config: `.claude/orchestrator.json`.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
dual-brain go
|
|
22
|
-
dual-brain
|
|
23
|
-
dual-brain
|
|
24
|
-
dual-brain
|
|
25
|
-
dual-brain
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
## Dual-Brain Collaboration
|
|
35
|
-
|
|
36
|
-
Dual-brain is a multi-round conversation between Claude and GPT — not a single-shot dispatch.
|
|
37
|
-
|
|
38
|
-
**Think flow** (architecture decisions):
|
|
39
|
-
1. Round 1: `node .claude/hooks/dual-brain-think.mjs --question "..."` → GPT gives independent analysis
|
|
40
|
-
2. You analyze the same question independently
|
|
41
|
-
3. Round 2: `node .claude/hooks/dual-brain-think.mjs --question "..." --round 2 --claude-says "<your analysis>"` → GPT responds with agreements, pushback, refined recommendation
|
|
42
|
-
4. You synthesize both rounds into a final decision
|
|
43
|
-
|
|
44
|
-
**Review flow** (code review):
|
|
45
|
-
1. Round 1: `node .claude/hooks/dual-brain-review.mjs` → GPT reviews the diff independently
|
|
46
|
-
2. You review the same diff independently
|
|
47
|
-
3. Round 2: `node .claude/hooks/dual-brain-review.mjs --round 2 --claude-review "<your findings>"` → GPT confirms shared findings, acknowledges misses
|
|
48
|
-
4. You synthesize into a final review verdict
|
|
49
|
-
|
|
50
|
-
## Routing Rules
|
|
51
|
-
|
|
52
|
-
1. Tasks under 3 min → Claude (Codex startup overhead not worth it)
|
|
53
|
-
2. Isolated tasks over 3 min → check balance: `node .claude/hooks/budget-balancer.mjs`
|
|
54
|
-
3. High-risk decisions → dual-brain think
|
|
55
|
-
4. When a task spans tiers: think > execute > search
|
|
56
|
-
|
|
57
|
-
## Mandatory Workload Distribution
|
|
58
|
-
|
|
59
|
-
**Claude MUST follow these rules before implementing multi-file changes:**
|
|
60
|
-
|
|
61
|
-
1. **Before starting any batch of 3+ file edits**: run `node .claude/hooks/budget-balancer.mjs` to check provider balance, then `dual-brain go --dry-run "description"` to classify tasks
|
|
62
|
-
2. **When budget-balancer recommends GPT**: dispatch via `src/dispatch.mjs` (or `node .claude/hooks/gpt-work-dispatcher.mjs --task "..." --tier execute`)
|
|
63
|
-
3. **Security/auth/credential changes**: always require dual-brain think flow before implementation
|
|
64
|
-
4. **Audit remediation batches**: plan waves with dual-brain think, dispatch execution to GPT, Claude reviews
|
|
65
|
-
5. **Claude's role in multi-task work**: define acceptance criteria, dispatch agents, review results — not solo-implement everything
|
|
66
|
-
|
|
67
|
-
**Triggers that require this workflow:** 3+ production files edited in one session · auth/credentials/tokens/secrets · changes to dispatcher, agent routing, or tier logic · audit remediation across multiple subsystems · Claude think capacity above 60% per budget-balancer.
|
|
68
|
-
|
|
69
|
-
**Failure to route is itself a bug.**
|
|
70
|
-
|
|
71
|
-
## Quality Gate
|
|
72
|
-
|
|
73
|
-
Before ending a session with code changes:
|
|
74
|
-
1. `node .claude/hooks/session-report.mjs` (allowed by head-guard for hook scripts)
|
|
75
|
-
2. `node .claude/hooks/quality-gate.mjs`
|
|
76
|
-
|
|
77
|
-
Gate statuses: `pass` (safe to end), `issues_found` (fix first), `needs_human_review` (GPT unavailable).
|
|
78
|
-
|
|
79
|
-
## Profiles
|
|
80
|
-
|
|
81
|
-
Profile persists to `.dualbrain/profile.json` (project-scoped, gitignored).
|
|
82
|
-
|
|
83
|
-
- **auto** (default): Adapts routing based on task risk, provider health, and outcomes
|
|
84
|
-
- **balanced**: Best model per tier, normal budgets, reviews at medium+ risk
|
|
85
|
-
- **cost-saver**: Prefer cheaper models, lower budgets, skip GPT for non-critical
|
|
86
|
-
- **quality-first**: Dual-brain for medium+ risk, higher budgets, stricter reviews
|
|
87
|
-
|
|
88
|
-
Switch via the interactive Profile screen in `dual-brain`, or set `bias` in `.dualbrain/profile.json`.
|
|
89
|
-
|
|
90
|
-
## Adaptive Routing (Auto Mode)
|
|
91
|
-
|
|
92
|
-
- **Risk classification**: auth/secrets→critical, billing/migrations→high, tests/utils→medium, docs→low
|
|
93
|
-
- **Failure detection**: 2+ failures on same prompt in 2 hours → auto-escalate tier or trigger dual-brain
|
|
94
|
-
- **Provider balance**: Routes to underused provider when one subscription is hot
|
|
95
|
-
- **Burst awareness**: Suppresses duplicate warnings during agent waves (3+ agents in 90s)
|
|
96
|
-
|
|
97
|
-
## Budget Balancer
|
|
98
|
-
|
|
99
|
-
`src/decide.mjs` handles routing decisions using the same token data internally. For inspection:
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
node .claude/hooks/budget-balancer.mjs
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Tracks 5-hour and 7-day rolling windows against subscription limits (Claude Pro/Max, ChatGPT Plus/Pro). The higher pressure window is the binding constraint. Uses actual `input_tokens + output_tokens` from usage logs.
|
|
106
|
-
|
|
107
|
-
**Subscription tiers** (configured in `orchestrator.json` → `subscriptions.*.plan`):
|
|
108
|
-
- Claude: Pro $20, Max x5 $100, Max x20 $200
|
|
109
|
-
- ChatGPT: Plus $20, Pro $100, Pro $200
|
|
110
|
-
|
|
111
|
-
## Multi-Step Work
|
|
112
|
-
|
|
113
|
-
The wave orchestrator is available for complex multi-step tasks:
|
|
114
|
-
|
|
115
|
-
```bash
|
|
116
|
-
node .claude/hooks/wave-orchestrator.mjs "fix the login bug and update the nav"
|
|
117
|
-
node .claude/hooks/wave-orchestrator.mjs --dry-run "refactor auth module"
|
|
118
|
-
node .claude/hooks/wave-orchestrator.mjs --resume <manifestId>
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
For most tasks, prefer `dual-brain go "..."` — it runs the same detect→decide→dispatch pipeline with less overhead.
|
|
122
|
-
|
|
123
|
-
## Available Tools
|
|
124
|
-
|
|
125
|
-
| Tool | Purpose |
|
|
126
|
-
|------|---------|
|
|
127
|
-
| `dual-brain go "..."` | Primary entry point: detect, decide, dispatch |
|
|
128
|
-
| `dual-brain status` | Provider health, budget, models |
|
|
129
|
-
| `node .claude/hooks/budget-balancer.mjs` | Token usage and routing recommendation |
|
|
130
|
-
| `node .claude/hooks/dual-brain-think.mjs` | Multi-round architecture decisions with GPT |
|
|
131
|
-
| `node .claude/hooks/dual-brain-review.mjs` | Multi-round code review with GPT |
|
|
132
|
-
| `node .claude/hooks/wave-orchestrator.mjs "..."` | Dependency-aware multi-wave dispatch |
|
|
133
|
-
| `node .claude/hooks/session-report.mjs` | End-of-session summary |
|
|
134
|
-
| `node .claude/hooks/quality-gate.mjs` | Gate check before ending session |
|
|
135
|
-
| `node .claude/hooks/health-check.mjs` | System health |
|
|
136
|
-
| `node .claude/hooks/test-orchestrator.mjs` | Self-tests (40 tests) |
|
|
137
|
-
| `node .claude/hooks/vibe-memory.mjs` | Persistent preferences across sessions |
|
|
138
|
-
| `dual-brain search "..."` | Search across all previous sessions |
|
|
139
|
-
|
|
140
|
-
## Cross-Session Context
|
|
141
|
-
|
|
142
|
-
When the user references past work ("we did this before", "yesterday we worked on", "remember when we", "didn't we already fix"), use the session search to find relevant context:
|
|
143
|
-
|
|
144
|
-
1. Run `dual-brain search "keyword"` to search the session index
|
|
145
|
-
2. Or use the MCP tool `dual_brain_search` if available
|
|
146
|
-
|
|
147
|
-
This surfaces previous conversations so HEAD can provide continuity across sessions without the user having to re-explain.
|
|
5
|
+
## HEAD Constitution
|
|
6
|
+
|
|
7
|
+
HEAD is the orchestration brain. Workers implement. This is enforced by architecture, not just policy.
|
|
8
|
+
|
|
9
|
+
1. **HEAD plans, workers implement.** HEAD dispatches typed task contracts via agents. HEAD never edits files, runs implementation commands, or writes code directly.
|
|
10
|
+
2. **Discuss before dispatching.** Every action task starts with intent classification. Ambiguous requests get clarified. Architecture decisions get discussed.
|
|
11
|
+
3. **Typed contracts are mandatory.** Every dispatch includes: objective, scope, acceptance criteria, risk level, allowed operations. Use `src/templates.mjs` to generate prompts.
|
|
12
|
+
4. **Dangerous work requires approval.** Auth, credentials, secrets, billing, migrations, destructive git — explicit user confirmation before dispatch.
|
|
13
|
+
5. **Runtime state is source of truth.** HEAD's state machine (`src/head.mjs`) tracks phase, intent, confidence, and drift. Not CLAUDE.md text.
|
|
14
|
+
6. **Hooks enforce boundaries.** head-guard blocks HEAD from implementing. enforce-tier ensures correct routing. Telemetry hooks observe but never block.
|
|
15
|
+
7. **Subscription-only auth.** Users authenticate via `claude login` / `codex login`. No API keys.
|
|
16
|
+
|
|
17
|
+
## Quick Reference
|
|
18
|
+
|
|
19
|
+
| Command | Purpose |
|
|
20
|
+
|---------|---------|
|
|
21
|
+
| `dual-brain go "..."` | Detect → decide → dispatch |
|
|
22
|
+
| `dual-brain status` | Provider health, budget |
|
|
23
|
+
| `dual-brain install --global` | Set dual-brain as default for all sessions |
|
|
24
|
+
| `node .claude/hooks/dual-brain-think.mjs --question "..."` | Multi-round architecture decisions |
|
|
25
|
+
| `node .claude/hooks/dual-brain-review.mjs` | Multi-round code review |
|
|
26
|
+
|
|
27
|
+
## Modules
|
|
28
|
+
|
|
29
|
+
Core pipeline: `profile.mjs` → `detect.mjs` → `decide.mjs` → `dispatch.mjs` → `pipeline.mjs`
|
|
30
|
+
HEAD brain: `head.mjs` (state machine, intent, confidence, drift)
|
|
31
|
+
Templates: `templates.mjs` (typed prompt generation)
|
|
32
|
+
Integrity: `integrity.mjs` (atomic writes, locks)
|
|
33
|
+
Quality: `prompt-audit.mjs` (prompt scoring, exchange logging)
|
package/bin/dual-brain.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { runPipeline, buildExecutionPlan, formatExecutionPlan } from '../src/pip
|
|
|
35
35
|
import { loadRepoCache } from '../src/repo.mjs';
|
|
36
36
|
import { loadSession, saveSession, formatSessionCard, importReplitSessions, getSessionMeta, saveSessionMeta, renameSession, pinSession, unpinSession, categorizeSession, enrichSessions, archiveSession, getArchivedSessions } from '../src/session.mjs';
|
|
37
37
|
|
|
38
|
-
import { box, bar, badge, menu, separator } from '../src/tui.mjs';
|
|
38
|
+
import { box, bar, badge, menu, separator, panel, divider, statusChip, headerBar, prompt as tuiPrompt, signalLine } from '../src/tui.mjs';
|
|
39
39
|
|
|
40
40
|
// ─── Dynamic imports for receipts + failure memory ───────────────────────────
|
|
41
41
|
|
|
@@ -2594,8 +2594,7 @@ async function mainScreen(rl, ask) {
|
|
|
2594
2594
|
|
|
2595
2595
|
// ── Recent work items (dim, max 3) ────────────────────────────────────────
|
|
2596
2596
|
const recentLines = recentWorkItems.slice(0, 3).map(item => {
|
|
2597
|
-
|
|
2598
|
-
return ` ${DIM}${prefix} ${item.text}${RST}`;
|
|
2597
|
+
return signalLine(item.ok ? 'success' : 'warning', `${DIM}${item.text}${RST}`);
|
|
2599
2598
|
});
|
|
2600
2599
|
|
|
2601
2600
|
// ── Resolve dashboard spinner before rendering ────────────────────────────
|
|
@@ -2606,26 +2605,48 @@ async function mainScreen(rl, ask) {
|
|
|
2606
2605
|
process.stdout.write(`${DIM}${staleCount} stale sessions (>7d) — type "sessions" to manage${RST}\n`);
|
|
2607
2606
|
}
|
|
2608
2607
|
|
|
2609
|
-
// ── Render Studio Console
|
|
2610
|
-
const
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2608
|
+
// ── Render Studio Console (paneled layout) ────────────────────────────────
|
|
2609
|
+
const CYAN = '\x1b[36m';
|
|
2610
|
+
const panelW = Math.min(sepW + 2, 72);
|
|
2611
|
+
|
|
2612
|
+
// Header panel — project, branch, providers, version
|
|
2613
|
+
const headerLeft = `${DIM}${projectName}${RST} ${BOLD}${branchStr}${RST} ${DIM}Claude${RST} ${claudeDot} ${DIM}GPT${RST} ${openaiDot}`;
|
|
2614
|
+
const headerRight = `${DIM}v${version}${RST}`;
|
|
2615
|
+
const headerContent = [headerBar(headerLeft, headerRight, panelW - 4)];
|
|
2616
|
+
process.stdout.write('\n' + panel('dual-brain', headerContent, { width: panelW, titleColor: CYAN }) + '\n\n');
|
|
2617
|
+
|
|
2618
|
+
// Resume / prompt panel (only when there is something to show)
|
|
2619
|
+
if (isReturning || !anyProviderAvail) {
|
|
2620
|
+
const resumeContent = [];
|
|
2621
|
+
if (!anyProviderAvail) {
|
|
2622
|
+
resumeContent.push(`${BOLD}Connect a provider to start working${RST}`);
|
|
2623
|
+
} else {
|
|
2624
|
+
const labelTrunc = (resumeState.label || 'last session').slice(0, 45);
|
|
2625
|
+
const agePart = resumeState.ageLabel ? ` · ${resumeState.ageLabel}` : '';
|
|
2626
|
+
const nextPart = resumeState.nextAction ? ` · next: ${resumeState.nextAction}` : '';
|
|
2627
|
+
resumeContent.push(`${DIM}Last task${RST} ${BOLD}${labelTrunc}${RST}${DIM}${agePart}${RST}`);
|
|
2628
|
+
if (nextPart) resumeContent.push(`${DIM}Next step${RST} ${nextPart.replace(/^ · /, '')}`);
|
|
2629
|
+
}
|
|
2630
|
+
resumeContent.push('');
|
|
2631
|
+
resumeContent.push(` ${CYAN}›${RST} ${BOLD}${suggestions[0]}${RST} ${DIM}${suggestions[1] || ''}${RST} ${DIM}${suggestions[2] || ''}${RST}`);
|
|
2632
|
+
process.stdout.write(panel(isReturning ? 'Resume work' : 'Get started', resumeContent, { width: panelW }) + '\n\n');
|
|
2633
|
+
} else {
|
|
2634
|
+
// Fresh / no-resume state — just show suggestions inline
|
|
2635
|
+
const suggestContent = [` ${CYAN}›${RST} ${BOLD}${suggestions[0]}${RST} ${DIM}${suggestions[1] || ''}${RST} ${DIM}${suggestions[2] || ''}${RST}`];
|
|
2636
|
+
process.stdout.write(panel('Get started', suggestContent, { width: panelW }) + '\n\n');
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// Signals panel — recent work items (only when there are items)
|
|
2619
2640
|
if (recentLines.length > 0) {
|
|
2620
|
-
|
|
2621
|
-
out.push(...recentLines); // ✓ / ! recent work items
|
|
2641
|
+
process.stdout.write(panel('Signals', recentLines, { width: panelW }) + '\n\n');
|
|
2622
2642
|
}
|
|
2623
|
-
out.push('');
|
|
2624
|
-
out.push(` ${sepLine}`); // ━━━━ separator
|
|
2625
|
-
// Input bar rendered inline — the key handler will overwrite this line
|
|
2626
|
-
out.push(` ${DIM}> task or command...${RST}${' '.repeat(Math.max(1, sepW - 22))}${DIM}[?] help${RST}`);
|
|
2627
2643
|
|
|
2628
|
-
|
|
2644
|
+
// Input bar — rendered below panels; the key handler will overwrite this line
|
|
2645
|
+
const inputHint = `${DIM}[?] help${RST}`;
|
|
2646
|
+
const inputLeft = tuiPrompt('task or command...');
|
|
2647
|
+
const inputLeftW = (inputLeft + ' task or command...').replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
2648
|
+
const inputGap = Math.max(1, panelW - inputLeftW - 8);
|
|
2649
|
+
process.stdout.write(` ${inputLeft}${' '.repeat(inputGap)}${inputHint}\n`);
|
|
2629
2650
|
|
|
2630
2651
|
// ── Key handling ──────────────────────────────────────────────────────────
|
|
2631
2652
|
// Use raw keypress mode so we can show a live type-to-start buffer.
|
|
@@ -3563,28 +3584,38 @@ async function settingsScreen(rl, ask) {
|
|
|
3563
3584
|
` ${DIM}Doctor${RESET} ${doctorStr}`,
|
|
3564
3585
|
];
|
|
3565
3586
|
|
|
3566
|
-
// ── Render
|
|
3567
|
-
const
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
...subsLines,
|
|
3573
|
-
` ${DIM}[a] add [r] remove [h] health check${RESET}`,
|
|
3574
|
-
'',
|
|
3575
|
-
` ${DIM}Work style${RESET}`,
|
|
3576
|
-
...wsLines,
|
|
3577
|
-
` ${DIM}[1-3] change${RESET}`,
|
|
3587
|
+
// ── Render (paneled layout) ───────────────────────────────────────────────
|
|
3588
|
+
const CYAN = '\x1b[36m';
|
|
3589
|
+
const settingsPanelW = 70;
|
|
3590
|
+
|
|
3591
|
+
const subsContent = [
|
|
3592
|
+
...subsLines.map(l => l.replace(/^ /, '')),
|
|
3578
3593
|
'',
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3594
|
+
signalLine('info', `${DIM}[a] add [r] remove [h] health check${RESET}`),
|
|
3595
|
+
];
|
|
3596
|
+
|
|
3597
|
+
const wsContent = [
|
|
3598
|
+
...wsLines.map(l => l.replace(/^ /, '')),
|
|
3582
3599
|
'',
|
|
3583
|
-
|
|
3584
|
-
|
|
3600
|
+
signalLine('info', `${DIM}[1-3] change${RESET}`),
|
|
3601
|
+
];
|
|
3602
|
+
|
|
3603
|
+
const sysContent = [
|
|
3604
|
+
...sysLines.map(l => l.replace(/^ /, '')),
|
|
3585
3605
|
'',
|
|
3606
|
+
signalLine('info', `${DIM}[d] run doctor [x] diagnostics${RESET}`),
|
|
3586
3607
|
];
|
|
3587
|
-
|
|
3608
|
+
|
|
3609
|
+
const navContent = [
|
|
3610
|
+
`${DIM}[e]${RESET} sessions ${DIM}[m]${RESET} subscriptions ${DIM}[b]${RESET} back`,
|
|
3611
|
+
...(settingsPRs.length > 0 ? [`${DIM}[p]${RESET} PR triage ${DIM}(${settingsPRs.length} open)${RESET}`] : []),
|
|
3612
|
+
];
|
|
3613
|
+
|
|
3614
|
+
process.stdout.write('\n');
|
|
3615
|
+
process.stdout.write(panel('Subscriptions', subsContent, { width: settingsPanelW, titleColor: CYAN }) + '\n\n');
|
|
3616
|
+
process.stdout.write(panel('Work style', wsContent, { width: settingsPanelW, titleColor: CYAN }) + '\n\n');
|
|
3617
|
+
process.stdout.write(panel('System', sysContent, { width: settingsPanelW, titleColor: CYAN }) + '\n\n');
|
|
3618
|
+
process.stdout.write(panel('Navigation', navContent, { width: settingsPanelW }) + '\n\n');
|
|
3588
3619
|
|
|
3589
3620
|
const raw = (await ask(' Choice: ')).trim();
|
|
3590
3621
|
const choice = raw.toLowerCase();
|
|
@@ -4045,15 +4076,20 @@ async function askDefaultShell(cwd, rl, fx) {
|
|
|
4045
4076
|
const cl = fx.colors || {};
|
|
4046
4077
|
const DIM = cl.dim || '';
|
|
4047
4078
|
const BOLD = cl.bold || '';
|
|
4048
|
-
const
|
|
4079
|
+
const CYAN = cl.cyan || '\x1b[36m';
|
|
4080
|
+
const YLW = cl.yellow || '\x1b[33m';
|
|
4049
4081
|
const GREEN = cl.green || '';
|
|
4050
4082
|
const RST = cl.reset || '';
|
|
4051
4083
|
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4084
|
+
const setupContent = [
|
|
4085
|
+
`${DIM}Start dual-brain automatically when this Replit opens?${RST}`,
|
|
4086
|
+
'',
|
|
4087
|
+
` ${DIM}modifies${RST} ${YLW}.replit onBoot${RST}`,
|
|
4088
|
+
` ${DIM}undo${RST} Settings → System → Startup`,
|
|
4089
|
+
'',
|
|
4090
|
+
` ${CYAN}[Y]${RST} Start on boot ${DIM}[n] Run manually${RST}`,
|
|
4091
|
+
];
|
|
4092
|
+
process.stdout.write('\n' + panel('dual-brain setup', setupContent) + '\n');
|
|
4057
4093
|
|
|
4058
4094
|
const answer = await new Promise(res => rl.question(' ', (a) => res(a.trim().toLowerCase())));
|
|
4059
4095
|
const yes = !answer || answer.startsWith('y');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,11 @@
|
|
|
27
27
|
"./continuity": "./src/continuity.mjs",
|
|
28
28
|
"./checkpoint": "./src/checkpoint.mjs",
|
|
29
29
|
"./pr-agent": "./src/pr-agent.mjs",
|
|
30
|
-
"./ci-triage": "./src/ci-triage.mjs"
|
|
30
|
+
"./ci-triage": "./src/ci-triage.mjs",
|
|
31
|
+
"./integrity": "./src/integrity.mjs",
|
|
32
|
+
"./prompt-audit": "./src/prompt-audit.mjs",
|
|
33
|
+
"./head": "./src/head.mjs",
|
|
34
|
+
"./templates": "./src/templates.mjs"
|
|
31
35
|
},
|
|
32
36
|
"keywords": [
|
|
33
37
|
"claude-code",
|
|
@@ -94,6 +98,10 @@
|
|
|
94
98
|
"src/checkpoint.mjs",
|
|
95
99
|
"src/ci-triage.mjs",
|
|
96
100
|
"src/pr-agent.mjs",
|
|
101
|
+
"src/integrity.mjs",
|
|
102
|
+
"src/prompt-audit.mjs",
|
|
103
|
+
"src/head.mjs",
|
|
104
|
+
"src/templates.mjs",
|
|
97
105
|
"bin/*.mjs",
|
|
98
106
|
"hooks/enforce-tier.mjs",
|
|
99
107
|
"hooks/cost-logger.mjs",
|
package/src/dispatch.mjs
CHANGED
|
@@ -527,6 +527,60 @@ function getRetryBudget() {
|
|
|
527
527
|
};
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
// ─── Preflight auth check ─────────────────────────────────────────────────────
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Verify a provider CLI is present and (optionally) responds to --version.
|
|
534
|
+
* Uses `which` for the fast path and a 3s-capped --version call to confirm.
|
|
535
|
+
*
|
|
536
|
+
* @param {'claude'|'openai'} provider
|
|
537
|
+
* @param {string} [cwd] Working directory (unused, kept for signature parity)
|
|
538
|
+
* @returns {Promise<{ ready: boolean, provider: string, error?: string, suggestion?: string }>}
|
|
539
|
+
*/
|
|
540
|
+
async function preflightAuth(provider, _cwd) {
|
|
541
|
+
const bin = provider === 'openai' ? 'codex' : 'claude';
|
|
542
|
+
|
|
543
|
+
// Fast path: check binary existence with `which`
|
|
544
|
+
const whichResult = await new Promise((resolve) => {
|
|
545
|
+
const p = spawn('which', [bin], { stdio: 'pipe' });
|
|
546
|
+
p.on('error', () => resolve(false));
|
|
547
|
+
p.on('close', (code) => resolve(code === 0));
|
|
548
|
+
setTimeout(() => { try { p.kill(); } catch {} resolve(false); }, 2000);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
if (!whichResult) {
|
|
552
|
+
const installHint = provider === 'openai'
|
|
553
|
+
? 'Install: npm install -g @openai/codex'
|
|
554
|
+
: 'Install: npm install -g @anthropic-ai/claude-code';
|
|
555
|
+
return {
|
|
556
|
+
ready: false,
|
|
557
|
+
provider,
|
|
558
|
+
error: `${bin} CLI not found in PATH`,
|
|
559
|
+
suggestion: installHint,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Version check: confirms the binary actually runs (catches broken installs)
|
|
564
|
+
const versionOk = await new Promise((resolve) => {
|
|
565
|
+
const p = spawn(bin, ['--version'], { stdio: 'pipe' });
|
|
566
|
+
p.on('error', () => resolve(false));
|
|
567
|
+
p.on('close', (code) => resolve(code === 0));
|
|
568
|
+
setTimeout(() => { try { p.kill(); } catch {} resolve(false); }, 3000);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
if (!versionOk) {
|
|
572
|
+
const loginHint = provider === 'openai' ? 'Run: codex login' : 'Run: claude login';
|
|
573
|
+
return {
|
|
574
|
+
ready: false,
|
|
575
|
+
provider,
|
|
576
|
+
error: `${bin} --version failed (auth may have expired)`,
|
|
577
|
+
suggestion: loginHint,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return { ready: true, provider };
|
|
582
|
+
}
|
|
583
|
+
|
|
530
584
|
// ─── Command builder ──────────────────────────────────────────────────────────
|
|
531
585
|
|
|
532
586
|
function buildCommand(decision, prompt, files = [], _cwd) {
|
|
@@ -840,6 +894,34 @@ async function dispatch(input = {}) {
|
|
|
840
894
|
}
|
|
841
895
|
}
|
|
842
896
|
|
|
897
|
+
// ── Preflight auth check ─────────────────────────────────────────────────
|
|
898
|
+
// Verify the target provider CLI is present and responsive before dispatching.
|
|
899
|
+
// Runs after model/provider resolution so we check the effective provider.
|
|
900
|
+
const preflight = await preflightAuth(effectiveProvider, cwd);
|
|
901
|
+
if (!preflight.ready) {
|
|
902
|
+
// Check if the other provider is available as a fallback
|
|
903
|
+
const otherProvider = effectiveProvider === 'claude' ? 'openai' : 'claude';
|
|
904
|
+
const otherPreflight = await preflightAuth(otherProvider, cwd);
|
|
905
|
+
const fallbackNote = otherPreflight.ready
|
|
906
|
+
? ` Fallback available: ${otherProvider}.`
|
|
907
|
+
: '';
|
|
908
|
+
const errMsg = `${preflight.error}. ${preflight.suggestion}${fallbackNote}`;
|
|
909
|
+
return {
|
|
910
|
+
status: 'error',
|
|
911
|
+
provider: effectiveProvider,
|
|
912
|
+
model: effectiveModel,
|
|
913
|
+
command: null,
|
|
914
|
+
exitCode: null,
|
|
915
|
+
summary: errMsg,
|
|
916
|
+
durationMs: 0,
|
|
917
|
+
usage: null,
|
|
918
|
+
error: errMsg,
|
|
919
|
+
authVerified: false,
|
|
920
|
+
suggestion: preflight.suggestion,
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
// ── End preflight auth check ─────────────────────────────────────────────
|
|
924
|
+
|
|
843
925
|
// ── Feature 2: Dirty-worktree guard for execute-tier dispatches ──────────
|
|
844
926
|
if (tier === 'execute' && decision.owns && !decision._force) {
|
|
845
927
|
const wtCheck = await checkWorktreeClean(decision.owns, cwd);
|
|
@@ -902,6 +984,7 @@ async function dispatch(input = {}) {
|
|
|
902
984
|
durationMs: 0,
|
|
903
985
|
usage: null,
|
|
904
986
|
error: null,
|
|
987
|
+
authVerified: true,
|
|
905
988
|
};
|
|
906
989
|
}
|
|
907
990
|
|
|
@@ -1014,6 +1097,7 @@ async function dispatch(input = {}) {
|
|
|
1014
1097
|
usage,
|
|
1015
1098
|
worktreeUsed: useWorktree,
|
|
1016
1099
|
autoReview,
|
|
1100
|
+
authVerified: true,
|
|
1017
1101
|
error: success ? null : errorText.slice(0, 200),
|
|
1018
1102
|
};
|
|
1019
1103
|
}
|
|
@@ -1021,7 +1105,7 @@ async function dispatch(input = {}) {
|
|
|
1021
1105
|
const command = buildCommand(effectiveDecision, prompt, files, cwd);
|
|
1022
1106
|
|
|
1023
1107
|
if (dryRun) {
|
|
1024
|
-
return { status: 'dry-run', provider: effectiveProvider, model: effectiveModel, specialist: specialist ?? 'generic', command, exitCode: null, summary: null, durationMs: 0, usage: null, error: null };
|
|
1108
|
+
return { status: 'dry-run', provider: effectiveProvider, model: effectiveModel, specialist: specialist ?? 'generic', command, exitCode: null, summary: null, durationMs: 0, usage: null, error: null, authVerified: true };
|
|
1025
1109
|
}
|
|
1026
1110
|
|
|
1027
1111
|
// Record this dispatch against the budget
|
|
@@ -1130,6 +1214,7 @@ async function dispatch(input = {}) {
|
|
|
1130
1214
|
usage,
|
|
1131
1215
|
worktreeUsed: useWorktree,
|
|
1132
1216
|
autoReview,
|
|
1217
|
+
authVerified: true,
|
|
1133
1218
|
error: success ? null : errorText.slice(0, 200),
|
|
1134
1219
|
};
|
|
1135
1220
|
}
|
|
@@ -1221,4 +1306,4 @@ if (process.argv[1] && new URL(import.meta.url).pathname === process.argv[1]) {
|
|
|
1221
1306
|
}
|
|
1222
1307
|
}
|
|
1223
1308
|
|
|
1224
|
-
export { dispatch, buildCommand, detectRuntime, compressResult, dispatchDualBrain, validateDispatch, checkWorktreeClean, getRetryBudget, isInsideClaude, buildNativeDispatch, normalizeResult, loadSpecialistPrompt };
|
|
1309
|
+
export { dispatch, buildCommand, detectRuntime, compressResult, dispatchDualBrain, validateDispatch, checkWorktreeClean, getRetryBudget, isInsideClaude, buildNativeDispatch, normalizeResult, loadSpecialistPrompt, preflightAuth };
|