dual-brain 0.2.6 → 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 +216 -79
- 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
|
|
|
@@ -1040,43 +1040,58 @@ async function installGlobal() {
|
|
|
1040
1040
|
return;
|
|
1041
1041
|
}
|
|
1042
1042
|
|
|
1043
|
-
//
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
];
|
|
1063
|
-
const postToolHooks = [
|
|
1064
|
-
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'cost-logger.mjs')} ${DB_MARKER}` }] },
|
|
1065
|
-
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'auto-update-wrapper.mjs')} ${DB_MARKER}` }] },
|
|
1066
|
-
];
|
|
1067
|
-
|
|
1068
|
-
// Remove any existing dual-brain hooks (idempotent)
|
|
1069
|
-
const isDBHook = (entry) => entry.hooks?.some(h => h.command?.includes(DB_MARKER));
|
|
1070
|
-
existing.hooks.PreToolUse = existing.hooks.PreToolUse.filter(e => !isDBHook(e));
|
|
1071
|
-
existing.hooks.PostToolUse = existing.hooks.PostToolUse.filter(e => !isDBHook(e));
|
|
1043
|
+
// Check if project-local hooks already exist (avoids double-firing)
|
|
1044
|
+
const projectLocalSettings = join(pkgRoot, '.claude', 'settings.local.json');
|
|
1045
|
+
const hasProjectLocalHooks = (() => {
|
|
1046
|
+
if (!existsSync(projectLocalSettings)) return false;
|
|
1047
|
+
try {
|
|
1048
|
+
const content = readFileSync(projectLocalSettings, 'utf8');
|
|
1049
|
+
return content.includes('dual-brain') || content.includes('head-guard');
|
|
1050
|
+
} catch { return false; }
|
|
1051
|
+
})();
|
|
1052
|
+
|
|
1053
|
+
if (hasProjectLocalHooks) {
|
|
1054
|
+
console.log(' hooks already configured project-locally, skipping global hooks');
|
|
1055
|
+
console.log(' (project .claude/settings.local.json already contains dual-brain hooks)');
|
|
1056
|
+
} else {
|
|
1057
|
+
// Load existing settings (merge, never clobber)
|
|
1058
|
+
let existing = {};
|
|
1059
|
+
if (existsSync(globalSettingsPath)) {
|
|
1060
|
+
try { existing = JSON.parse(readFileSync(globalSettingsPath, 'utf8')); } catch {}
|
|
1061
|
+
}
|
|
1072
1062
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1063
|
+
// Ensure hooks structure exists
|
|
1064
|
+
if (!existing.hooks) existing.hooks = {};
|
|
1065
|
+
if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = [];
|
|
1066
|
+
if (!existing.hooks.PostToolUse) existing.hooks.PostToolUse = [];
|
|
1076
1067
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1068
|
+
// Define dual-brain hooks with ownership marker
|
|
1069
|
+
const DB_MARKER = '# dual-brain-managed';
|
|
1070
|
+
const preToolHooks = [
|
|
1071
|
+
{ matcher: 'Edit', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1072
|
+
{ matcher: 'Write', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1073
|
+
{ matcher: 'NotebookEdit',hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1074
|
+
{ matcher: 'Bash', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1075
|
+
{ matcher: 'Agent', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'enforce-tier.mjs')} ${DB_MARKER}` }] },
|
|
1076
|
+
];
|
|
1077
|
+
const postToolHooks = [
|
|
1078
|
+
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'cost-logger.mjs')} ${DB_MARKER}` }] },
|
|
1079
|
+
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'auto-update-wrapper.mjs')} ${DB_MARKER}` }] },
|
|
1080
|
+
];
|
|
1081
|
+
|
|
1082
|
+
// Remove any existing dual-brain hooks (idempotent)
|
|
1083
|
+
const isDBHook = (entry) => entry.hooks?.some(h => h.command?.includes(DB_MARKER));
|
|
1084
|
+
existing.hooks.PreToolUse = existing.hooks.PreToolUse.filter(e => !isDBHook(e));
|
|
1085
|
+
existing.hooks.PostToolUse = existing.hooks.PostToolUse.filter(e => !isDBHook(e));
|
|
1086
|
+
|
|
1087
|
+
// Add dual-brain hooks
|
|
1088
|
+
existing.hooks.PreToolUse.push(...preToolHooks);
|
|
1089
|
+
existing.hooks.PostToolUse.push(...postToolHooks);
|
|
1090
|
+
|
|
1091
|
+
// Write merged settings
|
|
1092
|
+
mkdirSync(globalClaudeDir, { recursive: true });
|
|
1093
|
+
writeFileSync(globalSettingsPath, JSON.stringify(existing, null, 2) + '\n');
|
|
1094
|
+
}
|
|
1080
1095
|
|
|
1081
1096
|
// Write minimal global CLAUDE.md (only if none exists, or append section)
|
|
1082
1097
|
const globalClaudeMd = join(globalClaudeDir, 'CLAUDE.md');
|
|
@@ -1091,12 +1106,16 @@ async function installGlobal() {
|
|
|
1091
1106
|
}
|
|
1092
1107
|
}
|
|
1093
1108
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1109
|
+
if (!hasProjectLocalHooks) {
|
|
1110
|
+
console.log(' + dual-brain hooks installed globally');
|
|
1111
|
+
console.log(' hooks dir: ' + hooksDir);
|
|
1112
|
+
console.log(' settings: ' + globalSettingsPath);
|
|
1113
|
+
console.log('');
|
|
1114
|
+
console.log(' All new Claude sessions will load dual-brain hooks.');
|
|
1115
|
+
console.log(' Run "dual-brain uninstall --global" to remove.');
|
|
1116
|
+
}
|
|
1117
|
+
console.log(' + global CLAUDE.md updated');
|
|
1118
|
+
console.log(' path: ' + globalClaudeDir);
|
|
1100
1119
|
}
|
|
1101
1120
|
|
|
1102
1121
|
async function uninstallGlobal() {
|
|
@@ -2163,6 +2182,15 @@ async function mainScreen(rl, ask) {
|
|
|
2163
2182
|
dashSpinner = fx.spinner('Loading dashboard...').start();
|
|
2164
2183
|
}
|
|
2165
2184
|
|
|
2185
|
+
// ── One-time default shell prompt for returning users (never asked before) ─
|
|
2186
|
+
if (profile.setupComplete && !profile.defaultShellAsked) {
|
|
2187
|
+
if (dashSpinner) { dashSpinner.stop(); dashSpinner = null; }
|
|
2188
|
+
const wantsDefault = await askDefaultShell(cwd, rl, fx);
|
|
2189
|
+
profile.defaultShellAsked = true;
|
|
2190
|
+
profile.isDefaultShell = wantsDefault;
|
|
2191
|
+
saveProfile(profile, { cwd });
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2166
2194
|
const claudeSub = profile?.providers?.claude;
|
|
2167
2195
|
const openaiSub = profile?.providers?.openai;
|
|
2168
2196
|
|
|
@@ -2566,8 +2594,7 @@ async function mainScreen(rl, ask) {
|
|
|
2566
2594
|
|
|
2567
2595
|
// ── Recent work items (dim, max 3) ────────────────────────────────────────
|
|
2568
2596
|
const recentLines = recentWorkItems.slice(0, 3).map(item => {
|
|
2569
|
-
|
|
2570
|
-
return ` ${DIM}${prefix} ${item.text}${RST}`;
|
|
2597
|
+
return signalLine(item.ok ? 'success' : 'warning', `${DIM}${item.text}${RST}`);
|
|
2571
2598
|
});
|
|
2572
2599
|
|
|
2573
2600
|
// ── Resolve dashboard spinner before rendering ────────────────────────────
|
|
@@ -2578,26 +2605,48 @@ async function mainScreen(rl, ask) {
|
|
|
2578
2605
|
process.stdout.write(`${DIM}${staleCount} stale sessions (>7d) — type "sessions" to manage${RST}\n`);
|
|
2579
2606
|
}
|
|
2580
2607
|
|
|
2581
|
-
// ── Render Studio Console
|
|
2582
|
-
const
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
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)
|
|
2591
2640
|
if (recentLines.length > 0) {
|
|
2592
|
-
|
|
2593
|
-
out.push(...recentLines); // ✓ / ! recent work items
|
|
2641
|
+
process.stdout.write(panel('Signals', recentLines, { width: panelW }) + '\n\n');
|
|
2594
2642
|
}
|
|
2595
|
-
out.push('');
|
|
2596
|
-
out.push(` ${sepLine}`); // ━━━━ separator
|
|
2597
|
-
// Input bar rendered inline — the key handler will overwrite this line
|
|
2598
|
-
out.push(` ${DIM}> task or command...${RST}${' '.repeat(Math.max(1, sepW - 22))}${DIM}[?] help${RST}`);
|
|
2599
2643
|
|
|
2600
|
-
|
|
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`);
|
|
2601
2650
|
|
|
2602
2651
|
// ── Key handling ──────────────────────────────────────────────────────────
|
|
2603
2652
|
// Use raw keypress mode so we can show a live type-to-start buffer.
|
|
@@ -3535,28 +3584,38 @@ async function settingsScreen(rl, ask) {
|
|
|
3535
3584
|
` ${DIM}Doctor${RESET} ${doctorStr}`,
|
|
3536
3585
|
];
|
|
3537
3586
|
|
|
3538
|
-
// ── Render
|
|
3539
|
-
const
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
...subsLines,
|
|
3545
|
-
` ${DIM}[a] add [r] remove [h] health check${RESET}`,
|
|
3546
|
-
'',
|
|
3547
|
-
` ${DIM}Work style${RESET}`,
|
|
3548
|
-
...wsLines,
|
|
3549
|
-
` ${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(/^ /, '')),
|
|
3550
3593
|
'',
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3594
|
+
signalLine('info', `${DIM}[a] add [r] remove [h] health check${RESET}`),
|
|
3595
|
+
];
|
|
3596
|
+
|
|
3597
|
+
const wsContent = [
|
|
3598
|
+
...wsLines.map(l => l.replace(/^ /, '')),
|
|
3554
3599
|
'',
|
|
3555
|
-
|
|
3556
|
-
|
|
3600
|
+
signalLine('info', `${DIM}[1-3] change${RESET}`),
|
|
3601
|
+
];
|
|
3602
|
+
|
|
3603
|
+
const sysContent = [
|
|
3604
|
+
...sysLines.map(l => l.replace(/^ /, '')),
|
|
3557
3605
|
'',
|
|
3606
|
+
signalLine('info', `${DIM}[d] run doctor [x] diagnostics${RESET}`),
|
|
3607
|
+
];
|
|
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}`] : []),
|
|
3558
3612
|
];
|
|
3559
|
-
|
|
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');
|
|
3560
3619
|
|
|
3561
3620
|
const raw = (await ask(' Choice: ')).trim();
|
|
3562
3621
|
const choice = raw.toLowerCase();
|
|
@@ -3984,6 +4043,67 @@ function saveWizardCredentials(cwd, detectedProviders) {
|
|
|
3984
4043
|
* @param {object} rl readline interface
|
|
3985
4044
|
* @returns {object|null} profile object to save, or null if cancelled/skipped
|
|
3986
4045
|
*/
|
|
4046
|
+
function setAsDefaultShell(cwd) {
|
|
4047
|
+
const root = cwd || process.cwd();
|
|
4048
|
+
const replitPath = join(root, '.replit');
|
|
4049
|
+
if (!existsSync(replitPath)) return;
|
|
4050
|
+
|
|
4051
|
+
let content = readFileSync(replitPath, 'utf8');
|
|
4052
|
+
const newOnBoot = 'onBoot = "source /home/runner/workspace/.replit-tools/scripts/setup-claude-code.sh 2>/dev/null || true; ln -sf /home/runner/workspace/.replit-tools/.npm-persistent/.npmrc ~/.npmrc 2>/dev/null || true; dual-brain install --global 2>/dev/null || true"';
|
|
4053
|
+
|
|
4054
|
+
if (content.match(/^onBoot\s*=/m)) {
|
|
4055
|
+
content = content.replace(/^onBoot\s*=.*$/m, newOnBoot);
|
|
4056
|
+
} else {
|
|
4057
|
+
content += '\n' + newOnBoot + '\n';
|
|
4058
|
+
}
|
|
4059
|
+
writeFileSync(replitPath, content);
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
function removeAsDefaultShell(cwd) {
|
|
4063
|
+
const root = cwd || process.cwd();
|
|
4064
|
+
const replitPath = join(root, '.replit');
|
|
4065
|
+
if (!existsSync(replitPath)) return;
|
|
4066
|
+
|
|
4067
|
+
let content = readFileSync(replitPath, 'utf8');
|
|
4068
|
+
const origOnBoot = 'onBoot = "source /home/runner/workspace/.replit-tools/scripts/setup-claude-code.sh 2>/dev/null || true"';
|
|
4069
|
+
if (content.match(/^onBoot\s*=/m)) {
|
|
4070
|
+
content = content.replace(/^onBoot\s*=.*$/m, origOnBoot);
|
|
4071
|
+
writeFileSync(replitPath, content);
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
async function askDefaultShell(cwd, rl, fx) {
|
|
4076
|
+
const cl = fx.colors || {};
|
|
4077
|
+
const DIM = cl.dim || '';
|
|
4078
|
+
const BOLD = cl.bold || '';
|
|
4079
|
+
const CYAN = cl.cyan || '\x1b[36m';
|
|
4080
|
+
const YLW = cl.yellow || '\x1b[33m';
|
|
4081
|
+
const GREEN = cl.green || '';
|
|
4082
|
+
const RST = cl.reset || '';
|
|
4083
|
+
|
|
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');
|
|
4093
|
+
|
|
4094
|
+
const answer = await new Promise(res => rl.question(' ', (a) => res(a.trim().toLowerCase())));
|
|
4095
|
+
const yes = !answer || answer.startsWith('y');
|
|
4096
|
+
|
|
4097
|
+
if (yes) {
|
|
4098
|
+
setAsDefaultShell(cwd);
|
|
4099
|
+
process.stdout.write(` ${GREEN}+${RST} ${DIM}dual-brain will start on boot. Change anytime in Settings.${RST}\n`);
|
|
4100
|
+
} else {
|
|
4101
|
+
process.stdout.write(` ${DIM}No problem. Run dual-brain anytime from the command line.${RST}\n`);
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
return yes;
|
|
4105
|
+
}
|
|
4106
|
+
|
|
3987
4107
|
async function runOnboardingWizard(_detection, cwd, rl) {
|
|
3988
4108
|
const fx = await getFx();
|
|
3989
4109
|
const cl = fx.colors || {};
|
|
@@ -4278,6 +4398,23 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
4278
4398
|
finalProfile.bias = chosenBias;
|
|
4279
4399
|
finalProfile.workStyle = chosenBias;
|
|
4280
4400
|
|
|
4401
|
+
// Ask about default shell (only on first wizard run)
|
|
4402
|
+
if (!finalProfile.defaultShellAsked) {
|
|
4403
|
+
const wantsDefault = await askDefaultShell(cwd, rl, fx);
|
|
4404
|
+
finalProfile.defaultShellAsked = true;
|
|
4405
|
+
finalProfile.isDefaultShell = wantsDefault;
|
|
4406
|
+
saveProfile(finalProfile, { cwd });
|
|
4407
|
+
|
|
4408
|
+
// Also run global install if they said yes
|
|
4409
|
+
if (wantsDefault) {
|
|
4410
|
+
try {
|
|
4411
|
+
execSync('node ' + join(dirname(fileURLToPath(import.meta.url)), 'dual-brain.mjs') + ' install --global', {
|
|
4412
|
+
cwd, stdio: 'pipe', timeout: 10000,
|
|
4413
|
+
});
|
|
4414
|
+
} catch {}
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4281
4418
|
return finalProfile;
|
|
4282
4419
|
}
|
|
4283
4420
|
|
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",
|