verbalcoding 0.2.12 → 0.2.13

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 (169) hide show
  1. package/.env.example +74 -4
  2. package/README.es.md +3 -1
  3. package/README.fr.md +3 -1
  4. package/README.ja.md +3 -1
  5. package/README.ko.md +4 -2
  6. package/README.md +4 -2
  7. package/README.ru.md +3 -1
  8. package/README.zh.md +3 -1
  9. package/app-node/agent_adapters.test.mjs +14 -0
  10. package/app-node/agent_routing.mjs +148 -0
  11. package/app-node/agent_routing.test.mjs +138 -0
  12. package/app-node/agent_turn.mjs +86 -0
  13. package/app-node/agent_turn.test.mjs +109 -0
  14. package/app-node/bridge_context.mjs +73 -0
  15. package/app-node/bridge_context.test.mjs +54 -0
  16. package/app-node/bridge_state.mjs +4 -0
  17. package/app-node/bridge_wireup.test.mjs +462 -0
  18. package/app-node/cli_install.test.mjs +31 -0
  19. package/app-node/cross_agent_routing.test.mjs +78 -0
  20. package/app-node/discord_command_router.mjs +204 -0
  21. package/app-node/discord_command_router.test.mjs +311 -0
  22. package/app-node/discord_voice_setup.mjs +251 -0
  23. package/app-node/discord_voice_setup.test.mjs +86 -0
  24. package/app-node/hermes_profiles.test.mjs +12 -1
  25. package/app-node/install_config.mjs +110 -3
  26. package/app-node/install_config.test.mjs +8 -0
  27. package/app-node/instance_doctor.test.mjs +9 -0
  28. package/app-node/instances.test.mjs +8 -1
  29. package/app-node/main.mjs +488 -1368
  30. package/app-node/mcp_tools.test.mjs +7 -0
  31. package/app-node/notification_handler.mjs +89 -0
  32. package/app-node/notification_handler.test.mjs +187 -0
  33. package/app-node/plan_dispatcher.mjs +215 -0
  34. package/app-node/plan_dispatcher.test.mjs +101 -0
  35. package/app-node/plan_mode.mjs +36 -7
  36. package/app-node/plan_mode.test.mjs +78 -0
  37. package/app-node/progress_handler.mjs +220 -0
  38. package/app-node/progress_handler.test.mjs +193 -0
  39. package/app-node/progress_speech.mjs +54 -32
  40. package/app-node/progress_speech.test.mjs +12 -3
  41. package/app-node/project_sessions.mjs +5 -2
  42. package/app-node/project_sessions.test.mjs +7 -0
  43. package/app-node/research_mode.mjs +282 -0
  44. package/app-node/research_mode.test.mjs +264 -0
  45. package/app-node/restart_notice.mjs +3 -0
  46. package/app-node/restart_notice.test.mjs +11 -0
  47. package/app-node/session_ontology.mjs +271 -0
  48. package/app-node/session_ontology.test.mjs +130 -0
  49. package/app-node/smart_progress.mjs +1 -1
  50. package/app-node/stream_sentencer.mjs +32 -2
  51. package/app-node/stream_sentencer.test.mjs +65 -0
  52. package/app-node/streaming_tts_queue.mjs +5 -1
  53. package/app-node/streaming_tts_queue.test.mjs +7 -1
  54. package/app-node/stt_whisper.mjs +24 -0
  55. package/app-node/stt_whisper.test.mjs +32 -0
  56. package/app-node/text_routing.mjs +4 -2
  57. package/app-node/tts_backends.mjs +537 -3
  58. package/app-node/tts_backends.test.mjs +454 -0
  59. package/app-node/tts_player.mjs +164 -0
  60. package/app-node/tts_player.test.mjs +202 -0
  61. package/app-node/tts_runtime.mjs +134 -0
  62. package/app-node/tts_runtime.test.mjs +89 -0
  63. package/app-node/tts_settings.mjs +150 -3
  64. package/app-node/tts_settings.test.mjs +204 -0
  65. package/app-node/tts_voice_config.mjs +136 -2
  66. package/app-node/tts_voice_config.test.mjs +94 -0
  67. package/app-node/utterance_router.mjs +216 -0
  68. package/app-node/utterance_router.test.mjs +236 -0
  69. package/app-node/voice_autojoin.mjs +37 -0
  70. package/app-node/voice_autojoin.test.mjs +59 -0
  71. package/app-node/voice_io.mjs +272 -0
  72. package/app-node/voice_io.test.mjs +102 -0
  73. package/app-node/voice_turn_runner.mjs +449 -0
  74. package/app-node/voice_turn_runner.test.mjs +289 -0
  75. package/docs/CONFIGURATION.md +12 -2
  76. package/docs/HARNESSES.md +58 -0
  77. package/docs/HARNESS_AIDER.md +50 -0
  78. package/docs/HARNESS_CLAUDE.md +56 -0
  79. package/docs/HARNESS_CODEX.md +56 -0
  80. package/docs/HARNESS_CURSOR.md +45 -0
  81. package/docs/HARNESS_GEMINI.md +45 -0
  82. package/docs/HARNESS_HERMES.md +57 -0
  83. package/docs/HARNESS_OPENCLAW.md +44 -0
  84. package/docs/HARNESS_OPENCODE.md +44 -0
  85. package/docs/README.md +1 -0
  86. package/docs/ROADMAP.md +20 -5
  87. package/docs/TTS_BACKENDS.md +227 -0
  88. package/docs/USAGE.md +22 -0
  89. package/docs/i18n/AGENTS.es.md +34 -0
  90. package/docs/i18n/AGENTS.fr.md +34 -0
  91. package/docs/i18n/AGENTS.ja.md +34 -0
  92. package/docs/i18n/AGENTS.ko.md +34 -0
  93. package/docs/i18n/AGENTS.ru.md +34 -0
  94. package/docs/i18n/AGENTS.zh.md +34 -0
  95. package/docs/i18n/HARNESSES.es.md +58 -0
  96. package/docs/i18n/HARNESSES.fr.md +58 -0
  97. package/docs/i18n/HARNESSES.ja.md +58 -0
  98. package/docs/i18n/HARNESSES.ko.md +58 -0
  99. package/docs/i18n/HARNESSES.ru.md +58 -0
  100. package/docs/i18n/HARNESSES.zh.md +58 -0
  101. package/docs/i18n/HARNESS_AIDER.es.md +48 -0
  102. package/docs/i18n/HARNESS_AIDER.fr.md +48 -0
  103. package/docs/i18n/HARNESS_AIDER.ja.md +50 -0
  104. package/docs/i18n/HARNESS_AIDER.ko.md +50 -0
  105. package/docs/i18n/HARNESS_AIDER.ru.md +48 -0
  106. package/docs/i18n/HARNESS_AIDER.zh.md +48 -0
  107. package/docs/i18n/HARNESS_CLAUDE.es.md +55 -0
  108. package/docs/i18n/HARNESS_CLAUDE.fr.md +55 -0
  109. package/docs/i18n/HARNESS_CLAUDE.ja.md +56 -0
  110. package/docs/i18n/HARNESS_CLAUDE.ko.md +56 -0
  111. package/docs/i18n/HARNESS_CLAUDE.ru.md +55 -0
  112. package/docs/i18n/HARNESS_CLAUDE.zh.md +56 -0
  113. package/docs/i18n/HARNESS_CODEX.es.md +55 -0
  114. package/docs/i18n/HARNESS_CODEX.fr.md +55 -0
  115. package/docs/i18n/HARNESS_CODEX.ja.md +56 -0
  116. package/docs/i18n/HARNESS_CODEX.ko.md +56 -0
  117. package/docs/i18n/HARNESS_CODEX.ru.md +55 -0
  118. package/docs/i18n/HARNESS_CODEX.zh.md +56 -0
  119. package/docs/i18n/HARNESS_CURSOR.es.md +42 -0
  120. package/docs/i18n/HARNESS_CURSOR.fr.md +42 -0
  121. package/docs/i18n/HARNESS_CURSOR.ja.md +45 -0
  122. package/docs/i18n/HARNESS_CURSOR.ko.md +45 -0
  123. package/docs/i18n/HARNESS_CURSOR.ru.md +42 -0
  124. package/docs/i18n/HARNESS_CURSOR.zh.md +42 -0
  125. package/docs/i18n/HARNESS_GEMINI.es.md +44 -0
  126. package/docs/i18n/HARNESS_GEMINI.fr.md +44 -0
  127. package/docs/i18n/HARNESS_GEMINI.ja.md +45 -0
  128. package/docs/i18n/HARNESS_GEMINI.ko.md +45 -0
  129. package/docs/i18n/HARNESS_GEMINI.ru.md +44 -0
  130. package/docs/i18n/HARNESS_GEMINI.zh.md +45 -0
  131. package/docs/i18n/HARNESS_HERMES.es.md +54 -0
  132. package/docs/i18n/HARNESS_HERMES.fr.md +54 -0
  133. package/docs/i18n/HARNESS_HERMES.ja.md +57 -0
  134. package/docs/i18n/HARNESS_HERMES.ko.md +57 -0
  135. package/docs/i18n/HARNESS_HERMES.ru.md +54 -0
  136. package/docs/i18n/HARNESS_HERMES.zh.md +57 -0
  137. package/docs/i18n/HARNESS_OPENCLAW.es.md +41 -0
  138. package/docs/i18n/HARNESS_OPENCLAW.fr.md +41 -0
  139. package/docs/i18n/HARNESS_OPENCLAW.ja.md +44 -0
  140. package/docs/i18n/HARNESS_OPENCLAW.ko.md +44 -0
  141. package/docs/i18n/HARNESS_OPENCLAW.ru.md +41 -0
  142. package/docs/i18n/HARNESS_OPENCLAW.zh.md +42 -0
  143. package/docs/i18n/HARNESS_OPENCODE.es.md +41 -0
  144. package/docs/i18n/HARNESS_OPENCODE.fr.md +41 -0
  145. package/docs/i18n/HARNESS_OPENCODE.ja.md +44 -0
  146. package/docs/i18n/HARNESS_OPENCODE.ko.md +44 -0
  147. package/docs/i18n/HARNESS_OPENCODE.ru.md +41 -0
  148. package/docs/i18n/HARNESS_OPENCODE.zh.md +44 -0
  149. package/docs/superpowers/plans/2026-05-14-cross-agent-voice-transfer.md +625 -0
  150. package/docs/superpowers/plans/2026-05-21-audio-overview-narrated-diffs.md +95 -0
  151. package/docs/superpowers/plans/2026-05-21-autoresearch-ontology.md +83 -0
  152. package/docs/superpowers/plans/2026-05-21-phase11-push-to-talk-wakeword-v2.md +77 -0
  153. package/docs/superpowers/plans/2026-05-21-phase12-multi-user-voice.md +147 -0
  154. package/docs/superpowers/plans/2026-05-21-phase14-verbalbench.md +136 -0
  155. package/docs/superpowers/plans/2026-05-21-phase15-phone-companion.md +72 -0
  156. package/integrations/fireredtts2/mlx_llm.py +183 -0
  157. package/integrations/fireredtts2/synth.py +156 -0
  158. package/integrations/fireredtts2/synth_mlx.py +196 -0
  159. package/integrations/mlxaudio/synth.py +74 -0
  160. package/integrations/neuttsair/synth.py +104 -0
  161. package/integrations/omnivoice/synth.py +110 -0
  162. package/package.json +6 -1
  163. package/scripts/cli.mjs +84 -0
  164. package/scripts/doctor.mjs +104 -4
  165. package/scripts/install.mjs +5 -1
  166. package/scripts/install_fireredtts2.sh +109 -0
  167. package/scripts/install_mlxaudio.sh +34 -0
  168. package/scripts/install_mossttsnano.sh +46 -0
  169. package/scripts/postinstall.mjs +34 -0
package/.env.example CHANGED
@@ -8,6 +8,7 @@ DISCORD_CLIENT_ID=""
8
8
  DISCORD_ALLOWED_USERS=""
9
9
  AUTO_JOIN_VOICE_CHANNELS="일반,General,general"
10
10
  TRANSCRIPT_CHANNEL_ID=""
11
+ VOICE_CONNECT_TIMEOUT_MS="60000"
11
12
 
12
13
  # Agent harness: hermes, claude-code, claude, codex, gemini, opencode, openclaw, aider, cursor, custom
13
14
  # `vc setup` auto-detects which agents are installed and lets you pick.
@@ -20,9 +21,9 @@ AGENT_TASK_TIMEOUT_MS="0"
20
21
  AGENT_CHAT_TIMEOUT_MS="45000"
21
22
  AGENT_VERBOSE_PROGRESS="0" # default off; toggle in Discord with !verbose on/off
22
23
 
23
- # Streaming TTS pipeline: enables sentence-by-sentence playback while the agent is still writing.
24
- # First audio plays before the agent finishes. Default off; opt in once you've validated TTS latency.
25
- STREAMING_TTS="0"
24
+ # Streaming TTS pipeline: sentence-by-sentence playback while the agent is still writing.
25
+ # First audio plays before the agent finishes. Set to "0" to fall back to whole-reply playback.
26
+ STREAMING_TTS="1"
26
27
 
27
28
  # Smart progress summarization. When SMART_PROGRESS_API_KEY is set, raw progress events get
28
29
  # folded into a single human sentence via a small LLM (Groq OpenAI-compatible API by default).
@@ -50,7 +51,7 @@ VOICE_LANGUAGE="ko" # ko | en | auto; controls progress/status language
50
51
  WHISPER_CPP_LANGUAGE="ko" # ko | en | auto; auto omits forced whisper language
51
52
  STT_LANGUAGE="ko"
52
53
 
53
- TTS_BACKEND="edge" # edge | openvoice | speechswift | supertonic
54
+ TTS_BACKEND="edge" # edge | openvoice | speechswift | supertonic | omnivoice | qwen3tts | mlxaudio | fireredtts2 | mossttsnano | neuttsair
54
55
  EDGE_TTS_COMMAND="edge-tts"
55
56
  TTS_VOICE_TYPE="korean_female" # edge: korean_male | korean_female | korean_multilingual_male | english_male | english_female
56
57
  TTS_VOICE="ko-KR-SunHiNeural"
@@ -92,6 +93,33 @@ OPENVOICE_LANGUAGE="KR"
92
93
  OPENVOICE_STYLE="default"
93
94
  OPENVOICE_TIMEOUT_MS="90000"
94
95
  OPENVOICE_PROGRESS="0" # keep progress prompts fast via Edge unless set to 1
96
+
97
+ # Optional k2-fsa/OmniVoice TTS backend (600+ language zero-shot TTS / voice cloning).
98
+ # Recommended: create a separate env, install torch/torchaudio/soundfile/omnivoice, and keep progress prompts on Edge.
99
+ OMNIVOICE_PYTHON="./.venv-omnivoice/bin/python"
100
+ OMNIVOICE_MODEL="k2-fsa/OmniVoice"
101
+ OMNIVOICE_DEVICE="mps" # mps on Apple Silicon, cuda:0 on NVIDIA, cpu as fallback
102
+ OMNIVOICE_DTYPE="float16"
103
+ OMNIVOICE_REF_AUDIO="./voice-samples/user-reference.wav"
104
+ OMNIVOICE_REF_TEXT="" # optional transcript of the reference sample
105
+ OMNIVOICE_LANGUAGE="ko"
106
+ OMNIVOICE_SPEAKER="" # optional voice-design attributes when no ref sample is desired
107
+ OMNIVOICE_TIMEOUT_MS="180000"
108
+ OMNIVOICE_PROGRESS="0" # keep progress prompts fast via Edge unless set to 1
109
+
110
+ # Optional Qwen3-TTS CLI backend via speech-swift `audio speak`.
111
+ # Install speech-swift/audio separately, then set TTS_BACKEND="qwen3tts" or "qwen3".
112
+ QWEN3TTS_COMMAND="audio"
113
+ QWEN3TTS_MODE="custom" # custom | clone | design
114
+ QWEN3TTS_MODEL="customVoice" # customVoice for preset speakers, base/base-8bit for cloning
115
+ QWEN3TTS_LANGUAGE="korean"
116
+ QWEN3TTS_SPEAKER="sohee" # used in custom mode
117
+ QWEN3TTS_INSTRUCT="" # emotion/style instruction; used in custom/design modes
118
+ QWEN3TTS_REF_AUDIO="./voice-samples/user-reference.wav" # used in clone mode
119
+ QWEN3TTS_REF_TEXT="" # optional note for your reference sample
120
+ QWEN3TTS_STREAM="1"
121
+ QWEN3TTS_TIMEOUT_MS="120000"
122
+ QWEN3TTS_PROGRESS="0" # keep progress prompts fast via Edge unless set to 1
95
123
  REQUIRE_WAKE_WORD="0"
96
124
  MIN_UTTERANCE_SECONDS="1.0"
97
125
  # Wait for natural thinking pauses before STT. Lower for faster but more fragmented turns.
@@ -109,3 +137,45 @@ BARGE_IN_CONSERVATIVE_MIN_SECONDS="1.8"
109
137
  BARGE_IN_CONSERVATIVE_MIN_MEAN_VOLUME_DB="-27"
110
138
  BARGE_IN_CONSERVATIVE_MIN_MAX_VOLUME_DB="-12"
111
139
  MAX_DEFERRED_PROCESSING_UTTERANCES="0"
140
+
141
+
142
+ # Optional local TTS backends (final answers only by default; progress falls back to Edge)
143
+ # TTS_BACKEND=fireredtts2
144
+ FIREREDTTS2_COMMAND=./.local/bin/fireredtts2
145
+ FIREREDTTS2_PRETRAINED_DIR=pretrained_models/FireRedTTS2
146
+ FIREREDTTS2_DEVICE=auto
147
+ FIREREDTTS2_GEN_TYPE=monologue
148
+ FIREREDTTS2_SPEAKER=S1
149
+ FIREREDTTS2_PROMPT_AUDIO=voice-samples/user-reference.wav
150
+ FIREREDTTS2_PROMPT_TEXT=
151
+ FIREREDTTS2_BF16=0
152
+ FIREREDTTS2_TIMEOUT_MS=180000
153
+ FIREREDTTS2_PROGRESS=0
154
+
155
+ # TTS_BACKEND=mossttsnano
156
+ MOSSTTSNANO_COMMAND=python3
157
+ MOSSTTSNANO_SCRIPT=vendor/MOSS-TTS-Nano/infer.py
158
+ MOSSTTSNANO_CHECKPOINT=OpenMOSS-Team/MOSS-TTS-Nano
159
+ MOSSTTSNANO_AUDIO_TOKENIZER=
160
+ MOSSTTSNANO_MODE=continuation
161
+ MOSSTTSNANO_DEVICE=auto
162
+ MOSSTTSNANO_DTYPE=auto
163
+ MOSSTTSNANO_PROMPT_AUDIO=voice-samples/user-reference.wav
164
+ MOSSTTSNANO_PROMPT_TEXT=
165
+ MOSSTTSNANO_MAX_NEW_FRAMES=375
166
+ MOSSTTSNANO_TIMEOUT_MS=120000
167
+ MOSSTTSNANO_PROGRESS=0
168
+
169
+ # TTS_BACKEND=neuttsair # NeuTTS Air is English-only; progress falls back to Edge by default.
170
+ NEUTTSAIR_PYTHON=./.venv-neuttsair/bin/python
171
+ NEUTTSAIR_SCRIPT=integrations/neuttsair/synth.py
172
+ NEUTTSAIR_BACKBONE_REPO=neuphonic/neutts-air-q4-gguf
173
+ NEUTTSAIR_BACKBONE_DEVICE=mps
174
+ NEUTTSAIR_CODEC_REPO=neuphonic/neucodec
175
+ NEUTTSAIR_CODEC_DEVICE=mps
176
+ NEUTTSAIR_REF_AUDIO=voice-samples/user-reference.wav
177
+ NEUTTSAIR_REF_TEXT=
178
+ NEUTTSAIR_LANGUAGE=en
179
+ NEUTTSAIR_SAMPLE_RATE=24000
180
+ NEUTTSAIR_TIMEOUT_MS=120000
181
+ NEUTTSAIR_PROGRESS=0
package/README.es.md CHANGED
@@ -30,7 +30,8 @@ VerbalCoding convierte una sala de voz de Discord en una cabina manos libres par
30
30
  | Flujo tipo llamada | Habla, escucha, interrumpe y continúa en el mismo canal de voz de Discord. |
31
31
  | Configuración guiada | `vc setup` reúne prerequisites, Discord token/client ID, voice channel, transcript target, backend y TTS settings en un solo flujo. |
32
32
  | Bucle de voz local | Discord audio → local `whisper-cli` → selected CLI agent → TTS reply. |
33
- | Elección de agente | Hermes Agent, Claude Code, Codex, Gemini CLI, OpenCode, OpenClaw o custom command. |
33
+ | Elección de agente | Hermes Agent, Claude Code, Codex, Gemini CLI, OpenCode, OpenClaw, Aider, Cursor CLI o custom command. `vc setup` autodetecta lo que tienes instalado. |
34
+ | Enrutamiento de agente por voz | `"ask Codex what it thinks"` (un turno), `"switch to Aider"` (sticky), `"back to default"` para volver. Si el binario no está instalado, el puente ofrece fallback al agente por defecto. |
34
35
  | Más allá de la voz integrada de Hermes | Mantiene el mismo bucle de voz en VC y añade salas de proyecto, contexto compartido con `!ask`, interrupciones afinadas, voz de progreso/estado y control de backends multiagente. |
35
36
  | Operación real | Incluye doctor auto-fix, guía Docker UDP, latency metrics, multi-instance rooms y redacted config checks. |
36
37
 
@@ -90,6 +91,7 @@ vc instance start NAME # ejecutar ese bot en background
90
91
  | [Centro de documentación](docs/i18n/README.es.md) | Índice de guías localizadas. |
91
92
  | [Fresh Install](docs/i18n/FRESH_INSTALL.es.md) | npm/global setup, configuración de Discord y primera ejecución. |
92
93
  | [Usage](docs/i18n/USAGE.es.md) | Comandos CLI, comandos Discord, modos de ejecución y latency. |
94
+ | [Uso por harness](docs/i18n/HARNESSES.es.md) | Instalación, configuración y enrutamiento por voz para Claude Code, Codex, Aider y demás. |
93
95
  | [Voz integrada de Hermes vs VerbalCoding](docs/i18n/HERMES_VOICE.es.md) | La voz Discord que Hermes ya ofrece y la diferencia de VerbalCoding. |
94
96
  | [Configuration](docs/i18n/CONFIGURATION.es.md) | .env, agent backends, MCP, TTS y operación. |
95
97
  | [Troubleshooting](docs/i18n/TROUBLESHOOTING.es.md) | Docker UDP y comprobaciones de token/channel. |
package/README.fr.md CHANGED
@@ -30,7 +30,8 @@ VerbalCoding transforme un salon vocal Discord en poste de pilotage mains libres
30
30
  | Flux type appel | Parler, écouter, interrompre et continuer dans le même salon vocal Discord. |
31
31
  | Configuration guidée | `vc setup` couvre prerequisites, Discord token/client ID, voice channel, transcript target, backend et TTS settings en un seul flux. |
32
32
  | Boucle vocale locale | Discord audio → local `whisper-cli` → selected CLI agent → TTS reply. |
33
- | Choix de l’agent | Hermes Agent, Claude Code, Codex, Gemini CLI, OpenCode, OpenClaw ou custom command. |
33
+ | Choix de l’agent | Hermes Agent, Claude Code, Codex, Gemini CLI, OpenCode, OpenClaw, Aider, Cursor CLI ou custom command. `vc setup` détecte automatiquement ce qui est installé. |
34
+ | Routage d’agent par voix | `"ask Codex what it thinks"` pour un tour, `"switch to Aider"` en sticky, `"back to default"` pour revenir. Les binaires absents sont détectés et le pont propose un fallback vers l’agent par défaut. |
34
35
  | Au-delà de la voix intégrée de Hermes | Garde la même boucle vocale VC, puis ajoute salons de projet, contexte partagé `!ask`, interruptions réglées, annonces progression/état et contrôle de backends multiagents. |
35
36
  | Exploitation réelle | doctor auto-fix, guide Docker UDP, latency metrics, multi-instance rooms et redacted config checks inclus. |
36
37
 
@@ -90,6 +91,7 @@ vc instance start NAME # exécuter ce bot en background
90
91
  | [Centre de documentation](docs/i18n/README.fr.md) | Index des guides localisés. |
91
92
  | [Fresh Install](docs/i18n/FRESH_INSTALL.fr.md) | npm/global setup, configuration Discord, premier lancement. |
92
93
  | [Usage](docs/i18n/USAGE.fr.md) | Commandes CLI, commandes Discord, modes d’exécution, latency. |
94
+ | [Usage par harness](docs/i18n/HARNESSES.fr.md) | Installation, configuration et routage vocal pour Claude Code, Codex, Aider et les autres. |
93
95
  | [Voix intégrée Hermes vs VerbalCoding](docs/i18n/HERMES_VOICE.fr.md) | La voix Discord déjà fournie par Hermes et la différence VerbalCoding. |
94
96
  | [Configuration](docs/i18n/CONFIGURATION.fr.md) | .env, agent backends, MCP, TTS, exploitation. |
95
97
  | [Troubleshooting](docs/i18n/TROUBLESHOOTING.fr.md) | Docker UDP et vérifications token/channel. |
package/README.ja.md CHANGED
@@ -30,7 +30,8 @@ VerbalCoding は Discord の音声ルームを、コーディングエージェ
30
30
  | 電話のような流れ | 同じ Discord 音声チャンネルで話す、聞く、割り込む、続けるができます。 |
31
31
  | 人向けのガイド付き設定 | `vc setup` が prerequisites、Discord token/client ID、voice channel、transcript target、backend、TTS 設定を一連の流れで確認します。 |
32
32
  | ローカル音声ループ | Discord audio → local `whisper-cli` → selected CLI agent → TTS response。 |
33
- | エージェント選択 | Hermes Agent、Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw、custom command に対応します。 |
33
+ | エージェント選択 | Hermes Agent、Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw、Aider、Cursor CLI または custom command に対応します。`vc setup` がインストール済みのものを自動検出します。 |
34
+ | 音声でエージェントを切替 | `"ask Codex what it thinks"` で 1 ターンのみ、`"switch to Aider"` で sticky、`"back to default"` で復帰。未インストールのバイナリを検出して既定エージェントへの fallback を提案します。 |
34
35
  | Hermes 標準音声の先 | 同じ VC 音声ループを土台に、プロジェクトルーム、`!ask` 共有コンテキスト、細かな割り込み処理、進捗/状態の音声案内、複数エージェントバックエンド制御を追加します。 |
35
36
  | 運用向け機能 | doctor auto-fix、Docker UDP ガイド、latency metrics、multi-instance rooms、redacted config checks を備えています。 |
36
37
 
@@ -90,6 +91,7 @@ vc instance start NAME # その bot を background で実行
90
91
  | [ドキュメントハブ](docs/i18n/README.ja.md) | ローカライズ済みガイドの索引。 |
91
92
  | [Fresh Install](docs/i18n/FRESH_INSTALL.ja.md) | npm/global setup、Discord 設定、初回起動。 |
92
93
  | [Usage](docs/i18n/USAGE.ja.md) | CLI コマンド、Discord コマンド、実行モード、latency。 |
94
+ | [Harness 別の使い方](docs/i18n/HARNESSES.ja.md) | Claude Code、Codex、Aider などバックエンド別のインストール・設定・音声ルーティング。 |
93
95
  | [Hermes 標準音声 vs VerbalCoding](docs/i18n/HERMES_VOICE.ja.md) | Hermes がすでに提供する Discord 音声と VerbalCoding の違い。 |
94
96
  | [Configuration](docs/i18n/CONFIGURATION.ja.md) | .env、agent backends、MCP、TTS、運用。 |
95
97
  | [Troubleshooting](docs/i18n/TROUBLESHOOTING.ja.md) | Docker UDP、token/channel 不足チェック。 |
package/README.ko.md CHANGED
@@ -30,7 +30,8 @@ VerbalCoding은 Discord 음성 방을 코딩 에이전트용 핸즈프리 조종
30
30
  | 통화 같은 작업 흐름 | 한 Discord 음성 채널에서 말하고, 듣고, 끼어들고, 이어서 작업합니다. |
31
31
  | 안내형 사람용 설정 | `vc setup`이 prerequisites, Discord token/client ID, voice channel, transcript target, backend, TTS 설정을 한 흐름으로 묻습니다. |
32
32
  | 로컬 음성 루프 | Discord audio → local `whisper-cli` → selected CLI agent → TTS 답변. |
33
- | 에이전트 선택 | Hermes Agent, Claude Code, Codex, Gemini CLI, OpenCode, OpenClaw, custom command를 지원합니다. |
33
+ | 에이전트 선택 | Hermes Agent, Claude Code, Codex, Gemini CLI, OpenCode, OpenClaw, Aider, Cursor CLI 또는 custom command를 지원합니다. `vc setup`이 설치된 것을 자동 감지해요. |
34
+ | 음성으로 에이전트 라우팅 | `"코덱스한테 물어봐"`로 한 턴만 보내거나 `"aider로 전환"`으로 sticky 전환. `"기본으로 돌아가"`로 복귀. 없는 바이너리는 감지해서 기본 에이전트로 fallback할지 물어봐. |
34
35
  | Hermes 기본 음성 너머 | 같은 VC 음성 루프를 기반으로 프로젝트 방, `!ask` 공유 컨텍스트, 세밀한 끼어들기 처리, 진행/상태 음성 안내, 다중 에이전트 백엔드 제어를 더합니다. |
35
36
  | 운영 친화 기능 | doctor auto-fix, Docker UDP 안내, latency metrics, multi-instance rooms, redacted config checks가 포함됩니다. |
36
37
 
@@ -90,6 +91,7 @@ vc instance start NAME # 해당 bot을 background로 실행
90
91
  | [문서 허브](docs/i18n/README.ko.md) | 현지화된 가이드 색인. |
91
92
  | [Fresh Install](docs/i18n/FRESH_INSTALL.ko.md) | npm/global setup, Discord 설정, 첫 실행. |
92
93
  | [Usage](docs/i18n/USAGE.ko.md) | CLI 명령, Discord 명령, 실행 모드, latency. |
94
+ | [하니스 사용법](docs/i18n/HARNESSES.ko.md) | Claude Code, Codex, Aider 등 백엔드별 설치·설정·음성 라우팅. |
93
95
  | [Hermes 기본 음성 vs VerbalCoding](docs/i18n/HERMES_VOICE.ko.md) | Hermes가 이미 지원하는 Discord 음성과 VerbalCoding의 차이. |
94
96
  | [Configuration](docs/i18n/CONFIGURATION.ko.md) | .env, agent backends, MCP, TTS, 운영. |
95
97
  | [Troubleshooting](docs/i18n/TROUBLESHOOTING.ko.md) | Docker UDP, token/channel 누락 점검. |
@@ -101,7 +103,7 @@ vc instance start NAME # 해당 bot을 background로 실행
101
103
  |---|---|
102
104
  | Runtime | Node.js 20+와 npm. |
103
105
  | Audio | `ffmpeg`와 local `whisper-cli`. |
104
- | TTS | 기본 Edge TTS, 선택 OpenVoice, SpeechSwift/CosyVoice, Supertonic. |
106
+ | TTS | 기본 Edge TTS, 선택 OpenVoice, SpeechSwift/CosyVoice, Supertonic, OmniVoice, Qwen3 TTS CLI. |
105
107
  | Discord | Bot token, Message Content intent, voice permissions, 일치하는 channel names. |
106
108
  | Agent | 인증된 CLI harness 하나 이상, 기본은 Hermes Agent. |
107
109
 
package/README.md CHANGED
@@ -34,6 +34,7 @@ VerbalCoding turns a Discord voice channel into a hands-free cockpit for **any**
34
34
  - **Streaming pipeline** — first sentence plays while the agent is still writing (Hermes lists this as a future Phase-4 item).
35
35
  - **Smart progress narration** — describes intent ("wiring the new login route"), not file lists.
36
36
  - **Voice plan mode** — say "plan it first", edit by voice ("skip step 3"), say "approve" to execute.
37
+ - **Cross-agent routing by voice** — "ask Codex what it thinks" for a single turn, "switch to Aider" to make it sticky, "back to default" to restore. The plan can also emit a `which_agent` slot so the agent itself picks the next backend.
37
38
  - **Phone-down mode** — push notification with a voice summary when a long task completes and the room is empty.
38
39
 
39
40
  ## What feels different
@@ -41,8 +42,9 @@ VerbalCoding turns a Discord voice channel into a hands-free cockpit for **any**
41
42
  | Capability | Why it matters |
42
43
  |---|---|
43
44
  | Agent choice, first-class | Hermes Agent, Claude Code, Codex, Gemini CLI, OpenCode, OpenClaw, Aider, Cursor CLI, or any custom command. `vc setup` auto-detects what's installed. |
45
+ | Cross-agent voice routing | Say "ask Codex …" (single turn), "switch to Aider" (sticky), or "back to default". Missing binaries are detected and the bridge offers to fall back to the default agent. Handoff prompts carry recent utterances + last plan decisions to the new agent. |
44
46
  | Real barge-in | VAD thresholds tuned for indoor and noisy rooms; cut in mid-utterance and resume the conversation. |
45
- | Streaming end-to-end | `STREAMING_TTS=1` plays sentences as the agent produces them; first audio in well under a second on a warm cache. |
47
+ | Streaming end-to-end | Sentence-by-sentence playback while the agent is still writing; first audio in well under a second on a warm cache. On by default — set `STREAMING_TTS=0` to fall back to whole-reply playback. |
46
48
  | Smart progress | Optional LLM summarizer collapses raw events into one human sentence; falls back to the existing regex labels when no key is set. |
47
49
  | Plan-mode by voice | Narrated, editable, voice-driven plans without touching the keyboard. |
48
50
  | Phone-down handoff | Long task + empty VC = push notification (`ntfy`/`pushover`) with a redacted one-line summary and tap-to-rejoin link. |
@@ -156,7 +158,7 @@ The differentiation push is tracked in [docs/ROADMAP.md](./docs/ROADMAP.md). Fiv
156
158
  | Runtime | Node.js 20+ and npm; setup can install via Homebrew/apt/dnf/pacman where supported. |
157
159
  | Audio | `ffmpeg`; setup/doctor can install it on supported OSes. |
158
160
  | Speech recognition | Local `whisper-cli` from whisper.cpp plus `models/ggml-small-q5_1.bin`. |
159
- | TTS | Edge TTS by default; optional OpenVoice, SpeechSwift/CosyVoice, and Supertonic paths. |
161
+ | TTS | Edge TTS by default; optional OpenVoice, SpeechSwift/CosyVoice, Supertonic, OmniVoice, and Qwen3 TTS CLI paths. |
160
162
  | Discord | Bot token, Message Content intent, voice permissions, matching auto-join channel names. |
161
163
  | Agent | At least one CLI harness installed; `vc setup` auto-detects Hermes, Claude Code, Codex, Gemini, OpenCode, OpenClaw, Aider, Cursor CLI. |
162
164
  | Platform focus | macOS / Apple Silicon most tested; Linux bootstrap is best-effort; Windows unsupported for now. |
package/README.ru.md CHANGED
@@ -30,7 +30,8 @@ VerbalCoding превращает голосовую комнату Discord в h
30
30
  | Работа как звонок | Говорите, слушайте, перебивайте и продолжайте в одном голосовом канале Discord. |
31
31
  | Пошаговая настройка | `vc setup` проводит через prerequisites, Discord token/client ID, voice channel, transcript target, backend и TTS settings за один проход. |
32
32
  | Локальный голосовой цикл | Discord audio → local `whisper-cli` → selected CLI agent → TTS reply. |
33
- | Выбор агента | Hermes Agent, Claude Code, Codex, Gemini CLI, OpenCode, OpenClaw или custom command. |
33
+ | Выбор агента | Hermes Agent, Claude Code, Codex, Gemini CLI, OpenCode, OpenClaw, Aider, Cursor CLI или custom command. `vc setup` автоматически находит установленные. |
34
+ | Голосовая маршрутизация агента | `"ask Codex what it thinks"` — на один turn, `"switch to Aider"` — sticky, `"back to default"` — возврат. Отсутствующие бинарники определяются и мост предлагает fallback к агенту по умолчанию. |
34
35
  | Больше, чем встроенный голос Hermes | Сохраняет тот же VC-голосовой цикл и добавляет проектные комнаты, общий контекст `!ask`, тонкую обработку прерываний, голос прогресса/статуса и управление multi-agent бэкендами. |
35
36
  | Готовность к эксплуатации | doctor auto-fix, Docker UDP guide, latency metrics, multi-instance rooms и redacted config checks встроены. |
36
37
 
@@ -90,6 +91,7 @@ vc instance start NAME # запустить этот bot в ba
90
91
  | [Центр документации](docs/i18n/README.ru.md) | Индекс локализованных гайдов. |
91
92
  | [Fresh Install](docs/i18n/FRESH_INSTALL.ru.md) | npm/global setup, настройка Discord и первый запуск. |
92
93
  | [Usage](docs/i18n/USAGE.ru.md) | CLI-команды, Discord-команды, режимы запуска и latency. |
94
+ | [Использование по harness](docs/i18n/HARNESSES.ru.md) | Установка, настройка и голосовая маршрутизация для Claude Code, Codex, Aider и других. |
93
95
  | [Встроенный голос Hermes vs VerbalCoding](docs/i18n/HERMES_VOICE.ru.md) | Что Hermes уже умеет в Discord voice и чем отличается VerbalCoding. |
94
96
  | [Configuration](docs/i18n/CONFIGURATION.ru.md) | .env, agent backends, MCP, TTS и эксплуатация. |
95
97
  | [Troubleshooting](docs/i18n/TROUBLESHOOTING.ru.md) | Docker UDP и проверки token/channel. |
package/README.zh.md CHANGED
@@ -30,7 +30,8 @@ VerbalCoding 把 Discord 语音房间变成编码代理的免提驾驶舱。你
30
30
  | 电话式工作流 | 在同一个 Discord 语音频道里说话、收听、打断、继续。 |
31
31
  | 面向人的引导设置 | `vc setup` 一次引导 prerequisites、Discord token/client ID、voice channel、transcript target、backend 和 TTS 设置。 |
32
32
  | 本地语音闭环 | Discord audio → local `whisper-cli` → selected CLI agent → TTS reply。 |
33
- | 可选代理 | 支持 Hermes Agent、Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw 或 custom command |
33
+ | 可选代理 | 支持 Hermes Agent、Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw、Aider、Cursor CLI 或 custom command。`vc setup` 自动检测已安装项。 |
34
+ | 语音切换代理 | `"ask Codex what it thinks"` 单 turn 路由,`"switch to Aider"` 粘性切换,`"back to default"` 回默认。未安装的二进制会被检测并询问是否回退到默认代理。 |
34
35
  | 超越 Hermes 内置语音 | 在同一个 VC 语音闭环上增加项目房间、`!ask` 共享上下文、细粒度打断处理、进度/状态语音和多代理后端控制。 |
35
36
  | 真实运维支持 | 内置 doctor auto-fix、Docker UDP 指南、latency metrics、multi-instance rooms 和 redacted config checks。 |
36
37
 
@@ -90,6 +91,7 @@ vc instance start NAME # 后台运行该 bot
90
91
  | [文档中心](docs/i18n/README.zh.md) | 本地化指南索引。 |
91
92
  | [Fresh Install](docs/i18n/FRESH_INSTALL.zh.md) | npm/global setup、Discord 设置、首次运行。 |
92
93
  | [Usage](docs/i18n/USAGE.zh.md) | CLI 命令、Discord 命令、运行模式、latency。 |
94
+ | [Harness 使用](docs/i18n/HARNESSES.zh.md) | Claude Code、Codex、Aider 等各后端的安装、配置与语音路由。 |
93
95
  | [Configuration](docs/i18n/CONFIGURATION.zh.md) | .env、agent backends、MCP、TTS、运维。 |
94
96
  | [Troubleshooting](docs/i18n/TROUBLESHOOTING.zh.md) | Docker UDP、token/channel 缺失检查。 |
95
97
  | [Multi-Instance](docs/i18n/MULTI_INSTANCE.zh.md) | 每个项目一个固定语音房间。 |
@@ -456,6 +456,20 @@ test('signal failure with patch-like output returns a concise interruption messa
456
456
  assert.doesNotMatch(answer, /@@|review diff|old|new/);
457
457
  });
458
458
 
459
+ test('createAgentAdapter satisfies the agent adapter contract for every known backend', () => {
460
+ const backends = ['hermes', 'claude', 'codex', 'gemini', 'opencode', 'openclaw', 'aider', 'cursor'];
461
+ for (const backend of backends) {
462
+ const settings = buildAgentSettings({ ROOT: '/tmp/vc-test', env: { AGENT_BACKEND: backend } });
463
+ const adapter = createAgentAdapter(settings, {
464
+ execFileAsync: async () => ({ stdout: '', stderr: '' }),
465
+ log: () => {},
466
+ warn: () => {},
467
+ });
468
+ assert.doesNotThrow(() => assertAgentAdapterContract(adapter), `${backend} should satisfy contract`);
469
+ assert.equal(adapter.backend, backend);
470
+ }
471
+ });
472
+
459
473
  test('hermes adapter spawn carries HERMES_HOME from instance env into child env', async () => {
460
474
  const { buildHermesSpawnOptions } = await import('./agent_adapters.mjs');
461
475
  const opts = buildHermesSpawnOptions({
@@ -0,0 +1,148 @@
1
+ const BACKEND_ALIASES = {
2
+ hermes: ['hermes', '헤르메스'],
3
+ claude: ['claude code', 'claude-code', 'claude'],
4
+ codex: ['codex', '코덱스'],
5
+ gemini: ['gemini cli', 'gemini-cli', 'gemini', '제미나이'],
6
+ opencode: ['opencode', 'open code'],
7
+ openclaw: ['openclaw', 'open claw'],
8
+ aider: ['aider', '에이더'],
9
+ cursor: ['cursor cli', 'cursor-cli', 'cursor agent', 'cursor-agent', 'cursor'],
10
+ };
11
+
12
+ const BACKEND_LOOKUP = (() => {
13
+ const pairs = [];
14
+ for (const [backend, aliases] of Object.entries(BACKEND_ALIASES)) {
15
+ for (const alias of aliases) pairs.push([alias.toLowerCase(), backend]);
16
+ }
17
+ pairs.sort((a, b) => b[0].length - a[0].length);
18
+ return pairs;
19
+ })();
20
+
21
+ const BACKEND_LABELS = {
22
+ hermes: { en: 'Hermes', ko: '헤르메스' },
23
+ claude: { en: 'Claude Code', ko: 'Claude Code' },
24
+ codex: { en: 'Codex', ko: '코덱스' },
25
+ gemini: { en: 'Gemini', ko: 'Gemini' },
26
+ opencode: { en: 'OpenCode', ko: 'OpenCode' },
27
+ openclaw: { en: 'OpenClaw', ko: 'OpenClaw' },
28
+ aider: { en: 'Aider', ko: 'Aider' },
29
+ cursor: { en: 'Cursor CLI', ko: 'Cursor CLI' },
30
+ };
31
+
32
+ const ROUTING_SLOT_NAMES = new Set(['which_agent', 'agent', 'who_answers', 'router_agent']);
33
+
34
+ const ASK_EN = /\bask\s+([a-z][a-z0-9 \-]{1,30}?)(?:\s+(?:to|what|if|whether)\b|[?,.]|$)/i;
35
+ const SWITCH_EN = /\bswitch\s+to\s+([a-z][a-z0-9 \-]{1,30}?)(?:[?,.]|$)/i;
36
+ const LET_FINISH_EN = /\blet\s+([a-z][a-z0-9 \-]{1,30}?)\s+(?:finish|handle|do)\b/i;
37
+ const RESTORE_EN = /\b(back\s+to\s+default|use\s+the\s+default\s+agent|default\s+agent)\b/i;
38
+
39
+ const ASK_KO = /([가-힣A-Za-z][가-힣A-Za-z0-9\-]{1,30})(?:한테|에게|에)\s*(물어|질문)/;
40
+ const SWITCH_KO = /([가-힣A-Za-z][가-힣A-Za-z0-9\-]{1,30})(?:로|으로)\s*(전환|바꿔|바꿔줘)/;
41
+ const RESTORE_KO = /(기본(?:으로)?\s*(?:돌아|복귀)|기본\s*에이전트)/;
42
+
43
+ export function isRoutingOnlyUtterance(text) {
44
+ const t = String(text || '').trim();
45
+ if (!t) return false;
46
+ const normalized = t.toLowerCase().replace(/[.,!?]+$/u, '').trim();
47
+ if (/^(?:please\s+)?(?:back\s+to\s+default|use\s+the\s+default\s+agent|default\s+agent)$/i.test(normalized)) return true;
48
+ if (/^기본(?:으로)?\s*(?:돌아(?:가|가줘)?|복귀)$/.test(normalized)) return true;
49
+ const en = normalized.match(/^(?:please\s+)?(?:switch\s+to|use)\s+(.+)$/i);
50
+ if (en) return resolveBackendAlias(en[1], { strict: true }) !== null;
51
+ const ko = normalized.match(/^(.+?)(?:로|으로)\s*(?:전환|바꿔|바꿔줘)$/);
52
+ if (ko) return resolveBackendAlias(ko[1], { strict: true }) !== null;
53
+ return false;
54
+ }
55
+
56
+ export function resolveBackendAlias(rawName, { strict = false } = {}) {
57
+ const needle = String(rawName || '').toLowerCase().trim();
58
+ if (!needle) return null;
59
+ for (const [alias, backend] of BACKEND_LOOKUP) {
60
+ if (needle === alias) return backend;
61
+ }
62
+ if (strict) return null;
63
+ for (const [alias, backend] of BACKEND_LOOKUP) {
64
+ if (needle.includes(alias)) return backend;
65
+ }
66
+ return null;
67
+ }
68
+
69
+ export function parseAgentRoutingCommand(text, language = 'en') {
70
+ const t = String(text || '').trim();
71
+ if (!t) return { type: 'none' };
72
+ if (RESTORE_EN.test(t) || RESTORE_KO.test(t)) return { type: 'restore' };
73
+ const switchMatch = t.match(SWITCH_EN) || t.match(LET_FINISH_EN);
74
+ if (switchMatch) {
75
+ const backend = resolveBackendAlias(switchMatch[1]);
76
+ if (backend) return { type: 'route', backend, sticky: true };
77
+ }
78
+ const switchKo = t.match(SWITCH_KO);
79
+ if (switchKo) {
80
+ const backend = resolveBackendAlias(switchKo[1]);
81
+ if (backend) return { type: 'route', backend, sticky: true };
82
+ }
83
+ const askMatch = t.match(ASK_EN);
84
+ if (askMatch) {
85
+ const backend = resolveBackendAlias(askMatch[1]);
86
+ if (backend) return { type: 'route', backend, sticky: false };
87
+ }
88
+ const askKo = t.match(ASK_KO);
89
+ if (askKo) {
90
+ const backend = resolveBackendAlias(askKo[1]);
91
+ if (backend) return { type: 'route', backend, sticky: false };
92
+ }
93
+ return { type: 'none' };
94
+ }
95
+
96
+ export function isAgentRoutingDecision(decision) {
97
+ if (!decision || typeof decision !== 'object') return false;
98
+ const slot = String(decision.slot || '').toLowerCase();
99
+ return ROUTING_SLOT_NAMES.has(slot);
100
+ }
101
+
102
+ export function renderAgentPrefix(backend, language = 'en') {
103
+ const key = String(backend || '').toLowerCase();
104
+ if (!BACKEND_LABELS[key]) return '';
105
+ const en = /^en/i.test(String(language || ''));
106
+ const label = BACKEND_LABELS[key][en ? 'en' : 'ko'];
107
+ return en ? `${label} says: ` : `${label}: `;
108
+ }
109
+
110
+ function labelFor(backend, language) {
111
+ const key = String(backend || '').toLowerCase();
112
+ if (!BACKEND_LABELS[key]) return key || 'agent';
113
+ return BACKEND_LABELS[key][/^en/i.test(String(language || '')) ? 'en' : 'ko'];
114
+ }
115
+
116
+ export function buildCrossAgentPrompt({
117
+ prompt, fromBackend, toBackend,
118
+ resolvedDecisions = {}, priorUtterances = [], language = 'en',
119
+ } = {}) {
120
+ const en = /^en/i.test(String(language || ''));
121
+ const fromLabel = labelFor(fromBackend, language);
122
+ const toLabel = labelFor(toBackend, language);
123
+ const lines = [];
124
+ lines.push(en
125
+ ? `[Cross-agent handoff from ${fromLabel} to ${toLabel}]`
126
+ : `[에이전트 핸드오프: ${fromLabel} → ${toLabel}]`);
127
+ const decKeys = Object.keys(resolvedDecisions || {});
128
+ if (decKeys.length) {
129
+ const parts = decKeys.map(k => `${k}=${resolvedDecisions[k] === null ? '(agent picks)' : resolvedDecisions[k]}`);
130
+ lines.push(en ? `Prior decisions: ${parts.join(', ')}` : `이전 결정: ${parts.join(', ')}`);
131
+ }
132
+ const utterances = (priorUtterances || []).filter(Boolean).slice(-4);
133
+ if (utterances.length) {
134
+ lines.push(en
135
+ ? `Recent user voice: ${utterances.join(' | ')}`
136
+ : `최근 사용자 음성: ${utterances.join(' | ')}`);
137
+ }
138
+ lines.push(en ? `User request: ${prompt}` : `사용자 요청: ${prompt}`);
139
+ return lines.join('\n');
140
+ }
141
+
142
+ export function buildFallbackDecision(missingBackend, fallbackLabel, language = 'en') {
143
+ const en = /^en/i.test(String(language || ''));
144
+ const question = en
145
+ ? `${missingBackend} is not installed. Use ${fallbackLabel} instead?`
146
+ : `${missingBackend}이(가) 설치되어 있지 않아. ${fallbackLabel}로 대신 진행할까?`;
147
+ return { slot: 'fallback', question, options: ['yes', 'no'] };
148
+ }
@@ -0,0 +1,138 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ parseAgentRoutingCommand,
5
+ resolveBackendAlias,
6
+ isAgentRoutingDecision,
7
+ renderAgentPrefix,
8
+ buildCrossAgentPrompt,
9
+ buildFallbackDecision,
10
+ isRoutingOnlyUtterance,
11
+ } from './agent_routing.mjs';
12
+
13
+ test('parseAgentRoutingCommand recognizes "ask X" as single-turn', () => {
14
+ assert.deepEqual(parseAgentRoutingCommand('ask Codex what it thinks', 'en'),
15
+ { type: 'route', backend: 'codex', sticky: false });
16
+ assert.deepEqual(parseAgentRoutingCommand('ask aider to write the test', 'en'),
17
+ { type: 'route', backend: 'aider', sticky: false });
18
+ });
19
+
20
+ test('parseAgentRoutingCommand recognizes "switch to X" as sticky', () => {
21
+ assert.deepEqual(parseAgentRoutingCommand('switch to Aider', 'en'),
22
+ { type: 'route', backend: 'aider', sticky: true });
23
+ assert.deepEqual(parseAgentRoutingCommand('switch to claude code', 'en'),
24
+ { type: 'route', backend: 'claude', sticky: true });
25
+ });
26
+
27
+ test('parseAgentRoutingCommand recognizes Korean routing phrases', () => {
28
+ assert.deepEqual(parseAgentRoutingCommand('코덱스한테 물어봐', 'ko'),
29
+ { type: 'route', backend: 'codex', sticky: false });
30
+ assert.deepEqual(parseAgentRoutingCommand('aider로 전환해', 'ko'),
31
+ { type: 'route', backend: 'aider', sticky: true });
32
+ });
33
+
34
+ test('parseAgentRoutingCommand recognizes restore-default phrases', () => {
35
+ assert.deepEqual(parseAgentRoutingCommand('back to default', 'en'),
36
+ { type: 'restore' });
37
+ assert.deepEqual(parseAgentRoutingCommand('use the default agent', 'en'),
38
+ { type: 'restore' });
39
+ assert.deepEqual(parseAgentRoutingCommand('기본으로 돌아가', 'ko'),
40
+ { type: 'restore' });
41
+ });
42
+
43
+ test('parseAgentRoutingCommand returns none on unrelated input', () => {
44
+ assert.deepEqual(parseAgentRoutingCommand('just write the function', 'en'),
45
+ { type: 'none' });
46
+ assert.deepEqual(parseAgentRoutingCommand('plan it first', 'en'),
47
+ { type: 'none' });
48
+ });
49
+
50
+ test('resolveBackendAlias maps user-facing names to canonical backends', () => {
51
+ assert.equal(resolveBackendAlias('Claude Code'), 'claude');
52
+ assert.equal(resolveBackendAlias('claude'), 'claude');
53
+ assert.equal(resolveBackendAlias('cursor cli'), 'cursor');
54
+ assert.equal(resolveBackendAlias('gemini cli'), 'gemini');
55
+ assert.equal(resolveBackendAlias('코덱스'), 'codex');
56
+ assert.equal(resolveBackendAlias('unknown'), null);
57
+ });
58
+
59
+ test('isAgentRoutingDecision detects which_agent slot', () => {
60
+ assert.equal(isAgentRoutingDecision({ slot: 'which_agent', options: ['codex', 'aider'] }), true);
61
+ assert.equal(isAgentRoutingDecision({ slot: 'oauth_provider', options: ['google', 'github'] }), false);
62
+ assert.equal(isAgentRoutingDecision({ slot: 'agent', options: ['codex', 'aider'] }), true);
63
+ assert.equal(isAgentRoutingDecision(null), false);
64
+ });
65
+
66
+ test('renderAgentPrefix uses English label for en', () => {
67
+ assert.equal(renderAgentPrefix('codex', 'en'), 'Codex says: ');
68
+ assert.equal(renderAgentPrefix('claude', 'en'), 'Claude Code says: ');
69
+ });
70
+
71
+ test('renderAgentPrefix uses Korean label for ko', () => {
72
+ assert.equal(renderAgentPrefix('codex', 'ko'), '코덱스: ');
73
+ assert.equal(renderAgentPrefix('claude', 'ko'), 'Claude Code: ');
74
+ });
75
+
76
+ test('renderAgentPrefix returns empty when backend unknown', () => {
77
+ assert.equal(renderAgentPrefix('', 'en'), '');
78
+ assert.equal(renderAgentPrefix(null, 'en'), '');
79
+ assert.equal(renderAgentPrefix('unknownbackend', 'en'), '');
80
+ });
81
+
82
+ test('buildCrossAgentPrompt prepends handoff block in English', () => {
83
+ const out = buildCrossAgentPrompt({
84
+ prompt: 'Refactor the login route to use OAuth.',
85
+ fromBackend: 'claude', toBackend: 'codex',
86
+ resolvedDecisions: { oauth_provider: 'github' },
87
+ priorUtterances: ['plan it first', 'skip step 2'],
88
+ language: 'en',
89
+ });
90
+ assert.match(out, /Cross-agent handoff from Claude Code to Codex/);
91
+ assert.match(out, /Prior decisions: oauth_provider=github/);
92
+ assert.match(out, /Recent user voice: plan it first \| skip step 2/);
93
+ assert.match(out, /User request: Refactor the login route to use OAuth\./);
94
+ });
95
+
96
+ test('buildCrossAgentPrompt omits empty sections', () => {
97
+ const out = buildCrossAgentPrompt({
98
+ prompt: 'do it', fromBackend: 'claude', toBackend: 'codex',
99
+ resolvedDecisions: {}, priorUtterances: [], language: 'en',
100
+ });
101
+ assert.doesNotMatch(out, /Prior decisions:/);
102
+ assert.doesNotMatch(out, /Recent user voice:/);
103
+ assert.match(out, /User request: do it/);
104
+ });
105
+
106
+ test('buildCrossAgentPrompt renders Korean header for ko', () => {
107
+ const out = buildCrossAgentPrompt({
108
+ prompt: '로그인 라우트 리팩토링해줘',
109
+ fromBackend: 'claude', toBackend: 'codex',
110
+ resolvedDecisions: {}, priorUtterances: [], language: 'ko',
111
+ });
112
+ assert.match(out, /에이전트 핸드오프: Claude Code → 코덱스/);
113
+ assert.match(out, /사용자 요청: 로그인 라우트 리팩토링해줘/);
114
+ });
115
+
116
+ test('buildFallbackDecision yields a yes/no shape', () => {
117
+ const d = buildFallbackDecision('codex', 'Claude Code', 'en');
118
+ assert.equal(d.slot, 'fallback');
119
+ assert.deepEqual(d.options, ['yes', 'no']);
120
+ assert.match(d.question, /codex/);
121
+ assert.match(d.question, /Claude Code/);
122
+ });
123
+
124
+ test('buildFallbackDecision yields a Korean prompt for ko', () => {
125
+ const d = buildFallbackDecision('codex', 'Claude Code', 'ko');
126
+ assert.match(d.question, /codex/);
127
+ assert.match(d.question, /Claude Code/);
128
+ });
129
+
130
+ test('isRoutingOnlyUtterance detects command-only utterances', () => {
131
+ assert.equal(isRoutingOnlyUtterance('switch to codex'), true);
132
+ assert.equal(isRoutingOnlyUtterance('switch to Aider.'), true);
133
+ assert.equal(isRoutingOnlyUtterance('back to default'), true);
134
+ assert.equal(isRoutingOnlyUtterance('codex로 전환'), true);
135
+ assert.equal(isRoutingOnlyUtterance('기본으로 돌아가'), true);
136
+ assert.equal(isRoutingOnlyUtterance('switch to codex and write a test'), false);
137
+ assert.equal(isRoutingOnlyUtterance('ask codex what it thinks'), false);
138
+ });