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.
@@ -568,7 +568,8 @@ class Router {
568
568
 
569
569
  // 1. Scan stories via scan-stories.sh
570
570
  let byStatus = {};
571
- let totalStories = 0;
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
- totalStories = parseInt(val, 10) || 0;
588
- } else if (key === 'Epics') {
589
- totalEpics = parseInt(val, 10) || 0;
590
- } else if (key === 'CurrentEpic') {
591
- currentEpic = parseInt(val, 10) || 0;
592
- } else if (key === 'CurrentStory') {
593
- currentStory = val.trim();
594
- } else if (key === 'StatusDone') {
595
- doneStories++;
596
- byStatus.Done = (byStatus.Done || 0) + 1;
597
- } else if (key === 'StatusInProgress') {
598
- byStatus.InProgress = (byStatus.InProgress || 0) + 1;
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, 5);
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 pct = totalStories > 0 ? Math.round(doneStories / totalStories * 100) : 0;
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}/${totalStories} (${pct}%)`);
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: '', AwaitingArchReview: '🏛️', RequiresRevision: '🔧', Escalated: '⚠️' };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "4.1.9",
3
+ "version": "4.2.1",
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": {
@@ -3,55 +3,78 @@
3
3
  # Usage: scan-stories.sh <project_root>
4
4
  #
5
5
  # Outputs:
6
- # {Status}:{count} for each status (Done, InProgress, etc.)
7
- # Total:{N} total story files
8
- # Epics:{N} total epics (max N from docs/prd/epic-N-*)
9
- # CurrentEpic:{N} current epic (max prefix from story filenames)
10
- # CurrentStory:{id} current story being worked on
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 "NO_STORIES_DIR"
18
- exit 1
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
- # Count total story files
22
- total=$(ls "$STORIES_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ')
23
- echo "Total:$total"
45
+ created=$(ls "$STORIES_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ')
46
+ echo "Created:$created"
24
47
 
25
- # Extract status from each story file
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 first: "## Status" then next non-empty line
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 -oP 'Status:\s*\K\S+' "$f" 2>/dev/null | head -1)
59
+ status=$(grep -m1 -oE 'Status:\s*\S+' "$f" 2>/dev/null | sed 's/Status:\s*//')
38
60
  fi
39
61
 
40
- # Normalize
41
- status=$(echo "$status" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
62
+ # Normalize to lowercase, strip whitespace
63
+ status_lower=$(echo "$status" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
42
64
 
43
- case "$status" in
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
- *) echo "StatusOther:$fname" ;;
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
- # Count epics from docs/prd/epic-N-* (max N = total epics)
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
- # Current epic (max prefix from story filenames)
63
- current_epic=$(ls "$STORIES_DIR" 2>/dev/null | grep -oE '^[0-9]+' | sort -n | tail -1)
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
- latest=$(ls "$STORIES_DIR"/*.md 2>/dev/null | sort -t/ -k2 -V | tail -1)
71
- if [ -n "$latest" ]; then
72
- echo "CurrentStory:$(basename "$latest" .md)"
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