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.
@@ -226,11 +226,11 @@ try {
226
226
  if (burstMode) {
227
227
  // In burst mode, only warn on exact hash matches (same description+prompt)
228
228
  if (duplicate.prompt_hash === promptHash) {
229
- duplicateWarning = `**[Wave] [Duplicate Warning]** A similar agent task was dispatched ${minutesAgo} minute${minutesAgo !== 1 ? 's' : ''} ago. Reuse the prior result unless the scope changed.`;
229
+ duplicateWarning = `Heads up a similar task ran ${minutesAgo} minute${minutesAgo !== 1 ? 's' : ''} ago (wave detected). Reuse that result if the scope hasn't changed.`;
230
230
  }
231
231
  // Otherwise suppress — similar-but-different agents in a wave are expected
232
232
  } else {
233
- duplicateWarning = `**[Duplicate Warning]** A similar agent task was dispatched ${minutesAgo} minute${minutesAgo !== 1 ? 's' : ''} ago. Reuse the prior result unless the scope changed.`;
233
+ duplicateWarning = `Heads up a similar task ran ${minutesAgo} minute${minutesAgo !== 1 ? 's' : ''} ago. Reuse that result if the scope hasn't changed.`;
234
234
  }
235
235
  }
236
236
 
@@ -278,10 +278,10 @@ try {
278
278
  ].filter(Boolean);
279
279
 
280
280
  if (detectedTiers.length > 1) {
281
- const splitMsg = `**[Tier Enforcer]** This spans **${detectedTiers.join(' + ')}** work. Consider splitting: ` +
281
+ const splitMsg = `This spans ${detectedTiers.join(' + ')} work. Consider splitting: ` +
282
282
  (hasSearch ? 'search first (haiku), ' : '') +
283
283
  (hasExecute ? 'then execute edits (sonnet), ' : '') +
284
- (hasThink ? 'keep planning/review on think tier (opus).' : '');
284
+ (hasThink ? 'keep planning/review on the main session (opus).' : '');
285
285
  const fullMsg = prependWarnings(splitMsg.replace(/, $/, '.'));
286
286
  logRecommendation({
287
287
  tier: detectedTiers.join('+'),
@@ -310,8 +310,8 @@ try {
310
310
  if ((riskResult.level === 'critical' || riskResult.level === 'high') && tier !== 'think') {
311
311
  tier = 'think';
312
312
  autoStatus = riskResult.level === 'critical'
313
- ? `Dual-brain: dual-brain review recommended — ${riskResult.reason.split(':')[0]} detected`
314
- : `Dual-brain: promoting to think tier — ${riskResult.reason.split(':')[0]}`;
313
+ ? `This touches ${riskResult.reason.split(':')[0].toLowerCase()} — recommending dual-brain review for safety.`
314
+ : `Promoting to think tier — this is ${riskResult.reason.split(':')[0].toLowerCase()}.`;
315
315
  }
316
316
 
317
317
  // Failure loop detection
@@ -320,11 +320,11 @@ try {
320
320
  if (failureCheck.isLoop) {
321
321
  if (failureCheck.suggestion === 'promote_tier' && tier === 'execute') {
322
322
  tier = 'think';
323
- autoStatus = 'Dual-brain: escalating to think tier — previous attempt failed';
323
+ autoStatus = 'Escalating to think tier — this has failed before, let\'s take a different approach.';
324
324
  } else if (failureCheck.suggestion === 'escalate_to_dual_brain') {
325
- autoStatus = 'Dual-brain: dual-brain review recommended repeated failures detected';
325
+ autoStatus = 'Repeated failures detected — recommending dual-brain review to diagnose the issue.';
326
326
  }
327
- failureMessage = `**[Failure Loop]** ${failureCheck.count} failed attempts in 2hrs. Consider: \`node .claude/hooks/dual-brain-think.mjs --question "why is this failing?"\``;
327
+ failureMessage = `⚠️ This has failed ${failureCheck.count} times in the last 2 hours. Consider a dual-brain think session to diagnose the root cause.`;
328
328
  }
329
329
 
330
330
  // Apply profile-driven tier adjustments
@@ -344,7 +344,7 @@ try {
344
344
  const biasThreshold = profileSettings.bias >= 0 ? 10 : 20;
345
345
  if (balance && balance.claudeCalls > balance.openaiCalls * 2 && balance.claudeCalls > biasThreshold) {
346
346
  const dispatchModel = tier === 'think' ? 'gpt-5.5' : tier === 'execute' ? 'gpt-5.4' : 'gpt-4.1-mini';
347
- balanceHint = `\n\n💡 **Balance tip:** Claude has ${balance.claudeCalls} ${tier} calls vs OpenAI's ${balance.openaiCalls} in the last 5hrs. Consider dispatching isolated work to GPT: \`node .claude/hooks/gpt-work-dispatcher.mjs --task "..." --model ${dispatchModel}\``;
347
+ balanceHint = `\n\n💡 Claude is handling most work right now (${balance.claudeCalls} ${tier} calls vs ${balance.openaiCalls} GPT). For isolated tasks, consider routing to GPT to balance subscriptions.`;
348
348
  }
349
349
  }
350
350
  }
@@ -374,8 +374,7 @@ try {
374
374
  // If we get here, a non-think model is being used for think work
375
375
  const thinkBestFor = intelligence[expected || 'opus']?.best_for;
376
376
  const thinkBestForSuffix = thinkBestFor ? ` (best for: ${thinkBestFor})` : '';
377
- const msg = `**[Tier Enforcer]** This looks like **think** work (architecture/review/planning). ` +
378
- `Don't send it to "${currentModel}" — keep it on the main session (${expected || 'opus'}${thinkBestForSuffix}) for best results.`;
377
+ const msg = `This looks like think-level work (architecture/review/planning) better kept on the main session (${expected || 'opus'}${thinkBestForSuffix}) rather than delegated to ${currentModel}.`;
379
378
  logRecommendation({
380
379
  tier,
381
380
  recommended: expected,
@@ -406,8 +405,7 @@ try {
406
405
  const savings = tier === 'search' ? 'Haiku is 19x cheaper than Opus for read-only lookups.' : 'Sonnet is 5x cheaper than Opus for implementation work.';
407
406
  const bestFor = intelligence[expected]?.best_for;
408
407
  const bestForSuffix = bestFor ? ` (best for: ${bestFor})` : '';
409
- const msg = `**[Tier Enforcer]** This looks like **${tier}** work. ` +
410
- `Use \`model: "${expected}"\`${bestForSuffix} instead of "${currentModel || 'opus (inherited)'}". ${savings}`;
408
+ const msg = `This looks like ${tier} work — use ${expected}${bestForSuffix} instead of ${currentModel || 'opus (inherited)'}. ${savings}`;
411
409
  logRecommendation({
412
410
  tier,
413
411
  recommended: expected,
@@ -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
+ }
@@ -26,9 +26,9 @@ const ALIASES = {
26
26
  // balanced
27
27
  'balanced': 'balanced', 'even': 'balanced', 'equal': 'balanced',
28
28
  // cost-saver
29
- 'cost-saver': 'cost-saver', 'cheap': 'cost-saver', 'save': 'cost-saver', 'conservative': 'cost-saver', 'frugal': 'cost-saver', 'budget': '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
30
  // quality-first
31
- 'quality-first': 'quality-first', 'aggressive': 'quality-first', 'quality': 'quality-first', 'max': 'quality-first', 'full': 'quality-first', 'both': '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
32
  };
33
33
 
34
34
  function resolveProfileName(input) {
@@ -693,8 +693,8 @@ test('enforce-tier: burst mode suppresses duplicate warnings', () => {
693
693
 
694
694
  // In burst mode: either no duplicate warning at all, or a [Wave]-prefixed one
695
695
  const msg = parsed.systemMessage || '';
696
- const hasDuplicateWarning = msg.toLowerCase().includes('duplicate');
697
- if (hasDuplicateWarning && !msg.includes('[Wave]'))
696
+ const hasDuplicateWarning = msg.toLowerCase().includes('duplicate') || msg.toLowerCase().includes('similar task');
697
+ if (hasDuplicateWarning && !msg.includes('[Wave]') && !msg.includes('wave detected'))
698
698
  return `expected no duplicate warning or [Wave]-prefixed in burst mode, got: ${msg}`;
699
699
  return true;
700
700
  } finally {
@@ -720,7 +720,7 @@ test('enforce-tier: non-burst mode still warns on duplicates', () => {
720
720
  if (!parsed) return 'no valid JSON output';
721
721
 
722
722
  const msg = parsed.systemMessage || '';
723
- if (!msg.toLowerCase().includes('duplicate'))
723
+ if (!msg.toLowerCase().includes('similar task') && !msg.toLowerCase().includes('duplicate'))
724
724
  return `expected duplicate warning in non-burst mode, got: ${msg || '(empty)'}`;
725
725
  return true;
726
726
  } finally {