dual-brain 3.8.1 → 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.
- package/README.md +1 -1
- package/hooks/control-panel.mjs +27 -3
- package/hooks/cost-logger.mjs +2 -3
- package/hooks/enforce-tier.mjs +15 -18
- package/hooks/failure-detector.mjs +15 -1
- package/hooks/plan-generator.mjs +544 -0
- package/hooks/profiles.mjs +35 -4
- package/hooks/test-orchestrator.mjs +67 -3
- package/hooks/vibe-memory.mjs +463 -0
- package/hooks/vibe-router.mjs +262 -0
- package/install.mjs +33 -15
- package/package.json +1 -1
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* plan-generator.mjs — Generates Steve-style markdown execution plans.
|
|
4
|
+
*
|
|
5
|
+
* For complex requests, produces a 3-part plan:
|
|
6
|
+
* Part 1: Numbered tasks ordered by dependency
|
|
7
|
+
* Part 2: User stories and edge cases
|
|
8
|
+
* Part 3: Questions with suggested answers
|
|
9
|
+
*
|
|
10
|
+
* Export: generatePlan(vibeResult, context?) → { markdown, planPath }
|
|
11
|
+
* CLI: node plan-generator.mjs --utterance "..." [--write]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
15
|
+
import { dirname, join } from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
import { getActiveProfile } from './profiles.mjs';
|
|
18
|
+
import { classifyRisk } from './risk-classifier.mjs';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const PLANS_DIR = join(__dirname, '..', 'plans');
|
|
22
|
+
|
|
23
|
+
// ─── Tier ordering for dependency sort ─────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const TIER_ORDER = { search: 0, execute: 1, think: 2, review: 3 };
|
|
26
|
+
|
|
27
|
+
// ─── Dependency resolution ─────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sort tasks by dependency and tier order:
|
|
31
|
+
* - Search tasks before execute tasks on the same topic
|
|
32
|
+
* - Think/review tasks after execute tasks
|
|
33
|
+
* - Independent tasks remain in original order
|
|
34
|
+
*/
|
|
35
|
+
function resolveDependencies(tasks) {
|
|
36
|
+
if (!tasks || tasks.length === 0) return [];
|
|
37
|
+
|
|
38
|
+
const indexed = tasks.map((t, i) => ({
|
|
39
|
+
...t,
|
|
40
|
+
_origIndex: i,
|
|
41
|
+
tier: (t.tier || 'execute').toLowerCase(),
|
|
42
|
+
topic: t.topic || t.title || '',
|
|
43
|
+
dependencies: t.dependencies || [],
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Group tasks by topic to infer intra-topic dependencies
|
|
47
|
+
const byTopic = new Map();
|
|
48
|
+
for (const t of indexed) {
|
|
49
|
+
const key = t.topic.toLowerCase().replace(/\s+/g, '-') || `task-${t._origIndex}`;
|
|
50
|
+
if (!byTopic.has(key)) byTopic.set(key, []);
|
|
51
|
+
byTopic.get(key).push(t);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Within each topic group, sort by tier order
|
|
55
|
+
for (const [, group] of byTopic) {
|
|
56
|
+
group.sort((a, b) => (TIER_ORDER[a.tier] ?? 1) - (TIER_ORDER[b.tier] ?? 1));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Flatten back, preserving intra-topic order and original order for cross-topic
|
|
60
|
+
const sorted = [];
|
|
61
|
+
const placed = new Set();
|
|
62
|
+
|
|
63
|
+
// Place topic groups in the order their first task appeared
|
|
64
|
+
const topicOrder = [];
|
|
65
|
+
for (const t of indexed) {
|
|
66
|
+
const key = t.topic.toLowerCase().replace(/\s+/g, '-') || `task-${t._origIndex}`;
|
|
67
|
+
if (!topicOrder.includes(key)) topicOrder.push(key);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const key of topicOrder) {
|
|
71
|
+
const group = byTopic.get(key) || [];
|
|
72
|
+
for (const t of group) {
|
|
73
|
+
if (!placed.has(t._origIndex)) {
|
|
74
|
+
placed.add(t._origIndex);
|
|
75
|
+
sorted.push(t);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Assign sequential IDs and compute dependency labels
|
|
81
|
+
const numbered = sorted.map((t, i) => {
|
|
82
|
+
const num = i + 1;
|
|
83
|
+
const deps = [];
|
|
84
|
+
|
|
85
|
+
// Explicit dependencies
|
|
86
|
+
if (t.dependencies.length > 0) {
|
|
87
|
+
for (const dep of t.dependencies) {
|
|
88
|
+
if (typeof dep === 'number') {
|
|
89
|
+
deps.push(`Task ${dep}`);
|
|
90
|
+
} else {
|
|
91
|
+
// Find by title match
|
|
92
|
+
const match = sorted.findIndex(s => s.title === dep || s.topic === dep);
|
|
93
|
+
if (match >= 0 && match < i) deps.push(`Task ${match + 1}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Implicit: within same topic, each task depends on the previous
|
|
99
|
+
const topicKey = t.topic.toLowerCase().replace(/\s+/g, '-') || `task-${t._origIndex}`;
|
|
100
|
+
const topicGroup = byTopic.get(topicKey) || [];
|
|
101
|
+
const posInGroup = topicGroup.indexOf(t);
|
|
102
|
+
if (posInGroup > 0 && deps.length === 0) {
|
|
103
|
+
const prevInGroup = topicGroup[posInGroup - 1];
|
|
104
|
+
const prevNum = sorted.indexOf(prevInGroup) + 1;
|
|
105
|
+
if (prevNum > 0 && prevNum < num) deps.push(`Task ${prevNum}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
num,
|
|
110
|
+
title: t.title || `Task ${num}`,
|
|
111
|
+
tier: t.tier,
|
|
112
|
+
risk: t.risk || classifyRisk(t.files || []).level,
|
|
113
|
+
dependencies: deps.length > 0 ? deps.join(', ') : '—',
|
|
114
|
+
files: t.files || [],
|
|
115
|
+
description: t.description || '',
|
|
116
|
+
canParallel: deps.length === 0 && posInGroup === 0,
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return numbered;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── User stories derivation ───────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
function deriveUserStories(tasks) {
|
|
126
|
+
const stories = [];
|
|
127
|
+
for (const t of tasks) {
|
|
128
|
+
const verb = t.tier === 'search' ? 'find' :
|
|
129
|
+
t.tier === 'execute' ? 'use' :
|
|
130
|
+
t.tier === 'think' ? 'understand' : 'verify';
|
|
131
|
+
const subject = t.title.toLowerCase()
|
|
132
|
+
.replace(/^(add|create|update|fix|implement|refactor|write|build)\s+/i, '');
|
|
133
|
+
stories.push(`As a user, I should be able to ${verb} ${subject}`);
|
|
134
|
+
}
|
|
135
|
+
return stories;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Edge cases derivation ─────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
function deriveEdgeCases(tasks) {
|
|
141
|
+
const cases = [];
|
|
142
|
+
const riskTasks = tasks.filter(t => t.risk === 'high' || t.risk === 'critical');
|
|
143
|
+
|
|
144
|
+
for (const t of riskTasks) {
|
|
145
|
+
cases.push(`${t.title} touches ${t.risk}-risk files — verify no regressions`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const parallelTasks = tasks.filter(t => t.canParallel);
|
|
149
|
+
if (parallelTasks.length > 1) {
|
|
150
|
+
cases.push('Multiple tasks can run in parallel — ensure no file conflicts between agents');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const executeTasks = tasks.filter(t => t.tier === 'execute');
|
|
154
|
+
if (executeTasks.length > 1) {
|
|
155
|
+
cases.push('Multiple execute agents editing code — watch for merge conflicts');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (cases.length === 0) {
|
|
159
|
+
cases.push('No high-risk edge cases identified — standard testing applies');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return cases;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Questions derivation ──────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
function deriveQuestions(tasks, context) {
|
|
168
|
+
const questions = [];
|
|
169
|
+
|
|
170
|
+
// Check for ambiguous tiers
|
|
171
|
+
const searchAndExecute = new Set();
|
|
172
|
+
for (const t of tasks) {
|
|
173
|
+
const topicKey = t.title.toLowerCase();
|
|
174
|
+
if (searchAndExecute.has(topicKey)) continue;
|
|
175
|
+
searchAndExecute.add(topicKey);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check if tests are mentioned
|
|
179
|
+
const hasTestTask = tasks.some(t =>
|
|
180
|
+
/test/i.test(t.title) || t.tier === 'search' && /test/i.test(t.description)
|
|
181
|
+
);
|
|
182
|
+
if (!hasTestTask && tasks.some(t => t.tier === 'execute')) {
|
|
183
|
+
questions.push({
|
|
184
|
+
q: 'Should a dedicated test task be added for the new code?',
|
|
185
|
+
a: 'Yes — add test coverage for all execute-tier changes',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check for critical-risk files
|
|
190
|
+
const criticalTasks = tasks.filter(t => t.risk === 'critical');
|
|
191
|
+
if (criticalTasks.length > 0) {
|
|
192
|
+
questions.push({
|
|
193
|
+
q: `Task "${criticalTasks[0].title}" touches critical files — should dual-brain review be required?`,
|
|
194
|
+
a: 'Yes — dual-brain review recommended for critical-risk changes',
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check for missing context
|
|
199
|
+
if (!context?.projectName) {
|
|
200
|
+
questions.push({
|
|
201
|
+
q: 'What is the target project/module for these changes?',
|
|
202
|
+
a: 'Current working directory project',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (questions.length === 0) {
|
|
207
|
+
questions.push({
|
|
208
|
+
q: 'Are there any project-specific constraints or conventions to follow?',
|
|
209
|
+
a: 'Follow existing code style and patterns in the codebase',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return questions;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── Wave strategy explanation ─────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
function explainWaveStrategy(wave) {
|
|
219
|
+
if (!wave) return 'Sequential — run tasks one at a time in dependency order';
|
|
220
|
+
|
|
221
|
+
const explanations = {
|
|
222
|
+
sequential: 'Run tasks one at a time in dependency order. Safest for interdependent work.',
|
|
223
|
+
parallel: 'Run independent tasks simultaneously across providers. Fastest for isolated work.',
|
|
224
|
+
'wave-2': 'Two waves: first wave handles search/setup, second wave handles execution. Good balance of speed and safety.',
|
|
225
|
+
'wave-3': 'Three waves: search, then execute, then review. Full pipeline for complex changes.',
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
return explanations[wave] || `${wave} — follow the dependency chain in the task table`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─── Plan generation ───────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Generate a markdown execution plan from vibe-router output.
|
|
235
|
+
*
|
|
236
|
+
* @param {Object} vibeResult - Output from routeVibe():
|
|
237
|
+
* { complexity, tasks, quality_gates, wave_recommendation }
|
|
238
|
+
* @param {Object} [context] - Optional context:
|
|
239
|
+
* { projectName, recentFiles, summary }
|
|
240
|
+
* @returns {{ markdown: string, planPath: string|null }}
|
|
241
|
+
*/
|
|
242
|
+
function generatePlan(vibeResult, context = {}) {
|
|
243
|
+
const {
|
|
244
|
+
complexity = 'structured',
|
|
245
|
+
tasks: rawTasks = [],
|
|
246
|
+
quality_gates: qualityGates = [],
|
|
247
|
+
wave_recommendation: waveRec = 'sequential',
|
|
248
|
+
} = vibeResult;
|
|
249
|
+
|
|
250
|
+
const profile = getActiveProfile();
|
|
251
|
+
const timestamp = new Date().toISOString();
|
|
252
|
+
const summary = context.summary || deriveSummary(rawTasks);
|
|
253
|
+
|
|
254
|
+
// For simple/structured complexity, generate a lighter plan
|
|
255
|
+
if (complexity === 'simple' || (complexity === 'structured' && rawTasks.length <= 2)) {
|
|
256
|
+
return generateLightPlan(rawTasks, { complexity, profile, timestamp, summary });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Full 3-part plan for complex requests
|
|
260
|
+
const tasks = resolveDependencies(rawTasks);
|
|
261
|
+
const userStories = deriveUserStories(tasks);
|
|
262
|
+
const edgeCases = deriveEdgeCases(tasks);
|
|
263
|
+
const questions = deriveQuestions(tasks, context);
|
|
264
|
+
|
|
265
|
+
const lines = [];
|
|
266
|
+
|
|
267
|
+
// Header
|
|
268
|
+
lines.push(`# Execution Plan — ${summary}`);
|
|
269
|
+
lines.push(`Generated: ${timestamp} | Profile: ${profile.name} | Complexity: ${complexity}`);
|
|
270
|
+
lines.push('');
|
|
271
|
+
|
|
272
|
+
// Part 1: Tasks
|
|
273
|
+
lines.push('## Part 1: Tasks (ordered by dependency)');
|
|
274
|
+
lines.push('');
|
|
275
|
+
lines.push('| # | Task | Tier | Risk | Dependencies |');
|
|
276
|
+
lines.push('|---|------|------|------|-------------|');
|
|
277
|
+
for (const t of tasks) {
|
|
278
|
+
lines.push(`| ${t.num} | ${t.title} | ${t.tier} | ${t.risk} | ${t.dependencies} |`);
|
|
279
|
+
}
|
|
280
|
+
lines.push('');
|
|
281
|
+
|
|
282
|
+
// Agent instructions
|
|
283
|
+
lines.push('### Agent Instructions');
|
|
284
|
+
lines.push('- Each agent: read this plan before starting');
|
|
285
|
+
lines.push('- Write tests for your changes before finishing');
|
|
286
|
+
lines.push('- Run tests and fix until green');
|
|
287
|
+
lines.push('- Do not revert other agents’ work');
|
|
288
|
+
lines.push('- Other agents may be working in this repo simultaneously');
|
|
289
|
+
lines.push('');
|
|
290
|
+
|
|
291
|
+
// Part 2: User Stories & Edge Cases
|
|
292
|
+
lines.push('## Part 2: User Stories & Edge Cases');
|
|
293
|
+
lines.push('');
|
|
294
|
+
lines.push('### User Stories');
|
|
295
|
+
for (const s of userStories) {
|
|
296
|
+
lines.push(`- ${s}`);
|
|
297
|
+
}
|
|
298
|
+
lines.push('');
|
|
299
|
+
lines.push('### Edge Cases');
|
|
300
|
+
for (const c of edgeCases) {
|
|
301
|
+
lines.push(`- ${c}`);
|
|
302
|
+
}
|
|
303
|
+
lines.push('');
|
|
304
|
+
|
|
305
|
+
// Part 3: Questions
|
|
306
|
+
lines.push('## Part 3: Questions');
|
|
307
|
+
lines.push('');
|
|
308
|
+
lines.push('> These are questions the orchestrator couldn\'t resolve from the codebase.');
|
|
309
|
+
lines.push('> Suggested answers are provided — correct any that are wrong before launching agents.');
|
|
310
|
+
lines.push('');
|
|
311
|
+
for (let i = 0; i < questions.length; i++) {
|
|
312
|
+
lines.push(`${i + 1}. ${questions[i].q} — **Suggested:** ${questions[i].a}`);
|
|
313
|
+
}
|
|
314
|
+
lines.push('');
|
|
315
|
+
|
|
316
|
+
// Quality Gates
|
|
317
|
+
lines.push('## Quality Gates');
|
|
318
|
+
if (qualityGates.length > 0) {
|
|
319
|
+
for (const g of qualityGates) {
|
|
320
|
+
lines.push(`- ${typeof g === 'string' ? g : g.description || JSON.stringify(g)}`);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
lines.push(`- Sensitivity floor: ${profile.quality_gate.sensitivity_floor}`);
|
|
324
|
+
lines.push(`- Dual-brain minimum: ${profile.quality_gate.dual_brain_minimum}`);
|
|
325
|
+
lines.push('- Run tests before marking complete');
|
|
326
|
+
}
|
|
327
|
+
lines.push('');
|
|
328
|
+
|
|
329
|
+
// Wave Strategy
|
|
330
|
+
lines.push('## Wave Strategy');
|
|
331
|
+
lines.push(`${waveRec} — ${explainWaveStrategy(waveRec)}`);
|
|
332
|
+
lines.push('');
|
|
333
|
+
|
|
334
|
+
const markdown = lines.join('\n');
|
|
335
|
+
return { markdown, planPath: null };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ─── Light plan for simple/structured requests ─────────────────────────────
|
|
339
|
+
|
|
340
|
+
function generateLightPlan(rawTasks, { complexity, profile, timestamp, summary }) {
|
|
341
|
+
const tasks = resolveDependencies(rawTasks);
|
|
342
|
+
const lines = [];
|
|
343
|
+
|
|
344
|
+
lines.push(`# Execution Plan — ${summary}`);
|
|
345
|
+
lines.push(`Generated: ${timestamp} | Profile: ${profile.name} | Complexity: ${complexity}`);
|
|
346
|
+
lines.push('');
|
|
347
|
+
|
|
348
|
+
if (tasks.length > 0) {
|
|
349
|
+
lines.push('## Tasks');
|
|
350
|
+
lines.push('');
|
|
351
|
+
for (const t of tasks) {
|
|
352
|
+
const depNote = t.dependencies !== '—' ? ` (after ${t.dependencies})` : '';
|
|
353
|
+
lines.push(`${t.num}. **[${t.tier}]** ${t.title}${depNote}`);
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
lines.push('## Tasks');
|
|
357
|
+
lines.push('');
|
|
358
|
+
lines.push('1. Execute the request directly');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
lines.push('');
|
|
362
|
+
lines.push('---');
|
|
363
|
+
lines.push('*Light plan — full 3-part plan generated for complex requests.*');
|
|
364
|
+
lines.push('');
|
|
365
|
+
|
|
366
|
+
const markdown = lines.join('\n');
|
|
367
|
+
return { markdown, planPath: null };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ─── Summary derivation ────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
function deriveSummary(tasks) {
|
|
373
|
+
if (!tasks || tasks.length === 0) return 'Unnamed Plan';
|
|
374
|
+
if (tasks.length === 1) return tasks[0].title || 'Single Task';
|
|
375
|
+
|
|
376
|
+
const titles = tasks.map(t => t.title || '').filter(Boolean);
|
|
377
|
+
if (titles.length <= 2) return titles.join(' + ');
|
|
378
|
+
|
|
379
|
+
// Find common theme
|
|
380
|
+
const words = titles.flatMap(t => t.toLowerCase().split(/\s+/));
|
|
381
|
+
const freq = new Map();
|
|
382
|
+
for (const w of words) {
|
|
383
|
+
if (w.length > 3) freq.set(w, (freq.get(w) || 0) + 1);
|
|
384
|
+
}
|
|
385
|
+
const common = [...freq.entries()]
|
|
386
|
+
.filter(([, c]) => c >= 2)
|
|
387
|
+
.sort((a, b) => b[1] - a[1])
|
|
388
|
+
.map(([w]) => w);
|
|
389
|
+
|
|
390
|
+
if (common.length > 0) {
|
|
391
|
+
return `${common.slice(0, 2).join(' ')} (${tasks.length} tasks)`;
|
|
392
|
+
}
|
|
393
|
+
return `${tasks.length}-task plan`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ─── Write plan to disk ────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
function writePlan(markdown) {
|
|
399
|
+
mkdirSync(PLANS_DIR, { recursive: true });
|
|
400
|
+
|
|
401
|
+
const ts = new Date().toISOString()
|
|
402
|
+
.replace(/[:.]/g, '-')
|
|
403
|
+
.replace('T', '_')
|
|
404
|
+
.slice(0, 19);
|
|
405
|
+
const filename = `${ts}-plan.md`;
|
|
406
|
+
const planPath = join(PLANS_DIR, filename);
|
|
407
|
+
|
|
408
|
+
writeFileSync(planPath, markdown);
|
|
409
|
+
return planPath;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ─── CLI ───────────────────────────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
async function cli() {
|
|
415
|
+
const args = process.argv.slice(2);
|
|
416
|
+
const flagIndex = (f) => args.indexOf(f);
|
|
417
|
+
const flagVal = (f) => {
|
|
418
|
+
const i = flagIndex(f);
|
|
419
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] : null;
|
|
420
|
+
};
|
|
421
|
+
const hasFlag = (f) => args.includes(f);
|
|
422
|
+
|
|
423
|
+
if (hasFlag('--help') || hasFlag('-h')) {
|
|
424
|
+
console.log(`
|
|
425
|
+
plan-generator.mjs — Generate Steve-style execution plans
|
|
426
|
+
|
|
427
|
+
Usage:
|
|
428
|
+
node plan-generator.mjs --utterance "..." [--write]
|
|
429
|
+
node plan-generator.mjs --help
|
|
430
|
+
|
|
431
|
+
Options:
|
|
432
|
+
--utterance "..." The request to plan for
|
|
433
|
+
--write Write plan to .claude/plans/
|
|
434
|
+
--json Output as JSON instead of markdown
|
|
435
|
+
--help Show this help
|
|
436
|
+
|
|
437
|
+
The plan is generated from vibe-router output. If vibe-router
|
|
438
|
+
is not available, a basic plan is created from the utterance.
|
|
439
|
+
`);
|
|
440
|
+
process.exit(0);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const utterance = flagVal('--utterance');
|
|
444
|
+
const shouldWrite = hasFlag('--write');
|
|
445
|
+
const jsonOutput = hasFlag('--json');
|
|
446
|
+
|
|
447
|
+
if (!utterance) {
|
|
448
|
+
console.error(' Error: --utterance is required');
|
|
449
|
+
console.error(' Usage: node plan-generator.mjs --utterance "build a login page"');
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Try to load vibe-router; fall back to a basic vibeResult
|
|
454
|
+
let vibeResult;
|
|
455
|
+
try {
|
|
456
|
+
const { routeVibe } = await import('./vibe-router.mjs');
|
|
457
|
+
vibeResult = routeVibe(utterance);
|
|
458
|
+
} catch {
|
|
459
|
+
// vibe-router not available — construct a basic vibeResult from utterance
|
|
460
|
+
vibeResult = fallbackVibeResult(utterance);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const result = generatePlan(vibeResult, { summary: utterance.slice(0, 60) });
|
|
464
|
+
|
|
465
|
+
if (shouldWrite) {
|
|
466
|
+
result.planPath = writePlan(result.markdown);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (jsonOutput) {
|
|
470
|
+
console.log(JSON.stringify({
|
|
471
|
+
planPath: result.planPath,
|
|
472
|
+
markdown: result.markdown,
|
|
473
|
+
}, null, 2));
|
|
474
|
+
} else {
|
|
475
|
+
console.log(result.markdown);
|
|
476
|
+
if (result.planPath) {
|
|
477
|
+
console.log(`\nPlan written to: ${result.planPath}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ─── Fallback when vibe-router is unavailable ──────────────────────────────
|
|
483
|
+
|
|
484
|
+
function fallbackVibeResult(utterance) {
|
|
485
|
+
const lower = utterance.toLowerCase();
|
|
486
|
+
|
|
487
|
+
// Estimate complexity from utterance length and keywords
|
|
488
|
+
const complexWords = ['and', 'then', 'also', 'plus', 'with', 'including', 'across', 'multiple', 'refactor', 'migrate'];
|
|
489
|
+
const matchCount = complexWords.filter(w => lower.includes(w)).length;
|
|
490
|
+
const complexity = matchCount >= 3 ? 'complex' :
|
|
491
|
+
matchCount >= 1 ? 'structured' : 'simple';
|
|
492
|
+
|
|
493
|
+
// Extract rough tasks from utterance
|
|
494
|
+
const tasks = [];
|
|
495
|
+
const segments = utterance.split(/(?:\band\b|\bthen\b|\bplus\b|,|;)/i).map(s => s.trim()).filter(Boolean);
|
|
496
|
+
|
|
497
|
+
for (const seg of segments) {
|
|
498
|
+
const isSearch = /\b(find|search|look|check|explore|list|grep)\b/i.test(seg);
|
|
499
|
+
const isThink = /\b(decide|evaluate|compare|review|plan|architect|design)\b/i.test(seg);
|
|
500
|
+
const tier = isSearch ? 'search' : isThink ? 'think' : 'execute';
|
|
501
|
+
|
|
502
|
+
tasks.push({
|
|
503
|
+
title: seg.charAt(0).toUpperCase() + seg.slice(1),
|
|
504
|
+
tier,
|
|
505
|
+
topic: seg.split(/\s+/).slice(0, 3).join(' '),
|
|
506
|
+
files: [],
|
|
507
|
+
dependencies: [],
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (tasks.length === 0) {
|
|
512
|
+
tasks.push({
|
|
513
|
+
title: utterance.slice(0, 80),
|
|
514
|
+
tier: 'execute',
|
|
515
|
+
topic: utterance.slice(0, 20),
|
|
516
|
+
files: [],
|
|
517
|
+
dependencies: [],
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
complexity,
|
|
523
|
+
tasks,
|
|
524
|
+
quality_gates: [],
|
|
525
|
+
wave_recommendation: tasks.length > 2 ? 'wave-2' : 'sequential',
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ─── Exports ───────────────────────────────────────────────────────────────
|
|
530
|
+
|
|
531
|
+
export { generatePlan, writePlan, resolveDependencies };
|
|
532
|
+
|
|
533
|
+
// ─── Run CLI if invoked directly ───────────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
const isMain = process.argv[1] &&
|
|
536
|
+
(process.argv[1].endsWith('plan-generator.mjs') ||
|
|
537
|
+
process.argv[1] === fileURLToPath(import.meta.url));
|
|
538
|
+
|
|
539
|
+
if (isMain) {
|
|
540
|
+
cli().catch(err => {
|
|
541
|
+
console.error(` Error: ${err.message}`);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
});
|
|
544
|
+
}
|
package/hooks/profiles.mjs
CHANGED
|
@@ -20,6 +20,25 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
20
20
|
const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
|
|
21
21
|
const CONFIG_FILE = join(__dirname, '..', 'orchestrator.json');
|
|
22
22
|
|
|
23
|
+
const ALIASES = {
|
|
24
|
+
// auto
|
|
25
|
+
'auto': 'auto', 'adaptive': 'auto', 'smart': 'auto', 'default': 'auto', 'normal': 'auto',
|
|
26
|
+
// balanced
|
|
27
|
+
'balanced': 'balanced', 'even': 'balanced', 'equal': 'balanced',
|
|
28
|
+
// cost-saver
|
|
29
|
+
'cost-saver': 'cost-saver', 'cheap': 'cost-saver', 'save': 'cost-saver', 'conservative': 'cost-saver', 'frugal': 'cost-saver', 'budget': 'cost-saver', 'fast': 'cost-saver', 'quick': 'cost-saver',
|
|
30
|
+
// quality-first
|
|
31
|
+
'quality-first': 'quality-first', 'aggressive': 'quality-first', 'quality': 'quality-first', 'max': 'quality-first', 'full': 'quality-first', 'both': 'quality-first', 'careful': 'quality-first', 'thorough': 'quality-first', 'safe': 'quality-first',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function resolveProfileName(input) {
|
|
35
|
+
if (!input) return null;
|
|
36
|
+
const cleaned = input.toLowerCase().trim()
|
|
37
|
+
.replace(/^(go|be|use|switch to|set|mode)\s+/i, '')
|
|
38
|
+
.replace(/\s+mode$/i, '');
|
|
39
|
+
return ALIASES[cleaned] || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
const PROFILES = {
|
|
24
43
|
auto: {
|
|
25
44
|
description: 'Adapts routing based on task risk, provider health, and outcomes',
|
|
@@ -140,12 +159,22 @@ function getActiveProfile() {
|
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
function setActiveProfile(name, customOverrides = null) {
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
let resolved = name;
|
|
163
|
+
if (!PROFILES[resolved]) {
|
|
164
|
+
const alias = resolveProfileName(name);
|
|
165
|
+
if (alias) {
|
|
166
|
+
resolved = alias;
|
|
167
|
+
} else {
|
|
168
|
+
const aliasHint = Object.entries(ALIASES)
|
|
169
|
+
.filter(([k, v]) => k !== v)
|
|
170
|
+
.map(([k, v]) => `${k} → ${v}`)
|
|
171
|
+
.join(', ');
|
|
172
|
+
return { ok: false, error: `Unknown profile: ${name}. Available: ${Object.keys(PROFILES).join(', ')}. Aliases: ${aliasHint}` };
|
|
173
|
+
}
|
|
145
174
|
}
|
|
146
175
|
|
|
147
176
|
const data = {
|
|
148
|
-
active:
|
|
177
|
+
active: resolved,
|
|
149
178
|
switched_at: new Date().toISOString(),
|
|
150
179
|
};
|
|
151
180
|
if (customOverrides) data.custom_overrides = customOverrides;
|
|
@@ -154,7 +183,7 @@ function setActiveProfile(name, customOverrides = null) {
|
|
|
154
183
|
const tmp = PROFILE_FILE + '.tmp.' + process.pid;
|
|
155
184
|
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
156
185
|
renameSync(tmp, PROFILE_FILE);
|
|
157
|
-
return { ok: true, profile: PROFILES[
|
|
186
|
+
return { ok: true, profile: PROFILES[resolved], resolvedName: resolved };
|
|
158
187
|
} catch (err) {
|
|
159
188
|
return { ok: false, error: `Failed to write profile: ${err.message}` };
|
|
160
189
|
}
|
|
@@ -216,6 +245,8 @@ function getProfileOverrides(system) {
|
|
|
216
245
|
|
|
217
246
|
export {
|
|
218
247
|
PROFILES,
|
|
248
|
+
ALIASES,
|
|
249
|
+
resolveProfileName,
|
|
219
250
|
getActiveProfile,
|
|
220
251
|
setActiveProfile,
|
|
221
252
|
setBudgetOverrides,
|