orchestrix-yuri 4.7.12 → 4.8.1

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.
@@ -9,6 +9,7 @@ const yaml = require('js-yaml');
9
9
  const YURI_GLOBAL = path.join(os.homedir(), '.yuri');
10
10
  const SESSION_FILE = path.join(YURI_GLOBAL, 'gateway-session.json');
11
11
  const { log } = require('../log');
12
+ const { msg } = require('./messages');
12
13
 
13
14
  // ── Shared Utilities ───────────────────────────────────────────────────────────
14
15
 
@@ -309,7 +310,7 @@ function runClaude(args, cwd, timeout) {
309
310
  }, (err, stdout, stderr) => {
310
311
  if (err && err.killed) {
311
312
  log.warn('Claude CLI timed out');
312
- return resolve({ reply: '⏱ Claude is still thinking — your request is being processed. I\'ll follow up when ready. You can also check `*status` for current state.', raw: '' });
313
+ return resolve({ reply: msg('timeout'), raw: '' });
313
314
  }
314
315
  // Claude CLI may return non-zero exit code even with valid JSON output
315
316
  // (e.g., stderr "Warning: no stdin data received" causes exit code 1).
@@ -317,7 +318,7 @@ function runClaude(args, cwd, timeout) {
317
318
  if (err && !stdout.trim()) {
318
319
  log.error(`Claude CLI error: ${err.message.slice(0, 200)}`);
319
320
  if (stderr) log.info(`stderr: ${stderr.slice(0, 200)}`);
320
- return resolve({ reply: `❌ Claude CLI error: ${err.message.slice(0, 200)}`, raw: stderr });
321
+ return resolve({ reply: msg('cli_error'), raw: stderr });
321
322
  }
322
323
 
323
324
  try {
@@ -478,7 +479,7 @@ function composePrompt(userMessage) {
478
479
  if (l2) sections.push(l2);
479
480
 
480
481
  log.engine('Memory context changed, injecting refresh into prompt');
481
- return `[CONTEXT UPDATE Your memory has been updated]\n${sections.join('\n\n')}\n[END CONTEXT UPDATE]\n\n${userMessage}`;
482
+ return `<system-context-update silent="true">\nDo not mention or reference this context update in your response. This is an automatic memory refresh.\n${sections.join('\n\n')}\n</system-context-update>\n\n${userMessage}`;
482
483
  }
483
484
 
484
485
  /**
@@ -67,7 +67,8 @@ class Dispatcher {
67
67
  async classify(text) {
68
68
  // Re-entry guard
69
69
  if (this._busy) {
70
- return { action: 'conversation', description: text, reasoning: 'dispatcher busy' };
70
+ log.warn('Dispatcher: busy, defaulting to change');
71
+ return { action: 'change', description: text, reasoning: 'dispatcher busy' };
71
72
  }
72
73
 
73
74
  // Auto-recover if session died
@@ -75,7 +76,8 @@ class Dispatcher {
75
76
  this._ready = false;
76
77
  await this.start();
77
78
  if (!this._ready) {
78
- return { action: 'conversation', description: text, reasoning: 'dispatcher unavailable' };
79
+ log.warn('Dispatcher: unavailable after recovery attempt, defaulting to change');
80
+ return { action: 'change', description: text, reasoning: 'dispatcher unavailable' };
79
81
  }
80
82
  }
81
83
 
@@ -165,17 +167,32 @@ class Dispatcher {
165
167
  }
166
168
  }
167
169
 
168
- // Strategy 3: Look for bare action words in the last few lines
169
- const tail = lines.slice(-5).join(' ').toLowerCase();
170
- for (const action of valid) {
171
- if (action !== 'conversation' && tail.includes(`"${action}"`)) {
172
- return { action, description: originalText, reasoning: 'keyword match' };
170
+ // Strategy 3: Claude re-classify (replaces keyword matching)
171
+ // Only reached when tmux output was garbled or unparseable — rare but important.
172
+ log.warn('Dispatcher: JSON parse failed, attempting Claude re-classify');
173
+ try {
174
+ const engine = require('./claude-sdk');
175
+ const binary = engine.findClaudeBinary();
176
+ const safeText = originalText.slice(0, 500).replace(/'/g, "'\\''");
177
+ const prompt = `Classify this user message into ONE action. Valid actions: ${valid.join(', ')}. Reply with ONLY a JSON object like {"action":"change","description":"summary","reasoning":"why"}. User message: "${safeText}"`;
178
+ const result = execSync(
179
+ `${binary} -p '${prompt.replace(/'/g, "'\\''")}' --output-format json 2>/dev/null`,
180
+ { encoding: 'utf8', timeout: 15000 }
181
+ ).trim();
182
+ const json = JSON.parse(result);
183
+ const reply = json.result || '';
184
+ const reParsed = JSON.parse(reply);
185
+ if (reParsed.action && valid.includes(reParsed.action)) {
186
+ log.engine(`Dispatcher: re-classified as "${reParsed.action}"`);
187
+ return { action: reParsed.action, description: reParsed.description || originalText, reasoning: 'reclassified' };
173
188
  }
189
+ } catch (err) {
190
+ log.warn(`Dispatcher: re-classify failed: ${err.message}`);
174
191
  }
175
192
 
176
- // Parse failed — default to "change" (bias toward action, not conversation)
177
- log.warn('Dispatcher: failed to parse response, defaulting to change');
178
- return { action: 'change', description: originalText, reasoning: 'parse failed' };
193
+ // Absolute last resort — default to "change" (bias toward action)
194
+ log.warn('Dispatcher: all strategies failed, defaulting to change');
195
+ return { action: 'change', description: originalText, reasoning: 'all parse strategies failed' };
179
196
  }
180
197
 
181
198
  /**
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ // ── Language Detection ──────────────────────────────────────────────────────
8
+
9
+ let _cachedLang = null;
10
+ let _langCheckedAt = 0;
11
+ const LANG_CACHE_TTL = 300000; // 5 min
12
+
13
+ /**
14
+ * Detect user language from boss/preferences.yaml.
15
+ * Falls back to 'en'. Caches for 5 min.
16
+ */
17
+ function detectLang() {
18
+ const now = Date.now();
19
+ if (_cachedLang && now - _langCheckedAt < LANG_CACHE_TTL) return _cachedLang;
20
+
21
+ _langCheckedAt = now;
22
+ try {
23
+ const prefPath = path.join(os.homedir(), '.yuri', 'boss', 'preferences.yaml');
24
+ if (fs.existsSync(prefPath)) {
25
+ const content = fs.readFileSync(prefPath, 'utf8');
26
+ // Quick extraction without yaml dependency (preferences.yaml is simple)
27
+ const match = content.match(/language:\s*["']?(\w+)/);
28
+ if (match && match[1]) {
29
+ const lang = match[1].toLowerCase();
30
+ _cachedLang = lang.startsWith('zh') ? 'zh' : 'en';
31
+ return _cachedLang;
32
+ }
33
+ }
34
+ } catch { /* fallback */ }
35
+
36
+ _cachedLang = 'en';
37
+ return _cachedLang;
38
+ }
39
+
40
+ /** Override language (useful for testing). */
41
+ function setLang(lang) {
42
+ _cachedLang = lang;
43
+ _langCheckedAt = Date.now();
44
+ }
45
+
46
+ // ── Message Templates ───────────────────────────────────────────────────────
47
+
48
+ const MESSAGES = {
49
+ // ── Phase Completion ──
50
+ plan_complete: {
51
+ en: '🎉 Planning complete!{summary}\n\nNext: review the docs above, then run `*develop` to start building.',
52
+ zh: '🎉 规划完成!{summary}\n\n下一步:检查上面的文档,然后运行 `*develop` 开始开发。',
53
+ },
54
+ dev_complete: {
55
+ en: '🎉 Development complete! All stories implemented.\n\nNext: run `*test` to validate each epic with smoke tests.',
56
+ zh: '🎉 开发完成!所有 story 已实现。\n\n下一步:运行 `*test` 对每个 epic 进行冒烟测试。',
57
+ },
58
+ test_all_passed: {
59
+ en: '\n🚀 All tests passed! Run `*deploy` when ready.',
60
+ zh: '\n🚀 全部测试通过!准备好后运行 `*deploy` 部署。',
61
+ },
62
+ test_some_failed: {
63
+ en: '\n⚠️ {count} epic(s) failed. Review and fix manually, or run `*test` again.',
64
+ zh: '\n⚠️ {count} 个 epic 未通过。手动修复后重新运行 `*test`。',
65
+ },
66
+ iterate_launched: {
67
+ en: '🔄 New iteration launched!\n\nSM is drafting new stories. Agents will chain automatically (SM → Architect → Dev → QA).',
68
+ zh: '🔄 新迭代已启动!\n\nSM 正在拆分新 story,Agent 将自动接力(SM → Architect → Dev → QA)。',
69
+ },
70
+
71
+ // ── Phase Start ──
72
+ dev_started: {
73
+ en: '🚀 Development started! 4 agents (Architect, SM, Dev, QA) are running.\n\nAgents chain automatically via handoff-detector. I\'ll send a progress report every {minutes} minutes.',
74
+ zh: '🚀 开发已启动!4 个 Agent(Architect、SM、Dev、QA)正在工作。\n\nAgent 通过 handoff-detector 自动接力。我每 {minutes} 分钟发送一次进度报告。',
75
+ },
76
+
77
+ // ── Change / Direct Agent ──
78
+ change_small: {
79
+ en: '🔧 Small change started → Dev *solo\n\n"{desc}"\n\nI\'ll notify you when it\'s done.',
80
+ zh: '🔧 小改动已启动 → Dev *solo\n\n"{desc}"\n\n完成后我会通知你。',
81
+ },
82
+ change_medium: {
83
+ en: '🔧 {scope} change started → PO *route-change\n\n"{desc}"\n\nPO will assess and route to the right agent. I\'ll keep you updated.',
84
+ zh: '🔧 {scope}级改动已启动 → PO *route-change\n\n"{desc}"\n\nPO 评估后分配给合适的 Agent,我会持续更新。',
85
+ },
86
+ direct_agent: {
87
+ en: '🎯 → **{agent}**\n\n"{desc}"\n\nI\'ll notify you when done.',
88
+ zh: '🎯 → **{agent}**\n\n"{desc}"\n\n完成后通知你。',
89
+ },
90
+ quickfix_started: {
91
+ en: '🐛 Quick fix started → Dev *quick-fix\n\n"{desc}"\n\nI\'ll notify you when it\'s done.',
92
+ zh: '🐛 快速修复已启动 → Dev *quick-fix\n\n"{desc}"\n\n完成后通知你。',
93
+ },
94
+
95
+ // ── Progress ──
96
+ agent_handoff: {
97
+ en: '🔄 {from} → **{to}**{story}',
98
+ zh: '🔄 {from} → **{to}**{story}',
99
+ },
100
+ monitoring_dev: {
101
+ en: '🔄 Now monitoring dev cycle (SM → Architect → Dev → QA). I\'ll report agent handoffs and progress.',
102
+ zh: '🔄 正在监控开发流程(SM → Architect → Dev → QA),我会汇报 Agent 交接和进度。',
103
+ },
104
+ change_complete: {
105
+ en: '✅ Change complete.\n\n{summary}\n\nWhat would you like to do next?',
106
+ zh: '✅ 改动完成。\n\n{summary}\n\n接下来要做什么?',
107
+ },
108
+
109
+ // ── Errors ──
110
+ error_recovery: {
111
+ en: {
112
+ plan: 'Run `*plan` to restart, or `*status` to check saved progress.',
113
+ develop: 'Run `*develop` to restart, or `*status` to see completed stories.',
114
+ test: 'Run `*test` to restart testing.',
115
+ change: 'Send your change request again.',
116
+ iterate: 'Run `*iterate` again.',
117
+ default: 'Use `*status` to check current state.',
118
+ },
119
+ zh: {
120
+ plan: '运行 `*plan` 重新开始,或 `*status` 查看已保存的进度。',
121
+ develop: '运行 `*develop` 重新开始,或 `*status` 查看已完成的 story。',
122
+ test: '运行 `*test` 重新测试。',
123
+ change: '重新发送你的改动请求。',
124
+ iterate: '重新运行 `*iterate`。',
125
+ default: '使用 `*status` 查看当前状态。',
126
+ },
127
+ },
128
+ timeout: {
129
+ en: '⏱ This is taking longer than usual. The operation may still be running.\n\nTry `*status` to check progress, or send your message again.',
130
+ zh: '⏱ 处理时间较长,操作可能仍在进行中。\n\n试试 `*status` 查看进度,或重新发送消息。',
131
+ },
132
+ cli_error: {
133
+ en: '❌ Something went wrong. Try again, or use `*status` to check state.',
134
+ zh: '❌ 出了点问题。请重试,或使用 `*status` 查看状态。',
135
+ },
136
+
137
+ // ── Status ──
138
+ no_phase: {
139
+ en: 'No active phase. Available commands: *plan, *develop, *test, *deploy, *projects, *switch',
140
+ zh: '当前无活跃阶段。可用命令:*plan、*develop、*test、*deploy、*projects、*switch',
141
+ },
142
+ };
143
+
144
+ // ── Public API ──────────────────────────────────────────────────────────────
145
+
146
+ /**
147
+ * Get a localized message by key with parameter substitution.
148
+ * @param {string} key - Message key (e.g., 'plan_complete')
149
+ * @param {object} params - Substitution params (e.g., { summary: '...' })
150
+ * @returns {string}
151
+ */
152
+ function msg(key, params = {}) {
153
+ const lang = detectLang();
154
+ const template = MESSAGES[key];
155
+ if (!template) return key;
156
+
157
+ let text = template[lang] || template.en || key;
158
+
159
+ // Handle nested objects (like error_recovery)
160
+ if (typeof text === 'object') {
161
+ const subKey = params._sub || 'default';
162
+ text = text[subKey] || text.default || JSON.stringify(text);
163
+ }
164
+
165
+ // Substitute {param} placeholders
166
+ return text.replace(/\{(\w+)\}/g, (_, k) => (params[k] != null ? params[k] : `{${k}}`));
167
+ }
168
+
169
+ module.exports = { msg, detectLang, setLang, MESSAGES };
@@ -7,6 +7,7 @@ const os = require('os');
7
7
  const yaml = require('js-yaml');
8
8
  const tmx = require('./tmux-utils');
9
9
  const { log } = require('../log');
10
+ const { msg } = require('./messages');
10
11
 
11
12
  const YURI_GLOBAL = path.join(os.homedir(), '.yuri');
12
13
  const SKILL_DIR = path.join(os.homedir(), '.claude', 'skills', 'yuri');
@@ -194,7 +195,6 @@ class PhaseOrchestrator {
194
195
 
195
196
  const waiting = this._waitingForInput ? ' (WAITING FOR YOUR INPUT)' : '';
196
197
  return `[LIVE AGENT CONTEXT] Phase: plan, Agent: ${agent.name} (${this._step + 1}/${PLAN_AGENTS.length})${waiting}\n` +
197
- `tmux session: ${this._session}, window: ${agent.window}\n` +
198
198
  `--- Agent output (last 40 lines) ---\n${pane}\n--- End agent output ---`;
199
199
  }
200
200
 
@@ -205,9 +205,9 @@ class PhaseOrchestrator {
205
205
  for (let w = 0; w < 4; w++) {
206
206
  const tail = tmx.capturePane(this._session, w, 5);
207
207
  const lastLine = tail.split('\n').filter((l) => l.trim()).pop() || '(empty)';
208
- summaries.push(` Window ${w} (${windows[w]}): ${lastLine.trim().slice(0, 80)}`);
208
+ summaries.push(` ${windows[w]}: ${lastLine.trim().slice(0, 80)}`);
209
209
  }
210
- return `[LIVE AGENT CONTEXT] Phase: develop, Session: ${this._session}\n${summaries.join('\n')}`;
210
+ return `[LIVE AGENT CONTEXT] Phase: develop\n${summaries.join('\n')}`;
211
211
  }
212
212
 
213
213
  return null;
@@ -539,8 +539,19 @@ class PhaseOrchestrator {
539
539
  this._phase = null;
540
540
  this._step = 0;
541
541
 
542
+ // Build output summary
543
+ const outputs = [];
544
+ for (const agent of PLAN_AGENTS) {
545
+ if (agent.output) {
546
+ const outputPath = path.join(this._projectRoot, agent.output);
547
+ const exists = fs.existsSync(outputPath);
548
+ outputs.push(` ${exists ? '✅' : '⚠️'} ${agent.name} → ${agent.output}`);
549
+ }
550
+ }
551
+ const outputSummary = outputs.length > 0 ? `\n\n${outputs.join('\n')}` : '';
552
+
542
553
  log.engine('Plan phase complete');
543
- this.onComplete('plan', '🎉 Planning phase complete! All 6 agents finished.\n\nRun *develop to start automated development.');
554
+ this.onComplete('plan', msg('plan_complete', { summary: outputSummary }));
544
555
  }
545
556
 
546
557
  /**
@@ -642,7 +653,7 @@ class PhaseOrchestrator {
642
653
 
643
654
  const reportMin = Math.round(this._reportInterval / 60000);
644
655
  log.engine(`Dev phase started: session=${this._session}, report every ${reportMin}min`);
645
- return `🚀 Development started! 4 agents (Architect, SM, Dev, QA) are running.\n\nAgents chain automatically via handoff-detector. I'll send a progress report every ${reportMin} minutes.`;
656
+ return msg('dev_started', { minutes: reportMin });
646
657
  }
647
658
 
648
659
  _pollDevSession() {
@@ -819,7 +830,7 @@ class PhaseOrchestrator {
819
830
  // Current activity
820
831
  if (p.currentAgent) {
821
832
  const storyInfo = p.currentStory ? ` → ${p.currentStory}` : '';
822
- lines.push(`Current: ${p.currentAgent} (window ${p.currentWindow})${storyInfo}`);
833
+ lines.push(`Current: ${p.currentAgent}${storyInfo}`);
823
834
  } else {
824
835
  lines.push(`Current: waiting for next handoff`);
825
836
  }
@@ -875,7 +886,7 @@ class PhaseOrchestrator {
875
886
  }
876
887
 
877
888
  log.engine('Dev phase complete');
878
- this.onComplete('develop', '🎉 Development complete! All stories finished.\n\nRun *test to start smoke testing.');
889
+ this.onComplete('develop', msg('dev_complete'));
879
890
  }
880
891
 
881
892
  // ── Test Phase ──────────────────────────────────────────────────────────────
@@ -1312,9 +1323,9 @@ class PhaseOrchestrator {
1312
1323
  }
1313
1324
 
1314
1325
  if (failed === 0) {
1315
- lines.push('\n🚀 All tests passed! Run *deploy when ready.');
1326
+ lines.push(msg('test_all_passed'));
1316
1327
  } else {
1317
- lines.push(`\n⚠️ ${failed} epic(s) failed. Review and fix manually, or run *test again.`);
1328
+ lines.push(msg('test_some_failed', { count: failed }));
1318
1329
  }
1319
1330
 
1320
1331
  this._phase = null;
@@ -1432,7 +1443,7 @@ class PhaseOrchestrator {
1432
1443
 
1433
1444
  this._changeContext = null;
1434
1445
  log.engine('Iterate complete — dev automation started');
1435
- this.onComplete('iterate', `🔄 New iteration launched!\n\nSM is drafting stories in dev session: ${devSession}\nAgents will chain automatically via handoff-detector.`);
1446
+ this.onComplete('iterate', msg('iterate_launched'));
1436
1447
 
1437
1448
  // Transition to dev monitoring so SM → Architect → Dev → QA cycle is tracked.
1438
1449
  // Without this, the entire dev cycle runs unmonitored after iterate completes.
@@ -1447,7 +1458,7 @@ class PhaseOrchestrator {
1447
1458
  const pollInterval = this.config.dev_poll_interval || 300000;
1448
1459
  this._timer = setInterval(() => this._pollDevSession(), pollInterval);
1449
1460
  log.engine(`Iterate → dev monitoring: session=${devSession}, poll every ${Math.round(pollInterval / 60000)}min`);
1450
- this.onProgress('🔄 Now monitoring dev cycle (SM → Architect → Dev → QA). I\'ll report agent handoffs and progress.');
1461
+ this.onProgress(msg('monitoring_dev'));
1451
1462
  } else {
1452
1463
  this._phase = null;
1453
1464
  }
@@ -1501,7 +1512,7 @@ class PhaseOrchestrator {
1501
1512
  this._timer = setInterval(() => this._pollChange(), pollInterval);
1502
1513
 
1503
1514
  log.engine(`Quick fix started: "${bugDesc.slice(0, 60)}..."`);
1504
- return `🐛 Quick fix started → Dev *quick-fix\n\n"${bugDesc.slice(0, 100)}"\n\nI'll notify you when it's done.`;
1515
+ return msg('quickfix_started', { desc: bugDesc.slice(0, 100) });
1505
1516
  }
1506
1517
 
1507
1518
  // ── Change Management ───────────────────────────────────────────────────────
@@ -1563,7 +1574,7 @@ class PhaseOrchestrator {
1563
1574
  this._changeContext = { scope: 'small', description };
1564
1575
  this._timer = setInterval(() => this._pollChange(), pollInterval);
1565
1576
 
1566
- return `🔧 Small change started → Dev *solo\n\n"${description.slice(0, 100)}"\n\nI'll notify you when it's done.`;
1577
+ return msg('change_small', { desc: description.slice(0, 100) });
1567
1578
  }
1568
1579
 
1569
1580
  /**
@@ -1593,7 +1604,7 @@ class PhaseOrchestrator {
1593
1604
  };
1594
1605
  this._timer = setInterval(() => this._pollChange(), pollInterval);
1595
1606
 
1596
- return `🔧 ${scope === 'large' ? 'Large' : 'Medium'} change started → PO *route-change\n\n"${description.slice(0, 100)}"\n\nPO will assess and route to the right agent. I'll keep you updated.`;
1607
+ return msg('change_medium', { scope: scope === 'large' ? 'Large' : 'Medium', desc: description.slice(0, 100) });
1597
1608
  }
1598
1609
 
1599
1610
  /**
@@ -1695,7 +1706,7 @@ class PhaseOrchestrator {
1695
1706
  const pollInterval = this.config.dev_poll_interval || 300000;
1696
1707
  this._timer = setInterval(() => this._pollDevSession(), pollInterval);
1697
1708
  log.engine(`Change → dev monitoring: session=${this._session}, poll every ${Math.round(pollInterval / 60000)}min`);
1698
- this.onProgress('🔄 Now monitoring dev cycle (SM → Architect → Dev → QA). I\'ll report agent handoffs and progress.');
1709
+ this.onProgress(msg('monitoring_dev'));
1699
1710
  } else {
1700
1711
  this._phase = null;
1701
1712
  this._changeContext = null;
@@ -1711,7 +1722,17 @@ class PhaseOrchestrator {
1711
1722
  }
1712
1723
  this._phase = null;
1713
1724
  log.error(`Phase ${phase} error: ${message}`);
1714
- this.onError(phase, message);
1725
+
1726
+ // Sanitize internal details from user-facing message
1727
+ const cleanMsg = message
1728
+ .replace(/orchestrix-[\w-]+/g, 'dev session')
1729
+ .replace(/op-[\w-]+/g, 'planning session')
1730
+ .replace(/yuri-[\w-]+/g, 'dispatcher')
1731
+ .replace(/\/Users\/\S+/g, '')
1732
+ .replace(/tmux session/gi, 'agent session');
1733
+
1734
+ const recovery = msg('error_recovery', { _sub: phase });
1735
+ this.onError(phase, `❌ ${cleanMsg}\n\n${recovery}`);
1715
1736
  }
1716
1737
 
1717
1738
  _updatePlanMemory(status) {
@@ -11,6 +11,7 @@ const engine = require('./engine/claude-sdk');
11
11
  const { runReflect } = require('./engine/reflect');
12
12
  const { PhaseOrchestrator } = require('./engine/phase-orchestrator');
13
13
  const { Dispatcher } = require('./engine/dispatcher');
14
+ const { msg } = require('./engine/messages');
14
15
  const { log } = require('./log');
15
16
 
16
17
  const YURI_GLOBAL = path.join(os.homedir(), '.yuri');
@@ -65,7 +66,7 @@ class Router {
65
66
  config: config.engine,
66
67
  onProgress: (msg) => this._sendProactive(msg),
67
68
  onComplete: (phase, summary) => this._sendProactive(summary),
68
- onError: (phase, err) => this._sendProactive(`❌ Phase ${phase} error: ${err}`),
69
+ onError: (phase, err) => this._sendProactive(err),
69
70
  onQuestionAsked: (text) => this._sendProactiveWithId(text),
70
71
  });
71
72
 
@@ -526,7 +527,7 @@ class Router {
526
527
  }, pollInterval);
527
528
 
528
529
  this.history.append(msg.chatId, 'user', msg.text);
529
- const reply = `🎯 Direct **${matched.slug}**\n\n"${cleanDesc.slice(0, 120)}"\n\nI'll notify you when done.`;
530
+ const reply = msg('direct_agent', { agent: matched.slug, desc: cleanDesc.slice(0, 120) });
530
531
  this.history.append(msg.chatId, 'assistant', reply);
531
532
  this._updateGlobalFocus(msg, projectRoot);
532
533
 
@@ -648,7 +649,7 @@ Reply with ONLY one word: small, medium, or large. Nothing else.`;
648
649
  }
649
650
 
650
651
  if (parts.length === 0) {
651
- parts.push('No active phase. Available commands: *plan, *develop, *test, *deploy, *projects, *switch');
652
+ parts.push(msg('no_phase'));
652
653
  }
653
654
 
654
655
  this.history.append(msg.chatId, 'user', msg.text);
@@ -1123,7 +1124,7 @@ Reply with ONLY one word: small, medium, or large. Nothing else.`;
1123
1124
  const last = lines.slice(-3).map((l) => l.trim().slice(0, 100)).join('\n ');
1124
1125
  summaries.push(` Window ${w} (${windows[w]}):\n ${last || '(idle)'}`);
1125
1126
  }
1126
- return `[LIVE DEV SESSION] ${devSession} (orchestrator not actively tracking)\n${summaries.join('\n')}`;
1127
+ return `[LIVE DEV SESSION] (agents running independently)\n${summaries.join('\n')}`;
1127
1128
  } catch { return null; }
1128
1129
  }
1129
1130
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "4.7.12",
3
+ "version": "4.8.1",
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": {