remote-coder 0.5.1__tar.gz → 0.5.3__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 (162) hide show
  1. {remote_coder-0.5.1 → remote_coder-0.5.3}/PKG-INFO +6 -3
  2. {remote_coder-0.5.1 → remote_coder-0.5.3}/README.md +5 -2
  3. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/__init__.py +1 -1
  4. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/router.py +2 -1
  5. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/i18n.js +5 -0
  6. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/templates/admin.html +4 -1
  7. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/base.py +100 -16
  8. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/codex.py +3 -3
  9. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/gemini.py +2 -2
  10. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/cli.py +5 -1
  11. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/diagnostics.py +12 -1
  12. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/execution_pipeline.py +6 -5
  13. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/fix_pipeline.py +3 -0
  14. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/manager.py +13 -0
  15. remote_coder-0.5.3/app/jobs/result_writer.py +223 -0
  16. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/schemas.py +8 -0
  17. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/store.py +43 -0
  18. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/worktree_planner.py +2 -2
  19. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/main.py +40 -1
  20. remote_coder-0.5.3/app/system_startup.py +94 -0
  21. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/__init__.py +2 -1
  22. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/base.py +22 -4
  23. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/branch.py +24 -4
  24. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/registry.py +10 -2
  25. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/status.py +70 -0
  26. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/system.py +13 -1
  27. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/command_flow.py +1 -1
  28. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/job_submission.py +50 -64
  29. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/natural_flow.py +6 -1
  30. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/presenters.py +9 -6
  31. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/update_handler.py +7 -2
  32. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/i18n.py +25 -10
  33. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/messages.py +12 -5
  34. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/notifier.py +9 -2
  35. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/notifier_protocol.py +8 -1
  36. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/parser.py +14 -12
  37. {remote_coder-0.5.1 → remote_coder-0.5.3}/pyproject.toml +1 -1
  38. {remote_coder-0.5.1 → remote_coder-0.5.3}/remote_coder.egg-info/PKG-INFO +6 -3
  39. {remote_coder-0.5.1 → remote_coder-0.5.3}/remote_coder.egg-info/SOURCES.txt +4 -0
  40. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_admin_router.py +2 -0
  41. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_ai_base.py +8 -0
  42. remote_coder-0.5.3/tests/test_base_runner.py +43 -0
  43. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_claude_runner.py +24 -0
  44. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_cli.py +2 -1
  45. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_codex_runner.py +23 -0
  46. remote_coder-0.5.3/tests/test_command_flow.py +83 -0
  47. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_command_parser.py +25 -0
  48. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_commands.py +282 -4
  49. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_diagnostics.py +14 -0
  50. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_gemini_runner.py +26 -0
  51. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_job_manager.py +145 -0
  52. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_job_store.py +108 -0
  53. remote_coder-0.5.3/tests/test_job_submission.py +44 -0
  54. remote_coder-0.5.3/tests/test_main_lifespan.py +99 -0
  55. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_notifier.py +40 -5
  56. remote_coder-0.5.3/tests/test_result_writer.py +68 -0
  57. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_system_startup.py +83 -1
  58. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_webhook.py +109 -2
  59. remote_coder-0.5.1/app/jobs/result_writer.py +0 -104
  60. remote_coder-0.5.1/app/system_startup.py +0 -34
  61. remote_coder-0.5.1/tests/test_main_lifespan.py +0 -67
  62. {remote_coder-0.5.1 → remote_coder-0.5.3}/LICENSE +0 -0
  63. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/__init__.py +0 -0
  64. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/advanced_settings.py +0 -0
  65. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/database_browser.py +0 -0
  66. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/admin.css +0 -0
  67. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/icons/advanced.svg +0 -0
  68. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/icons/database.svg +0 -0
  69. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/icons/download.svg +0 -0
  70. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/icons/home.svg +0 -0
  71. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/icons/logs.svg +0 -0
  72. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/icons/projects.svg +0 -0
  73. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/summary.js +0 -0
  74. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/templates/advanced.html +0 -0
  75. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/templates/database.html +0 -0
  76. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/templates/logs.html +0 -0
  77. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/templates/projects.html +0 -0
  78. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/__init__.py +0 -0
  79. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/claude.py +0 -0
  80. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/factory.py +0 -0
  81. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/model_catalog.py +0 -0
  82. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/usage.py +0 -0
  83. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/config.py +0 -0
  84. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/git/__init__.py +0 -0
  85. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/git/ai_commit.py +0 -0
  86. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/git/branch_naming.py +0 -0
  87. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/git/commit_message.py +0 -0
  88. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/git/service.py +0 -0
  89. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/git/worktree_service.py +0 -0
  90. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/__init__.py +0 -0
  91. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/fix_support.py +0 -0
  92. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/heartbeat.py +0 -0
  93. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/plan_decisions.py +0 -0
  94. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/models.py +0 -0
  95. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/__init__.py +0 -0
  96. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/code.py +0 -0
  97. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/events.py +0 -0
  98. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/git.py +0 -0
  99. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/log_buffer.py +0 -0
  100. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/memory.py +0 -0
  101. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/model.py +0 -0
  102. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/projects/__init__.py +0 -0
  103. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/projects/registry.py +0 -0
  104. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/security/__init__.py +0 -0
  105. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/security/auth.py +0 -0
  106. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/__init__.py +0 -0
  107. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/bot_instances.py +0 -0
  108. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/clear_stop.py +0 -0
  109. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/fix.py +0 -0
  110. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/model.py +0 -0
  111. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/monitor.py +0 -0
  112. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/confirmations.py +0 -0
  113. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/__init__.py +0 -0
  114. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/collaborators.py +0 -0
  115. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/context.py +0 -0
  116. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/models.py +0 -0
  117. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/protocols.py +0 -0
  118. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/sqlite_rows.py +0 -0
  119. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/sqlite_store.py +0 -0
  120. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/store.py +0 -0
  121. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/formatting.py +0 -0
  122. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/__init__.py +0 -0
  123. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/callback_dispatcher.py +0 -0
  124. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/fix_flow.py +0 -0
  125. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/plan_flow.py +0 -0
  126. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/recent_updates.py +0 -0
  127. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/request.py +0 -0
  128. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/session_binding.py +0 -0
  129. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/lists.py +0 -0
  130. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/model_preferences.py +0 -0
  131. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/plan_decisions_flow.py +0 -0
  132. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/webhook.py +0 -0
  133. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/webhook_registration.py +0 -0
  134. {remote_coder-0.5.1 → remote_coder-0.5.3}/app/tunnel.py +0 -0
  135. {remote_coder-0.5.1 → remote_coder-0.5.3}/remote_coder.egg-info/dependency_links.txt +0 -0
  136. {remote_coder-0.5.1 → remote_coder-0.5.3}/remote_coder.egg-info/entry_points.txt +0 -0
  137. {remote_coder-0.5.1 → remote_coder-0.5.3}/remote_coder.egg-info/requires.txt +0 -0
  138. {remote_coder-0.5.1 → remote_coder-0.5.3}/remote_coder.egg-info/top_level.txt +0 -0
  139. {remote_coder-0.5.1 → remote_coder-0.5.3}/setup.cfg +0 -0
  140. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_ai_commit.py +0 -0
  141. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_ai_factory.py +0 -0
  142. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_auth.py +0 -0
  143. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_bot_instance_manager.py +0 -0
  144. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_branch_naming.py +0 -0
  145. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_commit_message_formatter.py +0 -0
  146. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_config.py +0 -0
  147. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_conversation_store.py +0 -0
  148. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_database_browser.py +0 -0
  149. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_event_logger.py +0 -0
  150. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_git_service.py +0 -0
  151. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_i18n.py +0 -0
  152. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_job_status.py +0 -0
  153. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_lists.py +0 -0
  154. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_log_buffer.py +0 -0
  155. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_monitoring.py +0 -0
  156. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_plan_decisions.py +0 -0
  157. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_project_registry.py +0 -0
  158. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_project_scoped_state.py +0 -0
  159. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_telegram_formatting.py +0 -0
  160. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_tunnel.py +0 -0
  161. {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_webhook_multibot.py +0 -0
  162. {remote_coder-0.5.1 → remote_coder-0.5.3}/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.3
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
 
@@ -292,7 +292,8 @@ Each project uses its own bot. The webhook path contains the first 16 hex charac
292
292
  |---|---|
293
293
  | `/start`, `/help` | Open the menu or command help |
294
294
  | `/model` | View or change the chat's default model |
295
- | `/status [job_id]` | Inspect recent or specific jobs |
295
+ | `/status [job_id]` | Inspect recent or specific jobs; running jobs show the latest captured AI output when available |
296
+ | `/log [job_id]` | Pick a recent job or show captured AI stdout for a specific job |
296
297
  | `/branch [name]` | Show or switch the bound project's local branch |
297
298
  | `/pull` | Fetch remotes and pull the current branch |
298
299
  | `/rebase [branch]` | Rebase and fast-forward a completed branch into `main` or `master` |
@@ -311,6 +312,7 @@ Natural-language examples:
311
312
  Fix the login validation bug with model: codex
312
313
  plan: outline the migration before changing code
313
314
  /ask what test command does this repo use?
315
+ /research compare current Telegram webhook security guidance
314
316
  수정: 방금 작업에서 README 문구만 더 간결하게 바꿔줘
315
317
  ```
316
318
 
@@ -321,6 +323,7 @@ Day-to-day setup happens in the local admin UI. Files live under `REMOTE_CODER_H
321
323
  - `projects.json` stores project records, bot tokens, allowlists, root paths, and default models.
322
324
  - `advanced_settings.json` stores global behavior such as timeouts, sandbox mode, language, worktree retention, and memory limits.
323
325
  - `worktrees/<project>/` contains managed job worktrees and logs.
326
+ - On server startup, queued Jobs stored in SQLite are rerun; Jobs that were running when the server stopped are marked failed with `server_restart` for `/status` review.
324
327
 
325
328
  Useful overrides: `REMOTE_CODER_HOME`, `PROJECTS_CONFIG_PATH`, `CONVERSATION_DB_PATH`, and `JOB_DB_PATH`.
326
329
 
@@ -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
 
@@ -71,7 +71,8 @@ Each project uses its own bot. The webhook path contains the first 16 hex charac
71
71
  |---|---|
72
72
  | `/start`, `/help` | Open the menu or command help |
73
73
  | `/model` | View or change the chat's default model |
74
- | `/status [job_id]` | Inspect recent or specific jobs |
74
+ | `/status [job_id]` | Inspect recent or specific jobs; running jobs show the latest captured AI output when available |
75
+ | `/log [job_id]` | Pick a recent job or show captured AI stdout for a specific job |
75
76
  | `/branch [name]` | Show or switch the bound project's local branch |
76
77
  | `/pull` | Fetch remotes and pull the current branch |
77
78
  | `/rebase [branch]` | Rebase and fast-forward a completed branch into `main` or `master` |
@@ -90,6 +91,7 @@ Natural-language examples:
90
91
  Fix the login validation bug with model: codex
91
92
  plan: outline the migration before changing code
92
93
  /ask what test command does this repo use?
94
+ /research compare current Telegram webhook security guidance
93
95
  수정: 방금 작업에서 README 문구만 더 간결하게 바꿔줘
94
96
  ```
95
97
 
@@ -100,6 +102,7 @@ Day-to-day setup happens in the local admin UI. Files live under `REMOTE_CODER_H
100
102
  - `projects.json` stores project records, bot tokens, allowlists, root paths, and default models.
101
103
  - `advanced_settings.json` stores global behavior such as timeouts, sandbox mode, language, worktree retention, and memory limits.
102
104
  - `worktrees/<project>/` contains managed job worktrees and logs.
105
+ - On server startup, queued Jobs stored in SQLite are rerun; Jobs that were running when the server stopped are marked failed with `server_restart` for `/status` review.
103
106
 
104
107
  Useful overrides: `REMOTE_CODER_HOME`, `PROJECTS_CONFIG_PATH`, `CONVERSATION_DB_PATH`, and `JOB_DB_PATH`.
105
108
 
@@ -1,3 +1,3 @@
1
1
  """Remote AI Coder application package."""
2
2
 
3
- __version__ = "0.5.1"
3
+ __version__ = "0.5.3"
@@ -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() {
@@ -4,9 +4,11 @@ import re
4
4
  import subprocess
5
5
  import threading
6
6
  from abc import ABC, abstractmethod
7
+ from collections.abc import Callable
7
8
  from dataclasses import dataclass, field
8
9
  from datetime import UTC, datetime
9
10
  from pathlib import Path
11
+ from typing import Literal
10
12
 
11
13
  from app.jobs.schemas import JobMode
12
14
  from app.monitoring.events import EventLogger
@@ -57,6 +59,18 @@ _PLAN_DECISIONS_INSTRUCTION = (
57
59
  )
58
60
 
59
61
 
62
+ _RESEARCH_INSTRUCTION = (
63
+ "You are in RESEARCH mode. Read the repository context and answer the user's research "
64
+ "question. Do not modify files.\n\n"
65
+ "Use internet search when it is useful or necessary for the question, similar to a deep "
66
+ "research workflow. Compare multiple perspectives or sources when possible, and clearly "
67
+ "separate repository-derived facts from external findings. Include citations or source "
68
+ "links for external claims, call out uncertainty or limitations, and finish with a direct "
69
+ "answer to the user's problem.\n\n"
70
+ "User research request:\n"
71
+ )
72
+
73
+
60
74
  def instruction_for_runner_mode(instruction: str, mode: JobMode) -> str:
61
75
  if mode == JobMode.PLAN:
62
76
  return f"{_PLAN_DECISIONS_INSTRUCTION}{instruction}"
@@ -66,6 +80,8 @@ def instruction_for_runner_mode(instruction: str, mode: JobMode) -> str:
66
80
  "Do not modify files.\n\n"
67
81
  f"User question:\n{instruction}"
68
82
  )
83
+ if mode == JobMode.RESEARCH:
84
+ return f"{_RESEARCH_INSTRUCTION}{instruction}"
69
85
  return instruction
70
86
 
71
87
 
@@ -81,6 +97,9 @@ class RunnerInput:
81
97
  session_id: str | None = None
82
98
  resume_token: str | None = None
83
99
  native_resume_cwd_stable: bool = True
100
+ output_callback: Callable[[Literal["stdout", "stderr"], str], None] | None = field(
101
+ default=None, compare=False
102
+ )
84
103
 
85
104
 
86
105
  @dataclass
@@ -199,23 +218,30 @@ class BaseCliRunner(AiRunner):
199
218
  cancelled.set()
200
219
 
201
220
  threading.Thread(target=_watch, daemon=True).start()
202
- try:
203
- stdout_data, stderr_data = proc.communicate(timeout=runner_input.timeout_seconds)
204
- except subprocess.TimeoutExpired as exc:
205
- proc.kill()
206
- stdout_data, stderr_data = proc.communicate()
207
- self._log.warning(
208
- "timeout after %ds stdout_len=%d stderr_len=%d",
209
- runner_input.timeout_seconds,
210
- len(stdout_data),
211
- len(stderr_data),
221
+ if runner_input.output_callback is None:
222
+ try:
223
+ stdout_data, stderr_data = proc.communicate(timeout=runner_input.timeout_seconds)
224
+ except subprocess.TimeoutExpired as exc:
225
+ proc.kill()
226
+ stdout_data, stderr_data = proc.communicate()
227
+ self._log.warning(
228
+ "timeout after %ds stdout_len=%d stderr_len=%d",
229
+ runner_input.timeout_seconds,
230
+ len(stdout_data),
231
+ len(stderr_data),
232
+ )
233
+ raise RunnerExecutionError(
234
+ f"runner timed out after {runner_input.timeout_seconds}s",
235
+ stdout=stdout_data,
236
+ stderr=stderr_data,
237
+ started_at=started_at,
238
+ ) from exc
239
+ else:
240
+ stdout_data, stderr_data = self._communicate_with_output_callback(
241
+ proc,
242
+ runner_input,
243
+ started_at,
212
244
  )
213
- raise RunnerExecutionError(
214
- f"runner timed out after {runner_input.timeout_seconds}s",
215
- stdout=stdout_data,
216
- stderr=stderr_data,
217
- started_at=started_at,
218
- ) from exc
219
245
  finished_at = datetime.now(UTC)
220
246
  if cancelled.is_set():
221
247
  raise RunnerExecutionError(
@@ -241,3 +267,61 @@ class BaseCliRunner(AiRunner):
241
267
  finished_at=finished_at,
242
268
  session_id=self._resolve_result_session_id(runner_input, session_files_before),
243
269
  )
270
+
271
+ def _communicate_with_output_callback(
272
+ self,
273
+ proc: subprocess.Popen[str],
274
+ runner_input: RunnerInput,
275
+ started_at: datetime,
276
+ ) -> tuple[str, str]:
277
+ output_callback = runner_input.output_callback
278
+ if output_callback is None:
279
+ raise ValueError("output_callback is required")
280
+ stdout_parts: list[str] = []
281
+ stderr_parts: list[str] = []
282
+
283
+ def _read_stream(
284
+ stream_name: Literal["stdout", "stderr"],
285
+ parts: list[str],
286
+ ) -> None:
287
+ stream = proc.stdout if stream_name == "stdout" else proc.stderr
288
+ if stream is None:
289
+ return
290
+ while True:
291
+ chunk = stream.readline()
292
+ if chunk == "":
293
+ break
294
+ parts.append(chunk)
295
+ try:
296
+ output_callback(stream_name, chunk)
297
+ except Exception: # pylint: disable=broad-except
298
+ self._log.warning("output callback failed stream=%s", stream_name)
299
+
300
+ stdout_thread = threading.Thread(target=_read_stream, args=("stdout", stdout_parts))
301
+ stderr_thread = threading.Thread(target=_read_stream, args=("stderr", stderr_parts))
302
+ stdout_thread.start()
303
+ stderr_thread.start()
304
+ try:
305
+ proc.wait(timeout=runner_input.timeout_seconds)
306
+ except subprocess.TimeoutExpired as exc:
307
+ proc.kill()
308
+ proc.wait()
309
+ stdout_thread.join()
310
+ stderr_thread.join()
311
+ stdout_data = "".join(stdout_parts)
312
+ stderr_data = "".join(stderr_parts)
313
+ self._log.warning(
314
+ "timeout after %ds stdout_len=%d stderr_len=%d",
315
+ runner_input.timeout_seconds,
316
+ len(stdout_data),
317
+ len(stderr_data),
318
+ )
319
+ raise RunnerExecutionError(
320
+ f"runner timed out after {runner_input.timeout_seconds}s",
321
+ stdout=stdout_data,
322
+ stderr=stderr_data,
323
+ started_at=started_at,
324
+ ) from exc
325
+ stdout_thread.join()
326
+ stderr_thread.join()
327
+ return "".join(stdout_parts), "".join(stderr_parts)
@@ -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()
@@ -83,6 +83,7 @@ def run_job(manager, job_id: str) -> Job:
83
83
  len(job.request.instruction),
84
84
  **manager._job_ctx(job),
85
85
  )
86
+ runner_log = manager._start_incremental_runner_log(job, worktree_base)
86
87
  heartbeat = manager._start_heartbeat(job)
87
88
  try:
88
89
  runner_result = runner.run(
@@ -97,10 +98,12 @@ def run_job(manager, job_id: str) -> Job:
97
98
  session_id=job.request.session_id,
98
99
  resume_token=job.request.resume_session_token,
99
100
  native_resume_cwd_stable=not created_worktree_for_job,
101
+ output_callback=runner_log.output_callback,
100
102
  )
101
103
  )
102
104
  finally:
103
105
  heartbeat.set()
106
+ runner_log.flush()
104
107
  manager._save_runner_log(job, runner_result, worktree_base)
105
108
  _joblog.info(
106
109
  "runner exit=%d stdout_len=%d stderr_len=%d",
@@ -238,9 +241,7 @@ def run_job(manager, job_id: str) -> Job:
238
241
  finally:
239
242
  manager._cancel_events.pop(job_id, None)
240
243
  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
- )
244
+ read_only_succeeded = is_read_only_job_mode(job.request.mode) and job.status.value == "succeeded"
244
245
  cleanup_on_success = read_only_succeeded or not manager._effective_keep_worktree_on_success()
245
246
  _joblog.info(
246
247
  "job finalizing status=%s created_worktree=%s cleanup_on_success=%s",
@@ -79,6 +79,7 @@ def run_fix_job(manager, job_id: str) -> Job:
79
79
  runner = manager._runner_factory.create(job.request.model)
80
80
  timeout_seconds = manager._effective_job_timeout_seconds()
81
81
  fix_prompt = manager.compose_fix_source_prompt(parent_job, job.request.instruction)
82
+ runner_log = manager._start_incremental_runner_log(job, worktree_base)
82
83
  heartbeat = manager._start_heartbeat(job)
83
84
  try:
84
85
  runner_result = runner.run(
@@ -93,10 +94,12 @@ def run_fix_job(manager, job_id: str) -> Job:
93
94
  session_id=job.request.session_id,
94
95
  resume_token=job.request.resume_session_token,
95
96
  native_resume_cwd_stable=not created_worktree_for_job,
97
+ output_callback=runner_log.output_callback,
96
98
  )
97
99
  )
98
100
  finally:
99
101
  heartbeat.set()
102
+ runner_log.flush()
100
103
  manager._save_runner_log(job, runner_result, worktree_base)
101
104
  if runner_result.exit_code != 0:
102
105
  raise RuntimeError(runner_result.stderr.strip() or "runner failed")
@@ -26,6 +26,7 @@ from app.jobs.result_writer import (
26
26
  make_output_summary,
27
27
  preserve_partial_output,
28
28
  save_runner_log,
29
+ start_incremental_runner_log,
29
30
  strip_links_for_stdout_summary,
30
31
  )
31
32
  from app.jobs.schemas import FixKind, Job, JobMode, JobRequest
@@ -186,6 +187,15 @@ class JobManager:
186
187
  with self._project_lock(job.request.project):
187
188
  return run_job(self, job_id)
188
189
 
190
+ def recover(self, job_id: str) -> Job:
191
+ job = self._job_store.get(job_id)
192
+ if job is None:
193
+ return run_job(self, job_id)
194
+ with self._project_lock(job.request.project):
195
+ if job.request.mode is JobMode.AGENT_FIX:
196
+ return self._run_fix(job_id)
197
+ return run_job(self, job_id)
198
+
189
199
  def _project_lock(self, project: str) -> threading.Lock:
190
200
  with self._project_locks_guard:
191
201
  lock = self._project_locks.get(project)
@@ -282,6 +292,9 @@ class JobManager:
282
292
  def _save_runner_log(self, job: Job, runner_result, worktree_base: Path) -> None:
283
293
  save_runner_log(job, runner_result, worktree_base)
284
294
 
295
+ def _start_incremental_runner_log(self, job: Job, worktree_base: Path):
296
+ return start_incremental_runner_log(job, worktree_base, self._job_store.update)
297
+
285
298
  @classmethod
286
299
  def _strip_links_for_stdout_summary(cls, text: str) -> str:
287
300
  return strip_links_for_stdout_summary(text)