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
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
156
|
-
return `🚀 Planning started! Agent ${this._step + 1}/${PLAN_AGENTS.length} (${
|
|
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;
|
package/lib/gateway/router.js
CHANGED
|
@@ -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
|
|
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
|
}
|