orchestrix-yuri 4.8.0 → 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: '⏱ This is taking longer than usual. The operation may still be running.\n\nTry `*status` to check progress, or send your message again.', 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: '❌ Something went wrong. Try again, or use `*status` to check state.', raw: stderr });
321
+ return resolve({ reply: msg('cli_error'), raw: stderr });
321
322
  }
322
323
 
323
324
  try {
@@ -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');
@@ -550,7 +551,7 @@ class PhaseOrchestrator {
550
551
  const outputSummary = outputs.length > 0 ? `\n\n${outputs.join('\n')}` : '';
551
552
 
552
553
  log.engine('Plan phase complete');
553
- this.onComplete('plan', `🎉 Planning complete!${outputSummary}\n\nNext: review the docs above, then run \`*develop\` to start building.`);
554
+ this.onComplete('plan', msg('plan_complete', { summary: outputSummary }));
554
555
  }
555
556
 
556
557
  /**
@@ -652,7 +653,7 @@ class PhaseOrchestrator {
652
653
 
653
654
  const reportMin = Math.round(this._reportInterval / 60000);
654
655
  log.engine(`Dev phase started: session=${this._session}, report every ${reportMin}min`);
655
- 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 });
656
657
  }
657
658
 
658
659
  _pollDevSession() {
@@ -885,7 +886,7 @@ class PhaseOrchestrator {
885
886
  }
886
887
 
887
888
  log.engine('Dev phase complete');
888
- this.onComplete('develop', '🎉 Development complete! All stories implemented.\n\nNext: run `*test` to validate each epic with smoke tests.');
889
+ this.onComplete('develop', msg('dev_complete'));
889
890
  }
890
891
 
891
892
  // ── Test Phase ──────────────────────────────────────────────────────────────
@@ -1322,9 +1323,9 @@ class PhaseOrchestrator {
1322
1323
  }
1323
1324
 
1324
1325
  if (failed === 0) {
1325
- lines.push('\n🚀 All tests passed! Run *deploy when ready.');
1326
+ lines.push(msg('test_all_passed'));
1326
1327
  } else {
1327
- lines.push(`\n⚠️ ${failed} epic(s) failed. Review and fix manually, or run *test again.`);
1328
+ lines.push(msg('test_some_failed', { count: failed }));
1328
1329
  }
1329
1330
 
1330
1331
  this._phase = null;
@@ -1442,7 +1443,7 @@ class PhaseOrchestrator {
1442
1443
 
1443
1444
  this._changeContext = null;
1444
1445
  log.engine('Iterate complete — dev automation started');
1445
- this.onComplete('iterate', `🔄 New iteration launched!\n\nSM is drafting new stories. Agents will chain automatically (SM → Architect → Dev → QA).`);
1446
+ this.onComplete('iterate', msg('iterate_launched'));
1446
1447
 
1447
1448
  // Transition to dev monitoring so SM → Architect → Dev → QA cycle is tracked.
1448
1449
  // Without this, the entire dev cycle runs unmonitored after iterate completes.
@@ -1457,7 +1458,7 @@ class PhaseOrchestrator {
1457
1458
  const pollInterval = this.config.dev_poll_interval || 300000;
1458
1459
  this._timer = setInterval(() => this._pollDevSession(), pollInterval);
1459
1460
  log.engine(`Iterate → dev monitoring: session=${devSession}, poll every ${Math.round(pollInterval / 60000)}min`);
1460
- this.onProgress('🔄 Now monitoring dev cycle (SM → Architect → Dev → QA). I\'ll report agent handoffs and progress.');
1461
+ this.onProgress(msg('monitoring_dev'));
1461
1462
  } else {
1462
1463
  this._phase = null;
1463
1464
  }
@@ -1511,7 +1512,7 @@ class PhaseOrchestrator {
1511
1512
  this._timer = setInterval(() => this._pollChange(), pollInterval);
1512
1513
 
1513
1514
  log.engine(`Quick fix started: "${bugDesc.slice(0, 60)}..."`);
1514
- 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) });
1515
1516
  }
1516
1517
 
1517
1518
  // ── Change Management ───────────────────────────────────────────────────────
@@ -1573,7 +1574,7 @@ class PhaseOrchestrator {
1573
1574
  this._changeContext = { scope: 'small', description };
1574
1575
  this._timer = setInterval(() => this._pollChange(), pollInterval);
1575
1576
 
1576
- 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) });
1577
1578
  }
1578
1579
 
1579
1580
  /**
@@ -1603,7 +1604,7 @@ class PhaseOrchestrator {
1603
1604
  };
1604
1605
  this._timer = setInterval(() => this._pollChange(), pollInterval);
1605
1606
 
1606
- 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) });
1607
1608
  }
1608
1609
 
1609
1610
  /**
@@ -1705,7 +1706,7 @@ class PhaseOrchestrator {
1705
1706
  const pollInterval = this.config.dev_poll_interval || 300000;
1706
1707
  this._timer = setInterval(() => this._pollDevSession(), pollInterval);
1707
1708
  log.engine(`Change → dev monitoring: session=${this._session}, poll every ${Math.round(pollInterval / 60000)}min`);
1708
- this.onProgress('🔄 Now monitoring dev cycle (SM → Architect → Dev → QA). I\'ll report agent handoffs and progress.');
1709
+ this.onProgress(msg('monitoring_dev'));
1709
1710
  } else {
1710
1711
  this._phase = null;
1711
1712
  this._changeContext = null;
@@ -1730,15 +1731,7 @@ class PhaseOrchestrator {
1730
1731
  .replace(/\/Users\/\S+/g, '')
1731
1732
  .replace(/tmux session/gi, 'agent session');
1732
1733
 
1733
- const hints = {
1734
- plan: 'Run `*plan` to restart, or `*status` to check saved progress.',
1735
- develop: 'Run `*develop` to restart, or `*status` to see completed stories.',
1736
- test: 'Run `*test` to restart testing.',
1737
- change: 'Send your change request again.',
1738
- iterate: 'Run `*iterate` again.',
1739
- };
1740
-
1741
- const recovery = hints[phase] || 'Use `*status` to check current state.';
1734
+ const recovery = msg('error_recovery', { _sub: phase });
1742
1735
  this.onError(phase, `❌ ${cleanMsg}\n\n${recovery}`);
1743
1736
  }
1744
1737
 
@@ -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');
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "4.8.0",
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": {