dual-brain 6.1.1 → 7.0.1
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/AGENTS.md +97 -0
- package/agents/implementer.md +22 -0
- package/agents/researcher.md +25 -0
- package/agents/verifier.md +30 -0
- package/bin/dual-brain.mjs +100 -8
- package/hooks/enforce-tier.mjs +42 -1
- package/hooks/head-guard.mjs +69 -0
- package/hooks/head-guard.sh +8 -6
- package/mcp-server/README.md +81 -0
- package/mcp-server/index.mjs +330 -0
- package/package.json +8 -2
- package/plugin.json +22 -0
- package/skills/go.md +22 -0
- package/skills/review.md +19 -0
- package/skills/status.md +13 -0
- package/skills/think.md +22 -0
- package/src/dispatch.mjs +251 -3
- package/src/install-hooks.mjs +2 -2
- package/src/profile.mjs +308 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Dual-Brain Orchestrator — Codex Agent Instructions
|
|
2
|
+
|
|
3
|
+
You are a **work provider** in a dual-brain system. Claude Code is the orchestrator.
|
|
4
|
+
You are dispatched by `src/dispatch.mjs` to handle execute-tier tasks. Do not orchestrate — implement.
|
|
5
|
+
|
|
6
|
+
## Your Role
|
|
7
|
+
|
|
8
|
+
- **Tier**: Execute (`gpt-4.1` default, `o4-mini` for search, `gpt-5.4`/`gpt-5.5` for think-heavy work)
|
|
9
|
+
- **Dispatched by**: `node .claude/hooks/gpt-work-dispatcher.mjs --task "..." --tier execute`
|
|
10
|
+
- **You receive**: a scoped task, acceptance criteria, and file context
|
|
11
|
+
- **You return**: structured output (files changed, tests run, edge cases found)
|
|
12
|
+
|
|
13
|
+
You are NOT the orchestrator. Do not run `dual-brain go` or re-route tasks. Complete the work handed to you.
|
|
14
|
+
|
|
15
|
+
## Core Architecture (v6)
|
|
16
|
+
|
|
17
|
+
Four modules in `src/` form the decision pipeline:
|
|
18
|
+
|
|
19
|
+
- **`profile.mjs`** — Active profile, provider availability, subscription plan
|
|
20
|
+
- **`detect.mjs`** — Task intent, risk, complexity, tier classification
|
|
21
|
+
- **`decide.mjs`** — Provider/model/tier routing; budget pressure and dual-brain threshold
|
|
22
|
+
- **`dispatch.mjs`** — Executes decisions: Claude subagent, GPT via Codex, or dual-brain flow
|
|
23
|
+
|
|
24
|
+
## Tier System
|
|
25
|
+
|
|
26
|
+
| Tier | Model | Scope |
|
|
27
|
+
|------|-------|-------|
|
|
28
|
+
| Search | `o4-mini` | Read-only lookups, grep, explore |
|
|
29
|
+
| Execute | `gpt-4.1` | Edits, tests, git ops |
|
|
30
|
+
| Think | `gpt-5.4` / `gpt-5.5` | Architecture (usually Claude-side) |
|
|
31
|
+
|
|
32
|
+
## Structured Output Format
|
|
33
|
+
|
|
34
|
+
After completing any task, output a JSON block so the orchestrator can parse results:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"status": "done",
|
|
39
|
+
"files_changed": ["src/foo.mjs", "src/bar.mjs"],
|
|
40
|
+
"tests_run": ["npm test -- --grep foo"],
|
|
41
|
+
"edge_cases": ["what happens when X is null"],
|
|
42
|
+
"notes": "optional freeform"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For search-tier tasks, include `"files_found"` and `"line_refs"` instead of `files_changed`.
|
|
47
|
+
|
|
48
|
+
## Security Rules (No Exceptions)
|
|
49
|
+
|
|
50
|
+
- **Never** write secrets, tokens, or credentials to files
|
|
51
|
+
- **Never** implement auth/credential changes without a task brief that includes dual-brain approval
|
|
52
|
+
- If the task touches auth, credentials, billing, or migrations: stop, output `"status": "needs_approval"`, and explain why
|
|
53
|
+
- Use `--sandbox` mode when available; prefer `--approval-mode suggest` for destructive operations
|
|
54
|
+
|
|
55
|
+
## Quality Gate
|
|
56
|
+
|
|
57
|
+
Before finishing a session with code changes, run:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
node .claude/hooks/session-report.mjs
|
|
61
|
+
node .claude/hooks/quality-gate.mjs
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Gate statuses: `pass` (safe to end), `issues_found` (fix first), `needs_human_review` (escalate).
|
|
65
|
+
|
|
66
|
+
## Codex CLI Flags
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
codex --approval-mode suggest # Prompt before destructive shell ops
|
|
70
|
+
codex --sandbox # Isolate filesystem writes
|
|
71
|
+
codex exec --json "..." # Programmatic output (used by dispatch.mjs)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
When invoked by dispatch.mjs, `--json` output is expected. Always emit valid JSON in the structured output block.
|
|
75
|
+
|
|
76
|
+
## Routing Rules (for context)
|
|
77
|
+
|
|
78
|
+
1. Tasks under 3 min → Claude handles directly (Codex startup overhead not worth it)
|
|
79
|
+
2. Isolated tasks over 3 min → routed here by budget-balancer
|
|
80
|
+
3. High-risk decisions → dual-brain think (Claude + GPT deliberate before you implement)
|
|
81
|
+
4. Tier priority: think > execute > search
|
|
82
|
+
|
|
83
|
+
## Risk Classification
|
|
84
|
+
|
|
85
|
+
| Risk | Examples | Action |
|
|
86
|
+
|------|----------|--------|
|
|
87
|
+
| Critical | auth, secrets, tokens | Requires dual-brain approval before you touch it |
|
|
88
|
+
| High | billing, migrations | Confirm task brief includes approval |
|
|
89
|
+
| Medium | tests, utilities | Implement, note edge cases |
|
|
90
|
+
| Low | docs, comments | Implement freely |
|
|
91
|
+
|
|
92
|
+
## Hardcoded Stops
|
|
93
|
+
|
|
94
|
+
Do not proceed if:
|
|
95
|
+
- No task brief provided (ask for one via `"status": "needs_brief"`)
|
|
96
|
+
- Task scope exceeds 5 production files with no wave plan
|
|
97
|
+
- Task involves routing/dispatcher/tier logic changes without dual-brain sign-off
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Implementer Agent
|
|
2
|
+
|
|
3
|
+
You are a write-capable execution agent. Your role is to implement changes per a provided brief — no more, no less.
|
|
4
|
+
|
|
5
|
+
## Role
|
|
6
|
+
Execute changes exactly as specified in the brief. Run tests after every edit. Report what changed, what was tested, and any edge cases encountered.
|
|
7
|
+
|
|
8
|
+
## Allowed Tools
|
|
9
|
+
All tools are available: Read, Edit, Write, NotebookEdit, Bash, Agent, WebSearch, WebFetch.
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
- Implement only what the brief specifies — do not expand scope
|
|
13
|
+
- Run tests after completing edits (`node --test src/test.mjs` or the project test command)
|
|
14
|
+
- Never modify auth, credentials, or secrets without a dual-brain think decision on record
|
|
15
|
+
- If scope is unclear, stop and report — do not guess
|
|
16
|
+
|
|
17
|
+
## Output Format
|
|
18
|
+
Return:
|
|
19
|
+
- Files changed (absolute paths)
|
|
20
|
+
- Tests run and result (pass / fail / skipped)
|
|
21
|
+
- Edge cases encountered
|
|
22
|
+
- Any deviations from the brief (with reason)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Researcher Agent
|
|
2
|
+
|
|
3
|
+
You are a read-only research agent. Your role is to investigate, find code, and explore architecture — never to modify files.
|
|
4
|
+
|
|
5
|
+
## Role
|
|
6
|
+
Investigate the codebase, find relevant files, explore architecture, and report findings clearly with file paths and line references.
|
|
7
|
+
|
|
8
|
+
## Allowed Tools
|
|
9
|
+
- Read
|
|
10
|
+
- Bash (grep, find, cat — read-only commands only)
|
|
11
|
+
- WebSearch
|
|
12
|
+
- WebFetch
|
|
13
|
+
|
|
14
|
+
## Forbidden Tools
|
|
15
|
+
- Edit
|
|
16
|
+
- Write
|
|
17
|
+
- NotebookEdit
|
|
18
|
+
- Agent
|
|
19
|
+
|
|
20
|
+
## Output Format
|
|
21
|
+
Return:
|
|
22
|
+
- Files found (absolute paths)
|
|
23
|
+
- Line references for key code
|
|
24
|
+
- Confidence level (high / medium / low)
|
|
25
|
+
- Summary of findings
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Verifier Agent
|
|
2
|
+
|
|
3
|
+
You are a read-only verification agent. Your role is to run tests, lint, and type-check — never to modify files.
|
|
4
|
+
|
|
5
|
+
## Role
|
|
6
|
+
Verify correctness of the codebase after changes. Run all available test suites, report pass/fail, coverage delta, and any regressions.
|
|
7
|
+
|
|
8
|
+
## Allowed Tools
|
|
9
|
+
- Read
|
|
10
|
+
- Bash (test runners, lint, type-check — no file modifications)
|
|
11
|
+
|
|
12
|
+
## Forbidden Tools
|
|
13
|
+
- Edit
|
|
14
|
+
- Write
|
|
15
|
+
- NotebookEdit
|
|
16
|
+
- Agent
|
|
17
|
+
|
|
18
|
+
## Verification Steps
|
|
19
|
+
1. Run core tests: `node --test src/test.mjs`
|
|
20
|
+
2. Run hook tests if available: `node hooks/test-orchestrator.mjs`
|
|
21
|
+
3. Check for lint errors if a linter is configured
|
|
22
|
+
4. Report coverage delta if measurable
|
|
23
|
+
|
|
24
|
+
## Output Format
|
|
25
|
+
Return:
|
|
26
|
+
- Test result: pass / fail
|
|
27
|
+
- Tests run count and breakdown
|
|
28
|
+
- Regressions found (test name, failure message)
|
|
29
|
+
- Coverage delta (if available)
|
|
30
|
+
- Recommendation: safe to merge / needs fixes
|
package/bin/dual-brain.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ensureProfile, loadProfile, saveProfile, runOnboarding,
|
|
11
11
|
rememberPreference, forgetPreference, getActivePreferences,
|
|
12
12
|
getAvailableProviders, isSoloBrain, getHeadModel,
|
|
13
|
+
detectAuth, detectEnvironment,
|
|
13
14
|
} from '../src/profile.mjs';
|
|
14
15
|
|
|
15
16
|
import { detectTask } from '../src/detect.mjs';
|
|
@@ -45,6 +46,7 @@ dual-brain <command> [options]
|
|
|
45
46
|
|
|
46
47
|
Commands:
|
|
47
48
|
init First-time setup (providers, plans, optimization)
|
|
49
|
+
auth Show authentication status for all providers
|
|
48
50
|
install Install Claude Code hooks into the current project
|
|
49
51
|
go "task description" Detect → decide → dispatch a task
|
|
50
52
|
--dry-run Show routing decision without executing
|
|
@@ -64,6 +66,44 @@ Options:
|
|
|
64
66
|
`.trim());
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
// ─── Auth helpers ─────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Print a compact auth status table to stdout.
|
|
73
|
+
* @param {{ claude: object, openai: object }} auth Result from detectAuth()
|
|
74
|
+
*/
|
|
75
|
+
function printAuthTable(auth) {
|
|
76
|
+
const W = 55; // inner width (wide enough for source labels)
|
|
77
|
+
const bar = '═'.repeat(W);
|
|
78
|
+
const pad = (s) => {
|
|
79
|
+
const visible = s.replace(/[̀-ͯ]/g, ''); // strip combining chars for length
|
|
80
|
+
return s + ' '.repeat(Math.max(0, W - visible.length));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const claudeLine1 = auth.claude.found
|
|
84
|
+
? ` Claude: ✓ found via ${auth.claude.source}`
|
|
85
|
+
: ` Claude: ✗ not found`;
|
|
86
|
+
const claudeLine2 = auth.claude.found
|
|
87
|
+
? ` ${auth.claude.masked}`
|
|
88
|
+
: ` run: claude auth login`;
|
|
89
|
+
|
|
90
|
+
const openaiLine1 = auth.openai.found
|
|
91
|
+
? ` OpenAI: ✓ found via ${auth.openai.source}`
|
|
92
|
+
: ` OpenAI: ✗ not found`;
|
|
93
|
+
const openaiLine2 = auth.openai.found
|
|
94
|
+
? ` ${auth.openai.masked}`
|
|
95
|
+
: ` run: codex auth OR export OPENAI_API_KEY=sk-...`;
|
|
96
|
+
|
|
97
|
+
console.log(`╔${bar}╗`);
|
|
98
|
+
console.log(`║${pad(' Auth Status')}║`);
|
|
99
|
+
console.log(`╠${bar}╣`);
|
|
100
|
+
console.log(`║${pad(claudeLine1)}║`);
|
|
101
|
+
console.log(`║${pad(claudeLine2)}║`);
|
|
102
|
+
console.log(`║${pad(openaiLine1)}║`);
|
|
103
|
+
console.log(`║${pad(openaiLine2)}║`);
|
|
104
|
+
console.log(`╚${bar}╝`);
|
|
105
|
+
}
|
|
106
|
+
|
|
67
107
|
// ─── Card command (default) ──────────────────────────────────────────────────
|
|
68
108
|
|
|
69
109
|
async function cmdCard() {
|
|
@@ -83,19 +123,70 @@ async function cmdCard() {
|
|
|
83
123
|
const health = getHealth(cwd);
|
|
84
124
|
const card = formatSessionCard(session, repo, health);
|
|
85
125
|
console.log(card);
|
|
126
|
+
|
|
127
|
+
// Auth status warnings (non-blocking)
|
|
128
|
+
const auth = await detectAuth();
|
|
129
|
+
const warnings = [];
|
|
130
|
+
if (!auth.claude.found) warnings.push('Claude auth not found — run: claude auth login');
|
|
131
|
+
if (!auth.openai.found) warnings.push('OpenAI auth not found — run: codex auth OR export OPENAI_API_KEY=sk-...');
|
|
132
|
+
if (warnings.length > 0) {
|
|
133
|
+
console.log('\nAuth warnings:');
|
|
134
|
+
for (const w of warnings) console.log(` ⚠ ${w}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Environment info
|
|
138
|
+
const env = detectEnvironment();
|
|
139
|
+
if (env.isReplit || env.hasReplitTools) {
|
|
140
|
+
const envLabel = env.hasReplitTools ? 'Replit + replit-tools' : 'Replit';
|
|
141
|
+
console.log(`\nRuntime: ${envLabel}`);
|
|
142
|
+
}
|
|
86
143
|
}
|
|
87
144
|
|
|
88
145
|
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
89
146
|
|
|
90
147
|
async function cmdInit() {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
148
|
+
const cwd = process.cwd();
|
|
149
|
+
|
|
150
|
+
// --- Step 1: Auth preflight ---
|
|
151
|
+
const auth = await detectAuth();
|
|
152
|
+
printAuthTable(auth);
|
|
153
|
+
|
|
154
|
+
const noneFound = !auth.claude.found && !auth.openai.found;
|
|
155
|
+
if (noneFound) {
|
|
156
|
+
console.log('\nNo AI provider credentials found. Set up at least one before continuing:\n');
|
|
157
|
+
console.log(' Claude : claude auth login');
|
|
158
|
+
console.log(' OpenAI : codex auth OR export OPENAI_API_KEY=sk-...\n');
|
|
159
|
+
console.log('Re-run "dual-brain init" after authenticating.');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Step 2: Run onboarding wizard, skipping tiers for auto-detected plans ---
|
|
164
|
+
const profile = await runOnboarding({ interactive: true, detectedAuth: auth });
|
|
165
|
+
saveProfile(profile, { cwd });
|
|
166
|
+
|
|
167
|
+
// --- Step 3: Show dashboard + next step ---
|
|
168
|
+
console.log('');
|
|
169
|
+
const repo = loadRepoCache(cwd);
|
|
170
|
+
const session = loadSession(cwd);
|
|
171
|
+
const health = getHealth(cwd);
|
|
172
|
+
const card = formatSessionCard(session, repo, health);
|
|
173
|
+
console.log(card);
|
|
174
|
+
console.log('\nReady! Try: dual-brain go "your task here"\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function cmdAuth() {
|
|
178
|
+
const auth = await detectAuth();
|
|
179
|
+
printAuthTable(auth);
|
|
180
|
+
|
|
181
|
+
// If anything is missing, print setup commands
|
|
182
|
+
if (!auth.claude.found) {
|
|
183
|
+
console.log('\nTo set up Claude:');
|
|
184
|
+
console.log(' claude auth login');
|
|
185
|
+
}
|
|
186
|
+
if (!auth.openai.found) {
|
|
187
|
+
console.log('\nTo set up OpenAI:');
|
|
188
|
+
console.log(' codex auth OR export OPENAI_API_KEY=sk-...');
|
|
189
|
+
}
|
|
99
190
|
}
|
|
100
191
|
|
|
101
192
|
async function cmdGo(args) {
|
|
@@ -410,6 +501,7 @@ async function main() {
|
|
|
410
501
|
|
|
411
502
|
if (cmd === 'init') { await cmdInit(); return; }
|
|
412
503
|
if (cmd === 'install') { await cmdInstall(); return; }
|
|
504
|
+
if (cmd === 'auth') { await cmdAuth(); return; }
|
|
413
505
|
if (cmd === 'go') { await cmdGo(args.slice(1)); return; }
|
|
414
506
|
if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
|
|
415
507
|
if (cmd === 'hot') { cmdHot(args[1]); return; }
|
package/hooks/enforce-tier.mjs
CHANGED
|
@@ -195,6 +195,20 @@ function quickPressureCheck(tier) {
|
|
|
195
195
|
const SEARCH_WORDS = /\b(explore|search|find|grep|locate|where\s+is|list\s+files|read[-\s]?only|lookup|scan)\b/i;
|
|
196
196
|
const THINK_WORDS = /\b(plan|design|architect|review|audit|security|code[-\s]?review|threat[-\s]?model|complex[-\s]?debug)\b/i;
|
|
197
197
|
|
|
198
|
+
// ─── Write-intent enforcement ─────────────────────────────────────────────────
|
|
199
|
+
// Keywords that indicate an agent will mutate files or system state.
|
|
200
|
+
const WRITE_INTENT_WORDS = /\b(edit|fix|change|update|create|write|modify|implement|refactor|add|remove|delete|build|install|configure|patch|apply|move|rename|migrate|replace|rewrite|generate|scaffold|init(?:ialize)?|setup|deploy|run\s+tests?|commit|push|install|uninstall)\b/i;
|
|
201
|
+
|
|
202
|
+
// Dispatch marker prefix stamped by src/dispatch.mjs for all legitimate dispatches.
|
|
203
|
+
const DISPATCH_MARKER_RE = /<!--\s*dual-brain-dispatch:\s*[a-z0-9]+\s*-->/i;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Determine whether a prompt is purely read-only (no write keywords at all).
|
|
207
|
+
*/
|
|
208
|
+
function isReadOnly(prompt) {
|
|
209
|
+
return !WRITE_INTENT_WORDS.test(prompt);
|
|
210
|
+
}
|
|
211
|
+
|
|
198
212
|
function preferredModel(config, tier) {
|
|
199
213
|
const models = config?.subscriptions?.claude?.models ?? {};
|
|
200
214
|
for (const [name, meta] of Object.entries(models)) {
|
|
@@ -212,10 +226,37 @@ try {
|
|
|
212
226
|
}
|
|
213
227
|
|
|
214
228
|
const ti = input.tool_input || {};
|
|
215
|
-
|
|
229
|
+
// Use the raw prompt for dispatch-marker and write-intent checks (before lowercasing).
|
|
230
|
+
const rawPrompt = `${ti.description || ''} ${ti.prompt || ''}`;
|
|
231
|
+
const text = rawPrompt.toLowerCase();
|
|
216
232
|
const subType = (ti.subagent_type || '').toLowerCase();
|
|
217
233
|
const currentModel = (ti.model || '').toLowerCase();
|
|
218
234
|
|
|
235
|
+
// ── Dispatch pipeline gate ─────────────────────────────────────────────────
|
|
236
|
+
// Block write-capable agents that did NOT come through src/dispatch.mjs.
|
|
237
|
+
// Legitimate dispatches have a <!-- dual-brain-dispatch: <runId> --> marker
|
|
238
|
+
// prepended to the prompt by dispatch() / dispatchDualBrain().
|
|
239
|
+
//
|
|
240
|
+
// Skip enforcement when already inside a subagent (agent_id present) —
|
|
241
|
+
// nested agent spawns from within a work agent are fine.
|
|
242
|
+
const hasMarker = DISPATCH_MARKER_RE.test(rawPrompt);
|
|
243
|
+
const inSubagent = Boolean(input.agent_id);
|
|
244
|
+
|
|
245
|
+
if (!inSubagent && !hasMarker && !isReadOnly(rawPrompt)) {
|
|
246
|
+
// Write-intent detected in HEAD session without the dispatch marker → block.
|
|
247
|
+
process.stdout.write(JSON.stringify({
|
|
248
|
+
hookSpecificOutput: {
|
|
249
|
+
hookEventName: 'PreToolUse',
|
|
250
|
+
permissionDecision: 'deny',
|
|
251
|
+
permissionDecisionReason:
|
|
252
|
+
'[dual-brain] Write-capable agents must go through dispatch. Use: dual-brain go "task"',
|
|
253
|
+
},
|
|
254
|
+
}));
|
|
255
|
+
process.exit(2);
|
|
256
|
+
}
|
|
257
|
+
// (If hasMarker is true OR the prompt is read-only we fall through to normal
|
|
258
|
+
// tier-routing logic below.)
|
|
259
|
+
|
|
219
260
|
// Compute prompt hash early for duplicate detection and logging
|
|
220
261
|
const promptHash = computePromptHash(ti);
|
|
221
262
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// head-guard.mjs — Blocks HEAD from using mutation tools.
|
|
3
|
+
// Reads Claude Code hook stdin JSON protocol (PreToolUse event).
|
|
4
|
+
//
|
|
5
|
+
// Protocol (Claude Code sends this on stdin):
|
|
6
|
+
// { session_id, hook_event_name, tool_name, tool_input,
|
|
7
|
+
// tool_use_id, agent_id?, agent_type? }
|
|
8
|
+
//
|
|
9
|
+
// Exit behaviour:
|
|
10
|
+
// exit 0 → allow
|
|
11
|
+
// exit 2 + stdout JSON → block (permissionDecision: "deny")
|
|
12
|
+
//
|
|
13
|
+
// Key insight: `agent_id` is present when the hook fires inside a spawned
|
|
14
|
+
// subagent (work agent). If absent we are in the HEAD session.
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from 'fs';
|
|
17
|
+
|
|
18
|
+
const BLOCKED_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit', 'Bash']);
|
|
19
|
+
|
|
20
|
+
// Read stdin JSON payload
|
|
21
|
+
let input;
|
|
22
|
+
try {
|
|
23
|
+
const raw = readFileSync('/dev/stdin', 'utf8');
|
|
24
|
+
input = JSON.parse(raw);
|
|
25
|
+
} catch {
|
|
26
|
+
// If we can't read / parse input, fail open — don't break sessions
|
|
27
|
+
// that aren't using dual-brain at all.
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const toolName = input.tool_name || '';
|
|
32
|
+
|
|
33
|
+
// If this hook is firing inside a subagent, ALLOW — subagents are work agents
|
|
34
|
+
// and are permitted to edit/write/bash.
|
|
35
|
+
if (input.agent_id) {
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// HEAD session: block direct mutation tools
|
|
40
|
+
if (BLOCKED_TOOLS.has(toolName)) {
|
|
41
|
+
const output = {
|
|
42
|
+
hookSpecificOutput: {
|
|
43
|
+
hookEventName: 'PreToolUse',
|
|
44
|
+
permissionDecision: 'deny',
|
|
45
|
+
permissionDecisionReason:
|
|
46
|
+
`[dual-brain] HEAD cannot use ${toolName} directly. Dispatch via: dual-brain go "task description"`,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
process.stdout.write(JSON.stringify(output));
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Also block MCP filesystem write tools (any mcp__ tool with write/create/
|
|
54
|
+
// delete/remove/move/rename in the name).
|
|
55
|
+
if (toolName.startsWith('mcp__') && /write|create|delete|remove|move|rename/i.test(toolName)) {
|
|
56
|
+
const output = {
|
|
57
|
+
hookSpecificOutput: {
|
|
58
|
+
hookEventName: 'PreToolUse',
|
|
59
|
+
permissionDecision: 'deny',
|
|
60
|
+
permissionDecisionReason:
|
|
61
|
+
'[dual-brain] HEAD cannot use MCP write tools. Dispatch via: dual-brain go "task description"',
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
process.stdout.write(JSON.stringify(output));
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Allow everything else (Read, Agent handled by enforce-tier, etc.)
|
|
69
|
+
process.exit(0);
|
package/hooks/head-guard.sh
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# head-guard.sh —
|
|
2
|
+
# head-guard.sh — DEPRECATED. Replaced by head-guard.mjs.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# This file is kept for reference only. It never worked correctly because it
|
|
5
|
+
# reads CLAUDE_TOOL_NAME from the environment, but Claude Code delivers tool
|
|
6
|
+
# info via stdin JSON, not environment variables.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# The replacement (head-guard.mjs) reads stdin JSON, detects HEAD vs subagent
|
|
9
|
+
# via `agent_id`, and returns the correct permissionDecision block format.
|
|
10
|
+
#
|
|
11
|
+
# Do not use this file. See hooks/head-guard.mjs instead.
|
|
10
12
|
|
|
11
13
|
BLOCK_MSG='[dual-brain] HEAD cannot use this tool directly. Dispatch via: dual-brain go "task description"'
|
|
12
14
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# dual-brain MCP Server
|
|
2
|
+
|
|
3
|
+
Exposes dual-brain's routing engine as MCP tools so any MCP-compatible client (VS Code, Cursor, Windsurf, etc.) can use smart provider/model routing.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Add to your MCP client config:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"dual-brain": {
|
|
13
|
+
"command": "node",
|
|
14
|
+
"args": ["node_modules/dual-brain/mcp-server/index.mjs"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or if installed globally:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"dual-brain": {
|
|
26
|
+
"command": "node",
|
|
27
|
+
"args": ["/path/to/dual-brain/mcp-server/index.mjs"]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Tools
|
|
34
|
+
|
|
35
|
+
### `dual_brain_detect`
|
|
36
|
+
Classify a task into intent, risk, complexity, and tier.
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{ "prompt": "refactor the auth module", "files": ["src/auth.mjs"] }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Returns: `{ intent, risk, complexity, effort, tier, requiresWrite, explanation }`
|
|
43
|
+
|
|
44
|
+
### `dual_brain_decide`
|
|
45
|
+
Full routing decision — detect + route to provider and model.
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{ "prompt": "fix the login bug", "files": ["src/auth.mjs"], "profile": "quality-first" }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Returns: `{ provider, model, effort, tier, dualBrain, explanation, detection }`
|
|
52
|
+
|
|
53
|
+
### `dual_brain_status`
|
|
54
|
+
Provider health, routing scores, and session stats.
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Returns: `{ providers: { claude: {...}, openai: {...} }, session, profile }`
|
|
61
|
+
|
|
62
|
+
### `dual_brain_remember`
|
|
63
|
+
Save a routing preference that persists across sessions.
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{ "preference": "prefer claude for architecture tasks" }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Returns: `{ saved: true, preference, preferences: string[] }`
|
|
70
|
+
|
|
71
|
+
## Standalone test
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
node mcp-server/index.mjs
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then send JSON-RPC over stdin:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | node mcp-server/index.mjs
|
|
81
|
+
```
|