dual-brain 3.9.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * vibe-router.mjs — Intent compiler for vibe coding.
4
+ * Decomposes casual natural language into structured work orders.
5
+ *
6
+ * Export: routeVibe(utterance) → { tasks, profile_hint, quality_gates }
7
+ * CLI: node vibe-router.mjs "fix login bug and update the nav"
8
+ */
9
+
10
+ import { classifyRisk, extractPaths } from './risk-classifier.mjs';
11
+
12
+ // ─── Tier Detection Patterns ───────────────────────────────────────────────
13
+ // Aligned with enforce-tier.mjs SEARCH_WORDS, THINK_WORDS, and execute patterns.
14
+
15
+ const SEARCH_WORDS = /\b(explore|search|find|grep|locate|where\s+is|list\s+files|read[-\s]?only|lookup|scan|check|look|where|what)\b/i;
16
+ const THINK_WORDS = /\b(review|plan|design|architect|decide|analyze|audit|security|code[-\s]?review|threat[-\s]?model|complex[-\s]?debug|evaluate|compare|assess)\b/i;
17
+ const EXECUTE_WORDS = /\b(fix|build|add|update|edit|implement|refactor|delete|commit|test|run|create|modify|write|change|remove|rename|move|install|deploy|migrate|convert|replace|rewrite)\b/i;
18
+
19
+ // ─── Risk Keyword Patterns ─────────────────────────────────────────────────
20
+
21
+ const RISK_KEYWORDS = [
22
+ { level: 'critical', regex: /\b(auth|credential|secret|\.env|key[s]?|token[s]?|password|encrypt|certificate)\b/i, label: 'security-sensitive' },
23
+ { level: 'high', regex: /\b(login|payment|billing|deploy|migration|ci[-/]?cd|permission|policy|schema|api[-_]?contract)\b/i, label: 'high-impact' },
24
+ { level: 'medium', regex: /\b(test|spec|config|integration|shared|util|lib)\b/i, label: 'shared/tested code' },
25
+ { level: 'low', regex: /\b(readme|docs?|comment|format|lint|style|typo|changelog|nav|ui|css|color|font|margin|padding)\b/i, label: 'docs/UI' },
26
+ ];
27
+
28
+ const LEVEL_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
29
+
30
+ // ─── Task Splitting ────────────────────────────────────────────────────────
31
+
32
+ const TASK_SEPARATORS = /\b(?:and\s+(?:also\s+)?|also\s+|plus\s+|then\s+|after\s+that\s+|,\s*(?:and\s+)?)/i;
33
+
34
+ /**
35
+ * Split a casual utterance into individual task segments.
36
+ * Handles "and", "also", "plus", "then", "after that", and comma separators.
37
+ */
38
+ function splitTasks(utterance) {
39
+ if (!utterance) return [];
40
+
41
+ const segments = utterance
42
+ .split(TASK_SEPARATORS)
43
+ .map(s => s.trim())
44
+ .filter(s => s.length > 2);
45
+
46
+ // If no split happened, the whole utterance is a single task
47
+ return segments.length === 0 ? [utterance.trim()] : segments;
48
+ }
49
+
50
+ // ─── Per-Task Classification ───────────────────────────────────────────────
51
+
52
+ function classifyTier(text) {
53
+ if (THINK_WORDS.test(text)) return 'think';
54
+ if (EXECUTE_WORDS.test(text)) return 'execute';
55
+ if (SEARCH_WORDS.test(text)) return 'search';
56
+ return 'execute'; // default
57
+ }
58
+
59
+ function classifyKeywordRisk(text) {
60
+ let highest = { level: 'low', reason: 'general task' };
61
+
62
+ for (const pattern of RISK_KEYWORDS) {
63
+ const match = text.match(pattern.regex);
64
+ if (match && LEVEL_ORDER[pattern.level] > LEVEL_ORDER[highest.level]) {
65
+ highest = { level: pattern.level, reason: `${pattern.label} (${match[0]})` };
66
+ if (pattern.level === 'critical') return highest;
67
+ }
68
+ }
69
+
70
+ return highest;
71
+ }
72
+
73
+ function classifyTask(segment) {
74
+ const tier = classifyTier(segment);
75
+
76
+ // Check keyword-based risk
77
+ const keywordRisk = classifyKeywordRisk(segment);
78
+
79
+ // Check file-path-based risk (uses risk-classifier.mjs)
80
+ const paths = extractPaths(segment);
81
+ const pathRisk = classifyRisk(paths);
82
+
83
+ // Take the higher of keyword risk and path risk
84
+ const risk = LEVEL_ORDER[pathRisk.level] > LEVEL_ORDER[keywordRisk.level]
85
+ ? pathRisk
86
+ : keywordRisk;
87
+
88
+ // Generate a clean title: capitalize first letter, trim trailing punctuation
89
+ const title = segment.charAt(0).toUpperCase() + segment.slice(1).replace(/[.!?]+$/, '');
90
+
91
+ return {
92
+ title,
93
+ tier,
94
+ risk: risk.level,
95
+ reason: risk.reason,
96
+ };
97
+ }
98
+
99
+ // ─── Profile Hint Detection ────────────────────────────────────────────────
100
+
101
+ const QUALITY_HINT_WORDS = /\b(be\s+careful|take\s+your\s+time|thorough|deep\s+dive|carefully|exhaustive|comprehensive)\b/i;
102
+ const COST_HINT_WORDS = /\b(quick|fast|just|quickly|rapid|simple|straightforward)\b/i;
103
+
104
+ function detectProfileHint(utterance) {
105
+ if (QUALITY_HINT_WORDS.test(utterance)) return 'quality-first';
106
+ if (COST_HINT_WORDS.test(utterance)) return 'cost-saver';
107
+ return null;
108
+ }
109
+
110
+ // ─── Quality Gates ─────────────────────────────────────────────────────────
111
+
112
+ function determineQualityGates(tasks) {
113
+ const gates = new Set();
114
+
115
+ let highestRisk = 'low';
116
+ for (const task of tasks) {
117
+ if (LEVEL_ORDER[task.risk] > LEVEL_ORDER[highestRisk]) {
118
+ highestRisk = task.risk;
119
+ }
120
+ }
121
+
122
+ switch (highestRisk) {
123
+ case 'critical':
124
+ gates.add('dual_brain_required');
125
+ gates.add('tests');
126
+ gates.add('user_permission');
127
+ break;
128
+ case 'high':
129
+ gates.add('dual_brain_review');
130
+ gates.add('tests');
131
+ break;
132
+ case 'medium':
133
+ gates.add('tests');
134
+ break;
135
+ case 'low':
136
+ gates.add('self_check');
137
+ break;
138
+ }
139
+
140
+ return [...gates];
141
+ }
142
+
143
+ // ─── Complexity + Wave Recommendation ──────────────────────────────────────
144
+
145
+ function determineComplexity(tasks) {
146
+ const highestRisk = tasks.reduce(
147
+ (max, t) => LEVEL_ORDER[t.risk] > LEVEL_ORDER[max] ? t.risk : max,
148
+ 'low'
149
+ );
150
+
151
+ if (tasks.length >= 4 || highestRisk === 'high' || highestRisk === 'critical') {
152
+ return 'complex';
153
+ }
154
+ if (tasks.length >= 2 || highestRisk === 'medium') {
155
+ return 'structured';
156
+ }
157
+ return 'simple';
158
+ }
159
+
160
+ function determineWave(tasks, complexity) {
161
+ if (tasks.length === 1) return 'single';
162
+
163
+ // If any task depends on another (sequential markers like "then", "after that"
164
+ // were used), we already split them but keep sequential recommendation.
165
+ // For now, check if tasks share the same tier — parallel is fine for independent work.
166
+ const tiers = new Set(tasks.map(t => t.tier));
167
+ const hasHighRisk = tasks.some(t => t.risk === 'high' || t.risk === 'critical');
168
+
169
+ if (hasHighRisk) return 'sequential'; // high-risk tasks need careful ordering
170
+ if (tiers.size === 1 && complexity !== 'complex') return 'parallel';
171
+ return 'parallel';
172
+ }
173
+
174
+ // ─── Summary Generation ────────────────────────────────────────────────────
175
+
176
+ function generateSummary(tasks, complexity, wave, qualityGates, profileHint) {
177
+ const parts = [];
178
+
179
+ if (tasks.length === 1) {
180
+ const t = tasks[0];
181
+ parts.push(`Single ${t.tier} task: ${t.title} (${t.risk} risk).`);
182
+ } else {
183
+ const taskDescs = tasks.map(t => `${t.title.toLowerCase()} (${t.risk} risk, ${t.tier})`);
184
+ parts.push(`Split into ${tasks.length} tasks: ${taskDescs.join(' + ')}.`);
185
+ }
186
+
187
+ if (wave === 'parallel' && tasks.length > 1) {
188
+ parts.push('Recommend parallel agents.');
189
+ } else if (wave === 'sequential') {
190
+ parts.push('Recommend sequential execution.');
191
+ }
192
+
193
+ if (qualityGates.includes('dual_brain_required')) {
194
+ parts.push('Dual-brain review required for critical changes.');
195
+ } else if (qualityGates.includes('dual_brain_review')) {
196
+ parts.push('Dual-brain review recommended for high-risk changes.');
197
+ }
198
+
199
+ if (profileHint) {
200
+ parts.push(`Profile hint: ${profileHint}.`);
201
+ }
202
+
203
+ return parts.join(' ');
204
+ }
205
+
206
+ // ─── Main Entry Point ──────────────────────────────────────────────────────
207
+
208
+ /**
209
+ * routeVibe(utterance) — Decompose a casual natural language utterance
210
+ * into structured work orders with tier, risk, and quality gate assignments.
211
+ *
212
+ * @param {string} utterance - The user's casual description
213
+ * @returns {{ complexity, tasks, profile_hint, quality_gates, wave_recommendation, summary }}
214
+ */
215
+ function routeVibe(utterance) {
216
+ if (!utterance || typeof utterance !== 'string' || !utterance.trim()) {
217
+ return {
218
+ complexity: 'simple',
219
+ tasks: [],
220
+ profile_hint: null,
221
+ quality_gates: ['self_check'],
222
+ wave_recommendation: 'single',
223
+ summary: 'No input provided.',
224
+ };
225
+ }
226
+
227
+ const segments = splitTasks(utterance);
228
+ const tasks = segments.map(classifyTask);
229
+ const profileHint = detectProfileHint(utterance);
230
+ const qualityGates = determineQualityGates(tasks);
231
+ const complexity = determineComplexity(tasks);
232
+ const wave = determineWave(tasks, complexity);
233
+ const summary = generateSummary(tasks, complexity, wave, qualityGates, profileHint);
234
+
235
+ return {
236
+ complexity,
237
+ tasks,
238
+ profile_hint: profileHint,
239
+ quality_gates: qualityGates,
240
+ wave_recommendation: wave,
241
+ summary,
242
+ };
243
+ }
244
+
245
+ export { routeVibe, splitTasks, classifyTask, detectProfileHint };
246
+
247
+ // ─── CLI ───────────────────────────────────────────────────────────────────
248
+
249
+ const isMain = process.argv[1] && (
250
+ process.argv[1].endsWith('vibe-router.mjs') ||
251
+ process.argv[1].endsWith('vibe-router')
252
+ );
253
+
254
+ if (isMain) {
255
+ const utterance = process.argv.slice(2).join(' ');
256
+ if (!utterance) {
257
+ console.error('Usage: node vibe-router.mjs "fix the login bug and also update the nav"');
258
+ process.exit(1);
259
+ }
260
+ const result = routeVibe(utterance);
261
+ console.log(JSON.stringify(result, null, 2));
262
+ }
package/install.mjs CHANGED
@@ -315,6 +315,7 @@ function generateGitignoreEntries(workspace) {
315
315
  '.claude/hooks/usage-summary-*.json',
316
316
  '.claude/hooks/decision-ledger.jsonl',
317
317
  '.claude/.launched',
318
+ '.claude/dual-brain.memory.json',
318
319
  ];
319
320
  let existing = '';
320
321
  try { existing = readFileSync(join(workspace, '.gitignore'), 'utf8'); } catch {}
@@ -338,6 +339,7 @@ function install(workspace, env, mode) {
338
339
  'gpt-work-dispatcher.mjs', 'profiles.mjs',
339
340
  'summary-checkpoint.mjs', 'decision-ledger.mjs', 'control-panel.mjs',
340
341
  'risk-classifier.mjs', 'failure-detector.mjs',
342
+ 'vibe-router.mjs', 'plan-generator.mjs', 'vibe-memory.mjs',
341
343
  ];
342
344
  for (const h of HOOKS) cpSync(join(__dirname, 'hooks', h), join(target, 'hooks', h));
343
345
  actions.push(`✓ ${HOOKS.length} hook scripts`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "3.9.0",
3
+ "version": "4.0.0",
4
4
  "description": "Dual-provider orchestration for Claude Code — tiered routing, budget balancing, and GPT dual-brain review across Claude + OpenAI subscriptions",
5
5
  "type": "module",
6
6
  "bin": {