orchestrix-yuri 4.7.6 → 4.7.8
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 +143 -2
- 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
|
}
|
|
@@ -358,8 +361,16 @@ class Router {
|
|
|
358
361
|
case 'iterate':
|
|
359
362
|
case 'deploy':
|
|
360
363
|
return this._handlePhaseCommand(classified.action, msg);
|
|
361
|
-
case 'status':
|
|
364
|
+
case 'status': {
|
|
365
|
+
// Agent-specific or decision queries need Claude reasoning with tmux context,
|
|
366
|
+
// not a canned status card. Fall through to conversation handler.
|
|
367
|
+
const agentRe = /\b(sm|architect|dev|qa|po|pm|ux)\b/i;
|
|
368
|
+
const decisionRe = /决策|决定|确认|需要我|block|decide|should i|approve/i;
|
|
369
|
+
if (agentRe.test(msg.text) || decisionRe.test(msg.text)) {
|
|
370
|
+
return null; // → conversation handler (Claude with tmux context)
|
|
371
|
+
}
|
|
362
372
|
return this._handleStatusQuery(msg);
|
|
373
|
+
}
|
|
363
374
|
default:
|
|
364
375
|
return null;
|
|
365
376
|
}
|
|
@@ -411,6 +422,105 @@ class Router {
|
|
|
411
422
|
return { text: response };
|
|
412
423
|
}
|
|
413
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
|
+
const AGENT_MAP = {
|
|
433
|
+
// Planning session agents
|
|
434
|
+
'analyst': { session: 'planning', slug: 'analyst', window: 0 },
|
|
435
|
+
'pm': { session: 'planning', slug: 'pm', window: 1 },
|
|
436
|
+
'ux-expert': { session: 'planning', slug: 'ux-expert', window: 2 },
|
|
437
|
+
'ux': { session: 'planning', slug: 'ux-expert', window: 2 },
|
|
438
|
+
// Dev session agents
|
|
439
|
+
'architect': { session: 'dev', slug: 'architect', window: 0 },
|
|
440
|
+
'sm': { session: 'dev', slug: 'sm', window: 1 },
|
|
441
|
+
'dev': { session: 'dev', slug: 'dev', window: 2 },
|
|
442
|
+
'qa': { session: 'dev', slug: 'qa', window: 3 },
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// Match agent name in user message (case-insensitive, word boundary)
|
|
446
|
+
const text = msg.text.toLowerCase();
|
|
447
|
+
let matched = null;
|
|
448
|
+
for (const [name, info] of Object.entries(AGENT_MAP)) {
|
|
449
|
+
if (text.includes(name)) {
|
|
450
|
+
// Prefer longer match (ux-expert over ux)
|
|
451
|
+
if (!matched || name.length > matched.name.length) {
|
|
452
|
+
matched = { name, ...info };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (!matched) return null;
|
|
457
|
+
|
|
458
|
+
// Guard: don't hijack if orchestrator is busy with something else
|
|
459
|
+
if (this.orchestrator.isRunning()) {
|
|
460
|
+
return null; // let normal flow handle it
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
log.router(`Direct agent route: ${matched.slug} (${matched.session} session, window ${matched.window})`);
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
const { execSync } = require('child_process');
|
|
467
|
+
const scriptPath = path.join(os.homedir(), '.claude', 'skills', 'yuri', 'scripts', 'ensure-session.sh');
|
|
468
|
+
const result = execSync(`bash "${scriptPath}" ${matched.session} "${projectRoot}"`, {
|
|
469
|
+
encoding: 'utf8', timeout: 60000,
|
|
470
|
+
}).trim();
|
|
471
|
+
const lines = result.split('\n');
|
|
472
|
+
const session = lines[lines.length - 1].trim();
|
|
473
|
+
|
|
474
|
+
const tmx = require('./engine/tmux-utils');
|
|
475
|
+
tmx.sendKeysWithEnter(session, matched.window, '/clear');
|
|
476
|
+
execSync('sleep 2');
|
|
477
|
+
tmx.sendKeysWithEnter(session, matched.window, `/o ${matched.slug}`);
|
|
478
|
+
execSync('sleep 12');
|
|
479
|
+
|
|
480
|
+
// Send the user's instruction directly
|
|
481
|
+
const safeDesc = description.replace(/"/g, '\\"');
|
|
482
|
+
tmx.sendKeysWithEnter(session, matched.window, safeDesc);
|
|
483
|
+
|
|
484
|
+
// Set up polling via orchestrator (reuses change/small flow)
|
|
485
|
+
this.orchestrator._projectRoot = projectRoot;
|
|
486
|
+
this.orchestrator._phase = 'change';
|
|
487
|
+
this.orchestrator._session = session;
|
|
488
|
+
this.orchestrator._lastHash = '';
|
|
489
|
+
this.orchestrator._stableCount = 0;
|
|
490
|
+
this.orchestrator._step = 0;
|
|
491
|
+
this.orchestrator._changeContext = { scope: 'direct', description, agent: matched.slug };
|
|
492
|
+
const pollInterval = this.orchestrator.config.phase_poll_interval || 30000;
|
|
493
|
+
this.orchestrator._timer = setInterval(() => {
|
|
494
|
+
// Poll the specific agent window
|
|
495
|
+
if (this.orchestrator._phase !== 'change') return;
|
|
496
|
+
if (!tmx.hasSession(session)) {
|
|
497
|
+
this.orchestrator._handleError('change', `${matched.slug} tmux session died`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const check = tmx.checkCompletion(session, matched.window, this.orchestrator._lastHash);
|
|
501
|
+
if (check.status === 'complete' || (check.status === 'stable' && ++this.orchestrator._stableCount >= 3)) {
|
|
502
|
+
if (this.orchestrator._timer) { clearInterval(this.orchestrator._timer); this.orchestrator._timer = null; }
|
|
503
|
+
this.orchestrator._phase = null;
|
|
504
|
+
this.orchestrator._changeContext = null;
|
|
505
|
+
this.orchestrator.onComplete('change', `✅ ${matched.slug} completed the task.`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (check.status !== 'stable') { this.orchestrator._stableCount = 0; this.orchestrator._lastHash = check.hash || ''; }
|
|
509
|
+
else { this.orchestrator._lastHash = check.hash; }
|
|
510
|
+
}, pollInterval);
|
|
511
|
+
|
|
512
|
+
this.history.append(msg.chatId, 'user', msg.text);
|
|
513
|
+
const reply = `🎯 Direct → **${matched.slug}** (window ${matched.window})\n\n"${description.slice(0, 120)}"\n\nI'll notify you when done.`;
|
|
514
|
+
this.history.append(msg.chatId, 'assistant', reply);
|
|
515
|
+
this._updateGlobalFocus(msg, projectRoot);
|
|
516
|
+
|
|
517
|
+
return { text: reply };
|
|
518
|
+
} catch (err) {
|
|
519
|
+
log.warn(`Direct agent route failed: ${err.message}`);
|
|
520
|
+
return null; // fall through to normal change flow
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
414
524
|
/**
|
|
415
525
|
* Handle *change command in two steps:
|
|
416
526
|
* Step 1: Claude assesses the scope (small/medium/large) — quick claude -p call
|
|
@@ -540,13 +650,19 @@ Reply with ONLY one word: small, medium, or large. Nothing else.`;
|
|
|
540
650
|
|
|
541
651
|
const projectRoot = engine.resolveProjectRoot();
|
|
542
652
|
|
|
543
|
-
//
|
|
653
|
+
// Inject tmux pane context so Claude can see agent state
|
|
544
654
|
let prompt = engine.composePrompt(msg.text);
|
|
545
655
|
if (this.orchestrator.isRunning()) {
|
|
546
656
|
const agentContext = this.orchestrator.captureCurrentAgentContext();
|
|
547
657
|
if (agentContext) {
|
|
548
658
|
prompt = `${agentContext}\n\n---\n\nUser message: ${msg.text}`;
|
|
549
659
|
}
|
|
660
|
+
} else if (projectRoot) {
|
|
661
|
+
// Even without active orchestrator, check for live dev sessions
|
|
662
|
+
const tmxContext = this._captureLiveDevContext(projectRoot);
|
|
663
|
+
if (tmxContext) {
|
|
664
|
+
prompt = `${tmxContext}\n\n---\n\nUser message: ${msg.text}`;
|
|
665
|
+
}
|
|
550
666
|
}
|
|
551
667
|
|
|
552
668
|
log.router(`Processing: "${msg.text.slice(0, 80)}..." → cwd: ${projectRoot || '~'}`);
|
|
@@ -970,6 +1086,31 @@ Reply with ONLY one word: small, medium, or large. Nothing else.`;
|
|
|
970
1086
|
return lines.join('\n');
|
|
971
1087
|
}
|
|
972
1088
|
|
|
1089
|
+
// ── Live Dev Context (for conversation when orchestrator is not tracking) ─────
|
|
1090
|
+
|
|
1091
|
+
_captureLiveDevContext(projectRoot) {
|
|
1092
|
+
const { execSync } = require('child_process');
|
|
1093
|
+
const tmx = require('./engine/tmux-utils');
|
|
1094
|
+
try {
|
|
1095
|
+
const sessions = execSync('tmux list-sessions -F "#{session_name}" 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
1096
|
+
const projectName = path.basename(projectRoot).toLowerCase();
|
|
1097
|
+
const devSession = sessions.split('\n').find((s) =>
|
|
1098
|
+
s.startsWith('orchestrix-') && s.toLowerCase().includes(projectName)
|
|
1099
|
+
) || sessions.split('\n').find((s) => s.startsWith('orchestrix-'));
|
|
1100
|
+
if (!devSession || !tmx.hasSession(devSession)) return null;
|
|
1101
|
+
|
|
1102
|
+
const windows = ['Architect', 'SM', 'Dev', 'QA'];
|
|
1103
|
+
const summaries = [];
|
|
1104
|
+
for (let w = 0; w < 4; w++) {
|
|
1105
|
+
const tail = tmx.capturePane(devSession, w, 10);
|
|
1106
|
+
const lines = tail.split('\n').filter((l) => l.trim());
|
|
1107
|
+
const last = lines.slice(-3).map((l) => l.trim().slice(0, 100)).join('\n ');
|
|
1108
|
+
summaries.push(` Window ${w} (${windows[w]}):\n ${last || '(idle)'}`);
|
|
1109
|
+
}
|
|
1110
|
+
return `[LIVE DEV SESSION] ${devSession} (orchestrator not actively tracking)\n${summaries.join('\n')}`;
|
|
1111
|
+
} catch { return null; }
|
|
1112
|
+
}
|
|
1113
|
+
|
|
973
1114
|
// ── Help ──────────────────────────────────────────────────────────────────────
|
|
974
1115
|
|
|
975
1116
|
_buildHelpText() {
|