orchestrix-yuri 4.6.5 โ†’ 4.6.6

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);
@@ -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}`);
@@ -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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "4.6.5",
3
+ "version": "4.6.6",
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": {