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.
package/lib/gateway/config.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
553
|
-
|
|
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
|
-
//
|
|
565
|
-
const
|
|
566
|
-
|
|
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
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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() {
|