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.
@@ -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
- // If a phase is running, inject tmux pane context so Claude can see agent state
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "4.7.6",
3
+ "version": "4.7.8",
4
4
  "description": "Yuri — Meta-Orchestrator for Orchestrix. Drive your entire project lifecycle with natural language.",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {