orchestrix-yuri 4.1.9 → 4.2.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.
- package/lib/gateway/router.js +35 -32
- package/package.json +1 -1
- package/skill/scripts/scan-stories.sh +68 -32
package/lib/gateway/router.js
CHANGED
|
@@ -568,7 +568,8 @@ class Router {
|
|
|
568
568
|
|
|
569
569
|
// 1. Scan stories via scan-stories.sh
|
|
570
570
|
let byStatus = {};
|
|
571
|
-
let
|
|
571
|
+
let totalPlanned = 0; // total stories defined in epic YAMLs
|
|
572
|
+
let createdStories = 0; // stories with files in docs/stories/
|
|
572
573
|
let doneStories = 0;
|
|
573
574
|
let totalEpics = 0;
|
|
574
575
|
let currentEpic = 0;
|
|
@@ -581,30 +582,21 @@ class Router {
|
|
|
581
582
|
const colonIdx = line.indexOf(':');
|
|
582
583
|
if (colonIdx < 0) continue;
|
|
583
584
|
const key = line.slice(0, colonIdx);
|
|
584
|
-
const val = line.slice(colonIdx + 1);
|
|
585
|
-
|
|
586
|
-
if (key === 'Total')
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
} else if (key === 'StatusReview') {
|
|
600
|
-
byStatus.Review = (byStatus.Review || 0) + 1;
|
|
601
|
-
} else if (key === 'StatusBlocked') {
|
|
602
|
-
byStatus.Blocked = (byStatus.Blocked || 0) + 1;
|
|
603
|
-
} else if (key === 'StatusApproved') {
|
|
604
|
-
byStatus.Approved = (byStatus.Approved || 0) + 1;
|
|
605
|
-
} else if (key === 'StatusOther') {
|
|
606
|
-
byStatus.Other = (byStatus.Other || 0) + 1;
|
|
607
|
-
}
|
|
585
|
+
const val = line.slice(colonIdx + 1).trim();
|
|
586
|
+
|
|
587
|
+
if (key === 'Total') totalPlanned = parseInt(val, 10) || 0;
|
|
588
|
+
else if (key === 'Created') createdStories = parseInt(val, 10) || 0;
|
|
589
|
+
else if (key === 'Epics') totalEpics = parseInt(val, 10) || 0;
|
|
590
|
+
else if (key === 'CurrentEpic') currentEpic = parseInt(val, 10) || 0;
|
|
591
|
+
else if (key === 'CurrentStory') currentStory = val || null;
|
|
592
|
+
else if (key === 'StatusDone') { doneStories++; byStatus.Done = (byStatus.Done || 0) + 1; }
|
|
593
|
+
else if (key === 'StatusInProgress') byStatus.InProgress = (byStatus.InProgress || 0) + 1;
|
|
594
|
+
else if (key === 'StatusReview') byStatus.Review = (byStatus.Review || 0) + 1;
|
|
595
|
+
else if (key === 'StatusBlocked') byStatus.Blocked = (byStatus.Blocked || 0) + 1;
|
|
596
|
+
else if (key === 'StatusApproved') byStatus.Approved = (byStatus.Approved || 0) + 1;
|
|
597
|
+
else if (key === 'StatusDraft') byStatus.Draft = (byStatus.Draft || 0) + 1;
|
|
598
|
+
else if (key === 'StatusNoStatus') byStatus.NoStatus = (byStatus.NoStatus || 0) + 1;
|
|
599
|
+
else if (key === 'StatusOther') byStatus.Other = (byStatus.Other || 0) + 1;
|
|
608
600
|
}
|
|
609
601
|
}
|
|
610
602
|
}
|
|
@@ -627,17 +619,27 @@ class Router {
|
|
|
627
619
|
const tmx = require('./engine/tmux-utils');
|
|
628
620
|
if (devSession && tmx.hasSession(devSession)) {
|
|
629
621
|
const windowNames = ['Architect', 'SM', 'Dev', 'QA'];
|
|
622
|
+
// Pass 1: find window with active spinner
|
|
630
623
|
for (let w = 0; w < 4; w++) {
|
|
631
|
-
const pane = tmx.capturePane(devSession, w,
|
|
632
|
-
const lines = pane.split('\n').filter((l) => l.trim());
|
|
624
|
+
const pane = tmx.capturePane(devSession, w, 10);
|
|
633
625
|
if (/●|⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/.test(pane)) {
|
|
634
626
|
currentAgent = windowNames[w];
|
|
635
627
|
currentWindow = w;
|
|
636
|
-
const storyMatch = pane.match(/story[_-]?\d+[\._-]\d+/i) || pane.match(/\d+\.\d+/);
|
|
637
|
-
if (storyMatch) currentStory = storyMatch[0];
|
|
638
628
|
break;
|
|
639
629
|
}
|
|
640
630
|
}
|
|
631
|
+
// Pass 2: if no spinner, find window with most output (likely active)
|
|
632
|
+
if (!currentAgent) {
|
|
633
|
+
for (let w = 0; w < 4; w++) {
|
|
634
|
+
const pane = tmx.capturePane(devSession, w, 10);
|
|
635
|
+
const contentLines = pane.split('\n').filter((l) => l.trim() && !/^❯\s*$/.test(l));
|
|
636
|
+
if (contentLines.length > 3) {
|
|
637
|
+
currentAgent = windowNames[w];
|
|
638
|
+
currentWindow = w;
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
641
643
|
}
|
|
642
644
|
|
|
643
645
|
// 4. Count epics from docs/prd/ (epic YAML files)
|
|
@@ -660,7 +662,8 @@ class Router {
|
|
|
660
662
|
const elapsed = runMin < 60 ? `${runMin}min` : `${Math.floor(runMin / 60)}h ${runMin % 60}min`;
|
|
661
663
|
|
|
662
664
|
// 6. Build card
|
|
663
|
-
const
|
|
665
|
+
const total = totalPlanned || createdStories; // prefer planned, fallback to created
|
|
666
|
+
const pct = total > 0 ? Math.round(doneStories / total * 100) : 0;
|
|
664
667
|
const barLen = 20;
|
|
665
668
|
const filled = Math.round(pct / 100 * barLen);
|
|
666
669
|
const bar = '▓'.repeat(filled) + '░'.repeat(barLen - filled);
|
|
@@ -669,12 +672,12 @@ class Router {
|
|
|
669
672
|
if (totalEpics > 0) {
|
|
670
673
|
lines.push(`Epic: ${currentEpic}/${totalEpics}`);
|
|
671
674
|
}
|
|
672
|
-
lines.push(`Story: ${doneStories}/${
|
|
675
|
+
lines.push(`Story: ${doneStories}/${total} done (${pct}%) | ${createdStories} created`);
|
|
673
676
|
lines.push(`${bar} ${pct}%`);
|
|
674
677
|
lines.push('━━━━━━━━━━━━━━━━━━━━━');
|
|
675
678
|
|
|
676
679
|
// Status breakdown
|
|
677
|
-
const icons = { Done: '✅', InProgress: '🔄', Review: '👀', Blocked: '🚫', Approved: '
|
|
680
|
+
const icons = { Done: '✅', InProgress: '🔄', Review: '👀', Blocked: '🚫', Approved: '📋', Draft: '📝', NoStatus: '❓', Other: '·' };
|
|
678
681
|
const statusEntries = Object.entries(byStatus).filter(([, v]) => v > 0);
|
|
679
682
|
if (statusEntries.length > 0) {
|
|
680
683
|
for (const [s, n] of statusEntries) {
|
package/package.json
CHANGED
|
@@ -3,55 +3,78 @@
|
|
|
3
3
|
# Usage: scan-stories.sh <project_root>
|
|
4
4
|
#
|
|
5
5
|
# Outputs:
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# Total:{N} total planned stories (from epic YAML definitions)
|
|
7
|
+
# Created:{N} stories with files in docs/stories/
|
|
8
|
+
# StatusDone:{id} per-file status (one line per story file)
|
|
9
|
+
# StatusInProgress:{id}
|
|
10
|
+
# StatusApproved:{id}
|
|
11
|
+
# StatusReview:{id}
|
|
12
|
+
# StatusBlocked:{id}
|
|
13
|
+
# StatusOther:{id}
|
|
14
|
+
# Epics:{N} total epics (max N from docs/prd/epic-N-*)
|
|
15
|
+
# CurrentEpic:{N} current epic (max prefix from created stories)
|
|
16
|
+
# CurrentStory:{id} current story being worked on
|
|
11
17
|
set +e
|
|
12
18
|
|
|
13
19
|
PROJECT_ROOT="$1"
|
|
14
20
|
STORIES_DIR="$PROJECT_ROOT/docs/stories"
|
|
21
|
+
PRD_DIR="$PROJECT_ROOT/docs/prd"
|
|
22
|
+
|
|
23
|
+
# ── Total planned stories from epic YAML definitions ──
|
|
24
|
+
# Count "- id:" entries under "stories:" in each epic YAML
|
|
25
|
+
total_planned=0
|
|
26
|
+
if [ -d "$PRD_DIR" ]; then
|
|
27
|
+
for yaml in "$PRD_DIR"/epic-*.yaml; do
|
|
28
|
+
[ -f "$yaml" ] || continue
|
|
29
|
+
# Count lines matching " - id:" (story entries in the stories array)
|
|
30
|
+
n=$(grep -cE '^\s+- id:' "$yaml" 2>/dev/null || echo 0)
|
|
31
|
+
total_planned=$((total_planned + n))
|
|
32
|
+
done
|
|
33
|
+
fi
|
|
34
|
+
echo "Total:$total_planned"
|
|
15
35
|
|
|
36
|
+
# ── Created stories (files in docs/stories/) ──
|
|
16
37
|
if [ ! -d "$STORIES_DIR" ]; then
|
|
17
|
-
echo "
|
|
18
|
-
|
|
38
|
+
echo "Created:0"
|
|
39
|
+
echo "Epics:0"
|
|
40
|
+
echo "CurrentEpic:0"
|
|
41
|
+
echo "CurrentStory:none"
|
|
42
|
+
exit 0
|
|
19
43
|
fi
|
|
20
44
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
echo "Total:$total"
|
|
45
|
+
created=$(ls "$STORIES_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
46
|
+
echo "Created:$created"
|
|
24
47
|
|
|
25
|
-
#
|
|
26
|
-
# Supports: "## Status\n\nDone" (heading format) and "Status: Done" (inline)
|
|
48
|
+
# ── Per-file status extraction ──
|
|
27
49
|
in_progress_story=""
|
|
28
50
|
for f in "$STORIES_DIR"/*.md; do
|
|
29
51
|
[ -f "$f" ] || continue
|
|
30
52
|
fname=$(basename "$f" .md)
|
|
31
53
|
|
|
32
|
-
# Try heading format
|
|
54
|
+
# Try heading format: "## Status" then next non-empty line
|
|
33
55
|
status=$(awk '/^## Status/{found=1; next} found && /^[[:space:]]*$/{next} found{print; exit}' "$f" 2>/dev/null)
|
|
34
56
|
|
|
35
57
|
# Fallback: inline "Status: XXX"
|
|
36
58
|
if [ -z "$status" ]; then
|
|
37
|
-
status=$(grep -
|
|
59
|
+
status=$(grep -m1 -oE 'Status:\s*\S+' "$f" 2>/dev/null | sed 's/Status:\s*//')
|
|
38
60
|
fi
|
|
39
61
|
|
|
40
|
-
# Normalize
|
|
41
|
-
|
|
62
|
+
# Normalize to lowercase, strip whitespace
|
|
63
|
+
status_lower=$(echo "$status" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
|
42
64
|
|
|
43
|
-
case "$
|
|
44
|
-
done) echo "StatusDone:$fname" ;;
|
|
45
|
-
inprogress|in_progress|in-progress) echo "StatusInProgress:$fname"; in_progress_story="$fname" ;;
|
|
46
|
-
review|inreview) echo "StatusReview:$fname" ;;
|
|
65
|
+
case "$status_lower" in
|
|
66
|
+
done|complete|completed) echo "StatusDone:$fname" ;;
|
|
67
|
+
inprogress|in_progress|in-progress|wip) echo "StatusInProgress:$fname"; in_progress_story="$fname" ;;
|
|
68
|
+
review|inreview|in_review|underreview) echo "StatusReview:$fname" ;;
|
|
47
69
|
blocked) echo "StatusBlocked:$fname" ;;
|
|
48
|
-
approved) echo "StatusApproved:$fname" ;;
|
|
49
|
-
|
|
70
|
+
approved|ready) echo "StatusApproved:$fname" ;;
|
|
71
|
+
draft|new|todo|pending|open) echo "StatusDraft:$fname" ;;
|
|
72
|
+
"") echo "StatusNoStatus:$fname" ;;
|
|
73
|
+
*) echo "StatusOther:$fname:$status" ;;
|
|
50
74
|
esac
|
|
51
75
|
done
|
|
52
76
|
|
|
53
|
-
#
|
|
54
|
-
PRD_DIR="$PROJECT_ROOT/docs/prd"
|
|
77
|
+
# ── Epics: max N from docs/prd/epic-N-* ──
|
|
55
78
|
if [ -d "$PRD_DIR" ]; then
|
|
56
79
|
epics=$(ls "$PRD_DIR" 2>/dev/null | grep -oE '^epic-[0-9]+' | grep -oE '[0-9]+' | sort -n | tail -1)
|
|
57
80
|
echo "Epics:${epics:-0}"
|
|
@@ -59,16 +82,29 @@ else
|
|
|
59
82
|
echo "Epics:0"
|
|
60
83
|
fi
|
|
61
84
|
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
echo "CurrentEpic:${current_epic:-0}"
|
|
65
|
-
|
|
66
|
-
# Current story: InProgress one, or highest numbered
|
|
85
|
+
# ── CurrentEpic + CurrentStory: based on InProgress story ──
|
|
86
|
+
# Priority: InProgress story > last non-Done story > highest numbered
|
|
67
87
|
if [ -n "$in_progress_story" ]; then
|
|
68
88
|
echo "CurrentStory:$in_progress_story"
|
|
89
|
+
current_epic=$(echo "$in_progress_story" | grep -oE '^[0-9]+')
|
|
90
|
+
echo "CurrentEpic:${current_epic:-0}"
|
|
69
91
|
else
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
# Find last non-Done story (most likely being worked on next)
|
|
93
|
+
last_non_done=""
|
|
94
|
+
for f in $(ls "$STORIES_DIR"/*.md 2>/dev/null | sort -V); do
|
|
95
|
+
[ -f "$f" ] || continue
|
|
96
|
+
s=$(awk '/^## Status/{found=1; next} found && /^[[:space:]]*$/{next} found{print; exit}' "$f" 2>/dev/null)
|
|
97
|
+
s_lower=$(echo "$s" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
|
98
|
+
if [ "$s_lower" != "done" ]; then
|
|
99
|
+
last_non_done=$(basename "$f" .md)
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
if [ -n "$last_non_done" ]; then
|
|
103
|
+
echo "CurrentStory:$last_non_done"
|
|
104
|
+
current_epic=$(echo "$last_non_done" | grep -oE '^[0-9]+')
|
|
105
|
+
echo "CurrentEpic:${current_epic:-0}"
|
|
106
|
+
else
|
|
107
|
+
echo "CurrentStory:none"
|
|
108
|
+
echo "CurrentEpic:0"
|
|
73
109
|
fi
|
|
74
110
|
fi
|