orchestrix-yuri 4.7.7 → 4.7.9
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/lib/gateway/router.js +105 -0
- package/package.json +1 -1
package/lib/gateway/router.js
CHANGED
|
@@ -349,6 +349,9 @@ class Router {
|
|
|
349
349
|
}
|
|
350
350
|
case 'change': {
|
|
351
351
|
const desc = classified.description || msg.text;
|
|
352
|
+
// Direct agent routing: user explicitly names an agent → skip scope/PO
|
|
353
|
+
const directResult = await this._tryDirectAgentRoute(msg, projectRoot, desc);
|
|
354
|
+
if (directResult) return directResult;
|
|
352
355
|
msg.text = `*change ${desc}`;
|
|
353
356
|
return this._handleChangeCommand(msg, projectRoot);
|
|
354
357
|
}
|
|
@@ -419,6 +422,108 @@ class Router {
|
|
|
419
422
|
return { text: response };
|
|
420
423
|
}
|
|
421
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Direct agent routing: when user explicitly names a planning or dev agent,
|
|
427
|
+
* skip scope assessment and PO routing — send the instruction directly.
|
|
428
|
+
* Returns null if no agent match (caller should fall through to normal change flow).
|
|
429
|
+
*/
|
|
430
|
+
async _tryDirectAgentRoute(msg, projectRoot, description) {
|
|
431
|
+
// Agent name patterns → { session type, agent slug for /o, window }
|
|
432
|
+
// Planning agents all use window 0 (ensure-session.sh only creates one window).
|
|
433
|
+
// Dev agents use their fixed windows (start-orchestrix.sh creates all 4).
|
|
434
|
+
const AGENT_MAP = {
|
|
435
|
+
// Planning session agents (all window 0)
|
|
436
|
+
'analyst': { session: 'planning', slug: 'analyst', window: 0 },
|
|
437
|
+
'pm': { session: 'planning', slug: 'pm', window: 0 },
|
|
438
|
+
'ux-expert': { session: 'planning', slug: 'ux-expert', window: 0 },
|
|
439
|
+
'ux': { session: 'planning', slug: 'ux-expert', window: 0 },
|
|
440
|
+
'po': { session: 'planning', slug: 'po', window: 0 },
|
|
441
|
+
// Dev session agents (fixed windows from start-orchestrix.sh)
|
|
442
|
+
'architect': { session: 'dev', slug: 'architect', window: 0 },
|
|
443
|
+
'sm': { session: 'dev', slug: 'sm', window: 1 },
|
|
444
|
+
'dev': { session: 'dev', slug: 'dev', window: 2 },
|
|
445
|
+
'qa': { session: 'dev', slug: 'qa', window: 3 },
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Match agent name in user message (case-insensitive, word boundary)
|
|
449
|
+
const text = msg.text.toLowerCase();
|
|
450
|
+
let matched = null;
|
|
451
|
+
for (const [name, info] of Object.entries(AGENT_MAP)) {
|
|
452
|
+
if (text.includes(name)) {
|
|
453
|
+
// Prefer longer match (ux-expert over ux)
|
|
454
|
+
if (!matched || name.length > matched.name.length) {
|
|
455
|
+
matched = { name, ...info };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (!matched) return null;
|
|
460
|
+
|
|
461
|
+
// Guard: don't hijack if orchestrator is busy with something else
|
|
462
|
+
if (this.orchestrator.isRunning()) {
|
|
463
|
+
return null; // let normal flow handle it
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
log.router(`Direct agent route: ${matched.slug} (${matched.session} session, window ${matched.window})`);
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
const { execSync } = require('child_process');
|
|
470
|
+
const scriptPath = path.join(os.homedir(), '.claude', 'skills', 'yuri', 'scripts', 'ensure-session.sh');
|
|
471
|
+
const result = execSync(`bash "${scriptPath}" ${matched.session} "${projectRoot}"`, {
|
|
472
|
+
encoding: 'utf8', timeout: 60000,
|
|
473
|
+
}).trim();
|
|
474
|
+
const lines = result.split('\n');
|
|
475
|
+
const session = lines[lines.length - 1].trim();
|
|
476
|
+
|
|
477
|
+
const tmx = require('./engine/tmux-utils');
|
|
478
|
+
tmx.sendKeysWithEnter(session, matched.window, '/clear');
|
|
479
|
+
execSync('sleep 2');
|
|
480
|
+
tmx.sendKeysWithEnter(session, matched.window, `/o ${matched.slug}`);
|
|
481
|
+
execSync('sleep 12');
|
|
482
|
+
|
|
483
|
+
// Send the user's instruction directly
|
|
484
|
+
const safeDesc = description.replace(/"/g, '\\"');
|
|
485
|
+
tmx.sendKeysWithEnter(session, matched.window, safeDesc);
|
|
486
|
+
|
|
487
|
+
// Set up polling via orchestrator (reuses change/small flow)
|
|
488
|
+
this.orchestrator._projectRoot = projectRoot;
|
|
489
|
+
this.orchestrator._phase = 'change';
|
|
490
|
+
this.orchestrator._session = session;
|
|
491
|
+
this.orchestrator._lastHash = '';
|
|
492
|
+
this.orchestrator._stableCount = 0;
|
|
493
|
+
this.orchestrator._step = 0;
|
|
494
|
+
this.orchestrator._changeContext = { scope: 'direct', description, agent: matched.slug };
|
|
495
|
+
const pollInterval = this.orchestrator.config.phase_poll_interval || 30000;
|
|
496
|
+
this.orchestrator._timer = setInterval(() => {
|
|
497
|
+
// Poll the specific agent window
|
|
498
|
+
if (this.orchestrator._phase !== 'change') return;
|
|
499
|
+
if (!tmx.hasSession(session)) {
|
|
500
|
+
this.orchestrator._handleError('change', `${matched.slug} tmux session died`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const check = tmx.checkCompletion(session, matched.window, this.orchestrator._lastHash);
|
|
504
|
+
if (check.status === 'complete' || (check.status === 'stable' && ++this.orchestrator._stableCount >= 3)) {
|
|
505
|
+
if (this.orchestrator._timer) { clearInterval(this.orchestrator._timer); this.orchestrator._timer = null; }
|
|
506
|
+
this.orchestrator._phase = null;
|
|
507
|
+
this.orchestrator._changeContext = null;
|
|
508
|
+
this.orchestrator.onComplete('change', `✅ ${matched.slug} completed the task.`);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (check.status !== 'stable') { this.orchestrator._stableCount = 0; this.orchestrator._lastHash = check.hash || ''; }
|
|
512
|
+
else { this.orchestrator._lastHash = check.hash; }
|
|
513
|
+
}, pollInterval);
|
|
514
|
+
|
|
515
|
+
this.history.append(msg.chatId, 'user', msg.text);
|
|
516
|
+
const reply = `🎯 Direct → **${matched.slug}** (window ${matched.window})\n\n"${description.slice(0, 120)}"\n\nI'll notify you when done.`;
|
|
517
|
+
this.history.append(msg.chatId, 'assistant', reply);
|
|
518
|
+
this._updateGlobalFocus(msg, projectRoot);
|
|
519
|
+
|
|
520
|
+
return { text: reply };
|
|
521
|
+
} catch (err) {
|
|
522
|
+
log.warn(`Direct agent route failed: ${err.message}`);
|
|
523
|
+
return null; // fall through to normal change flow
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
422
527
|
/**
|
|
423
528
|
* Handle *change command in two steps:
|
|
424
529
|
* Step 1: Claude assesses the scope (small/medium/large) — quick claude -p call
|