orchestrix-yuri 2.3.4 → 2.5.0

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.
@@ -143,24 +143,28 @@ function tmuxSafe(cmd) {
143
143
 
144
144
  // ── Claude Code TUI Indicators ─────────────────────────────────────────────────
145
145
  //
146
- // Claude Code uses three circle symbols as primary state indicators:
146
+ // Claude Code TUI state indicators vary by version and statusline config:
147
147
  //
148
- // (U+25CB) IDLE — Claude is waiting for user input
149
- // (U+25CF) PROCESSING Claude is actively generating a response
150
- // ◐ (U+25D0) APPROVAL Claude is waiting for permission approval
148
+ // Idle indicators (any of these = ready for input):
149
+ // (U+25CB) — circle idle indicator (shown with certain statusline configs)
150
+ //prompt cursor (always shown when idle, most reliable)
151
151
  //
152
- // During processing, a Braille spinner animates: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏
153
- // with cycling verbs like "Baking...", "Computing...", "Thinking..."
152
+ // Processing indicators:
153
+ // ● (U+25CF) filled circle, active generation
154
+ // Braille spinner: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏
154
155
  //
155
- // Completion message format (past-tense verb + duration):
156
- // "Baked for 31s", "Worked for 2m 45s", "Cooked for 1m 6s"
157
- // Pattern: /[A-Z][a-z]*ed for \d+/
156
+ // Status line elements (NOT state indicators):
157
+ // ◐ — effort level indicator (e.g. " medium · /effort"), NOT approval prompt
158
+ //
159
+ // Completion message (past-tense verb + duration):
160
+ // "Baked for 31s", "Worked for 2m 45s"
161
+ // Pattern: /[A-Z][a-z]*ed for \d+/
158
162
  //
159
163
  // ────────────────────────────────────────────────────────────────────────────────
160
164
 
161
165
  const BRAILLE_SPINNER = /[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/;
162
166
  const COMPLETION_RE = /[A-Z][a-z]*ed for \d+/;
163
- const IDLE_RE = /○/;
167
+ const IDLE_RE = /[○❯]/;
164
168
  const PROCESSING_RE = /●/;
165
169
 
166
170
  /**
@@ -200,43 +204,31 @@ function paneTail(name, n) {
200
204
  }
201
205
 
202
206
  /**
203
- * Detect Claude Code's current state from pane output.
204
- *
205
- * Note: We launch with --dangerously-skip-permissions, so approval prompts (◐)
206
- * should never appear. The detection is kept for robustness but no auto-approve
207
- * action is taken — sending blind 'y' keystrokes is dangerous.
207
+ * Check if Claude Code has started up and is ready for input.
208
+ * Used ONLY during session initialization — looks for the ❯ input prompt
209
+ * which appears once Claude Code has fully loaded.
208
210
  *
209
- * @returns {'idle'|'processing'|'complete'|'unknown'}
211
+ * DO NOT use this for response completion detection — ❯ is always visible.
210
212
  */
211
- function detectState(name) {
213
+ function isStarted(name) {
212
214
  const tail = paneTail(name, 15);
215
+ return IDLE_RE.test(tail);
216
+ }
213
217
 
214
- // Priority 1: Completion message — most reliable signal
215
- // e.g. "Baked for 31s", "Worked for 2m 45s"
216
- if (COMPLETION_RE.test(tail)) {
217
- return 'complete';
218
- }
219
-
220
- // Priority 2: Idle indicator — waiting for input
221
- if (IDLE_RE.test(tail)) {
222
- return 'idle';
223
- }
224
-
225
- // Priority 3: Processing indicator — still working
226
- if (PROCESSING_RE.test(tail) || BRAILLE_SPINNER.test(tail)) {
227
- return 'processing';
228
- }
229
-
230
- return 'unknown';
218
+ /**
219
+ * Check if a completion message is present in the pane output.
220
+ * e.g. "Baked for 31s", "Worked for 2m 45s"
221
+ * This is the most reliable signal that Claude has finished responding.
222
+ */
223
+ function hasCompletionMessage(text) {
224
+ return COMPLETION_RE.test(text);
231
225
  }
232
226
 
233
227
  /**
234
- * Detect if Claude Code is idle (ready for input).
235
- * Checks for ○ idle indicator or completion message.
228
+ * Check if Claude Code is actively processing ( spinner visible).
236
229
  */
237
- function isIdle(name) {
238
- const state = detectState(name);
239
- return state === 'idle' || state === 'complete';
230
+ function isProcessing(text) {
231
+ return PROCESSING_RE.test(text) || BRAILLE_SPINNER.test(text);
240
232
  }
241
233
 
242
234
  // ── Context Management ─────────────────────────────────────────────────────────
@@ -304,7 +296,7 @@ async function proactiveCompact(name) {
304
296
  log.tmux('Proactive /compact triggered');
305
297
  injectMessage(name, '/compact focus on the most recent user conversation and any pending operations');
306
298
 
307
- const ok = await waitForIdle(name, 120000); // compact can take up to 2min
299
+ const ok = await waitForReady(name, 120000); // compact can take up to 2min
308
300
  if (ok) {
309
301
  _messageCount = 0;
310
302
  log.tmux('Proactive /compact completed');
@@ -354,7 +346,7 @@ async function createSession(engineConfig) {
354
346
  // Default 60s — Claude Code needs time to load CLAUDE.md, connect MCP servers, etc.
355
347
  const startupTimeout = engineConfig.startup_timeout || 60000;
356
348
  log.tmux(`Waiting for Claude Code to become idle (timeout: ${startupTimeout / 1000}s)...`);
357
- const started = await waitForIdle(sessionName, startupTimeout);
349
+ const started = await waitForReady(sessionName, startupTimeout);
358
350
  if (!started) {
359
351
  // Don't kill session on failure — let user debug with tmux attach
360
352
  const tail = paneTail(sessionName, 10);
@@ -371,7 +363,7 @@ async function createSession(engineConfig) {
371
363
  if (l1) {
372
364
  log.tmux('Injecting L1 context...');
373
365
  await injectMessage(sessionName, l1);
374
- await waitForIdle(sessionName, 120000); // allow up to 2min for L1 processing
366
+ await waitForReady(sessionName, 120000); // allow up to 2min for L1 processing
375
367
  }
376
368
 
377
369
  _sessionReady = true;
@@ -379,10 +371,12 @@ async function createSession(engineConfig) {
379
371
  }
380
372
 
381
373
  /**
382
- * Wait for Claude Code to become idle.
383
- * @returns {Promise<boolean>} true if idle detected, false if timeout
374
+ * Wait for Claude Code to be ready (❯ prompt visible).
375
+ * Used for session init and after /compact NOT for response capture.
376
+ *
377
+ * @returns {Promise<boolean>} true if ready detected, false if timeout
384
378
  */
385
- function waitForIdle(name, timeoutMs) {
379
+ function waitForReady(name, timeoutMs) {
386
380
  const pollInterval = 2000;
387
381
  return new Promise((resolve) => {
388
382
  const deadline = Date.now() + timeoutMs;
@@ -394,7 +388,7 @@ function waitForIdle(name, timeoutMs) {
394
388
  return resolve(false);
395
389
  }
396
390
 
397
- if (isIdle(name)) {
391
+ if (isStarted(name)) {
398
392
  return resolve(true);
399
393
  }
400
394
  setTimeout(poll, pollInterval);
@@ -422,10 +416,14 @@ function injectMessage(name, text) {
422
416
  /**
423
417
  * Capture the response after injecting a message.
424
418
  *
425
- * Detection priority:
419
+ * Detection strategy (❯ prompt is always visible, so we CANNOT use idle detection):
426
420
  * P1: Completion message — "[Verb]ed for [N]s/m" (e.g. "Baked for 31s")
427
- * P2: Idle indicator appears in pane tail
428
- * P3: Content stability — 3 consecutive polls with identical MD5 hash
421
+ * Most reliable signal. Appears exactly once when Claude finishes.
422
+ * P2: Content stability — pane output unchanged for N consecutive polls.
423
+ * Fallback for edge cases where completion message is missed.
424
+ *
425
+ * We also track whether content has changed since injection (via marker)
426
+ * to avoid returning before Claude has even started responding.
429
427
  */
430
428
  async function captureResponse(name, marker, engineConfig) {
431
429
  const timeout = engineConfig.timeout || 300000;
@@ -435,7 +433,12 @@ async function captureResponse(name, marker, engineConfig) {
435
433
  const deadline = Date.now() + timeout;
436
434
  let lastHash = '';
437
435
  let stableCount = 0;
438
- let sawProcessing = false;
436
+ let contentChanged = false;
437
+
438
+ // Capture baseline right after injection
439
+ const baselineRaw = capturePaneRaw(name, 500);
440
+ const baselineHash = crypto.createHash('md5').update(baselineRaw).digest('hex');
441
+ lastHash = baselineHash;
439
442
 
440
443
  return new Promise((resolve) => {
441
444
  const poll = () => {
@@ -451,31 +454,22 @@ async function captureResponse(name, marker, engineConfig) {
451
454
  return resolve({ reply: '❌ Claude Code session terminated unexpectedly.', raw: '' });
452
455
  }
453
456
 
454
- const state = detectState(name);
455
457
  const raw = capturePaneRaw(name, 500);
456
458
  const hash = crypto.createHash('md5').update(raw).digest('hex');
457
459
 
458
- // Track that Claude has started processing (● appeared)
459
- // This prevents premature completion detection if is still visible
460
- // from the previous idle state before Claude begins processing.
461
- if (state === 'processing') {
462
- sawProcessing = true;
463
- stableCount = 0;
464
- lastHash = hash;
465
- return setTimeout(poll, pollInterval);
460
+ // Track if content has changed since injection
461
+ if (hash !== baselineHash) {
462
+ contentChanged = true;
466
463
  }
467
464
 
468
465
  // P1: Completion message — most reliable done signal
469
- if (state === 'complete' && sawProcessing) {
470
- return resolve(extractResponse(raw, marker));
471
- }
472
-
473
- // P2: Idle indicator — done if we saw processing start
474
- if (state === 'idle' && sawProcessing) {
466
+ // Only check after content has changed (Claude has started responding)
467
+ if (contentChanged && hasCompletionMessage(paneTail(name, 15))) {
475
468
  return resolve(extractResponse(raw, marker));
476
469
  }
477
470
 
478
- // P4: Content stability fallback
471
+ // P2: Content stability — pane unchanged for N polls
472
+ // Only trigger after content has changed from baseline
479
473
  if (hash === lastHash) {
480
474
  stableCount++;
481
475
  } else {
@@ -483,7 +477,7 @@ async function captureResponse(name, marker, engineConfig) {
483
477
  lastHash = hash;
484
478
  }
485
479
 
486
- if (stableCount >= stableThreshold && sawProcessing) {
480
+ if (contentChanged && stableCount >= stableThreshold) {
487
481
  log.tmux('Response detected via content stability');
488
482
  return resolve(extractResponse(raw, marker));
489
483
  }
@@ -492,7 +486,6 @@ async function captureResponse(name, marker, engineConfig) {
492
486
  };
493
487
 
494
488
  // Initial delay: give Claude time to start processing
495
- // before first poll (avoids false-positive idle detection)
496
489
  setTimeout(poll, Math.max(pollInterval, 3000));
497
490
  });
498
491
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "2.3.4",
3
+ "version": "2.5.0",
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": {