orchestrix-yuri 4.0.6 โ†’ 4.1.0

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.
@@ -42,7 +42,7 @@ class FeishuAdapter {
42
42
  appId: this.appId,
43
43
  appSecret: this.appSecret,
44
44
  domain: lark.Domain.Feishu,
45
- loggerLevel: lark.LoggerLevel.WARN,
45
+ loggerLevel: lark.LoggerLevel.ERROR,
46
46
  });
47
47
 
48
48
  await this.wsClient.start({ eventDispatcher: dispatcher });
@@ -24,6 +24,7 @@ const DEFAULTS = {
24
24
  compact_every: 50, // proactive /compact after N messages
25
25
  phase_poll_interval: 30000, // plan phase: poll agent every 30s
26
26
  dev_poll_interval: 300000, // dev phase: poll progress every 5 min
27
+ report_interval: 1800000, // dev phase: progress report every 30 min
27
28
  },
28
29
  };
29
30
 
@@ -50,6 +50,11 @@ class PhaseOrchestrator {
50
50
  this._waitingForInput = false; // true when agent asked a question
51
51
  this._waitingMessageId = null; // Telegram message_id of the question notification
52
52
  this._onQuestionAsked = opts.onQuestionAsked || (() => Promise.resolve(null));
53
+
54
+ // Dev phase progress reporting
55
+ this._devStartedAt = null; // timestamp when dev phase started
56
+ this._lastReportTime = 0; // last progress report timestamp
57
+ this._reportInterval = (opts.config && opts.config.report_interval) || 1800000; // 30 min default
53
58
  // onQuestionAsked(text) โ†’ sends to Telegram, returns { messageId }
54
59
  }
55
60
 
@@ -101,6 +106,8 @@ class PhaseOrchestrator {
101
106
  this._phase = 'develop';
102
107
  this._projectRoot = projectRoot;
103
108
  this._session = phase3.tmux.session;
109
+ this._devStartedAt = phase3.started_at ? new Date(phase3.started_at).getTime() : Date.now();
110
+ this._lastReportTime = Date.now(); // don't send report immediately on recover
104
111
 
105
112
  const pollInterval = this.config.dev_poll_interval || 300000;
106
113
  this._timer = setInterval(() => this._pollDevSession(), pollInterval);
@@ -166,9 +173,11 @@ class PhaseOrchestrator {
166
173
  }
167
174
 
168
175
  if (this._phase === 'develop') {
176
+ const progress = this._gatherDevProgress();
177
+ const card = this._buildProgressCard(progress);
169
178
  return {
170
179
  phase: 'develop',
171
- message: '๐Ÿ’ป Development in progress. Agents running autonomously.',
180
+ message: card,
172
181
  };
173
182
  }
174
183
 
@@ -548,9 +557,12 @@ class PhaseOrchestrator {
548
557
  // Start polling (less frequent โ€” handoff-detector handles agent chaining)
549
558
  const pollInterval = this.config.dev_poll_interval || 300000; // 5 min
550
559
  this._timer = setInterval(() => this._pollDevSession(), pollInterval);
560
+ this._devStartedAt = Date.now();
561
+ this._lastReportTime = Date.now();
551
562
 
552
- log.engine(`Dev phase started: session=${this._session}`);
553
- return '๐Ÿš€ Development started! 4 agents (Architect, SM, Dev, QA) are running.\n\nAgents chain automatically via handoff-detector. I\'ll report progress every 5 minutes.';
563
+ const reportMin = Math.round(this._reportInterval / 60000);
564
+ log.engine(`Dev phase started: session=${this._session}, report every ${reportMin}min`);
565
+ 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.`;
554
566
  }
555
567
 
556
568
  _pollDevSession() {
@@ -561,25 +573,168 @@ class PhaseOrchestrator {
561
573
  return;
562
574
  }
563
575
 
564
- // Read story progress from scan-stories.sh or phase3.yaml
565
- const phase3Path = path.join(this._projectRoot, '.yuri', 'state', 'phase3.yaml');
566
- if (fs.existsSync(phase3Path)) {
576
+ // Gather progress data
577
+ const progress = this._gatherDevProgress();
578
+
579
+ // Check if all stories done
580
+ if (progress.totalStories > 0 && progress.doneStories >= progress.totalStories) {
581
+ this._completeDev();
582
+ return;
583
+ }
584
+
585
+ // Periodic progress report
586
+ const now = Date.now();
587
+ if (now - this._lastReportTime >= this._reportInterval) {
588
+ this._lastReportTime = now;
589
+ const card = this._buildProgressCard(progress);
590
+ this.onProgress(card);
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Gather dev progress from story files + tmux panes.
596
+ */
597
+ _gatherDevProgress() {
598
+ const result = {
599
+ totalEpics: 0, doneEpics: 0,
600
+ totalStories: 0, doneStories: 0,
601
+ byStatus: {},
602
+ currentAgent: null, currentWindow: null, currentStory: null,
603
+ runningFor: this._devStartedAt ? Date.now() - this._devStartedAt : 0,
604
+ };
605
+
606
+ // Method 1: scan-stories.sh
607
+ const scriptPath = path.join(SKILL_DIR, 'scripts', 'scan-stories.sh');
608
+ if (fs.existsSync(scriptPath) && this._projectRoot) {
567
609
  try {
568
- const phase3 = yaml.load(fs.readFileSync(phase3Path, 'utf8')) || {};
569
- const progress = phase3.progress || {};
570
- const byStatus = progress.by_status || {};
571
- const total = progress.total_stories || 0;
572
- const done = (byStatus.done || 0) + (byStatus.complete || 0);
573
-
574
- if (total > 0 && done >= total) {
575
- this._completeDev();
576
- return;
610
+ const output = execSync(`bash "${scriptPath}" "${this._projectRoot}"`, {
611
+ encoding: 'utf8', timeout: 10000,
612
+ }).trim();
613
+
614
+ if (output && output !== 'NO_STORIES_DIR') {
615
+ for (const line of output.split('\n')) {
616
+ const [status, countStr] = line.split(':');
617
+ const count = parseInt(countStr, 10) || 0;
618
+ if (status && count > 0) {
619
+ result.byStatus[status] = count;
620
+ result.totalStories += count;
621
+ if (status === 'Done') result.doneStories += count;
622
+ }
623
+ }
577
624
  }
625
+ } catch { /* fallback to phase3.yaml */ }
626
+ }
627
+
628
+ // Method 2: phase3.yaml fallback
629
+ if (result.totalStories === 0) {
630
+ const phase3Path = path.join(this._projectRoot, '.yuri', 'state', 'phase3.yaml');
631
+ if (fs.existsSync(phase3Path)) {
632
+ try {
633
+ const phase3 = yaml.load(fs.readFileSync(phase3Path, 'utf8')) || {};
634
+ const p = phase3.progress || {};
635
+ result.totalStories = p.total_stories || 0;
636
+ result.totalEpics = p.total_epics || 0;
637
+ const bs = p.by_status || {};
638
+ result.doneStories = (bs.done || 0) + (bs.complete || 0);
639
+ result.byStatus = bs;
640
+ } catch { /* ok */ }
641
+ }
642
+ }
578
643
 
579
- // Report progress
580
- this.onProgress(`๐Ÿ’ป Dev progress: ${done}/${total} stories complete`);
581
- } catch { /* continue polling */ }
644
+ // Count epics from docs/stories/ directory
645
+ const storiesDir = path.join(this._projectRoot, 'docs', 'stories');
646
+ if (fs.existsSync(storiesDir)) {
647
+ try {
648
+ const entries = fs.readdirSync(storiesDir);
649
+ const epicDirs = entries.filter((e) => {
650
+ return fs.statSync(path.join(storiesDir, e)).isDirectory() && e.startsWith('epic');
651
+ });
652
+ result.totalEpics = epicDirs.length || result.totalEpics;
653
+ } catch { /* ok */ }
582
654
  }
655
+
656
+ // Detect current active agent from tmux panes
657
+ if (this._session && tmx.hasSession(this._session)) {
658
+ const windowNames = ['Architect', 'SM', 'Dev', 'QA'];
659
+ for (let w = 0; w < 4; w++) {
660
+ const pane = tmx.capturePane(this._session, w, 5);
661
+ // Active = has processing indicator (โ—) or recent output (not just โฏ)
662
+ const lines = pane.split('\n').filter((l) => l.trim());
663
+ const lastLine = lines[lines.length - 1] || '';
664
+ if (/โ—|โ ‹|โ ™|โ น|โ ธ|โ ผ|โ ด|โ ฆ|โ ง|โ ‡|โ /.test(pane) || (lines.length > 2 && !/^โฏ\s*$/.test(lastLine))) {
665
+ result.currentAgent = windowNames[w];
666
+ result.currentWindow = w;
667
+
668
+ // Try to extract story ID from pane content
669
+ const storyMatch = pane.match(/story[_-]?\d+[\._-]\d+/i) || pane.match(/\d+\.\d+/);
670
+ if (storyMatch) result.currentStory = storyMatch[0];
671
+ break;
672
+ }
673
+ }
674
+ }
675
+
676
+ return result;
677
+ }
678
+
679
+ /**
680
+ * Build a formatted progress card for Telegram/Feishu.
681
+ */
682
+ _buildProgressCard(p) {
683
+ const pct = p.totalStories > 0 ? Math.round(p.doneStories / p.totalStories * 100) : 0;
684
+ const bar = this._progressBar(pct);
685
+ const elapsed = this._formatDuration(p.runningFor);
686
+
687
+ const lines = [
688
+ `๐Ÿ“Š **Dev Progress Report**`,
689
+ `โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”`,
690
+ ];
691
+
692
+ if (p.totalEpics > 0) {
693
+ lines.push(`Epic: ${p.doneEpics}/${p.totalEpics}`);
694
+ }
695
+ lines.push(`Story: ${p.doneStories}/${p.totalStories} (${pct}%)`);
696
+ lines.push(`${bar}`);
697
+ lines.push(`โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”`);
698
+
699
+ // Status breakdown
700
+ const statusEntries = Object.entries(p.byStatus).filter(([, v]) => v > 0);
701
+ if (statusEntries.length > 0) {
702
+ const statusIcons = {
703
+ Done: 'โœ…', InProgress: '๐Ÿ”„', Review: '๐Ÿ‘€', Blocked: '๐Ÿšซ',
704
+ Approved: 'โœ…', AwaitingArchReview: '๐Ÿ›๏ธ', RequiresRevision: '๐Ÿ”ง', Escalated: 'โš ๏ธ',
705
+ };
706
+ for (const [status, count] of statusEntries) {
707
+ lines.push(`${statusIcons[status] || 'ยท'} ${status}: ${count}`);
708
+ }
709
+ lines.push(`โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”`);
710
+ }
711
+
712
+ // Current activity
713
+ if (p.currentAgent) {
714
+ const storyInfo = p.currentStory ? ` โ†’ ${p.currentStory}` : '';
715
+ lines.push(`Current: ${p.currentAgent} (window ${p.currentWindow})${storyInfo}`);
716
+ } else {
717
+ lines.push(`Current: waiting for next handoff`);
718
+ }
719
+
720
+ lines.push(`โฑ Running for ${elapsed}`);
721
+
722
+ return lines.join('\n');
723
+ }
724
+
725
+ _progressBar(pct) {
726
+ const total = 20;
727
+ const filled = Math.round(pct / 100 * total);
728
+ const empty = total - filled;
729
+ return 'โ–“'.repeat(filled) + 'โ–‘'.repeat(empty) + ` ${pct}%`;
730
+ }
731
+
732
+ _formatDuration(ms) {
733
+ const min = Math.floor(ms / 60000);
734
+ if (min < 60) return `${min}min`;
735
+ const h = Math.floor(min / 60);
736
+ const m = min % 60;
737
+ return `${h}h ${m}min`;
583
738
  }
584
739
 
585
740
  _completeDev() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "4.0.6",
3
+ "version": "4.1.0",
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": {