remote-coder 0.5.1__tar.gz → 0.5.2__tar.gz

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 (155) hide show
  1. {remote_coder-0.5.1/remote_coder.egg-info → remote_coder-0.5.2}/PKG-INFO +3 -2
  2. {remote_coder-0.5.1 → remote_coder-0.5.2}/README.md +2 -1
  3. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/__init__.py +1 -1
  4. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/router.py +2 -1
  5. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/static/i18n.js +5 -0
  6. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/templates/admin.html +4 -1
  7. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/ai/base.py +14 -0
  8. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/ai/codex.py +3 -3
  9. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/ai/gemini.py +2 -2
  10. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/cli.py +5 -1
  11. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/diagnostics.py +12 -1
  12. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/jobs/execution_pipeline.py +3 -5
  13. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/jobs/schemas.py +8 -0
  14. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/jobs/worktree_planner.py +2 -2
  15. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/commands/base.py +21 -4
  16. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/commands/branch.py +24 -4
  17. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/commands/registry.py +8 -1
  18. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/commands/system.py +13 -1
  19. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/handlers/job_submission.py +2 -2
  20. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/handlers/natural_flow.py +6 -1
  21. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/handlers/presenters.py +9 -6
  22. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/handlers/update_handler.py +7 -2
  23. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/i18n.py +21 -10
  24. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/messages.py +7 -4
  25. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/parser.py +14 -12
  26. {remote_coder-0.5.1 → remote_coder-0.5.2}/pyproject.toml +1 -1
  27. {remote_coder-0.5.1 → remote_coder-0.5.2/remote_coder.egg-info}/PKG-INFO +3 -2
  28. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_admin_router.py +2 -0
  29. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_ai_base.py +8 -0
  30. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_claude_runner.py +24 -0
  31. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_cli.py +2 -1
  32. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_codex_runner.py +23 -0
  33. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_command_parser.py +25 -0
  34. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_commands.py +59 -4
  35. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_diagnostics.py +14 -0
  36. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_gemini_runner.py +26 -0
  37. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_job_manager.py +47 -0
  38. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_notifier.py +36 -5
  39. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_webhook.py +108 -1
  40. {remote_coder-0.5.1 → remote_coder-0.5.2}/LICENSE +0 -0
  41. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/__init__.py +0 -0
  42. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/advanced_settings.py +0 -0
  43. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/database_browser.py +0 -0
  44. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/static/admin.css +0 -0
  45. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/static/icons/advanced.svg +0 -0
  46. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/static/icons/database.svg +0 -0
  47. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/static/icons/download.svg +0 -0
  48. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/static/icons/home.svg +0 -0
  49. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/static/icons/logs.svg +0 -0
  50. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/static/icons/projects.svg +0 -0
  51. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/static/summary.js +0 -0
  52. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/templates/advanced.html +0 -0
  53. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/templates/database.html +0 -0
  54. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/templates/logs.html +0 -0
  55. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/admin/templates/projects.html +0 -0
  56. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/ai/__init__.py +0 -0
  57. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/ai/claude.py +0 -0
  58. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/ai/factory.py +0 -0
  59. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/ai/model_catalog.py +0 -0
  60. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/ai/usage.py +0 -0
  61. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/config.py +0 -0
  62. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/git/__init__.py +0 -0
  63. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/git/ai_commit.py +0 -0
  64. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/git/branch_naming.py +0 -0
  65. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/git/commit_message.py +0 -0
  66. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/git/service.py +0 -0
  67. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/git/worktree_service.py +0 -0
  68. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/jobs/__init__.py +0 -0
  69. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/jobs/fix_pipeline.py +0 -0
  70. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/jobs/fix_support.py +0 -0
  71. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/jobs/heartbeat.py +0 -0
  72. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/jobs/manager.py +0 -0
  73. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/jobs/plan_decisions.py +0 -0
  74. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/jobs/result_writer.py +0 -0
  75. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/jobs/store.py +0 -0
  76. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/main.py +0 -0
  77. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/models.py +0 -0
  78. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/monitoring/__init__.py +0 -0
  79. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/monitoring/code.py +0 -0
  80. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/monitoring/events.py +0 -0
  81. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/monitoring/git.py +0 -0
  82. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/monitoring/log_buffer.py +0 -0
  83. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/monitoring/memory.py +0 -0
  84. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/monitoring/model.py +0 -0
  85. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/projects/__init__.py +0 -0
  86. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/projects/registry.py +0 -0
  87. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/security/__init__.py +0 -0
  88. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/security/auth.py +0 -0
  89. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/system_startup.py +0 -0
  90. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/__init__.py +0 -0
  91. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/bot_instances.py +0 -0
  92. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/commands/__init__.py +0 -0
  93. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/commands/clear_stop.py +0 -0
  94. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/commands/fix.py +0 -0
  95. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/commands/model.py +0 -0
  96. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/commands/monitor.py +0 -0
  97. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/commands/status.py +0 -0
  98. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/confirmations.py +0 -0
  99. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/conversation/__init__.py +0 -0
  100. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/conversation/collaborators.py +0 -0
  101. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/conversation/context.py +0 -0
  102. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/conversation/models.py +0 -0
  103. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/conversation/protocols.py +0 -0
  104. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/conversation/sqlite_rows.py +0 -0
  105. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/conversation/sqlite_store.py +0 -0
  106. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/conversation/store.py +0 -0
  107. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/formatting.py +0 -0
  108. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/handlers/__init__.py +0 -0
  109. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/handlers/callback_dispatcher.py +0 -0
  110. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/handlers/command_flow.py +0 -0
  111. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/handlers/fix_flow.py +0 -0
  112. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/handlers/plan_flow.py +0 -0
  113. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/handlers/recent_updates.py +0 -0
  114. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/handlers/request.py +0 -0
  115. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/handlers/session_binding.py +0 -0
  116. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/lists.py +0 -0
  117. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/model_preferences.py +0 -0
  118. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/notifier.py +0 -0
  119. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/notifier_protocol.py +0 -0
  120. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/plan_decisions_flow.py +0 -0
  121. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/webhook.py +0 -0
  122. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/telegram/webhook_registration.py +0 -0
  123. {remote_coder-0.5.1 → remote_coder-0.5.2}/app/tunnel.py +0 -0
  124. {remote_coder-0.5.1 → remote_coder-0.5.2}/remote_coder.egg-info/SOURCES.txt +0 -0
  125. {remote_coder-0.5.1 → remote_coder-0.5.2}/remote_coder.egg-info/dependency_links.txt +0 -0
  126. {remote_coder-0.5.1 → remote_coder-0.5.2}/remote_coder.egg-info/entry_points.txt +0 -0
  127. {remote_coder-0.5.1 → remote_coder-0.5.2}/remote_coder.egg-info/requires.txt +0 -0
  128. {remote_coder-0.5.1 → remote_coder-0.5.2}/remote_coder.egg-info/top_level.txt +0 -0
  129. {remote_coder-0.5.1 → remote_coder-0.5.2}/setup.cfg +0 -0
  130. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_ai_commit.py +0 -0
  131. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_ai_factory.py +0 -0
  132. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_auth.py +0 -0
  133. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_bot_instance_manager.py +0 -0
  134. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_branch_naming.py +0 -0
  135. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_commit_message_formatter.py +0 -0
  136. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_config.py +0 -0
  137. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_conversation_store.py +0 -0
  138. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_database_browser.py +0 -0
  139. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_event_logger.py +0 -0
  140. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_git_service.py +0 -0
  141. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_i18n.py +0 -0
  142. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_job_status.py +0 -0
  143. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_job_store.py +0 -0
  144. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_lists.py +0 -0
  145. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_log_buffer.py +0 -0
  146. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_main_lifespan.py +0 -0
  147. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_monitoring.py +0 -0
  148. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_plan_decisions.py +0 -0
  149. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_project_registry.py +0 -0
  150. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_project_scoped_state.py +0 -0
  151. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_system_startup.py +0 -0
  152. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_telegram_formatting.py +0 -0
  153. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_tunnel.py +0 -0
  154. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_webhook_multibot.py +0 -0
  155. {remote_coder-0.5.1 → remote_coder-0.5.2}/tests/test_webhook_registration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remote-coder
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Telegram-based remote AI coding automation server
5
5
  Author: Remote AI Coder contributors
6
6
  License: Apache License
@@ -236,7 +236,7 @@ Run Claude Code, Codex, or Gemini on your local development machine by sending a
236
236
  - Claude, Codex, and Gemini runners behind the same job flow.
237
237
  - Reply-linked jobs continue the same AI CLI session, so follow-ups build on prior context.
238
238
  - Local admin UI for project setup, advanced settings, logs, and conversation memory.
239
- - Read-only `plan:` and `ask:` modes when you want analysis without commits. PLAN mode asks open decisions through inline buttons first, then finalizes the plan from your answers.
239
+ - Read-only `plan:`, `ask:`, and `research:` modes when you want analysis without commits. PLAN mode asks open decisions through inline buttons first, then finalizes the plan from your answers; RESEARCH mode asks the selected AI CLI to use internet search when useful.
240
240
 
241
241
  ## Quick Start
242
242
 
@@ -311,6 +311,7 @@ Natural-language examples:
311
311
  Fix the login validation bug with model: codex
312
312
  plan: outline the migration before changing code
313
313
  /ask what test command does this repo use?
314
+ /research compare current Telegram webhook security guidance
314
315
  수정: 방금 작업에서 README 문구만 더 간결하게 바꿔줘
315
316
  ```
316
317
 
@@ -15,7 +15,7 @@ Run Claude Code, Codex, or Gemini on your local development machine by sending a
15
15
  - Claude, Codex, and Gemini runners behind the same job flow.
16
16
  - Reply-linked jobs continue the same AI CLI session, so follow-ups build on prior context.
17
17
  - Local admin UI for project setup, advanced settings, logs, and conversation memory.
18
- - Read-only `plan:` and `ask:` modes when you want analysis without commits. PLAN mode asks open decisions through inline buttons first, then finalizes the plan from your answers.
18
+ - Read-only `plan:`, `ask:`, and `research:` modes when you want analysis without commits. PLAN mode asks open decisions through inline buttons first, then finalizes the plan from your answers; RESEARCH mode asks the selected AI CLI to use internet search when useful.
19
19
 
20
20
  ## Quick Start
21
21
 
@@ -90,6 +90,7 @@ Natural-language examples:
90
90
  Fix the login validation bug with model: codex
91
91
  plan: outline the migration before changing code
92
92
  /ask what test command does this repo use?
93
+ /research compare current Telegram webhook security guidance
93
94
  수정: 방금 작업에서 README 문구만 더 간결하게 바꿔줘
94
95
  ```
95
96
 
@@ -1,3 +1,3 @@
1
1
  """Remote AI Coder application package."""
2
2
 
3
- __version__ = "0.5.1"
3
+ __version__ = "0.5.2"
@@ -378,9 +378,10 @@ def create_admin_router(
378
378
  report = check_prerequisites()
379
379
  installed = [cli.name for cli in report.ai_clis if cli.installed]
380
380
  _adminlog.info(
381
- "prerequisites queried ngrok_ok=%s ai_clis=%s",
381
+ "prerequisites queried ngrok_ok=%s ai_clis=%s github_cli=%s",
382
382
  report.ngrok_ok,
383
383
  ",".join(installed) or "-",
384
+ report.github_cli.installed,
384
385
  )
385
386
  return report.model_dump()
386
387
 
@@ -146,6 +146,11 @@
146
146
  en: "None found (install at least one: claude / codex / gemini)",
147
147
  ko: "설치된 것 없음 (claude / codex / gemini 중 최소 1개 설치)",
148
148
  },
149
+ "setup.githubCli": { en: "GitHub CLI", ko: "GitHub CLI" },
150
+ "setup.githubCliNone": {
151
+ en: "Not found (install gh before using /pr)",
152
+ ko: "설치되지 않음 (/pr 사용 전 gh 설치 필요)",
153
+ },
149
154
  "setup.checking": { en: "Checking…", ko: "확인 중…" },
150
155
  "setup.recheck": { en: "Re-check", ko: "다시 확인" },
151
156
  "setup.cta": { en: "Add your first project", ko: "첫 프로젝트 추가" },
@@ -248,9 +248,12 @@
248
248
  const installed = (report.ai_clis || []).filter((c) => c.installed).map((c) => c.name);
249
249
  const aiOk = installed.length > 0;
250
250
  const aiDetail = aiOk ? installed.join(", ") : i18n.t("setup.aiCliNone");
251
+ const ghOk = !!(report.github_cli && report.github_cli.installed);
252
+ const ghDetail = ghOk ? "gh" : i18n.t("setup.githubCliNone");
251
253
  $("#setup-prereq").innerHTML =
252
254
  renderPrereqRow(!!report.ngrok_ok, i18n.t("setup.ngrok"), ngrokDetail) +
253
- renderPrereqRow(aiOk, i18n.t("setup.aiCli"), aiDetail);
255
+ renderPrereqRow(aiOk, i18n.t("setup.aiCli"), aiDetail) +
256
+ renderPrereqRow(ghOk, i18n.t("setup.githubCli"), ghDetail);
254
257
  }
255
258
 
256
259
  async function loadPrerequisites() {
@@ -57,6 +57,18 @@ _PLAN_DECISIONS_INSTRUCTION = (
57
57
  )
58
58
 
59
59
 
60
+ _RESEARCH_INSTRUCTION = (
61
+ "You are in RESEARCH mode. Read the repository context and answer the user's research "
62
+ "question. Do not modify files.\n\n"
63
+ "Use internet search when it is useful or necessary for the question, similar to a deep "
64
+ "research workflow. Compare multiple perspectives or sources when possible, and clearly "
65
+ "separate repository-derived facts from external findings. Include citations or source "
66
+ "links for external claims, call out uncertainty or limitations, and finish with a direct "
67
+ "answer to the user's problem.\n\n"
68
+ "User research request:\n"
69
+ )
70
+
71
+
60
72
  def instruction_for_runner_mode(instruction: str, mode: JobMode) -> str:
61
73
  if mode == JobMode.PLAN:
62
74
  return f"{_PLAN_DECISIONS_INSTRUCTION}{instruction}"
@@ -66,6 +78,8 @@ def instruction_for_runner_mode(instruction: str, mode: JobMode) -> str:
66
78
  "Do not modify files.\n\n"
67
79
  f"User question:\n{instruction}"
68
80
  )
81
+ if mode == JobMode.RESEARCH:
82
+ return f"{_RESEARCH_INSTRUCTION}{instruction}"
69
83
  return instruction
70
84
 
71
85
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
 
5
5
  from app.ai.base import BaseCliRunner, RunnerInput, instruction_for_runner_mode
6
- from app.jobs.schemas import JobMode
6
+ from app.jobs.schemas import is_read_only_job_mode
7
7
  from app.models import CodexSandboxMode
8
8
  from app.monitoring.events import EventLogger
9
9
 
@@ -16,7 +16,7 @@ class CodexRunner(BaseCliRunner):
16
16
  self._sandbox = sandbox
17
17
 
18
18
  def _resolve_sandbox(self, runner_input: RunnerInput) -> CodexSandboxMode:
19
- if runner_input.mode in (JobMode.PLAN, JobMode.ASK):
19
+ if is_read_only_job_mode(runner_input.mode):
20
20
  return CodexSandboxMode.READ_ONLY
21
21
  return self._sandbox
22
22
 
@@ -29,7 +29,7 @@ class CodexRunner(BaseCliRunner):
29
29
 
30
30
  def build_argv(self, runner_input: RunnerInput) -> list[str]:
31
31
  sandbox = self._resolve_sandbox(runner_input)
32
- if runner_input.mode in (JobMode.PLAN, JobMode.ASK):
32
+ if is_read_only_job_mode(runner_input.mode):
33
33
  instruction = instruction_for_runner_mode(runner_input.instruction, runner_input.mode)
34
34
  else:
35
35
  instruction = runner_input.instruction
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
 
5
5
  from app.ai.base import BaseCliRunner, RunnerInput, instruction_for_runner_mode
6
- from app.jobs.schemas import JobMode
6
+ from app.jobs.schemas import is_read_only_job_mode
7
7
  from app.monitoring.events import EventLogger
8
8
 
9
9
 
@@ -17,7 +17,7 @@ class GeminiRunner(BaseCliRunner):
17
17
  return Path.home() / ".gemini" / "sessions"
18
18
 
19
19
  def build_argv(self, runner_input: RunnerInput) -> list[str]:
20
- if runner_input.mode in (JobMode.PLAN, JobMode.ASK):
20
+ if is_read_only_job_mode(runner_input.mode):
21
21
  prompt = instruction_for_runner_mode(runner_input.instruction, runner_input.mode)
22
22
  argv = ["gemini", "--skip-trust", "-p", prompt]
23
23
  else:
@@ -32,7 +32,7 @@ def build_parser() -> argparse.ArgumentParser:
32
32
  help="Run the server only (skip ngrok and webhook registration)",
33
33
  )
34
34
 
35
- subparsers.add_parser("doctor", help="Check prerequisites (ngrok, AI CLIs)")
35
+ subparsers.add_parser("doctor", help="Check prerequisites (ngrok, AI CLIs, GitHub CLI)")
36
36
  return parser
37
37
 
38
38
 
@@ -128,6 +128,10 @@ def run_doctor() -> None:
128
128
  " ⚠️ AI CLI (claude/codex/gemini) not found. Install at least one. "
129
129
  "(e.g. npm install -g @anthropic-ai/claude-code)"
130
130
  )
131
+ if report.github_cli.installed:
132
+ print(" ✅ GitHub CLI: gh")
133
+ else:
134
+ print(" ⚠️ GitHub CLI (gh) not found. Install it before using /pr.")
131
135
 
132
136
 
133
137
  if __name__ == "__main__":
@@ -7,6 +7,7 @@ from pydantic import BaseModel
7
7
  from app import tunnel
8
8
 
9
9
  AI_CLI_TOOLS = ("claude", "codex", "gemini")
10
+ GITHUB_CLI_TOOL = "gh"
10
11
 
11
12
 
12
13
  class AiCliStatus(BaseModel):
@@ -18,6 +19,7 @@ class PrerequisitesReport(BaseModel):
18
19
  ngrok_ok: bool
19
20
  ngrok_detail: str
20
21
  ai_clis: list[AiCliStatus]
22
+ github_cli: AiCliStatus
21
23
 
22
24
 
23
25
  def check_prerequisites() -> PrerequisitesReport:
@@ -34,4 +36,13 @@ def check_prerequisites() -> PrerequisitesReport:
34
36
  AiCliStatus(name=tool, installed=shutil.which(tool) is not None)
35
37
  for tool in AI_CLI_TOOLS
36
38
  ]
37
- return PrerequisitesReport(ngrok_ok=ngrok_ok, ngrok_detail=ngrok_detail, ai_clis=ai_clis)
39
+ github_cli = AiCliStatus(
40
+ name=GITHUB_CLI_TOOL,
41
+ installed=shutil.which(GITHUB_CLI_TOOL) is not None,
42
+ )
43
+ return PrerequisitesReport(
44
+ ngrok_ok=ngrok_ok,
45
+ ngrok_detail=ngrok_detail,
46
+ ai_clis=ai_clis,
47
+ github_cli=github_cli,
48
+ )
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  from app.ai.base import RunnerInput
7
7
  from app.git.commit_message import CommitMessageFormatter
8
8
  from app.jobs.plan_decisions import PlanDecisionQuestion, parse_plan_decisions
9
- from app.jobs.schemas import Job, JobMode
9
+ from app.jobs.schemas import Job, JobMode, is_read_only_job_mode
10
10
  from app.monitoring.events import EventLogger
11
11
 
12
12
  _joblog = EventLogger("app.jobs.lifecycle", "job.lifecycle")
@@ -50,7 +50,7 @@ def run_job(manager, job_id: str) -> Job:
50
50
  created_worktree_for_job = False
51
51
  failed_stage: str | None = None
52
52
  remote = manager._effective_git_remote_name()
53
- read_only_job = job.request.mode in (JobMode.PLAN, JobMode.ASK)
53
+ read_only_job = is_read_only_job_mode(job.request.mode)
54
54
  plan_decision_questions: list[PlanDecisionQuestion] | None = None
55
55
  try:
56
56
  job.mark_running()
@@ -238,9 +238,7 @@ def run_job(manager, job_id: str) -> Job:
238
238
  finally:
239
239
  manager._cancel_events.pop(job_id, None)
240
240
  manager._cancelled_job_ids.discard(job_id)
241
- read_only_succeeded = (
242
- job.request.mode in (JobMode.PLAN, JobMode.ASK) and job.status.value == "succeeded"
243
- )
241
+ read_only_succeeded = is_read_only_job_mode(job.request.mode) and job.status.value == "succeeded"
244
242
  cleanup_on_success = read_only_succeeded or not manager._effective_keep_worktree_on_success()
245
243
  _joblog.info(
246
244
  "job finalizing status=%s created_worktree=%s cleanup_on_success=%s",
@@ -26,9 +26,17 @@ class JobMode(StrEnum):
26
26
  AGENT = "agent"
27
27
  PLAN = "plan"
28
28
  ASK = "ask"
29
+ RESEARCH = "research"
29
30
  AGENT_FIX = "agent_fix"
30
31
 
31
32
 
33
+ READ_ONLY_JOB_MODES = frozenset({JobMode.PLAN, JobMode.ASK, JobMode.RESEARCH})
34
+
35
+
36
+ def is_read_only_job_mode(mode: JobMode) -> bool:
37
+ return mode in READ_ONLY_JOB_MODES
38
+
39
+
32
40
  class FixKind(StrEnum):
33
41
  SOURCE = "source"
34
42
 
@@ -4,7 +4,7 @@ from dataclasses import dataclass
4
4
  from pathlib import Path
5
5
 
6
6
  from app.git.service import GitWorktreeService
7
- from app.jobs.schemas import Job, JobMode
7
+ from app.jobs.schemas import Job, is_read_only_job_mode
8
8
  from app.monitoring.events import EventLogger
9
9
 
10
10
  _joblog = EventLogger("app.jobs.lifecycle", "job.lifecycle")
@@ -27,7 +27,7 @@ def prepare_worktree_plan(
27
27
  job_ctx: dict[str, object],
28
28
  ) -> WorktreePlan:
29
29
  requested_branch = job.request.branch
30
- if job.request.mode in (JobMode.PLAN, JobMode.ASK):
30
+ if is_read_only_job_mode(job.request.mode):
31
31
  path = git_service.prepare_detached_worktree(
32
32
  project_path, job.id, worktree_base_dir=worktree_base
33
33
  )
@@ -100,8 +100,9 @@ HELP_TEXT = "\n".join(
100
100
  "- no commit",
101
101
  "- plan: <natural language> or /plan <natural language> - plan mode (plan only; no code changes)",
102
102
  "- ask: <natural language> or /ask <natural language> - ask mode (analysis and answers; no code edits)",
103
+ "- research: <natural language> or /research <natural language> - research mode (internet-backed answers; no code edits)",
103
104
  "- fix: <natural language> or /fix - fix mode (reply to a job result; amends that commit)",
104
- "- Korean aliases 계획:, 질문:, and 수정: instead of plan:/ask:/fix: (colons `:` or full-width `:` allowed)",
105
+ "- Korean aliases 계획:, 질문:, 조사:, and 수정: instead of plan:/ask:/research:/fix: (colons `:` or full-width `:` allowed)",
105
106
  "",
106
107
  "📋 Commands",
107
108
  render_command_list(_HELP_COMMAND_ROWS),
@@ -122,7 +123,7 @@ HELP_AGENT_TOPIC = "\n".join(
122
123
  "- model: codex branch: remote-auth strengthen tests",
123
124
  "- no commit just verify the doc wording",
124
125
  "",
125
- "A job is accepted after project/branch/model checks via `y`/`Y` or inline buttons.",
126
+ "A job is accepted after project/branch/model checks with inline Yes/No buttons.",
126
127
  ]
127
128
  )
128
129
 
@@ -131,7 +132,7 @@ HELP_PLAN_TOPIC = "\n".join(
131
132
  "📐 Plan mode (plan)",
132
133
  "",
133
134
  "Receive change plans only; no code edits. Like agent mode, a job is accepted after confirmation "
134
- "(`y`/`Y` or inline buttons).",
135
+ "with inline Yes/No buttons.",
135
136
  "",
136
137
  "💡 Examples",
137
138
  "- plan: summarize the login validation flow",
@@ -147,7 +148,7 @@ HELP_ASK_TOPIC = "\n".join(
147
148
  "❓ Ask mode (ask)",
148
149
  "",
149
150
  "Answer questions using the repository; no code edits, commits, or pushes. Jobs are accepted like "
150
- "agent mode after confirmation (`y`/`Y` or inline buttons).",
151
+ "agent mode after confirmation with inline Yes/No buttons.",
151
152
  "",
152
153
  "💡 Examples",
153
154
  "- ask: how do I run pytest in this project?",
@@ -158,6 +159,22 @@ HELP_ASK_TOPIC = "\n".join(
158
159
  ]
159
160
  )
160
161
 
162
+ HELP_RESEARCH_TOPIC = "\n".join(
163
+ [
164
+ "🔎 Research mode (research)",
165
+ "",
166
+ "Answer research questions using repository context and internet search when useful; no code edits, "
167
+ "commits, or pushes. Jobs are accepted like agent mode after confirmation with inline Yes/No buttons.",
168
+ "",
169
+ "💡 Examples",
170
+ "- research: compare webhook retry strategies for this service",
171
+ "- /research model: codex find current FastAPI deployment guidance",
172
+ "- 조사:Telegram webhook 보안 권장사항 조사",
173
+ "",
174
+ "See /help for more options.",
175
+ ]
176
+ )
177
+
161
178
  HELP_FIX_TOPIC = "\n".join(
162
179
  [
163
180
  "🔧 Fix mode (fix)",
@@ -17,6 +17,26 @@ from app.telegram.commands.base import (
17
17
  from app.telegram.i18n import ui_message
18
18
 
19
19
 
20
+ _MISSING_GH_ERROR = "GitHub CLI (gh) is not installed or not available on PATH."
21
+
22
+
23
+ def _format_pr_error(exc: RuntimeError) -> str:
24
+ message = str(exc)
25
+ if _MISSING_GH_ERROR in message:
26
+ return ui_message(
27
+ "pr.gh_missing",
28
+ "/pr failed: GitHub CLI (gh) is not installed or not available on PATH.\n\n"
29
+ "To create PRs from Telegram:\n"
30
+ "1. Install GitHub CLI:\n"
31
+ " - macOS: `brew install gh`\n"
32
+ " - Windows: `winget install --id GitHub.cli`\n"
33
+ " - Ubuntu/Debian: `sudo apt install gh`\n"
34
+ "2. Sign in: `gh auth login`\n"
35
+ "3. Restart Remote AI Coder: `remote-coder up`",
36
+ )
37
+ return f"/pr failed: {message}"
38
+
39
+
20
40
  class BranchCommand(TelegramCommand):
21
41
  name = "/branch"
22
42
  description = "Show the current branch or switch to a local branch"
@@ -261,7 +281,7 @@ class PrCommand(TelegramCommand):
261
281
  try:
262
282
  branches = self._load_pr_candidates(message, ctx)
263
283
  except RuntimeError as exc:
264
- return f"/pr failed: {exc}"
284
+ return _format_pr_error(exc)
265
285
  if not branches:
266
286
  return ui_message(
267
287
  "pr.no_candidates",
@@ -301,7 +321,7 @@ class PrCommand(TelegramCommand):
301
321
  entry.root_path, remote, ""
302
322
  )
303
323
  except RuntimeError as exc:
304
- return f"/pr failed: {exc}"
324
+ return _format_pr_error(exc)
305
325
  if branch not in remote_branches:
306
326
  return ui_message(
307
327
  "pr.remote_branch_missing",
@@ -314,7 +334,7 @@ class PrCommand(TelegramCommand):
314
334
  try:
315
335
  base_branch = ctx.git_service.resolve_integrate_branch(entry.root_path)
316
336
  except RuntimeError as exc:
317
- return f"/pr failed: {exc}"
337
+ return _format_pr_error(exc)
318
338
 
319
339
  title, body = self._build_pr_content(branch, project_name, message.chat_id, ctx)
320
340
 
@@ -327,7 +347,7 @@ class PrCommand(TelegramCommand):
327
347
  body,
328
348
  )
329
349
  except RuntimeError as exc:
330
- return f"/pr failed: {exc}"
350
+ return _format_pr_error(exc)
331
351
 
332
352
  return f"PR created:\n{pr_url}"
333
353
 
@@ -37,7 +37,7 @@ class CommandRegistry:
37
37
  ctx.confirmation_store.pop(scope_project, message.chat_id)
38
38
  return init_cmd.execute(message, ctx)
39
39
 
40
- if head in {"/plan", "/ask", "/fix"}:
40
+ if head in {"/plan", "/ask", "/research", "/fix"}:
41
41
  return None
42
42
 
43
43
  pending = ctx.confirmation_store.get(scope_project, message.chat_id)
@@ -89,6 +89,13 @@ class CommandRegistry:
89
89
  "command": "ask",
90
90
  "description": translate_text("ask mode message (example: /ask explain the JobManager role)", language),
91
91
  },
92
+ {
93
+ "command": "research",
94
+ "description": translate_text(
95
+ "research mode message (example: /research compare webhook retry strategies)",
96
+ language,
97
+ ),
98
+ },
92
99
  ]
93
100
 
94
101
 
@@ -6,6 +6,7 @@ from app.telegram.commands.base import (
6
6
  HELP_ASK_TOPIC,
7
7
  HELP_FIX_TOPIC,
8
8
  HELP_PLAN_TOPIC,
9
+ HELP_RESEARCH_TOPIC,
9
10
  HELP_TEXT,
10
11
  CommandContext,
11
12
  InlineButton,
@@ -98,6 +99,9 @@ class StartCommand(TelegramCommand):
98
99
  InlineButton("AGENTS mode", "/help agent"),
99
100
  InlineButton("PLAN mode", "/help plan"),
100
101
  InlineButton("ASK mode", "/help ask"),
102
+ ],
103
+ [
104
+ InlineButton("RESEARCH mode", "/help research"),
101
105
  InlineButton("FIX mode", "/help fix"),
102
106
  ],
103
107
  ],
@@ -121,7 +125,13 @@ class HelpCommand(TelegramCommand):
121
125
  tokens = message.text.strip().split()
122
126
  if len(tokens) >= 2:
123
127
  raw = tokens[1]
124
- topic_aliases = {"에이전트": "agent", "계획": "plan", "질문": "ask", "수정": "fix"}
128
+ topic_aliases = {
129
+ "에이전트": "agent",
130
+ "계획": "plan",
131
+ "질문": "ask",
132
+ "조사": "research",
133
+ "수정": "fix",
134
+ }
125
135
  topic = topic_aliases.get(raw, raw.lower())
126
136
  if topic in ("agent", "agents"):
127
137
  return HELP_AGENT_TOPIC
@@ -129,6 +139,8 @@ class HelpCommand(TelegramCommand):
129
139
  return HELP_PLAN_TOPIC
130
140
  if topic == "ask":
131
141
  return HELP_ASK_TOPIC
142
+ if topic == "research":
143
+ return HELP_RESEARCH_TOPIC
132
144
  if topic == "fix":
133
145
  return HELP_FIX_TOPIC
134
146
  if len(tokens) >= 2 and self._registry is not None:
@@ -7,7 +7,7 @@ from fastapi import BackgroundTasks
7
7
  from app.ai.model_catalog import format_model_selection
8
8
  from app.ai.usage import format_token_usage
9
9
  from app.jobs.manager import JobManager
10
- from app.jobs.schemas import Job, JobMode, JobRequest
10
+ from app.jobs.schemas import Job, JobRequest, is_read_only_job_mode
11
11
  from app.monitoring.events import EventLogger
12
12
  from app.telegram.conversation import SQLiteConversationStore
13
13
 
@@ -26,7 +26,7 @@ def format_job_result_memory_summary(final_job: Job) -> str:
26
26
  token_usage = format_token_usage(final_job.runner_token_usage)
27
27
  if token_usage:
28
28
  summary += f" tokens={token_usage}"
29
- if final_job.request.mode in (JobMode.PLAN, JobMode.ASK) and final_job.runner_stdout_summary:
29
+ if is_read_only_job_mode(final_job.request.mode) and final_job.runner_stdout_summary:
30
30
  preview = final_job.runner_stdout_summary[:_JOB_RESULT_MEMORY_READ_ONLY_STDOUT_PREVIEW]
31
31
  summary += f" stdout_preview={preview}"
32
32
  return summary
@@ -177,7 +177,12 @@ class NaturalFlow:
177
177
  ) -> dict[str, str]:
178
178
  cc = req.command_context
179
179
  cc.confirmation_store.pop(req.scope_project, req.chat_id)
180
- mode_prefix = "/plan" if pending.action == JobMode.PLAN.value else "/ask"
180
+ mode_prefixes = {
181
+ JobMode.PLAN.value: "/plan",
182
+ JobMode.ASK.value: "/ask",
183
+ JobMode.RESEARCH.value: "/research",
184
+ }
185
+ mode_prefix = mode_prefixes.get(pending.action, "/ask")
181
186
  try:
182
187
  parsed_request = self._parser.parse_natural(
183
188
  f"{mode_prefix} {req.message.text}",
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from app.ai.model_catalog import format_model_selection
4
- from app.jobs.schemas import JobMode, JobRequest
4
+ from app.jobs.schemas import JobMode, JobRequest, is_read_only_job_mode
5
5
  from app.telegram.commands import InlineButton
6
6
  from app.telegram.commands import NAV_CLOSE_CALLBACK
7
7
 
@@ -24,10 +24,8 @@ def format_natural_job_confirmation(
24
24
  f"- Work branch: {current_branch}",
25
25
  f"- Model: {format_model_selection(request.model, request.model_id)}",
26
26
  ]
27
- if request.mode is JobMode.PLAN:
28
- lines.append("- Mode: plan (read-only, no commit/push)")
29
- elif request.mode is JobMode.ASK:
30
- lines.append("- Mode: ask (read-only, no commit/push)")
27
+ if is_read_only_job_mode(request.mode):
28
+ lines.append(f"- Mode: {request.mode.value} (read-only, no commit/push)")
31
29
  else:
32
30
  lines.append("- Mode: agent (may edit code, commit, and push)")
33
31
  if request.branch:
@@ -81,6 +79,12 @@ def format_mode_input_prompt(mode: JobMode) -> str:
81
79
  "Example: Explain the JobManager flow\n"
82
80
  "Example: model: codex How do I run pytest?"
83
81
  )
82
+ if mode is JobMode.RESEARCH:
83
+ return (
84
+ "Send the research question to run in research mode.\n\n"
85
+ "Example: Compare FastAPI deployment options for this project\n"
86
+ "Example: model: codex Research the safest webhook retry strategy"
87
+ )
84
88
  raise AssertionError(mode)
85
89
 
86
90
 
@@ -98,4 +102,3 @@ def format_fix_requires_reply_message() -> str:
98
102
  "Example: reply to a job result, then send /fix\n"
99
103
  "Example: reply to a job result with fix: add missing tests"
100
104
  )
101
-
@@ -180,8 +180,13 @@ class WebhookUpdateHandler:
180
180
  if pending_result is not None:
181
181
  return pending_result
182
182
 
183
- if message_head_lower in {"/plan", "/ask"} and len(message_tokens) == 1:
184
- mode = JobMode.PLAN if message_head_lower == "/plan" else JobMode.ASK
183
+ if message_head_lower in {"/plan", "/ask", "/research"} and len(message_tokens) == 1:
184
+ mode_by_command = {
185
+ "/plan": JobMode.PLAN,
186
+ "/ask": JobMode.ASK,
187
+ "/research": JobMode.RESEARCH,
188
+ }
189
+ mode = mode_by_command[message_head_lower]
185
190
  return self._natural_flow_factory().prompt_for_mode_instruction(req, mode)
186
191
 
187
192
  if message_head_lower == "/fix" and len(message_tokens) == 1:
@@ -120,12 +120,14 @@ def command_parse_error_empty_instruction_plan_ask(language: UiLanguage) -> str:
120
120
  return (
121
121
  "작업 지시문이 비어 있습니다.\n\n"
122
122
  "예: plan: 로그인 수정 계획 세워줘\n"
123
- "예: /ask JobManager 흐름 설명해줘"
123
+ "예: /ask JobManager 흐름 설명해줘\n"
124
+ "예: /research webhook 보안 권장사항 조사"
124
125
  )
125
126
  return (
126
127
  "The work instruction is empty.\n\n"
127
128
  "Example: plan: outline the login refactor\n"
128
- "Example: /ask explain JobManager routing"
129
+ "Example: /ask explain JobManager routing\n"
130
+ "Example: /research compare webhook security guidance"
129
131
  )
130
132
 
131
133
 
@@ -191,6 +193,7 @@ _TEXT_REPLACEMENTS_KO_TO_EN_RAW: tuple[tuple[str, str], ...] = (
191
193
  ("옵션", "Options"),
192
194
  ("계획 모드", "plan mode"),
193
195
  ("질문 모드", "ask mode"),
196
+ ("조사 모드", "research mode"),
194
197
  ("명령어 목록:", "Commands:"),
195
198
  ("메뉴와 프로젝트 상태를 확인합니다", "Show the menu and project status"),
196
199
  ("사용 가능한 명령어를 확인합니다", "Show available commands"),
@@ -233,6 +236,7 @@ _TEXT_REPLACEMENTS_KO_TO_EN_RAW: tuple[tuple[str, str], ...] = (
233
236
  ),
234
237
  ("계획 모드 메시지", "plan mode message"),
235
238
  ("질문 모드 메시지", "ask mode message"),
239
+ ("조사 모드 메시지", "research mode message"),
236
240
  ("로그인 흐름 검토", "review login flow"),
237
241
  ("역할 설명", "explain the role"),
238
242
  ("기본 모델 변경", "Change the default model"),
@@ -305,6 +309,7 @@ _TEXT_REPLACEMENTS_KO_TO_EN_RAW: tuple[tuple[str, str], ...] = (
305
309
  ("🤖 AGENTS 모드 (agent)", "🤖 AGENTS mode (agent)"),
306
310
  ("📐 Plan 모드 (plan)", "📐 Plan mode (plan)"),
307
311
  ("❓ Ask 모드 (ask)", "❓ Ask mode (ask)"),
312
+ ("🔎 Research 모드 (research)", "🔎 Research mode (research)"),
308
313
  ("🔧 Fix 모드 (fix)", "🔧 Fix mode (fix)"),
309
314
  ("💡 팁: 작업 결과에 reply 후 `fix: ...`를 보내면 그 커밋을 보완합니다.", "💡 Tip: Reply to a job result and send `fix: ...` to amend that commit."),
310
315
  ("등록 정보 없음", "not registered"),
@@ -376,14 +381,8 @@ _TEXT_REPLACEMENTS_KO_TO_EN_RAW: tuple[tuple[str, str], ...] = (
376
381
  ("기억 저장소가 설정되지 않았습니다.", "Memory storage is not configured."),
377
382
  ("현재 할 작업을 확인하세요.", "Confirm the work to run."),
378
383
  ("실행 여부를 선택하세요.", "Choose whether to run it."),
379
- (
380
- "실행하려면 `y` 또는 `Y`를 입력하세요. "
381
- "새 자연어 요청으로 이 확인을 바꿀 수 있습니다. "
382
- "파싱되지 않는 입력은 대기 작업이 취소됩니다.",
383
- "Send `y` or `Y` to run. Another natural-language request replaces this confirmation. "
384
- "Unparseable input cancels the pending request.",
385
- ),
386
- ("실행하려면 `y` 또는 `Y`를 입력하세요. 그 외 응답은 취소됩니다.", "Send `y` or `Y` to run it. Any other response cancels it."),
384
+ ("인라인 Yes/No 버튼으로 확인한 뒤 작업이 접수됩니다.", "A job is accepted after confirmation with inline Yes/No buttons."),
385
+ ("인라인 Yes/No 버튼으로 작업 실행 여부를 선택하세요.", "Choose whether to run with inline Yes/No buttons."),
387
386
  ("작업 요청을 취소했습니다.", "Cancelled the work request."),
388
387
  ("알 수 없는 clear 작업입니다.", "Unknown clear action."),
389
388
  ("봇에 연결된 프로젝트가 없거나 레지스트리에서 찾을 수 없습니다.", "No project is bound to this bot or found in the registry."),
@@ -413,6 +412,7 @@ _TEXT_REPLACEMENTS_KO_TO_EN_RAW: tuple[tuple[str, str], ...] = (
413
412
  ("로컬 브랜치가 없습니다:", "No local branch:"),
414
413
  ("plan 모드로 실행할 작업 지시문을 보내주세요.", "Send the instruction to run in plan mode."),
415
414
  ("ask 모드로 실행할 질문을 보내주세요.", "Send the question to run in ask mode."),
415
+ ("research 모드로 실행할 조사 질문을 보내주세요.", "Send the research question to run in research mode."),
416
416
  ("읽기 전용 · 커밋·push 없음", "read-only — no commit/push"),
417
417
  ("코드 수정·커밋·push 가능", "allows edit, commit, and push"),
418
418
  ("요청 브랜치", "Requested branch"),
@@ -512,6 +512,16 @@ _MESSAGE_CATALOG_KO: dict[str, str] = {
512
512
  "원격 브랜치 `{branch}`를 `{remote}`에서 찾을 수 없습니다. "
513
513
  "Job 완료 후 삭제되었을 수 있습니다."
514
514
  ),
515
+ "pr.gh_missing": (
516
+ "/pr 실패: GitHub CLI(gh)가 설치되어 있지 않거나 PATH에서 찾을 수 없습니다.\n\n"
517
+ "Telegram에서 PR을 만들려면 다음 순서로 준비하세요.\n"
518
+ "1. GitHub CLI 설치:\n"
519
+ " - macOS: `brew install gh`\n"
520
+ " - Windows: `winget install --id GitHub.cli`\n"
521
+ " - Ubuntu/Debian: `sudo apt install gh`\n"
522
+ "2. GitHub 로그인: `gh auth login`\n"
523
+ "3. Remote AI Coder 재시작: `remote-coder up`"
524
+ ),
515
525
  "job.heartbeat": "{accepted}\n\n⏳ 실행 중 ({minutes}분 경과)",
516
526
  "job.cancelled": "{mode_prefix}⛔ 작업 중단됨\n\n- Job ID: {job_id}{session_line}\n- 프로젝트: {project}",
517
527
  "job.readonly_completed": (
@@ -634,6 +644,7 @@ _BUTTON_LABELS = {
634
644
  "AGENTS mode": "AGENTS 모드",
635
645
  "PLAN mode": "PLAN 모드",
636
646
  "ASK mode": "ASK 모드",
647
+ "RESEARCH mode": "RESEARCH 모드",
637
648
  }
638
649
 
639
650