orchestrix-yuri 4.6.5 โ†’ 4.6.7

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.
@@ -238,7 +238,8 @@ class PhaseOrchestrator {
238
238
  }
239
239
 
240
240
  if (this._phase === 'test') {
241
- return { phase: 'test', message: '๐Ÿงช Testing in progress. QA running smoke tests.' };
241
+ const card = this._buildTestProgressCard();
242
+ return { phase: 'test', message: card };
242
243
  }
243
244
 
244
245
  if (this._phase === 'iterate') {
@@ -947,11 +948,14 @@ class PhaseOrchestrator {
947
948
 
948
949
  // Start first epic test
949
950
  this._loadAndTestEpic();
951
+ this._testStartedAt = Date.now();
952
+ this._lastReportTime = Date.now();
950
953
 
951
954
  const pollInterval = this.config.phase_poll_interval || 30000;
952
955
  this._timer = setInterval(() => this._pollTest(), pollInterval);
953
956
 
954
- log.engine(`Test phase started: session=${this._session}, epics=${epicIds.join(',')}, startIdx=${startIdx}`);
957
+ const reportMin = Math.round(this._reportInterval / 60000);
958
+ log.engine(`Test phase started: session=${this._session}, epics=${epicIds.join(',')}, startIdx=${startIdx}, report every ${reportMin}min`);
955
959
  return `๐Ÿงช Testing started! ${epicIds.length} epic(s) to test.\n\nQA will smoke-test each epic. Failed tests auto-trigger Dev fixes (max 3 rounds).\nI'll notify you of results.`;
956
960
  }
957
961
 
@@ -1054,6 +1058,14 @@ class PhaseOrchestrator {
1054
1058
  return;
1055
1059
  }
1056
1060
 
1061
+ // Periodic progress report (reuses dev phase's _reportInterval)
1062
+ const now = Date.now();
1063
+ if (now - this._lastReportTime >= this._reportInterval) {
1064
+ this._lastReportTime = now;
1065
+ const card = this._buildTestProgressCard();
1066
+ this.onProgress(card);
1067
+ }
1068
+
1057
1069
  // Determine which window to watch
1058
1070
  const window = ctx.subPhase === 'dev-fixing' ? 2 : 3;
1059
1071
  const result = tmx.checkCompletion(this._session, window, this._lastHash);
@@ -1154,6 +1166,62 @@ class PhaseOrchestrator {
1154
1166
  }
1155
1167
  }
1156
1168
 
1169
+ /**
1170
+ * Build a progress card for the test phase (used by getStatus + periodic reports).
1171
+ */
1172
+ _buildTestProgressCard() {
1173
+ const ctx = this._testContext;
1174
+ if (!ctx) return '๐Ÿงช Testing in progress.';
1175
+
1176
+ const total = ctx.epicIds.length;
1177
+ const passed = Object.values(ctx.results).filter((r) => r.status === 'passed').length;
1178
+ const failed = Object.values(ctx.results).filter((r) => r.status === 'failed').length;
1179
+ const tested = passed + failed;
1180
+ const pct = total > 0 ? Math.round((tested / total) * 100) : 0;
1181
+
1182
+ const currentEpic = ctx.epicIds[ctx.epicIdx];
1183
+ const subPhaseLabel = {
1184
+ 'qa-testing': '๐Ÿ” QA smoke-testing',
1185
+ 'dev-fixing': '๐Ÿ”ง Dev fixing bug',
1186
+ }[ctx.subPhase] || 'โณ Loading agent';
1187
+
1188
+ const elapsed = this._devStartedAt ? this._formatDuration(Date.now() - this._devStartedAt) : '';
1189
+ const startedAt = this._testStartedAt ? this._formatDuration(Date.now() - this._testStartedAt) : '';
1190
+
1191
+ const lines = [
1192
+ `๐Ÿงช **Test Phase Progress**`,
1193
+ ``,
1194
+ this._progressBar(pct),
1195
+ ``,
1196
+ `๐Ÿ“Š Epics: ${tested}/${total} tested (โœ… ${passed} passed, โŒ ${failed} failed)`,
1197
+ ];
1198
+
1199
+ if (ctx.epicIdx < total) {
1200
+ lines.push(`๐ŸŽฏ Current: Epic ${currentEpic} โ€” ${subPhaseLabel} (round ${ctx.round + 1}/${ctx.maxRounds})`);
1201
+ }
1202
+
1203
+ // Per-epic summary
1204
+ if (tested > 0) {
1205
+ lines.push('', '๐Ÿ“‹ Results:');
1206
+ for (const epicId of ctx.epicIds) {
1207
+ const r = ctx.results[epicId];
1208
+ if (r && r.status === 'passed') {
1209
+ lines.push(` โœ… Epic ${epicId} โ€” passed (${r.rounds} round(s))`);
1210
+ } else if (r && r.status === 'failed') {
1211
+ lines.push(` โŒ Epic ${epicId} โ€” failed (${r.rounds} rounds)`);
1212
+ } else if (epicId === currentEpic) {
1213
+ lines.push(` โณ Epic ${epicId} โ€” in progress`);
1214
+ } else {
1215
+ lines.push(` โฌœ Epic ${epicId} โ€” pending`);
1216
+ }
1217
+ }
1218
+ }
1219
+
1220
+ if (startedAt) lines.push(`\nโฑ๏ธ Elapsed: ${startedAt}`);
1221
+
1222
+ return lines.join('\n');
1223
+ }
1224
+
1157
1225
  /**
1158
1226
  * Complete test phase: aggregate results, update state files, send summary.
1159
1227
  */
@@ -1220,6 +1288,7 @@ class PhaseOrchestrator {
1220
1288
 
1221
1289
  this._phase = null;
1222
1290
  this._testContext = null;
1291
+ this._testStartedAt = null;
1223
1292
  log.engine(`Test phase complete: ${passed}/${total} passed, ${failed} failed`);
1224
1293
  this.onComplete('test', lines.join('\n'));
1225
1294
  }
@@ -99,25 +99,38 @@ class Router {
99
99
 
100
100
  try {
101
101
  const focus = yaml.load(fs.readFileSync(focusPath, 'utf8')) || {};
102
- if (parseInt(focus.phase, 10) !== 3) return;
103
-
104
- // Skip if dev phase already complete
105
- const phase3Path = path.join(projectRoot, '.yuri', 'state', 'phase3.yaml');
106
- if (fs.existsSync(phase3Path)) {
107
- const phase3 = yaml.load(fs.readFileSync(phase3Path, 'utf8')) || {};
108
- if (phase3.status === 'complete') return;
109
- }
102
+ const phaseNum = parseInt(focus.phase, 10);
103
+
104
+ if (phaseNum === 3) {
105
+ // Skip if dev phase already complete
106
+ const phase3Path = path.join(projectRoot, '.yuri', 'state', 'phase3.yaml');
107
+ if (fs.existsSync(phase3Path)) {
108
+ const phase3 = yaml.load(fs.readFileSync(phase3Path, 'utf8')) || {};
109
+ if (phase3.status === 'complete') return;
110
+ }
110
111
 
111
- // Check tmux session is alive
112
- const { execSync } = require('child_process');
113
- const sessions = execSync('tmux list-sessions -F "#{session_name}" 2>/dev/null', { encoding: 'utf8' }).trim();
114
- if (!sessions.split('\n').some((s) => s.startsWith('orchestrix-'))) return;
112
+ // Check tmux session is alive
113
+ const { execSync } = require('child_process');
114
+ const sessions = execSync('tmux list-sessions -F "#{session_name}" 2>/dev/null', { encoding: 'utf8' }).trim();
115
+ if (!sessions.split('\n').some((s) => s.startsWith('orchestrix-'))) return;
115
116
 
116
- // Generate and send progress card
117
- const card = this._buildStatusCard(projectRoot, focus);
118
- if (card) {
119
- log.router('Auto-reporting dev progress');
120
- this._sendProactive(card);
117
+ const card = this._buildStatusCard(projectRoot, focus);
118
+ if (card) {
119
+ log.router('Auto-reporting dev progress');
120
+ this._sendProactive(card);
121
+ }
122
+ } else if (phaseNum === 4) {
123
+ // Skip if test phase already complete
124
+ const phase4Path = path.join(projectRoot, '.yuri', 'state', 'phase4.yaml');
125
+ if (!fs.existsSync(phase4Path)) return;
126
+ const phase4 = yaml.load(fs.readFileSync(phase4Path, 'utf8')) || {};
127
+ if (phase4.status === 'complete' || phase4.status === 'complete_with_failures') return;
128
+
129
+ const card = this._buildTestStatusCard(projectRoot);
130
+ if (card) {
131
+ log.router('Auto-reporting test progress');
132
+ this._sendProactive(card);
133
+ }
121
134
  }
122
135
  } catch { /* silent */ }
123
136
  }, interval);
@@ -290,7 +303,7 @@ class Router {
290
303
  async _handlePhaseCommand(phase, msg) {
291
304
  const projectRoot = engine.resolveProjectRoot();
292
305
  if (!projectRoot) {
293
- return { text: 'โŒ No active project found. Create one first with *create.' };
306
+ return { text: 'โŒ No active project found. Run `/yuri *create` in your terminal first.' };
294
307
  }
295
308
 
296
309
  let response;
@@ -407,6 +420,16 @@ Reply with ONLY one word: small, medium, or large. Nothing else.`;
407
420
  }
408
421
  }
409
422
 
423
+ // Test phase: generate test progress card from phase4.yaml
424
+ if (phaseNum === 4 && parts.length === 0) {
425
+ try {
426
+ const card = this._buildTestStatusCard(projectRoot);
427
+ if (card) parts.push(card);
428
+ } catch (err) {
429
+ log.warn(`Test progress card failed: ${err.message}`);
430
+ }
431
+ }
432
+
410
433
  // Fallback or non-dev phase
411
434
  if (parts.length === 0) {
412
435
  if (focus.pulse) parts.push(`Pulse: ${focus.pulse}`);
@@ -423,7 +446,7 @@ Reply with ONLY one word: small, medium, or large. Nothing else.`;
423
446
  }
424
447
 
425
448
  if (parts.length === 0) {
426
- parts.push('No active phase. Available commands: *create, *plan, *develop, *test, *deploy, *projects, *switch');
449
+ parts.push('No active phase. Available commands: *plan, *develop, *test, *deploy, *projects, *switch');
427
450
  }
428
451
 
429
452
  this.history.append(msg.chatId, 'user', msg.text);
@@ -609,13 +632,13 @@ Reply with ONLY one word: small, medium, or large. Nothing else.`;
609
632
  _handleProjects(msg) {
610
633
  const registryPath = path.join(YURI_GLOBAL, 'portfolio', 'registry.yaml');
611
634
  if (!fs.existsSync(registryPath)) {
612
- return { text: 'No projects registered yet. Use *create to create one.' };
635
+ return { text: 'No projects registered yet. Run `/yuri *create` in your terminal first.' };
613
636
  }
614
637
 
615
638
  const registry = yaml.load(fs.readFileSync(registryPath, 'utf8')) || {};
616
639
  const projects = registry.projects || [];
617
640
  if (projects.length === 0) {
618
- return { text: 'No projects registered yet. Use *create to create one.' };
641
+ return { text: 'No projects registered yet. Run `/yuri *create` in your terminal first.' };
619
642
  }
620
643
 
621
644
  // Read active project
@@ -824,6 +847,52 @@ Reply with ONLY one word: small, medium, or large. Nothing else.`;
824
847
  return lines.join('\n');
825
848
  }
826
849
 
850
+ // โ”€โ”€ Test Progress Card (from phase4.yaml, for *status when orchestrator is not tracking) โ”€โ”€
851
+
852
+ _buildTestStatusCard(projectRoot) {
853
+ const phase4Path = path.join(projectRoot, '.yuri', 'state', 'phase4.yaml');
854
+ if (!fs.existsSync(phase4Path)) return null;
855
+
856
+ const state = yaml.load(fs.readFileSync(phase4Path, 'utf8')) || {};
857
+ if (!Array.isArray(state.epics) || state.epics.length === 0) return null;
858
+
859
+ const total = state.epics.length;
860
+ const passed = state.epics.filter((e) => e.status === 'passed').length;
861
+ const failed = state.epics.filter((e) => e.status === 'failed').length;
862
+ const tested = passed + failed;
863
+ const pct = total > 0 ? Math.round((tested / total) * 100) : 0;
864
+
865
+ const barTotal = 20;
866
+ const filled = Math.round(pct / 100 * barTotal);
867
+ const bar = 'โ–“'.repeat(filled) + 'โ–‘'.repeat(barTotal - filled) + ` ${pct}%`;
868
+
869
+ const lines = [
870
+ '๐Ÿงช **Test Phase Progress**',
871
+ '',
872
+ bar,
873
+ '',
874
+ `๐Ÿ“Š Epics: ${tested}/${total} tested (โœ… ${passed} passed, โŒ ${failed} failed)`,
875
+ '',
876
+ '๐Ÿ“‹ Results:',
877
+ ];
878
+
879
+ for (const epic of state.epics) {
880
+ if (epic.status === 'passed') {
881
+ lines.push(` โœ… Epic ${epic.id} โ€” passed (${epic.rounds || 0} round(s))`);
882
+ } else if (epic.status === 'failed') {
883
+ lines.push(` โŒ Epic ${epic.id} โ€” failed (${epic.rounds || 0} rounds)`);
884
+ } else {
885
+ lines.push(` โฌœ Epic ${epic.id} โ€” ${epic.status || 'pending'}`);
886
+ }
887
+ }
888
+
889
+ if (state.status === 'complete' || state.status === 'complete_with_failures') {
890
+ lines.push(`\nโœ… Testing finished at ${state.completed_at || 'unknown'}`);
891
+ }
892
+
893
+ return lines.join('\n');
894
+ }
895
+
827
896
  // โ”€โ”€ Help โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
828
897
 
829
898
  _buildHelpText() {
@@ -832,7 +901,6 @@ Reply with ONLY one word: small, medium, or large. Nothing else.`;
832
901
  **Project Lifecycle**
833
902
  | Command | Description |
834
903
  |---------|-------------|
835
- | \`*create\` | Create a new project (interactive Q&A) |
836
904
  | \`*plan\` | Start planning phase (6 agents sequentially, background) |
837
905
  | \`*develop\` | Start development phase (4 agents with HANDOFF, background) |
838
906
  | \`*test\` | Start smoke testing (QA per epic, auto fix-retest) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "4.6.5",
3
+ "version": "4.6.7",
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": {