orchestrix-yuri 3.2.0 → 3.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.
@@ -49,6 +49,62 @@ class PhaseOrchestrator {
49
49
 
50
50
  isRunning() { return this._phase !== null; }
51
51
 
52
+ /**
53
+ * Try to recover an in-progress phase from YAML state + tmux session.
54
+ * Called once on gateway startup. If a phase was running when the gateway
55
+ * died, reconnect to the existing tmux session and resume polling.
56
+ */
57
+ tryRecover(projectRoot) {
58
+ if (this._phase) return; // already running
59
+ if (!projectRoot) return;
60
+
61
+ // Check phase2 (plan)
62
+ const phase2Path = path.join(projectRoot, '.yuri', 'state', 'phase2.yaml');
63
+ if (fs.existsSync(phase2Path)) {
64
+ const phase2 = yaml.load(fs.readFileSync(phase2Path, 'utf8')) || {};
65
+ if (phase2.status === 'in_progress' && phase2.tmux && phase2.tmux.session) {
66
+ if (tmx.hasSession(phase2.tmux.session)) {
67
+ // Recover: reconnect to existing session, resume polling
68
+ this._phase = 'plan';
69
+ this._projectRoot = projectRoot;
70
+ this._session = phase2.tmux.session;
71
+
72
+ // Find current step from YAML
73
+ if (Array.isArray(phase2.steps)) {
74
+ const lastComplete = phase2.steps.findLastIndex((s) => s.status === 'complete');
75
+ this._step = lastComplete >= 0 ? lastComplete + 1 : 0;
76
+ }
77
+
78
+ const pollInterval = this.config.phase_poll_interval || 30000;
79
+ this._timer = setInterval(() => this._pollPlanAgent(), pollInterval);
80
+
81
+ const agent = PLAN_AGENTS[this._step];
82
+ log.engine(`Recovered plan phase: session=${this._session}, step ${this._step + 1}/${PLAN_AGENTS.length} (${agent ? agent.name : '?'})`);
83
+ return;
84
+ }
85
+ }
86
+ }
87
+
88
+ // Check phase3 (develop)
89
+ const phase3Path = path.join(projectRoot, '.yuri', 'state', 'phase3.yaml');
90
+ if (fs.existsSync(phase3Path)) {
91
+ const phase3 = yaml.load(fs.readFileSync(phase3Path, 'utf8')) || {};
92
+ if (phase3.status === 'in_progress' && phase3.tmux && phase3.tmux.session) {
93
+ if (tmx.hasSession(phase3.tmux.session)) {
94
+ this._phase = 'develop';
95
+ this._projectRoot = projectRoot;
96
+ this._session = phase3.tmux.session;
97
+
98
+ const pollInterval = this.config.dev_poll_interval || 300000;
99
+ this._timer = setInterval(() => this._pollDevSession(), pollInterval);
100
+
101
+ log.engine(`Recovered dev phase: session=${this._session}`);
102
+ return;
103
+ }
104
+ }
105
+ }
106
+ }
107
+
52
108
  getStatus() {
53
109
  if (!this._phase) {
54
110
  return { phase: null, message: 'No phase is running.' };
@@ -140,20 +196,29 @@ class PhaseOrchestrator {
140
196
  // Update memory
141
197
  this._updatePlanMemory('in_progress');
142
198
 
143
- // Start first (or resumed) agent
144
- try {
145
- this._startPlanAgent(this._step);
146
- } catch (err) {
147
- this._phase = null;
148
- return `❌ Failed to start agent: ${err.message}`;
199
+ // Check if the current agent's window already has Claude Code running.
200
+ // This happens when gateway restarts while an agent is mid-execution.
201
+ // In that case, just start polling — don't re-send commands.
202
+ const agent = PLAN_AGENTS[this._step];
203
+ const windowHasActivity = this._isWindowActive(agent.window);
204
+
205
+ if (windowHasActivity) {
206
+ log.engine(`Agent ${this._step + 1} (${agent.name}) already active in window ${agent.window}, resuming polling`);
207
+ } else {
208
+ try {
209
+ this._startPlanAgent(this._step);
210
+ } catch (err) {
211
+ this._phase = null;
212
+ return `❌ Failed to start agent: ${err.message}`;
213
+ }
149
214
  }
150
215
 
151
216
  // Start polling
152
217
  const pollInterval = this.config.phase_poll_interval || 30000;
153
218
  this._timer = setInterval(() => this._pollPlanAgent(), pollInterval);
154
219
 
155
- const agent = PLAN_AGENTS[this._step];
156
- return `🚀 Planning started! Agent ${this._step + 1}/${PLAN_AGENTS.length} (${agent.name}) is running.\n\nI'll notify you as each agent completes. You can ask me anything in the meantime.`;
220
+ const currentAgent = PLAN_AGENTS[this._step];
221
+ return `🚀 Planning ${windowHasActivity ? 'resumed' : 'started'}! Agent ${this._step + 1}/${PLAN_AGENTS.length} (${currentAgent.name}) is running.\n\nI'll notify you as each agent completes. You can ask me anything in the meantime.`;
157
222
  }
158
223
 
159
224
  /**
@@ -254,6 +319,17 @@ class PhaseOrchestrator {
254
319
  this.onComplete('plan', '🎉 Planning phase complete! All 6 agents finished.\n\nRun *develop to start automated development.');
255
320
  }
256
321
 
322
+ /**
323
+ * Check if a tmux window has an active Claude Code session.
324
+ * Returns true if ❯ prompt or processing indicators are visible.
325
+ */
326
+ _isWindowActive(windowIdx) {
327
+ if (!this._session || !tmx.hasSession(this._session)) return false;
328
+ const pane = tmx.capturePane(this._session, windowIdx, 15);
329
+ // ❯ means Claude Code is running (idle or processing)
330
+ return /❯/.test(pane);
331
+ }
332
+
257
333
  _startPlanAgent(stepIdx) {
258
334
  const agent = PLAN_AGENTS[stepIdx];
259
335
  if (!agent) return;
@@ -55,6 +55,12 @@ class Router {
55
55
  onComplete: (phase, summary) => this._sendProactive(summary),
56
56
  onError: (phase, err) => this._sendProactive(`❌ Phase ${phase} error: ${err}`),
57
57
  });
58
+
59
+ // Auto-recover any in-progress phase from previous gateway run
60
+ const projectRoot = engine.resolveProjectRoot();
61
+ if (projectRoot) {
62
+ this.orchestrator.tryRecover(projectRoot);
63
+ }
58
64
  }
59
65
 
60
66
  /**
@@ -186,13 +192,13 @@ class Router {
186
192
  _handleStatusQuery(msg) {
187
193
  const parts = [];
188
194
 
189
- // Orchestrator status
195
+ // Orchestrator status (if actively tracking)
190
196
  if (this.orchestrator.isRunning()) {
191
197
  const status = this.orchestrator.getStatus();
192
198
  parts.push(status.message);
193
199
  }
194
200
 
195
- // Read project focus for additional context
201
+ // Read project state from YAML — works even if orchestrator isn't tracking
196
202
  const projectRoot = engine.resolveProjectRoot();
197
203
  if (projectRoot) {
198
204
  const focusPath = path.join(projectRoot, '.yuri', 'focus.yaml');
@@ -201,6 +207,21 @@ class Router {
201
207
  const focus = yaml.load(fs.readFileSync(focusPath, 'utf8')) || {};
202
208
  if (focus.pulse) parts.push(`Pulse: ${focus.pulse}`);
203
209
  if (focus.step) parts.push(`Step: ${focus.step}`);
210
+
211
+ // If YAML says in_progress but orchestrator isn't tracking, warn
212
+ if (!this.orchestrator.isRunning() && focus.phase) {
213
+ const phasePaths = {
214
+ 2: path.join(projectRoot, '.yuri', 'state', 'phase2.yaml'),
215
+ 3: path.join(projectRoot, '.yuri', 'state', 'phase3.yaml'),
216
+ };
217
+ const statePath = phasePaths[focus.phase];
218
+ if (statePath && fs.existsSync(statePath)) {
219
+ const state = yaml.load(fs.readFileSync(statePath, 'utf8')) || {};
220
+ if (state.status === 'in_progress') {
221
+ parts.push(`\n⚠️ Phase ${focus.phase} was in progress but orchestrator lost track (gateway restarted?). Send *plan or *develop to reconnect.`);
222
+ }
223
+ }
224
+ }
204
225
  } catch { /* ok */ }
205
226
  }
206
227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "3.2.0",
3
+ "version": "3.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": {