dual-brain 0.1.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.
- package/AGENTS.md +97 -0
- package/CLAUDE.md +147 -0
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/agents/implementer.md +22 -0
- package/agents/researcher.md +25 -0
- package/agents/verifier.md +30 -0
- package/bin/dual-brain.mjs +2868 -0
- package/hooks/auto-update-wrapper.mjs +102 -0
- package/hooks/auto-update.sh +67 -0
- package/hooks/budget-balancer.mjs +679 -0
- package/hooks/control-panel.mjs +1195 -0
- package/hooks/cost-logger.mjs +286 -0
- package/hooks/cost-report.mjs +351 -0
- package/hooks/decision-ledger.mjs +299 -0
- package/hooks/dual-brain-review.mjs +404 -0
- package/hooks/dual-brain-think.mjs +393 -0
- package/hooks/enforce-tier.mjs +469 -0
- package/hooks/failure-detector.mjs +138 -0
- package/hooks/gpt-work-dispatcher.mjs +512 -0
- package/hooks/head-guard.mjs +105 -0
- package/hooks/health-check.mjs +444 -0
- package/hooks/install-git-hooks.mjs +106 -0
- package/hooks/model-registry.mjs +859 -0
- package/hooks/plan-generator.mjs +544 -0
- package/hooks/profiles.mjs +254 -0
- package/hooks/quality-gate.mjs +355 -0
- package/hooks/risk-classifier.mjs +41 -0
- package/hooks/session-report.mjs +514 -0
- package/hooks/setup-wizard.mjs +130 -0
- package/hooks/summary-checkpoint.mjs +432 -0
- package/hooks/task-classifier.mjs +328 -0
- package/hooks/test-orchestrator.mjs +1077 -0
- package/hooks/vibe-memory.mjs +463 -0
- package/hooks/vibe-router.mjs +387 -0
- package/hooks/wave-orchestrator.mjs +1397 -0
- package/install.mjs +1541 -0
- package/mcp-server/README.md +81 -0
- package/mcp-server/index.mjs +388 -0
- package/orchestrator.json +215 -0
- package/package.json +108 -0
- package/playbooks/debug.json +49 -0
- package/playbooks/refactor.json +57 -0
- package/playbooks/security-audit.json +57 -0
- package/playbooks/security.json +38 -0
- package/playbooks/test-gen.json +48 -0
- package/plugin.json +22 -0
- package/review-rules.md +17 -0
- package/shell-hook.sh +26 -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/brief.mjs +266 -0
- package/src/decide.mjs +635 -0
- package/src/decompose.mjs +331 -0
- package/src/detect.mjs +345 -0
- package/src/dispatch.mjs +942 -0
- package/src/health.mjs +253 -0
- package/src/index.mjs +44 -0
- package/src/install-hooks.mjs +100 -0
- package/src/playbook.mjs +257 -0
- package/src/profile.mjs +990 -0
- package/src/redact.mjs +192 -0
- package/src/repo.mjs +292 -0
- package/src/session.mjs +1036 -0
- package/src/tui.mjs +197 -0
- package/src/update-check.mjs +35 -0
|
@@ -0,0 +1,387 @@
|
|
|
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
|
+
// ─── Ordered Language Detection ───────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
const DEPENDENCY_MARKERS = /\b(then|after\s+that|once\s+\S+\s+is\s+done|before|first|next|finally|afterwards|subsequently|followed\s+by|depends?\s+on|requires?)\b/i;
|
|
146
|
+
|
|
147
|
+
// ─── Subsystem Detection ─────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
const SUBSYSTEM_PATTERNS = [
|
|
150
|
+
{ key: 'auth', regex: /\b(auth|login|sign[-\s]?in|sign[-\s]?up|session|credential|password|oauth|jwt|token)\b/i },
|
|
151
|
+
{ key: 'billing', regex: /\b(billing|payment|subscription|invoice|charge|stripe|pricing)\b/i },
|
|
152
|
+
{ key: 'api', regex: /\b(api|endpoint|route|controller|handler|middleware|rest|graphql)\b/i },
|
|
153
|
+
{ key: 'ui', regex: /\b(ui|nav|button|page|component|layout|style|css|modal|form|menu|sidebar|header|footer|dashboard)\b/i },
|
|
154
|
+
{ key: 'db', regex: /\b(database|db|schema|migration|model|query|table|column|index|sql|prisma|sequelize|knex)\b/i },
|
|
155
|
+
{ key: 'infra', regex: /\b(deploy|ci|cd|docker|k8s|terraform|infra|pipeline|build|config|env)\b/i },
|
|
156
|
+
{ key: 'test', regex: /\b(test|spec|fixture|mock|stub|assert|coverage)\b/i },
|
|
157
|
+
{ key: 'docs', regex: /\b(doc|readme|changelog|guide|tutorial|comment)\b/i },
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
function detectSubsystems(text) {
|
|
161
|
+
const subs = new Set();
|
|
162
|
+
for (const pat of SUBSYSTEM_PATTERNS) {
|
|
163
|
+
if (pat.regex.test(text)) subs.add(pat.key);
|
|
164
|
+
}
|
|
165
|
+
return subs;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Risk Domain Extraction ──────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
function getRiskDomains(task) {
|
|
171
|
+
const domains = new Set();
|
|
172
|
+
// Use subsystem as risk domain
|
|
173
|
+
const subs = detectSubsystems(task.title);
|
|
174
|
+
for (const s of subs) domains.add(s);
|
|
175
|
+
// Also include explicit risk reason label
|
|
176
|
+
if (task.reason) {
|
|
177
|
+
const match = task.reason.match(/^([^(]+)/);
|
|
178
|
+
if (match) domains.add(match[1].trim().toLowerCase());
|
|
179
|
+
}
|
|
180
|
+
return domains;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Complexity + Wave Recommendation ──────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
function determineComplexity(tasks) {
|
|
186
|
+
const highestRisk = tasks.reduce(
|
|
187
|
+
(max, t) => LEVEL_ORDER[t.risk] > LEVEL_ORDER[max] ? t.risk : max,
|
|
188
|
+
'low'
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (tasks.length >= 4 || highestRisk === 'high' || highestRisk === 'critical') {
|
|
192
|
+
return 'complex';
|
|
193
|
+
}
|
|
194
|
+
if (tasks.length >= 2 || highestRisk === 'medium') {
|
|
195
|
+
return 'structured';
|
|
196
|
+
}
|
|
197
|
+
return 'simple';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* determineWave — Sequential by default, parallel only when tasks are truly independent.
|
|
202
|
+
*
|
|
203
|
+
* Returns { wave, reasons } where reasons is an array of reason codes:
|
|
204
|
+
* shared_surface — tasks likely touch same files
|
|
205
|
+
* high_risk — risky work should be sequential for review
|
|
206
|
+
* dependency_marker — ordered language detected in utterance
|
|
207
|
+
* same_subsystem — tasks in same domain/subsystem
|
|
208
|
+
* independent — truly independent, safe for parallel
|
|
209
|
+
*/
|
|
210
|
+
function determineWave(tasks, complexity, utterance) {
|
|
211
|
+
if (tasks.length === 1) return { wave: 'single', reasons: [] };
|
|
212
|
+
|
|
213
|
+
const reasons = [];
|
|
214
|
+
|
|
215
|
+
// 1. Check for ordered language in the original utterance
|
|
216
|
+
if (utterance && DEPENDENCY_MARKERS.test(utterance)) {
|
|
217
|
+
reasons.push('dependency_marker');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 2. Check for high/critical risk tasks
|
|
221
|
+
const hasHighRisk = tasks.some(t => t.risk === 'high' || t.risk === 'critical');
|
|
222
|
+
if (hasHighRisk) {
|
|
223
|
+
reasons.push('high_risk');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 3. Check for overlapping subsystems between tasks
|
|
227
|
+
const taskSubsystems = tasks.map(t => detectSubsystems(t.title));
|
|
228
|
+
let hasSharedSubsystem = false;
|
|
229
|
+
for (let i = 0; i < taskSubsystems.length; i++) {
|
|
230
|
+
for (let j = i + 1; j < taskSubsystems.length; j++) {
|
|
231
|
+
for (const sub of taskSubsystems[i]) {
|
|
232
|
+
if (taskSubsystems[j].has(sub)) {
|
|
233
|
+
hasSharedSubsystem = true;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (hasSharedSubsystem) break;
|
|
238
|
+
}
|
|
239
|
+
if (hasSharedSubsystem) break;
|
|
240
|
+
}
|
|
241
|
+
if (hasSharedSubsystem) {
|
|
242
|
+
reasons.push('same_subsystem');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 4. Check for overlapping file paths / shared surface area
|
|
246
|
+
const taskPaths = tasks.map(t => extractPaths(t.title));
|
|
247
|
+
let hasSharedPaths = false;
|
|
248
|
+
for (let i = 0; i < taskPaths.length; i++) {
|
|
249
|
+
for (let j = i + 1; j < taskPaths.length; j++) {
|
|
250
|
+
for (const p of taskPaths[i]) {
|
|
251
|
+
// Check if any path from task j shares a directory prefix or exact match
|
|
252
|
+
for (const q of taskPaths[j]) {
|
|
253
|
+
if (p === q || p.startsWith(q + '/') || q.startsWith(p + '/') ||
|
|
254
|
+
p.split('/').slice(0, -1).join('/') === q.split('/').slice(0, -1).join('/')) {
|
|
255
|
+
hasSharedPaths = true;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (hasSharedPaths) break;
|
|
260
|
+
}
|
|
261
|
+
if (hasSharedPaths) break;
|
|
262
|
+
}
|
|
263
|
+
if (hasSharedPaths) break;
|
|
264
|
+
}
|
|
265
|
+
if (hasSharedPaths) {
|
|
266
|
+
reasons.push('shared_surface');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 5. Check for shared risk domains
|
|
270
|
+
const taskDomains = tasks.map(t => getRiskDomains(t));
|
|
271
|
+
let hasSharedDomain = false;
|
|
272
|
+
for (let i = 0; i < taskDomains.length; i++) {
|
|
273
|
+
for (let j = i + 1; j < taskDomains.length; j++) {
|
|
274
|
+
for (const d of taskDomains[i]) {
|
|
275
|
+
if (taskDomains[j].has(d)) {
|
|
276
|
+
hasSharedDomain = true;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (hasSharedDomain) break;
|
|
281
|
+
}
|
|
282
|
+
if (hasSharedDomain) break;
|
|
283
|
+
}
|
|
284
|
+
// Only add same_subsystem if not already added (risk domains overlap with subsystems)
|
|
285
|
+
if (hasSharedDomain && !reasons.includes('same_subsystem')) {
|
|
286
|
+
reasons.push('same_subsystem');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Decision: parallel ONLY when no sequential reasons found
|
|
290
|
+
if (reasons.length === 0) {
|
|
291
|
+
reasons.push('independent');
|
|
292
|
+
return { wave: 'parallel', reasons };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { wave: 'sequential', reasons };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ─── Summary Generation ────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
function generateSummary(tasks, complexity, wave, qualityGates, profileHint) {
|
|
301
|
+
const parts = [];
|
|
302
|
+
|
|
303
|
+
if (tasks.length === 1) {
|
|
304
|
+
const t = tasks[0];
|
|
305
|
+
parts.push(`Single ${t.tier} task: ${t.title} (${t.risk} risk).`);
|
|
306
|
+
} else {
|
|
307
|
+
const taskDescs = tasks.map(t => `${t.title.toLowerCase()} (${t.risk} risk, ${t.tier})`);
|
|
308
|
+
parts.push(`Split into ${tasks.length} tasks: ${taskDescs.join(' + ')}.`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (wave === 'parallel' && tasks.length > 1) {
|
|
312
|
+
parts.push('Recommend parallel agents.');
|
|
313
|
+
} else if (wave === 'sequential') {
|
|
314
|
+
parts.push('Recommend sequential execution.');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (qualityGates.includes('dual_brain_required')) {
|
|
318
|
+
parts.push('Dual-brain review required for critical changes.');
|
|
319
|
+
} else if (qualityGates.includes('dual_brain_review')) {
|
|
320
|
+
parts.push('Dual-brain review recommended for high-risk changes.');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (profileHint) {
|
|
324
|
+
parts.push(`Profile hint: ${profileHint}.`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return parts.join(' ');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── Main Entry Point ──────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* routeVibe(utterance) — Decompose a casual natural language utterance
|
|
334
|
+
* into structured work orders with tier, risk, and quality gate assignments.
|
|
335
|
+
*
|
|
336
|
+
* @param {string} utterance - The user's casual description
|
|
337
|
+
* @returns {{ complexity, tasks, profile_hint, quality_gates, wave_recommendation, summary }}
|
|
338
|
+
*/
|
|
339
|
+
function routeVibe(utterance) {
|
|
340
|
+
if (!utterance || typeof utterance !== 'string' || !utterance.trim()) {
|
|
341
|
+
return {
|
|
342
|
+
complexity: 'simple',
|
|
343
|
+
tasks: [],
|
|
344
|
+
profile_hint: null,
|
|
345
|
+
quality_gates: ['self_check'],
|
|
346
|
+
wave_recommendation: 'single',
|
|
347
|
+
summary: 'No input provided.',
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const segments = splitTasks(utterance);
|
|
352
|
+
const tasks = segments.map(classifyTask);
|
|
353
|
+
const profileHint = detectProfileHint(utterance);
|
|
354
|
+
const qualityGates = determineQualityGates(tasks);
|
|
355
|
+
const complexity = determineComplexity(tasks);
|
|
356
|
+
const { wave, reasons } = determineWave(tasks, complexity, utterance);
|
|
357
|
+
const summary = generateSummary(tasks, complexity, wave, qualityGates, profileHint);
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
complexity,
|
|
361
|
+
tasks,
|
|
362
|
+
profile_hint: profileHint,
|
|
363
|
+
quality_gates: qualityGates,
|
|
364
|
+
wave_recommendation: wave,
|
|
365
|
+
wave_reasons: reasons,
|
|
366
|
+
summary,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export { routeVibe, splitTasks, classifyTask, detectProfileHint };
|
|
371
|
+
|
|
372
|
+
// ─── CLI ───────────────────────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
const isMain = process.argv[1] && (
|
|
375
|
+
process.argv[1].endsWith('vibe-router.mjs') ||
|
|
376
|
+
process.argv[1].endsWith('vibe-router')
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
if (isMain) {
|
|
380
|
+
const utterance = process.argv.slice(2).join(' ');
|
|
381
|
+
if (!utterance) {
|
|
382
|
+
console.error('Usage: node vibe-router.mjs "fix the login bug and also update the nav"');
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
const result = routeVibe(utterance);
|
|
386
|
+
console.log(JSON.stringify(result, null, 2));
|
|
387
|
+
}
|