aicodeman 0.5.2 → 0.5.4

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.
Files changed (144) hide show
  1. package/dist/ai-checker-base.d.ts.map +1 -1
  2. package/dist/ai-checker-base.js +3 -2
  3. package/dist/ai-checker-base.js.map +1 -1
  4. package/dist/bash-tool-parser.d.ts +6 -0
  5. package/dist/bash-tool-parser.d.ts.map +1 -1
  6. package/dist/bash-tool-parser.js +87 -101
  7. package/dist/bash-tool-parser.js.map +1 -1
  8. package/dist/file-stream-manager.d.ts.map +1 -1
  9. package/dist/file-stream-manager.js +2 -1
  10. package/dist/file-stream-manager.js.map +1 -1
  11. package/dist/orchestrator-loop.d.ts +2 -0
  12. package/dist/orchestrator-loop.d.ts.map +1 -1
  13. package/dist/orchestrator-loop.js +27 -22
  14. package/dist/orchestrator-loop.js.map +1 -1
  15. package/dist/orchestrator-verifier.d.ts +1 -1
  16. package/dist/orchestrator-verifier.d.ts.map +1 -1
  17. package/dist/orchestrator-verifier.js +3 -2
  18. package/dist/orchestrator-verifier.js.map +1 -1
  19. package/dist/plan-orchestrator.d.ts +4 -1
  20. package/dist/plan-orchestrator.d.ts.map +1 -1
  21. package/dist/plan-orchestrator.js +66 -88
  22. package/dist/plan-orchestrator.js.map +1 -1
  23. package/dist/ralph-status-parser.d.ts +2 -0
  24. package/dist/ralph-status-parser.d.ts.map +1 -1
  25. package/dist/ralph-status-parser.js +98 -102
  26. package/dist/ralph-status-parser.js.map +1 -1
  27. package/dist/ralph-tracker.d.ts +9 -0
  28. package/dist/ralph-tracker.d.ts.map +1 -1
  29. package/dist/ralph-tracker.js +52 -60
  30. package/dist/ralph-tracker.js.map +1 -1
  31. package/dist/respawn-controller.d.ts +18 -1
  32. package/dist/respawn-controller.d.ts.map +1 -1
  33. package/dist/respawn-controller.js +215 -181
  34. package/dist/respawn-controller.js.map +1 -1
  35. package/dist/session-auto-ops.d.ts.map +1 -1
  36. package/dist/session-auto-ops.js +57 -55
  37. package/dist/session-auto-ops.js.map +1 -1
  38. package/dist/session.d.ts +5 -0
  39. package/dist/session.d.ts.map +1 -1
  40. package/dist/session.js +182 -218
  41. package/dist/session.js.map +1 -1
  42. package/dist/state-store.d.ts +6 -0
  43. package/dist/state-store.d.ts.map +1 -1
  44. package/dist/state-store.js +67 -79
  45. package/dist/state-store.js.map +1 -1
  46. package/dist/subagent-watcher.d.ts +24 -0
  47. package/dist/subagent-watcher.d.ts.map +1 -1
  48. package/dist/subagent-watcher.js +215 -220
  49. package/dist/subagent-watcher.js.map +1 -1
  50. package/dist/tmux-manager.d.ts +17 -0
  51. package/dist/tmux-manager.d.ts.map +1 -1
  52. package/dist/tmux-manager.js +57 -66
  53. package/dist/tmux-manager.js.map +1 -1
  54. package/dist/tunnel-manager.d.ts.map +1 -1
  55. package/dist/tunnel-manager.js +2 -1
  56. package/dist/tunnel-manager.js.map +1 -1
  57. package/dist/web/public/api-client.3adebdc2.js.gz +0 -0
  58. package/dist/web/public/app.16290ae3.js +26 -0
  59. package/dist/web/public/app.16290ae3.js.br +0 -0
  60. package/dist/web/public/app.16290ae3.js.gz +0 -0
  61. package/dist/web/public/constants.64161167.js.gz +0 -0
  62. package/dist/web/public/index.html +7 -7
  63. package/dist/web/public/index.html.br +0 -0
  64. package/dist/web/public/index.html.gz +0 -0
  65. package/dist/web/public/input-cjk.88082175.js.gz +0 -0
  66. package/dist/web/public/keyboard-accessory.9fb81db6.js.gz +0 -0
  67. package/dist/web/public/mobile-handlers.1e2a8ef8.js.gz +0 -0
  68. package/dist/web/public/mobile.0b213796.css.gz +0 -0
  69. package/dist/web/public/notification-manager.2d5ea8ec.js.gz +0 -0
  70. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  71. package/dist/web/public/{panels-ui.8204db1e.js → panels-ui.2d5b9703.js} +1 -1
  72. package/dist/web/public/panels-ui.2d5b9703.js.br +0 -0
  73. package/dist/web/public/panels-ui.2d5b9703.js.gz +0 -0
  74. package/dist/web/public/{ralph-panel.a2733fd5.js → ralph-panel.61076370.js} +1 -1
  75. package/dist/web/public/ralph-panel.61076370.js.br +0 -0
  76. package/dist/web/public/ralph-panel.61076370.js.gz +0 -0
  77. package/dist/web/public/ralph-wizard.f31ab90e.js.gz +0 -0
  78. package/dist/web/public/{respawn-ui.372c6ea7.js → respawn-ui.60be6ef5.js} +1 -1
  79. package/dist/web/public/respawn-ui.60be6ef5.js.br +0 -0
  80. package/dist/web/public/respawn-ui.60be6ef5.js.gz +0 -0
  81. package/dist/web/public/{session-ui.72f2f538.js → session-ui.554092ae.js} +1 -1
  82. package/dist/web/public/session-ui.554092ae.js.br +0 -0
  83. package/dist/web/public/session-ui.554092ae.js.gz +0 -0
  84. package/dist/web/public/{settings-ui.bd3eaadb.js → settings-ui.c58b0b9b.js} +7 -7
  85. package/dist/web/public/settings-ui.c58b0b9b.js.br +0 -0
  86. package/dist/web/public/settings-ui.c58b0b9b.js.gz +0 -0
  87. package/dist/web/public/styles.111ff326.css.gz +0 -0
  88. package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
  89. package/dist/web/public/sw.js.gz +0 -0
  90. package/dist/web/public/terminal-ui.474f79df.js.gz +0 -0
  91. package/dist/web/public/upload.html.gz +0 -0
  92. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  93. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  94. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  95. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  96. package/dist/web/public/vendor/xterm.css.gz +0 -0
  97. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  98. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  99. package/dist/web/respawn-event-wiring.d.ts +51 -0
  100. package/dist/web/respawn-event-wiring.d.ts.map +1 -0
  101. package/dist/web/respawn-event-wiring.js +280 -0
  102. package/dist/web/respawn-event-wiring.js.map +1 -0
  103. package/dist/web/route-helpers.d.ts +23 -0
  104. package/dist/web/route-helpers.d.ts.map +1 -1
  105. package/dist/web/route-helpers.js +53 -0
  106. package/dist/web/route-helpers.js.map +1 -1
  107. package/dist/web/routes/case-routes.d.ts.map +1 -1
  108. package/dist/web/routes/case-routes.js +2 -11
  109. package/dist/web/routes/case-routes.js.map +1 -1
  110. package/dist/web/routes/file-routes.d.ts.map +1 -1
  111. package/dist/web/routes/file-routes.js +8 -24
  112. package/dist/web/routes/file-routes.js.map +1 -1
  113. package/dist/web/routes/orchestrator-routes.d.ts.map +1 -1
  114. package/dist/web/routes/orchestrator-routes.js +23 -30
  115. package/dist/web/routes/orchestrator-routes.js.map +1 -1
  116. package/dist/web/routes/system-routes.d.ts.map +1 -1
  117. package/dist/web/routes/system-routes.js +17 -71
  118. package/dist/web/routes/system-routes.js.map +1 -1
  119. package/dist/web/server.d.ts +4 -51
  120. package/dist/web/server.d.ts.map +1 -1
  121. package/dist/web/server.js +98 -941
  122. package/dist/web/server.js.map +1 -1
  123. package/dist/web/session-listener-wiring.d.ts +89 -0
  124. package/dist/web/session-listener-wiring.d.ts.map +1 -0
  125. package/dist/web/session-listener-wiring.js +290 -0
  126. package/dist/web/session-listener-wiring.js.map +1 -0
  127. package/dist/web/sse-stream-manager.d.ts +91 -0
  128. package/dist/web/sse-stream-manager.d.ts.map +1 -0
  129. package/dist/web/sse-stream-manager.js +426 -0
  130. package/dist/web/sse-stream-manager.js.map +1 -0
  131. package/package.json +1 -1
  132. package/dist/web/public/app.e09fd4a6.js +0 -26
  133. package/dist/web/public/app.e09fd4a6.js.br +0 -0
  134. package/dist/web/public/app.e09fd4a6.js.gz +0 -0
  135. package/dist/web/public/panels-ui.8204db1e.js.br +0 -0
  136. package/dist/web/public/panels-ui.8204db1e.js.gz +0 -0
  137. package/dist/web/public/ralph-panel.a2733fd5.js.br +0 -0
  138. package/dist/web/public/ralph-panel.a2733fd5.js.gz +0 -0
  139. package/dist/web/public/respawn-ui.372c6ea7.js.br +0 -0
  140. package/dist/web/public/respawn-ui.372c6ea7.js.gz +0 -0
  141. package/dist/web/public/session-ui.72f2f538.js.br +0 -0
  142. package/dist/web/public/session-ui.72f2f538.js.gz +0 -0
  143. package/dist/web/public/settings-ui.bd3eaadb.js.br +0 -0
  144. package/dist/web/public/settings-ui.bd3eaadb.js.gz +0 -0
@@ -53,12 +53,20 @@ import { RespawnAdaptiveTiming } from './respawn-adaptive-timing.js';
53
53
  import { RespawnCycleMetricsTracker } from './respawn-metrics.js';
54
54
  import { calculateHealthScore, shouldSkipClear } from './respawn-health.js';
55
55
  import { AI_CHECK_MODEL, AI_IDLE_CHECK_MAX_CONTEXT, AI_PLAN_CHECK_MAX_CONTEXT, AI_IDLE_CHECK_TIMEOUT_MS, AI_IDLE_CHECK_COOLDOWN_MS, AI_PLAN_CHECK_TIMEOUT_MS, AI_PLAN_CHECK_COOLDOWN_MS, } from './config/ai-defaults.js';
56
+ import { getErrorMessage, } from './types.js';
56
57
  // ========== Constants ==========
57
58
  // COMPLETION_TIME_PATTERN moved to ./respawn-patterns.ts
58
59
  /** Pre-filter: numbered option pattern for plan mode detection */
59
60
  const PLAN_MODE_OPTION_PATTERN = /\d+\.\s+(Yes|No|Type|Cancel|Skip|Proceed|Approve|Reject)/i;
60
61
  /** Pre-filter: selection indicator arrow for plan mode detection */
61
62
  const PLAN_MODE_SELECTOR_PATTERN = /[❯>]\s*\d+\./;
63
+ /**
64
+ * Convert milliseconds to a non-negative whole number of seconds for countdown display.
65
+ * Rounds up so that e.g. 1200 ms shows as 2 s (never under-reports remaining time).
66
+ */
67
+ function formatRemainingSeconds(ms) {
68
+ return Math.max(0, Math.ceil(ms / 1000));
69
+ }
62
70
  /** Default configuration values */
63
71
  const DEFAULT_CONFIG = {
64
72
  idleTimeoutMs: 10000, // 10 seconds of no activity after prompt (legacy, still used as fallback)
@@ -291,35 +299,39 @@ export class RespawnController extends EventEmitter {
291
299
  */
292
300
  validateConfig() {
293
301
  const c = this.config;
294
- // Ensure timeouts are positive
295
- if (c.idleTimeoutMs <= 0)
296
- c.idleTimeoutMs = DEFAULT_CONFIG.idleTimeoutMs;
297
- if (c.completionConfirmMs <= 0)
298
- c.completionConfirmMs = DEFAULT_CONFIG.completionConfirmMs;
299
- if (c.noOutputTimeoutMs <= 0)
300
- c.noOutputTimeoutMs = DEFAULT_CONFIG.noOutputTimeoutMs;
301
- if (c.autoAcceptDelayMs < 0)
302
- c.autoAcceptDelayMs = DEFAULT_CONFIG.autoAcceptDelayMs;
303
- if (c.interStepDelayMs <= 0)
304
- c.interStepDelayMs = DEFAULT_CONFIG.interStepDelayMs;
302
+ /**
303
+ * Validate that a timeout value is positive (or non-negative when allowZero is true).
304
+ * Falls back to the DEFAULT_CONFIG value if invalid.
305
+ */
306
+ const validatePositiveTimeout = (field, allowZero = false) => {
307
+ const value = c[field];
308
+ const invalid = allowZero ? value < 0 : value <= 0;
309
+ if (invalid) {
310
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
311
+ c[field] = DEFAULT_CONFIG[field];
312
+ }
313
+ };
314
+ const REQUIRED_TIMEOUT_FIELDS = [
315
+ 'idleTimeoutMs',
316
+ 'completionConfirmMs',
317
+ 'noOutputTimeoutMs',
318
+ 'interStepDelayMs',
319
+ 'aiIdleCheckTimeoutMs',
320
+ 'aiIdleCheckMaxContext',
321
+ 'aiPlanCheckTimeoutMs',
322
+ 'aiPlanCheckMaxContext',
323
+ ];
324
+ for (const field of REQUIRED_TIMEOUT_FIELDS) {
325
+ validatePositiveTimeout(field);
326
+ }
327
+ const ALLOW_ZERO_FIELDS = ['autoAcceptDelayMs', 'aiIdleCheckCooldownMs', 'aiPlanCheckCooldownMs'];
328
+ for (const field of ALLOW_ZERO_FIELDS) {
329
+ validatePositiveTimeout(field, true);
330
+ }
305
331
  // Ensure completion confirm doesn't exceed no-output timeout
306
332
  if (c.completionConfirmMs > c.noOutputTimeoutMs) {
307
333
  c.completionConfirmMs = c.noOutputTimeoutMs;
308
334
  }
309
- // Ensure AI check timeouts are positive
310
- if (c.aiIdleCheckTimeoutMs <= 0)
311
- c.aiIdleCheckTimeoutMs = DEFAULT_CONFIG.aiIdleCheckTimeoutMs;
312
- if (c.aiIdleCheckCooldownMs < 0)
313
- c.aiIdleCheckCooldownMs = DEFAULT_CONFIG.aiIdleCheckCooldownMs;
314
- if (c.aiIdleCheckMaxContext <= 0)
315
- c.aiIdleCheckMaxContext = DEFAULT_CONFIG.aiIdleCheckMaxContext;
316
- // Ensure plan check timeouts are positive
317
- if (c.aiPlanCheckTimeoutMs <= 0)
318
- c.aiPlanCheckTimeoutMs = DEFAULT_CONFIG.aiPlanCheckTimeoutMs;
319
- if (c.aiPlanCheckCooldownMs < 0)
320
- c.aiPlanCheckCooldownMs = DEFAULT_CONFIG.aiPlanCheckCooldownMs;
321
- if (c.aiPlanCheckMaxContext <= 0)
322
- c.aiPlanCheckMaxContext = DEFAULT_CONFIG.aiPlanCheckMaxContext;
323
335
  }
324
336
  /** Wire up AI checker events to controller events (removes existing listeners first to prevent duplicates) */
325
337
  setupAiCheckerListeners() {
@@ -437,12 +449,12 @@ export class RespawnController extends EventEmitter {
437
449
  }
438
450
  else if (this._state === 'confirming_idle') {
439
451
  statusText = `Confirming idle (${confidence}% confidence)`;
440
- waitingFor = `${Math.max(0, Math.ceil((this.config.completionConfirmMs - msSinceLastOutput) / 1000))}s more silence`;
452
+ waitingFor = `${formatRemainingSeconds(this.config.completionConfirmMs - msSinceLastOutput)}s more silence`;
441
453
  }
442
454
  else if (this._state === 'watching') {
443
455
  const aiState = this.aiChecker.getState();
444
456
  if (aiState.status === 'cooldown') {
445
- const remaining = Math.ceil(this.aiChecker.getCooldownRemainingMs() / 1000);
457
+ const remaining = formatRemainingSeconds(this.aiChecker.getCooldownRemainingMs());
446
458
  statusText = `AI Check: WORKING (cooldown ${remaining}s)`;
447
459
  waitingFor = 'Cooldown to expire';
448
460
  }
@@ -788,88 +800,13 @@ export class RespawnController extends EventEmitter {
788
800
  this.lastTokenCount = tokenCount;
789
801
  this.lastTokenChangeTime = now;
790
802
  }
791
- // Detect completion message FIRST (Layer 1) - PRIMARY DETECTION
792
- // Check this before working patterns because completion message indicates
793
- // the work is done, even if working patterns are still in the rolling window
794
- if (isCompletionMessage(data)) {
795
- // Clear the rolling window - completion marks a transition point
796
- this.clearWorkingPatternWindow();
797
- this.workingDetected = false;
798
- this.completionMessageTime = now;
799
- this.cancelAutoAcceptTimer(); // Normal idle flow handles this
800
- this.log(`Completion message detected: "${data.trim().substring(0, 50)}..."`);
801
- // In watching state, start completion confirmation timer
802
- if (this._state === 'watching') {
803
- this.startCompletionConfirmTimer();
804
- return;
805
- }
806
- // In waiting states, also use confirmation timer (same detection logic)
807
- // This ensures we wait for Claude to finish before proceeding
808
- // Note: 'watching' is already handled above and returns early
809
- switch (this._state) {
810
- case 'waiting_update':
811
- this.startStepConfirmTimer('update');
812
- break;
813
- case 'waiting_clear':
814
- this.checkClearComplete(); // /clear is quick, no need to wait
815
- break;
816
- case 'waiting_init':
817
- this.startStepConfirmTimer('init');
818
- break;
819
- case 'waiting_kickstart':
820
- this.startStepConfirmTimer('kickstart');
821
- break;
822
- // Non-waiting states: completion message is ignored
823
- case 'confirming_idle':
824
- case 'ai_checking':
825
- case 'sending_update':
826
- case 'sending_clear':
827
- case 'sending_init':
828
- case 'monitoring_init':
829
- case 'sending_kickstart':
830
- case 'stopped':
831
- // Completion message during these states is ignored
832
- break;
833
- default:
834
- assertNever(this._state, `Unhandled RespawnState in completion detection: ${this._state}`);
835
- }
803
+ // Layer 1: Completion message (PRIMARY) checked before working patterns
804
+ if (this._detectCompletionMessage(data, now))
836
805
  return;
837
- }
838
- // Detect working patterns (Layer 4)
839
- const isWorking = this.checkWorkingPattern(data);
840
- if (isWorking) {
841
- this.workingDetected = true;
842
- this.promptDetected = false;
843
- this.elicitationDetected = false; // Clear on new work cycle
844
- this.resetHookState(); // Clear hook signals on new work
845
- this.lastWorkingPatternTime = now;
846
- // Cancel hook confirmation timer if running
847
- this.cancelTrackedTimer('hook-confirm', 'working patterns detected');
848
- // Cancel any pending completion confirmation
849
- this.cancelCompletionConfirm();
850
- // Cancel any pending step confirmation (Claude is still working)
851
- this.cancelStepConfirm();
852
- // If AI check is running, cancel it (Claude is working)
853
- if (this._state === 'ai_checking') {
854
- this.log('Working patterns detected during AI check, cancelling');
855
- this.aiChecker.cancel();
856
- this.setState('watching');
857
- }
858
- // Cancel plan check if running (Claude started working)
859
- if (this.planChecker.status === 'checking') {
860
- this.log('Working patterns detected during plan check, cancelling');
861
- this.planChecker.cancel();
862
- }
863
- // If we're monitoring init and work started, go to watching (no kickstart needed)
864
- if (this._state === 'monitoring_init') {
865
- this.log('/init triggered work, skipping kickstart');
866
- this.emit('stepCompleted', 'init');
867
- this.completeCycle();
868
- }
806
+ // Layer 4: Working patterns
807
+ if (this._detectWorkingPattern(data, now))
869
808
  return;
870
- }
871
- // In confirming_idle or ai_checking state, substantial output cancels the flow.
872
- // This prevents false triggers when Claude pauses briefly mid-work.
809
+ // Substantial output during confirming_idle/ai_checking cancels the flow
873
810
  if (this._state === 'confirming_idle' || this._state === 'ai_checking') {
874
811
  // Strip ANSI escape codes to check if there's real content
875
812
  ANSI_ESCAPE_PATTERN_SIMPLE.lastIndex = 0;
@@ -888,42 +825,125 @@ export class RespawnController extends EventEmitter {
888
825
  return;
889
826
  }
890
827
  }
891
- // Legacy fallback: detect prompt characters (still useful for waiting_* states)
828
+ // Legacy fallback: prompt detection
829
+ this._detectPrompt(data);
830
+ }
831
+ _detectCompletionMessage(data, now) {
832
+ if (!isCompletionMessage(data))
833
+ return false;
834
+ // Clear the rolling window - completion marks a transition point
835
+ this.clearWorkingPatternWindow();
836
+ this.workingDetected = false;
837
+ this.completionMessageTime = now;
838
+ this.cancelAutoAcceptTimer(); // Normal idle flow handles this
839
+ this.log(`Completion message detected: "${data.trim().substring(0, 50)}..."`);
840
+ // In watching state, start completion confirmation timer
841
+ if (this._state === 'watching') {
842
+ this.startCompletionConfirmTimer();
843
+ return true;
844
+ }
845
+ // In waiting states, also use confirmation timer (same detection logic)
846
+ // This ensures we wait for Claude to finish before proceeding
847
+ // Note: 'watching' is already handled above and returns early
848
+ switch (this._state) {
849
+ case 'waiting_update':
850
+ this.startStepConfirmTimer('update');
851
+ break;
852
+ case 'waiting_clear':
853
+ this.checkClearComplete(); // /clear is quick, no need to wait
854
+ break;
855
+ case 'waiting_init':
856
+ this.startStepConfirmTimer('init');
857
+ break;
858
+ case 'waiting_kickstart':
859
+ this.startStepConfirmTimer('kickstart');
860
+ break;
861
+ // Non-waiting states: completion message is ignored
862
+ case 'confirming_idle':
863
+ case 'ai_checking':
864
+ case 'sending_update':
865
+ case 'sending_clear':
866
+ case 'sending_init':
867
+ case 'monitoring_init':
868
+ case 'sending_kickstart':
869
+ case 'stopped':
870
+ // Completion message during these states is ignored
871
+ break;
872
+ default:
873
+ assertNever(this._state, `Unhandled RespawnState in completion detection: ${this._state}`);
874
+ }
875
+ return true;
876
+ }
877
+ _detectWorkingPattern(data, now) {
878
+ const isWorking = this.checkWorkingPattern(data);
879
+ if (!isWorking)
880
+ return false;
881
+ this.workingDetected = true;
882
+ this.promptDetected = false;
883
+ this.elicitationDetected = false; // Clear on new work cycle
884
+ this.resetHookState(); // Clear hook signals on new work
885
+ this.lastWorkingPatternTime = now;
886
+ // Cancel hook confirmation timer if running
887
+ this.cancelTrackedTimer('hook-confirm', 'working patterns detected');
888
+ // Cancel any pending completion confirmation
889
+ this.cancelCompletionConfirm();
890
+ // Cancel any pending step confirmation (Claude is still working)
891
+ this.cancelStepConfirm();
892
+ // If AI check is running, cancel it (Claude is working)
893
+ if (this._state === 'ai_checking') {
894
+ this.log('Working patterns detected during AI check, cancelling');
895
+ this.aiChecker.cancel();
896
+ this.setState('watching');
897
+ }
898
+ // Cancel plan check if running (Claude started working)
899
+ if (this.planChecker.status === 'checking') {
900
+ this.log('Working patterns detected during plan check, cancelling');
901
+ this.planChecker.cancel();
902
+ }
903
+ // If we're monitoring init and work started, go to watching (no kickstart needed)
904
+ if (this._state === 'monitoring_init') {
905
+ this.log('/init triggered work, skipping kickstart');
906
+ this.emit('stepCompleted', 'init');
907
+ this.completeCycle();
908
+ }
909
+ return true;
910
+ }
911
+ _detectPrompt(data) {
892
912
  const hasPrompt = PROMPT_PATTERNS.some((pattern) => data.includes(pattern));
893
- if (hasPrompt) {
894
- this.promptDetected = true;
895
- this.workingDetected = false;
896
- // Handle legacy detection in waiting states - also use confirmation timers
897
- switch (this._state) {
898
- case 'waiting_update':
899
- this.startStepConfirmTimer('update');
900
- break;
901
- case 'waiting_clear':
902
- this.checkClearComplete(); // /clear is quick, no need to wait
903
- break;
904
- case 'waiting_init':
905
- this.startStepConfirmTimer('init');
906
- break;
907
- case 'monitoring_init':
908
- this.checkMonitoringInitIdle();
909
- break;
910
- case 'waiting_kickstart':
911
- this.startStepConfirmTimer('kickstart');
912
- break;
913
- // Non-waiting states: prompt detection is informational only
914
- case 'watching':
915
- case 'confirming_idle':
916
- case 'ai_checking':
917
- case 'sending_update':
918
- case 'sending_clear':
919
- case 'sending_init':
920
- case 'sending_kickstart':
921
- case 'stopped':
922
- // Prompt detection during these states doesn't trigger action
923
- break;
924
- default:
925
- assertNever(this._state, `Unhandled RespawnState in prompt detection: ${this._state}`);
926
- }
913
+ if (!hasPrompt)
914
+ return;
915
+ this.promptDetected = true;
916
+ this.workingDetected = false;
917
+ // Handle legacy detection in waiting states - also use confirmation timers
918
+ switch (this._state) {
919
+ case 'waiting_update':
920
+ this.startStepConfirmTimer('update');
921
+ break;
922
+ case 'waiting_clear':
923
+ this.checkClearComplete(); // /clear is quick, no need to wait
924
+ break;
925
+ case 'waiting_init':
926
+ this.startStepConfirmTimer('init');
927
+ break;
928
+ case 'monitoring_init':
929
+ this.checkMonitoringInitIdle();
930
+ break;
931
+ case 'waiting_kickstart':
932
+ this.startStepConfirmTimer('kickstart');
933
+ break;
934
+ // Non-waiting states: prompt detection is informational only
935
+ case 'watching':
936
+ case 'confirming_idle':
937
+ case 'ai_checking':
938
+ case 'sending_update':
939
+ case 'sending_clear':
940
+ case 'sending_init':
941
+ case 'sending_kickstart':
942
+ case 'stopped':
943
+ // Prompt detection during these states doesn't trigger action
944
+ break;
945
+ default:
946
+ assertNever(this._state, `Unhandled RespawnState in prompt detection: ${this._state}`);
927
947
  }
928
948
  }
929
949
  /**
@@ -1167,23 +1187,26 @@ export class RespawnController extends EventEmitter {
1167
1187
  case 'sending_init':
1168
1188
  case 'sending_kickstart':
1169
1189
  // For sending states, retry the send
1170
- this.log('Recovery: returning to watching state');
1171
- this.setState('watching');
1172
- this.startNoOutputTimer();
1173
- this.startPreFilterTimer();
1174
- if (this.config.autoAcceptPrompts) {
1175
- this.startAutoAcceptTimer();
1176
- }
1190
+ this.recoveryResetToWatching('returning to watching state');
1177
1191
  break;
1178
1192
  default:
1179
1193
  // Fallback: reset to watching
1180
- this.log('Recovery: fallback to watching state');
1181
- this.setState('watching');
1182
- this.startNoOutputTimer();
1183
- this.startPreFilterTimer();
1184
- if (this.config.autoAcceptPrompts) {
1185
- this.startAutoAcceptTimer();
1186
- }
1194
+ this.recoveryResetToWatching('fallback to watching state');
1195
+ }
1196
+ }
1197
+ /**
1198
+ * Reset the controller to watching state during stuck-state recovery.
1199
+ * Sets state to watching and restarts all detection timers.
1200
+ *
1201
+ * @param reason - Human-readable reason for the reset (logged)
1202
+ */
1203
+ recoveryResetToWatching(reason) {
1204
+ this.log(`Recovery: ${reason}`);
1205
+ this.setState('watching');
1206
+ this.startNoOutputTimer();
1207
+ this.startPreFilterTimer();
1208
+ if (this.config.autoAcceptPrompts) {
1209
+ this.startAutoAcceptTimer();
1187
1210
  }
1188
1211
  }
1189
1212
  /**
@@ -1372,7 +1395,7 @@ export class RespawnController extends EventEmitter {
1372
1395
  }
1373
1396
  // If on cooldown, don't start check - wait for cooldown to expire
1374
1397
  if (this.aiChecker.isOnCooldown()) {
1375
- this.log(`AI check on cooldown (${Math.ceil(this.aiChecker.getCooldownRemainingMs() / 1000)}s remaining), waiting...`);
1398
+ this.log(`AI check on cooldown (${formatRemainingSeconds(this.aiChecker.getCooldownRemainingMs())}s remaining), waiting...`);
1376
1399
  return;
1377
1400
  }
1378
1401
  // If already checking, don't start another
@@ -1448,7 +1471,7 @@ export class RespawnController extends EventEmitter {
1448
1471
  if (this._state === 'stopped')
1449
1472
  return; // Guard against stopped state
1450
1473
  if (this._state === 'ai_checking') {
1451
- const errorMsg = err instanceof Error ? err.message : String(err);
1474
+ const errorMsg = getErrorMessage(err);
1452
1475
  this.logAction('ai-check', `Failed: ${errorMsg.substring(0, 50)}`);
1453
1476
  this.emit('aiCheckFailed', errorMsg);
1454
1477
  this.setState('watching');
@@ -1500,33 +1523,13 @@ export class RespawnController extends EventEmitter {
1500
1523
  * @fires planCheckStarted
1501
1524
  */
1502
1525
  tryAutoAccept() {
1503
- // Only auto-accept in watching state (not during a respawn cycle)
1504
- if (this._state !== 'watching')
1526
+ if (!this.canAutoAccept())
1505
1527
  return;
1506
- // Don't auto-accept if a completion message was detected (normal idle handles it)
1507
- if (this.completionMessageTime !== null)
1508
- return;
1509
- // Don't auto-accept if disabled
1510
- if (!this.config.autoAcceptPrompts)
1511
- return;
1512
- // Don't auto-accept if we haven't received any output yet (prevents spurious Enter on fresh start)
1513
- if (!this.hasReceivedOutput)
1514
- return;
1515
- // Don't auto-accept if an elicitation dialog (AskUserQuestion) was detected
1516
- if (this.elicitationDetected) {
1517
- this.log('Skipping auto-accept: elicitation dialog detected (AskUserQuestion)');
1518
- return;
1519
- }
1520
- // Stage 1: Pre-filter — check if buffer looks like plan mode
1521
1528
  const buffer = this.terminalBuffer.value;
1522
- if (!this.isPlanModePreFilterMatch(buffer)) {
1523
- this.log('Skipping auto-accept: pre-filter did not match plan mode patterns');
1524
- return;
1525
- }
1526
1529
  // Stage 2: AI confirmation (if enabled and available)
1527
1530
  if (this.config.aiPlanCheckEnabled && this.planChecker.status !== 'disabled') {
1528
1531
  if (this.planChecker.isOnCooldown()) {
1529
- this.log(`Skipping auto-accept: plan checker on cooldown (${Math.ceil(this.planChecker.getCooldownRemainingMs() / 1000)}s remaining)`);
1532
+ this.log(`Skipping auto-accept: plan checker on cooldown (${formatRemainingSeconds(this.planChecker.getCooldownRemainingMs())}s remaining)`);
1530
1533
  return;
1531
1534
  }
1532
1535
  if (this.planChecker.status === 'checking') {
@@ -1540,6 +1543,37 @@ export class RespawnController extends EventEmitter {
1540
1543
  // AI plan check disabled — pre-filter passed, send Enter directly
1541
1544
  this.sendAutoAcceptEnter();
1542
1545
  }
1546
+ /**
1547
+ * Check whether all preconditions for auto-accept are met.
1548
+ * Validates state, config, and pre-filter conditions before attempting auto-accept.
1549
+ *
1550
+ * @returns True if auto-accept should proceed to the AI confirmation stage
1551
+ */
1552
+ canAutoAccept() {
1553
+ // Only auto-accept in watching state (not during a respawn cycle)
1554
+ if (this._state !== 'watching')
1555
+ return false;
1556
+ // Don't auto-accept if a completion message was detected (normal idle handles it)
1557
+ if (this.completionMessageTime !== null)
1558
+ return false;
1559
+ // Don't auto-accept if disabled
1560
+ if (!this.config.autoAcceptPrompts)
1561
+ return false;
1562
+ // Don't auto-accept if we haven't received any output yet (prevents spurious Enter on fresh start)
1563
+ if (!this.hasReceivedOutput)
1564
+ return false;
1565
+ // Don't auto-accept if an elicitation dialog (AskUserQuestion) was detected
1566
+ if (this.elicitationDetected) {
1567
+ this.log('Skipping auto-accept: elicitation dialog detected (AskUserQuestion)');
1568
+ return false;
1569
+ }
1570
+ // Stage 1: Pre-filter — check if buffer looks like plan mode
1571
+ if (!this.isPlanModePreFilterMatch(this.terminalBuffer.value)) {
1572
+ this.log('Skipping auto-accept: pre-filter did not match plan mode patterns');
1573
+ return false;
1574
+ }
1575
+ return true;
1576
+ }
1543
1577
  /**
1544
1578
  * Check if the terminal buffer matches plan mode pre-filter patterns.
1545
1579
  * Only checks the last 2000 chars (plan mode UI appears at the bottom).
@@ -1615,7 +1649,7 @@ export class RespawnController extends EventEmitter {
1615
1649
  }
1616
1650
  })
1617
1651
  .catch((err) => {
1618
- const errorMsg = err instanceof Error ? err.message : String(err);
1652
+ const errorMsg = getErrorMessage(err);
1619
1653
  this.emit('planCheckFailed', errorMsg);
1620
1654
  this.logAction('plan-check', `Failed: ${errorMsg.substring(0, 50)}`);
1621
1655
  });