orchestrix-yuri 4.7.3 → 4.7.5

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.
@@ -107,9 +107,9 @@ class Dispatcher {
107
107
  }
108
108
  }
109
109
 
110
- // Timeout — default to conversation
111
- log.warn('Dispatcher: classify timeout');
112
- return { action: 'conversation', description: text, reasoning: 'classifier timeout' };
110
+ // Timeout — default to change (bias toward action)
111
+ log.warn('Dispatcher: classify timeout, defaulting to change');
112
+ return { action: 'change', description: text, reasoning: 'classifier timeout' };
113
113
  } finally {
114
114
  this._busy = false;
115
115
  }
@@ -117,33 +117,65 @@ class Dispatcher {
117
117
 
118
118
  /**
119
119
  * Parse the dispatcher's tmux pane output to extract the JSON response.
120
- * Searches from bottom up for the first valid JSON line with "action".
120
+ * Handles: bare JSON lines, markdown code blocks, partial lines with JSON.
121
121
  */
122
122
  _parseResponse(output, originalText) {
123
- const lines = output.split('\n');
123
+ const valid = ['bugfix', 'change', 'plan', 'develop', 'test', 'deploy', 'status', 'iterate', 'conversation'];
124
124
 
125
+ // Strategy 1: Find JSON on its own line (bottom-up)
126
+ const lines = output.split('\n');
125
127
  for (let i = lines.length - 1; i >= 0; i--) {
126
128
  const line = lines[i].trim();
127
- // Look for JSON that contains "action"
128
- if (line.startsWith('{') && line.includes('"action"')) {
129
- try {
130
- const parsed = JSON.parse(line);
131
- if (parsed.action) {
132
- const valid = ['bugfix', 'change', 'plan', 'develop', 'test', 'deploy', 'status', 'iterate', 'conversation'];
133
- if (valid.includes(parsed.action)) {
129
+ if (line.includes('"action"')) {
130
+ // Extract JSON substring (may be embedded in other text)
131
+ const jsonMatch = line.match(/\{[^{}]*"action"\s*:\s*"[^"]+?"[^{}]*\}/);
132
+ if (jsonMatch) {
133
+ try {
134
+ const parsed = JSON.parse(jsonMatch[0]);
135
+ if (parsed.action && valid.includes(parsed.action)) {
134
136
  return {
135
137
  action: parsed.action,
136
138
  description: parsed.description || originalText,
137
139
  reasoning: parsed.reasoning || '',
138
140
  };
139
141
  }
140
- }
141
- } catch { /* not valid JSON, continue searching */ }
142
+ } catch { /* continue */ }
143
+ }
144
+ }
145
+ }
146
+
147
+ // Strategy 2: Search entire output for JSON pattern (handles multiline/code blocks)
148
+ const fullMatch = output.match(/\{\s*"action"\s*:\s*"(\w+)"[^}]*\}/);
149
+ if (fullMatch) {
150
+ try {
151
+ const parsed = JSON.parse(fullMatch[0]);
152
+ if (parsed.action && valid.includes(parsed.action)) {
153
+ return {
154
+ action: parsed.action,
155
+ description: parsed.description || originalText,
156
+ reasoning: parsed.reasoning || '',
157
+ };
158
+ }
159
+ } catch { /* continue */ }
160
+
161
+ // Even if full JSON parse fails, extract action from regex
162
+ const action = fullMatch[1];
163
+ if (valid.includes(action)) {
164
+ return { action, description: originalText, reasoning: 'partial parse' };
165
+ }
166
+ }
167
+
168
+ // Strategy 3: Look for bare action words in the last few lines
169
+ const tail = lines.slice(-5).join(' ').toLowerCase();
170
+ for (const action of valid) {
171
+ if (action !== 'conversation' && tail.includes(`"${action}"`)) {
172
+ return { action, description: originalText, reasoning: 'keyword match' };
142
173
  }
143
174
  }
144
175
 
145
- log.warn('Dispatcher: failed to parse response, defaulting to conversation');
146
- return { action: 'conversation', description: originalText, reasoning: 'parse failed' };
176
+ // Parse failed — default to "change" (bias toward action, not conversation)
177
+ log.warn('Dispatcher: failed to parse response, defaulting to change');
178
+ return { action: 'change', description: originalText, reasoning: 'parse failed' };
147
179
  }
148
180
 
149
181
  /**
@@ -108,6 +108,7 @@ class PhaseOrchestrator {
108
108
  this._session = phase3.tmux.session;
109
109
  this._devStartedAt = phase3.started_at ? new Date(phase3.started_at).getTime() : Date.now();
110
110
  this._lastReportTime = Date.now(); // don't send report immediately on recover
111
+ this._lastActiveAgent = null;
111
112
 
112
113
  const pollInterval = this.config.dev_poll_interval || 300000;
113
114
  this._timer = setInterval(() => this._pollDevSession(), pollInterval);
@@ -244,12 +245,19 @@ class PhaseOrchestrator {
244
245
 
245
246
  if (this._phase === 'iterate') {
246
247
  const ctx = this._changeContext || {};
247
- return { phase: 'iterate', message: `🔄 Iteration in progress. Current: ${ctx.iteratePhase || 'starting'}` };
248
+ const agentLabels = { pm: 'PM generating next-steps', architect: 'Architect resolving changes' };
249
+ const label = agentLabels[ctx.iteratePhase] || ctx.iteratePhase || 'starting';
250
+ return { phase: 'iterate', message: `🔄 Iteration in progress\n📍 ${label} (${this._step + 1}/2)` };
248
251
  }
249
252
 
250
253
  if (this._phase === 'change') {
251
254
  const ctx = this._changeContext || {};
252
- return { phase: 'change', message: `🔧 Change in progress (${ctx.scope || '?'}). Step ${this._step + 1}` };
255
+ const stepLabels = ['PO routing change', 'Architect resolving', 'SM applying proposal'];
256
+ const stepLabel = stepLabels[this._step] || `Step ${this._step + 1}`;
257
+ return {
258
+ phase: 'change',
259
+ message: `🔧 Change in progress (${ctx.scope || '?'})\n📍 ${stepLabel} (${this._step + 1}/3)`,
260
+ };
253
261
  }
254
262
 
255
263
  return { phase: this._phase, message: `Phase ${this._phase} is running.` };
@@ -630,6 +638,7 @@ class PhaseOrchestrator {
630
638
  this._timer = setInterval(() => this._pollDevSession(), pollInterval);
631
639
  this._devStartedAt = Date.now();
632
640
  this._lastReportTime = Date.now();
641
+ this._lastActiveAgent = null;
633
642
 
634
643
  const reportMin = Math.round(this._reportInterval / 60000);
635
644
  log.engine(`Dev phase started: session=${this._session}, report every ${reportMin}min`);
@@ -647,6 +656,18 @@ class PhaseOrchestrator {
647
656
  // Gather progress data
648
657
  const progress = this._gatherDevProgress();
649
658
 
659
+ // Detect agent handoff and notify user
660
+ if (progress.currentAgent && progress.currentAgent !== this._lastActiveAgent) {
661
+ const prev = this._lastActiveAgent;
662
+ this._lastActiveAgent = progress.currentAgent;
663
+ if (prev) {
664
+ const storyInfo = progress.currentStory ? ` — working on ${progress.currentStory}` : '';
665
+ this.onProgress(`🔄 ${prev} → **${progress.currentAgent}**${storyInfo}`);
666
+ }
667
+ } else if (progress.currentAgent) {
668
+ this._lastActiveAgent = progress.currentAgent;
669
+ }
670
+
650
671
  // Check if all stories done
651
672
  if (progress.totalStories > 0 && progress.doneStories >= progress.totalStories) {
652
673
  this._completeDev();
@@ -1394,14 +1415,32 @@ class PhaseOrchestrator {
1394
1415
 
1395
1416
  _completeIterate(devSession) {
1396
1417
  if (this._timer) { clearInterval(this._timer); this._timer = null; }
1397
- // Kill planning session
1418
+ // Kill planning session (no longer needed)
1398
1419
  if (this._session && tmx.hasSession(this._session)) {
1399
1420
  tmx.killSession(this._session);
1400
1421
  }
1401
- this._phase = null;
1422
+
1402
1423
  this._changeContext = null;
1403
1424
  log.engine('Iterate complete — dev automation started');
1404
1425
  this.onComplete('iterate', `🔄 New iteration launched!\n\nSM is drafting stories in dev session: ${devSession}\nAgents will chain automatically via handoff-detector.`);
1426
+
1427
+ // Transition to dev monitoring so SM → Architect → Dev → QA cycle is tracked.
1428
+ // Without this, the entire dev cycle runs unmonitored after iterate completes.
1429
+ if (devSession && tmx.hasSession(devSession)) {
1430
+ this._phase = 'develop';
1431
+ this._session = devSession;
1432
+ this._devStartedAt = Date.now();
1433
+ this._lastReportTime = Date.now();
1434
+ this._lastActiveAgent = null;
1435
+ this._stableCount = 0;
1436
+ this._lastHash = '';
1437
+ const pollInterval = this.config.dev_poll_interval || 300000;
1438
+ this._timer = setInterval(() => this._pollDevSession(), pollInterval);
1439
+ log.engine(`Iterate → dev monitoring: session=${devSession}, poll every ${Math.round(pollInterval / 60000)}min`);
1440
+ this.onProgress('🔄 Now monitoring dev cycle (SM → Architect → Dev → QA). I\'ll report agent handoffs and progress.');
1441
+ } else {
1442
+ this._phase = null;
1443
+ }
1405
1444
  }
1406
1445
 
1407
1446
  // ── Quick Fix ───────────────────────────────────────────────────────────────
@@ -1609,8 +1648,8 @@ class PhaseOrchestrator {
1609
1648
  execSync('sleep 12');
1610
1649
  tmx.sendKeysWithEnter(this._session, 1, '*draft');
1611
1650
 
1612
- // Now poll dev session SM window
1613
- this._changeContext._pollDevSM = true;
1651
+ // Planning window 0 (Architect) is already stable — next 3 stable polls
1652
+ // will trigger step 3 → _completeChange → transition to dev monitoring.
1614
1653
  } catch (err) {
1615
1654
  this._handleError('change', `Failed to apply in dev session: ${err.message}`);
1616
1655
  }
@@ -1629,10 +1668,28 @@ class PhaseOrchestrator {
1629
1668
  clearInterval(this._timer);
1630
1669
  this._timer = null;
1631
1670
  }
1632
- this._phase = null;
1633
- this._changeContext = null;
1671
+
1634
1672
  log.engine(`Change management complete: ${summary}`);
1635
1673
  this.onComplete('change', `✅ Change management complete.\n\n${summary}`);
1674
+
1675
+ // Transition to dev monitoring if SM was started in a dev session.
1676
+ // Without this, the SM → Architect → Dev → QA cycle runs unmonitored.
1677
+ if (this._session && tmx.hasSession(this._session)) {
1678
+ this._phase = 'develop';
1679
+ this._changeContext = null;
1680
+ this._devStartedAt = Date.now();
1681
+ this._lastReportTime = Date.now();
1682
+ this._lastActiveAgent = null;
1683
+ this._stableCount = 0;
1684
+ this._lastHash = '';
1685
+ const pollInterval = this.config.dev_poll_interval || 300000;
1686
+ this._timer = setInterval(() => this._pollDevSession(), pollInterval);
1687
+ log.engine(`Change → dev monitoring: session=${this._session}, poll every ${Math.round(pollInterval / 60000)}min`);
1688
+ this.onProgress('🔄 Now monitoring dev cycle (SM → Architect → Dev → QA). I\'ll report agent handoffs and progress.');
1689
+ } else {
1690
+ this._phase = null;
1691
+ this._changeContext = null;
1692
+ }
1636
1693
  }
1637
1694
 
1638
1695
  // ── Shared ─────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "4.7.3",
3
+ "version": "4.7.5",
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": {