orchestrix-yuri 3.2.1 → 3.3.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.
|
@@ -33,6 +33,9 @@ class TelegramAdapter {
|
|
|
33
33
|
chatId: String(ctx.chat.id),
|
|
34
34
|
text: ctx.message.text,
|
|
35
35
|
userName: ctx.from.first_name || ctx.from.username || 'Unknown',
|
|
36
|
+
replyToMessageId: ctx.message.reply_to_message
|
|
37
|
+
? String(ctx.message.reply_to_message.message_id)
|
|
38
|
+
: null,
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
try {
|
|
@@ -45,9 +45,16 @@ class PhaseOrchestrator {
|
|
|
45
45
|
this._timer = null;
|
|
46
46
|
this._lastHash = '';
|
|
47
47
|
this._stableCount = 0;
|
|
48
|
+
|
|
49
|
+
// Agent question bridging
|
|
50
|
+
this._waitingForInput = false; // true when agent asked a question
|
|
51
|
+
this._waitingMessageId = null; // Telegram message_id of the question notification
|
|
52
|
+
this._onQuestionAsked = opts.onQuestionAsked || (() => Promise.resolve(null));
|
|
53
|
+
// onQuestionAsked(text) → sends to Telegram, returns { messageId }
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
isRunning() { return this._phase !== null; }
|
|
57
|
+
isWaitingForInput() { return this._waitingForInput; }
|
|
51
58
|
|
|
52
59
|
/**
|
|
53
60
|
* Try to recover an in-progress phase from YAML state + tmux session.
|
|
@@ -105,6 +112,43 @@ class PhaseOrchestrator {
|
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Capture the current agent's tmux pane content for Claude context injection.
|
|
117
|
+
* Returns a formatted string or null if no agent is active.
|
|
118
|
+
*/
|
|
119
|
+
captureCurrentAgentContext() {
|
|
120
|
+
if (!this._phase || !this._session || !tmx.hasSession(this._session)) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this._phase === 'plan') {
|
|
125
|
+
const agent = PLAN_AGENTS[this._step];
|
|
126
|
+
if (!agent) return null;
|
|
127
|
+
|
|
128
|
+
const pane = tmx.capturePane(this._session, agent.window, 40);
|
|
129
|
+
if (!pane.trim()) return null;
|
|
130
|
+
|
|
131
|
+
const waiting = this._waitingForInput ? ' (WAITING FOR YOUR INPUT)' : '';
|
|
132
|
+
return `[LIVE AGENT CONTEXT] Phase: plan, Agent: ${agent.name} (${this._step + 1}/${PLAN_AGENTS.length})${waiting}\n` +
|
|
133
|
+
`tmux session: ${this._session}, window: ${agent.window}\n` +
|
|
134
|
+
`--- Agent output (last 40 lines) ---\n${pane}\n--- End agent output ---`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (this._phase === 'develop') {
|
|
138
|
+
// Capture all 4 dev windows briefly
|
|
139
|
+
const windows = ['Architect', 'SM', 'Dev', 'QA'];
|
|
140
|
+
const summaries = [];
|
|
141
|
+
for (let w = 0; w < 4; w++) {
|
|
142
|
+
const tail = tmx.capturePane(this._session, w, 5);
|
|
143
|
+
const lastLine = tail.split('\n').filter((l) => l.trim()).pop() || '(empty)';
|
|
144
|
+
summaries.push(` Window ${w} (${windows[w]}): ${lastLine.trim().slice(0, 80)}`);
|
|
145
|
+
}
|
|
146
|
+
return `[LIVE AGENT CONTEXT] Phase: develop, Session: ${this._session}\n${summaries.join('\n')}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
108
152
|
getStatus() {
|
|
109
153
|
if (!this._phase) {
|
|
110
154
|
return { phase: null, message: 'No phase is running.' };
|
|
@@ -239,6 +283,9 @@ class PhaseOrchestrator {
|
|
|
239
283
|
return;
|
|
240
284
|
}
|
|
241
285
|
|
|
286
|
+
// Skip polling while waiting for user input
|
|
287
|
+
if (this._waitingForInput) return;
|
|
288
|
+
|
|
242
289
|
// Check completion
|
|
243
290
|
const result = tmx.checkCompletion(this._session, agent.window, this._lastHash);
|
|
244
291
|
|
|
@@ -251,8 +298,21 @@ class PhaseOrchestrator {
|
|
|
251
298
|
this._stableCount++;
|
|
252
299
|
this._lastHash = result.hash;
|
|
253
300
|
if (this._stableCount >= 3) {
|
|
254
|
-
// Content stable for 3 polls —
|
|
255
|
-
this.
|
|
301
|
+
// Content stable for 3 polls — distinguish "done" vs "waiting for input"
|
|
302
|
+
const pane = tmx.capturePane(this._session, agent.window, 30);
|
|
303
|
+
const tail = pane.split('\n').slice(-15).join('\n');
|
|
304
|
+
|
|
305
|
+
if (/[A-Z][a-z]*ed for \d+/.test(tail)) {
|
|
306
|
+
// Completion message present — truly done
|
|
307
|
+
this._onAgentComplete(agent);
|
|
308
|
+
} else if (this._looksLikeQuestion(tail)) {
|
|
309
|
+
// Agent is asking a question — bridge to user
|
|
310
|
+
this._onAgentWaitingInput(agent, tail);
|
|
311
|
+
} else {
|
|
312
|
+
// Ambiguous — treat as done (agent may have finished without completion message)
|
|
313
|
+
log.engine(`Agent ${agent.name} stable without completion message, treating as done`);
|
|
314
|
+
this._onAgentComplete(agent);
|
|
315
|
+
}
|
|
256
316
|
return;
|
|
257
317
|
}
|
|
258
318
|
} else {
|
|
@@ -261,6 +321,82 @@ class PhaseOrchestrator {
|
|
|
261
321
|
}
|
|
262
322
|
}
|
|
263
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Detect if pane content looks like the agent is asking a question.
|
|
326
|
+
*/
|
|
327
|
+
_looksLikeQuestion(tail) {
|
|
328
|
+
// Has question mark near the end
|
|
329
|
+
if (/[??]\s*$/.test(tail) || /[??]\s*\n\s*❯/m.test(tail)) return true;
|
|
330
|
+
// Has numbered options (1. xxx 2. xxx)
|
|
331
|
+
if (/^\s*[1-9]\.\s+\S/m.test(tail) && /❯/.test(tail)) return true;
|
|
332
|
+
// Has Y/N prompt
|
|
333
|
+
if (/\(Y\/N\)/i.test(tail) || /\(y\/n\)/i.test(tail)) return true;
|
|
334
|
+
// Has "please confirm" or similar
|
|
335
|
+
if (/confirm|choose|select|which|请选择|请确认|是否/i.test(tail) && /❯/.test(tail)) return true;
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Agent is waiting for user input. Notify user via Telegram and pause polling.
|
|
341
|
+
*/
|
|
342
|
+
async _onAgentWaitingInput(agent, paneContent) {
|
|
343
|
+
this._waitingForInput = true;
|
|
344
|
+
|
|
345
|
+
// Extract the question from pane content (last meaningful lines before ❯)
|
|
346
|
+
const lines = paneContent.split('\n');
|
|
347
|
+
const promptIdx = lines.findLastIndex((l) => /❯/.test(l));
|
|
348
|
+
const questionLines = lines.slice(Math.max(0, promptIdx - 15), promptIdx).filter((l) => l.trim());
|
|
349
|
+
const question = questionLines.join('\n').trim() || '(unable to extract question)';
|
|
350
|
+
|
|
351
|
+
log.engine(`Agent ${agent.name} is asking a question, notifying user`);
|
|
352
|
+
|
|
353
|
+
const notification = `📋 **${agent.name}** is asking:\n\n${question}\n\n↩️ *Reply to this message to answer the agent.*`;
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const result = await this._onQuestionAsked(notification);
|
|
357
|
+
if (result && result.messageId) {
|
|
358
|
+
this._waitingMessageId = String(result.messageId);
|
|
359
|
+
}
|
|
360
|
+
} catch (err) {
|
|
361
|
+
log.warn(`Failed to send question notification: ${err.message}`);
|
|
362
|
+
// Auto-continue on failure
|
|
363
|
+
this._waitingForInput = false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Relay user's reply to the agent's tmux window.
|
|
369
|
+
* Called by router when user replies to the question notification.
|
|
370
|
+
*/
|
|
371
|
+
relayUserInput(text) {
|
|
372
|
+
if (!this._waitingForInput || !this._session) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const agent = PLAN_AGENTS[this._step];
|
|
377
|
+
if (!agent) return false;
|
|
378
|
+
|
|
379
|
+
log.engine(`Relaying user input to ${agent.name}: "${text.slice(0, 50)}..."`);
|
|
380
|
+
|
|
381
|
+
tmx.sendKeysWithEnter(this._session, agent.window, text);
|
|
382
|
+
|
|
383
|
+
// Resume polling
|
|
384
|
+
this._waitingForInput = false;
|
|
385
|
+
this._waitingMessageId = null;
|
|
386
|
+
this._stableCount = 0;
|
|
387
|
+
this._lastHash = '';
|
|
388
|
+
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get the Telegram message_id of the current question notification.
|
|
394
|
+
* Used by router to check if a reply-to matches.
|
|
395
|
+
*/
|
|
396
|
+
getWaitingMessageId() {
|
|
397
|
+
return this._waitingMessageId;
|
|
398
|
+
}
|
|
399
|
+
|
|
264
400
|
_onAgentComplete(agent) {
|
|
265
401
|
this._stableCount = 0;
|
|
266
402
|
this._lastHash = '';
|
package/lib/gateway/index.js
CHANGED
|
@@ -39,12 +39,16 @@ async function startGateway(opts = {}) {
|
|
|
39
39
|
});
|
|
40
40
|
adapters.push(telegram);
|
|
41
41
|
|
|
42
|
-
// Wire proactive messaging: orchestrator → Telegram
|
|
42
|
+
// Wire proactive messaging: orchestrator → Telegram (returns { messageId })
|
|
43
43
|
router.setSendCallback(null, async (chatId, text) => {
|
|
44
44
|
try {
|
|
45
|
-
await telegram.bot.api.sendMessage(chatId, text, { parse_mode: 'Markdown' });
|
|
45
|
+
const sent = await telegram.bot.api.sendMessage(chatId, text, { parse_mode: 'Markdown' });
|
|
46
|
+
return { messageId: String(sent.message_id) };
|
|
46
47
|
} catch {
|
|
47
|
-
|
|
48
|
+
try {
|
|
49
|
+
const sent = await telegram.bot.api.sendMessage(chatId, text);
|
|
50
|
+
return { messageId: String(sent.message_id) };
|
|
51
|
+
} catch { return null; }
|
|
48
52
|
}
|
|
49
53
|
});
|
|
50
54
|
|
package/lib/gateway/router.js
CHANGED
|
@@ -54,6 +54,7 @@ class Router {
|
|
|
54
54
|
onProgress: (msg) => this._sendProactive(msg),
|
|
55
55
|
onComplete: (phase, summary) => this._sendProactive(summary),
|
|
56
56
|
onError: (phase, err) => this._sendProactive(`❌ Phase ${phase} error: ${err}`),
|
|
57
|
+
onQuestionAsked: (text) => this._sendProactiveWithId(text),
|
|
57
58
|
});
|
|
58
59
|
|
|
59
60
|
// Auto-recover any in-progress phase from previous gateway run
|
|
@@ -80,6 +81,20 @@ class Router {
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Send a proactive message and return { messageId } for reply-to tracking.
|
|
86
|
+
*/
|
|
87
|
+
async _sendProactiveWithId(text) {
|
|
88
|
+
if (!this._sendCallback || !this._ownerChatId) return null;
|
|
89
|
+
try {
|
|
90
|
+
const result = await this._sendCallback(this._ownerChatId, text);
|
|
91
|
+
return result; // { messageId }
|
|
92
|
+
} catch (err) {
|
|
93
|
+
log.warn(`Proactive send failed: ${err.message}`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
/**
|
|
84
99
|
* Handle an incoming channel message.
|
|
85
100
|
*/
|
|
@@ -113,6 +128,17 @@ class Router {
|
|
|
113
128
|
return this._handleStatusQuery(msg);
|
|
114
129
|
}
|
|
115
130
|
|
|
131
|
+
// ═══ AGENT REPLY — user replied to an agent question notification ═══
|
|
132
|
+
if (msg.replyToMessageId && this.orchestrator.isWaitingForInput()) {
|
|
133
|
+
const waitingId = this.orchestrator.getWaitingMessageId();
|
|
134
|
+
if (waitingId && msg.replyToMessageId === waitingId) {
|
|
135
|
+
const relayed = this.orchestrator.relayUserInput(msg.text);
|
|
136
|
+
if (relayed) {
|
|
137
|
+
return { text: '✅ Reply sent to agent. Resuming...' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
116
142
|
// ═══ CANCEL — stop running phase ═══
|
|
117
143
|
if (PHASE_COMMANDS.cancel.test(msg.text.trim())) {
|
|
118
144
|
if (this.orchestrator.isRunning()) {
|
|
@@ -244,7 +270,15 @@ class Router {
|
|
|
244
270
|
await this._runCatchUp();
|
|
245
271
|
|
|
246
272
|
const projectRoot = engine.resolveProjectRoot();
|
|
247
|
-
|
|
273
|
+
|
|
274
|
+
// If a phase is running, inject tmux pane context so Claude can see agent state
|
|
275
|
+
let prompt = engine.composePrompt(msg.text);
|
|
276
|
+
if (this.orchestrator.isRunning()) {
|
|
277
|
+
const agentContext = this.orchestrator.captureCurrentAgentContext();
|
|
278
|
+
if (agentContext) {
|
|
279
|
+
prompt = `${agentContext}\n\n---\n\nUser message: ${msg.text}`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
248
282
|
|
|
249
283
|
log.router(`Processing: "${msg.text.slice(0, 80)}..." → cwd: ${projectRoot || '~'}`);
|
|
250
284
|
const result = await engine.callClaude({
|