@researai/deepscientist 1.5.12 → 1.5.14

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 (99) hide show
  1. package/bin/ds.js +20 -3
  2. package/docs/en/00_QUICK_START.md +24 -5
  3. package/docs/en/01_SETTINGS_REFERENCE.md +4 -0
  4. package/docs/en/05_TUI_GUIDE.md +466 -96
  5. package/docs/en/09_DOCTOR.md +24 -5
  6. package/docs/en/15_CODEX_PROVIDER_SETUP.md +113 -15
  7. package/docs/en/README.md +2 -0
  8. package/docs/zh/00_QUICK_START.md +24 -5
  9. package/docs/zh/01_SETTINGS_REFERENCE.md +4 -0
  10. package/docs/zh/05_TUI_GUIDE.md +465 -82
  11. package/docs/zh/09_DOCTOR.md +24 -5
  12. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +113 -15
  13. package/docs/zh/README.md +2 -0
  14. package/package.json +2 -1
  15. package/pyproject.toml +1 -1
  16. package/src/deepscientist/__init__.py +1 -1
  17. package/src/deepscientist/artifact/service.py +125 -2
  18. package/src/deepscientist/cli.py +3 -0
  19. package/src/deepscientist/codex_cli_compat.py +117 -0
  20. package/src/deepscientist/config/service.py +53 -6
  21. package/src/deepscientist/connector/lingzhu_support.py +23 -4
  22. package/src/deepscientist/daemon/app.py +111 -30
  23. package/src/deepscientist/mcp/server.py +161 -19
  24. package/src/deepscientist/prompts/builder.py +13 -54
  25. package/src/deepscientist/quest/service.py +99 -0
  26. package/src/deepscientist/quest/stage_views.py +134 -29
  27. package/src/deepscientist/runners/codex.py +11 -2
  28. package/src/deepscientist/runners/runtime_overrides.py +3 -0
  29. package/src/deepscientist/shared.py +6 -1
  30. package/src/prompts/system.md +220 -2065
  31. package/src/skills/baseline/SKILL.md +265 -994
  32. package/src/skills/baseline/references/artifact-payload-examples.md +39 -0
  33. package/src/skills/baseline/references/baseline-checklist-template.md +21 -32
  34. package/src/skills/baseline/references/baseline-plan-template.md +41 -57
  35. package/src/tui/dist/app/AppContainer.js +1442 -52
  36. package/src/tui/dist/components/Composer.js +1 -1
  37. package/src/tui/dist/components/ConfigScreen.js +190 -36
  38. package/src/tui/dist/components/GradientStatusText.js +1 -20
  39. package/src/tui/dist/components/InputPrompt.js +41 -32
  40. package/src/tui/dist/components/LoadingIndicator.js +1 -1
  41. package/src/tui/dist/components/Logo.js +61 -38
  42. package/src/tui/dist/components/MainContent.js +10 -3
  43. package/src/tui/dist/components/WelcomePanel.js +4 -12
  44. package/src/tui/dist/components/messages/AssistantMessage.js +1 -1
  45. package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -3
  46. package/src/tui/dist/components/messages/OperationMessage.js +1 -1
  47. package/src/tui/dist/index.js +28 -1
  48. package/src/tui/dist/layouts/DefaultAppLayout.js +3 -3
  49. package/src/tui/dist/lib/api.js +17 -0
  50. package/src/tui/dist/lib/connectorConfig.js +90 -0
  51. package/src/tui/dist/lib/connectors.js +261 -0
  52. package/src/tui/dist/lib/qr.js +21 -0
  53. package/src/tui/dist/semantic-colors.js +29 -19
  54. package/src/tui/package.json +2 -1
  55. package/src/ui/dist/assets/{AiManusChatView-CnJcXynW.js → AiManusChatView-DaF9Nge_.js} +12 -12
  56. package/src/ui/dist/assets/{AnalysisPlugin-DeyzPEhV.js → AnalysisPlugin-BSVx6dXE.js} +1 -1
  57. package/src/ui/dist/assets/{CliPlugin-CB1YODQn.js → CliPlugin-C9gzJX41.js} +9 -9
  58. package/src/ui/dist/assets/{CodeEditorPlugin-B-xicq1e.js → CodeEditorPlugin-DU9G0Tox.js} +8 -8
  59. package/src/ui/dist/assets/{CodeViewerPlugin-DT54ysXa.js → CodeViewerPlugin-DoX_fI9l.js} +5 -5
  60. package/src/ui/dist/assets/{DocViewerPlugin-DQtKT-VD.js → DocViewerPlugin-C4FWIXuU.js} +3 -3
  61. package/src/ui/dist/assets/{GitDiffViewerPlugin-hqHbCfnv.js → GitDiffViewerPlugin-BgfFMgtf.js} +20 -20
  62. package/src/ui/dist/assets/{ImageViewerPlugin-OcVo33jV.js → ImageViewerPlugin-tcPkfY_x.js} +5 -5
  63. package/src/ui/dist/assets/{LabCopilotPanel-DdGwhEUV.js → LabCopilotPanel-_dKV60Bf.js} +11 -11
  64. package/src/ui/dist/assets/{LabPlugin-Ciz1gDaX.js → LabPlugin-Bje0ayoC.js} +2 -2
  65. package/src/ui/dist/assets/{LatexPlugin-BhmjNQRC.js → LatexPlugin-CVsBzAln.js} +7 -7
  66. package/src/ui/dist/assets/{MarkdownViewerPlugin-BzdVH9Bx.js → MarkdownViewerPlugin-xjmrqv_8.js} +4 -4
  67. package/src/ui/dist/assets/{MarketplacePlugin-DmyHspXt.js → MarketplacePlugin-mMM2A8wP.js} +3 -3
  68. package/src/ui/dist/assets/{NotebookEditor-BTVYRGkm.js → NotebookEditor-3kVDSOBo.js} +11 -11
  69. package/src/ui/dist/assets/{NotebookEditor-BMXKrDRk.js → NotebookEditor-SoJ8X-MO.js} +1 -1
  70. package/src/ui/dist/assets/{PdfLoader-CvcjJHXv.js → PdfLoader-DElVuHl9.js} +1 -1
  71. package/src/ui/dist/assets/{PdfMarkdownPlugin-DW2ej8Vk.js → PdfMarkdownPlugin-Bq88XT4G.js} +2 -2
  72. package/src/ui/dist/assets/{PdfViewerPlugin-CmlDxbhU.js → PdfViewerPlugin-CsCXMo9S.js} +10 -10
  73. package/src/ui/dist/assets/{SearchPlugin-DAjQZPSv.js → SearchPlugin-oUPvy19k.js} +1 -1
  74. package/src/ui/dist/assets/{TextViewerPlugin-C-nVAZb_.js → TextViewerPlugin-CRkT9yNy.js} +5 -5
  75. package/src/ui/dist/assets/{VNCViewer-D7-dIYon.js → VNCViewer-BgbuvWhR.js} +10 -10
  76. package/src/ui/dist/assets/{bot-C_G4WtNI.js → bot-v_RASACv.js} +1 -1
  77. package/src/ui/dist/assets/{code-Cd7WfiWq.js → code-5hC9d0VH.js} +1 -1
  78. package/src/ui/dist/assets/{file-content-B57zsL9y.js → file-content-D1PxfOrp.js} +1 -1
  79. package/src/ui/dist/assets/{file-diff-panel-DVoheLFq.js → file-diff-panel-DG1oT_Hj.js} +1 -1
  80. package/src/ui/dist/assets/{file-socket-B5kXFxZP.js → file-socket-BmdFYQlk.js} +1 -1
  81. package/src/ui/dist/assets/{image-LLOjkMHF.js → image-Dqe2X2tW.js} +1 -1
  82. package/src/ui/dist/assets/{index-Dxa2eYMY.js → index-DVsMKK_y.js} +1 -1
  83. package/src/ui/dist/assets/{index-C3r2iGrp.js → index-Duvz8Ip0.js} +12 -12
  84. package/src/ui/dist/assets/{index-CLQauncb.js → index-Nt9hS4ck.js} +470 -165
  85. package/src/ui/dist/assets/{index-hOUOWbW2.js → index-RDlNXXx1.js} +2 -2
  86. package/src/ui/dist/assets/{monaco-BGGAEii3.js → monaco-DIXge1CP.js} +1 -1
  87. package/src/ui/dist/assets/{pdf-effect-queue-DlEr1_y5.js → pdf-effect-queue-BBTTQaO-.js} +1 -1
  88. package/src/ui/dist/assets/{popover-CWJbJuYY.js → popover-BWlolyxo.js} +1 -1
  89. package/src/ui/dist/assets/{project-sync-CRJiucYO.js → project-sync-BM5PkFH4.js} +1 -1
  90. package/src/ui/dist/assets/{select-CoHB7pvH.js → select-D4dAtrA8.js} +2 -2
  91. package/src/ui/dist/assets/{sigma-D5aJWR8J.js → sigma-CKbE5jJT.js} +1 -1
  92. package/src/ui/dist/assets/{square-check-big-DUK_mnkS.js → square-check-big-CZNGMgiB.js} +1 -1
  93. package/src/ui/dist/assets/{trash-ChU3SEE3.js → trash-DaB37xAz.js} +1 -1
  94. package/src/ui/dist/assets/{useCliAccess-BrJBV3tY.js → useCliAccess-C2OmAcWe.js} +1 -1
  95. package/src/ui/dist/assets/{useFileDiffOverlay-C2OQaVWc.js → useFileDiffOverlay-Dowd1Ij4.js} +1 -1
  96. package/src/ui/dist/assets/{wrap-text-C7Qqh-om.js → wrap-text-BGjAhAUq.js} +1 -1
  97. package/src/ui/dist/assets/{zoom-out-rtX0FKya.js → zoom-out-dMZQMXzc.js} +1 -1
  98. package/src/ui/dist/index.html +1 -1
  99. package/uv.lock +1 -1
@@ -25,12 +25,19 @@ ds
25
25
 
26
26
  ### 2. 临时使用 provider profile
27
27
 
28
- 如果你已经有一个可用的 Codex profile,例如 `minimax`、`glm`、`ark`、`bailian`,最简单的方式就是直接在启动 `ds` 时透传它。
28
+ 如果你已经有一个可用的 Codex profile,例如 `m27`、`glm`、`ark`、`bailian`,最简单的方式就是直接在启动 `ds` 时透传它。
29
29
 
30
30
  ```bash
31
- codex --profile minimax
32
- ds doctor --codex-profile minimax
33
- ds --codex-profile minimax
31
+ codex --profile m27
32
+ ds doctor --codex-profile m27
33
+ ds --codex-profile m27
34
+ ```
35
+
36
+ 如果你这一轮要强制指定某一个 Codex 可执行文件,也可以这样:
37
+
38
+ ```bash
39
+ ds doctor --codex /absolute/path/to/codex --codex-profile m27
40
+ ds --codex /absolute/path/to/codex --codex-profile m27
34
41
  ```
35
42
 
36
43
  这是最简单的路径。只是临时试用某个 provider 时,不需要先改 `runners.yaml`。
@@ -62,7 +69,7 @@ codex:
62
69
  | Provider | 官方文档 | 是否需要 Codex 登录 | DeepScientist 应该怎么用 |
63
70
  |---|---|---|---|
64
71
  | OpenAI | 正常 Codex 配置即可 | 是 | 不需要 profile,直接 `ds` |
65
- | MiniMax | [MiniMax Codex CLI](https://platform.minimaxi.com/docs/coding-plan/codex-cli) | 否 | 使用你自己的 Codex profile,例如 `ds --codex-profile minimax` |
72
+ | MiniMax | [MiniMax Codex CLI](https://platform.minimaxi.com/docs/coding-plan/codex-cli) | 否 | 使用你自己的 Codex profile,例如 `ds --codex-profile m27` |
66
73
  | GLM | [GLM Coding Plan:其他工具](https://docs.bigmodel.cn/cn/coding-plan/tool/others) | 否 | 使用一个指向 GLM coding endpoint 的 Codex profile |
67
74
  | 火山方舟 | [Ark Coding Plan 总览](https://www.volcengine.com/docs/82379/1925114?lang=zh) | 否 | 使用一个指向 Ark coding endpoint 的 Codex profile |
68
75
  | 阿里百炼 | [百炼 Coding Plan:其他工具](https://help.aliyun.com/zh/model-studio/other-tools-coding-plan) | 否 | 使用一个指向 Bailian coding endpoint 的 Codex profile |
@@ -100,15 +107,65 @@ MiniMax 是最典型的 profile 模式。它的官方 Codex CLI 文档直接给
100
107
 
101
108
  - <https://platform.minimaxi.com/docs/coding-plan/codex-cli>
102
109
 
110
+ ### 已验证的兼容性说明
111
+
112
+ 按 2026-03-25 对 MiniMax 官方 Codex CLI 页面和本地兼容性测试的核对结果:
113
+
114
+ - MiniMax 官方 Codex CLI 页面当前建议使用 `@openai/codex@0.57.0`
115
+ - MiniMax 当前应使用的 Coding Plan endpoint 是 `https://api.minimaxi.com/v1`
116
+ - MiniMax 官方页面示例 profile 名是 `m21`,但 profile 名本身只是本地别名;本仓库统一用 `m27` 作为示例名
117
+ - MiniMax 官方页面当前给出的 `codex-MiniMax-*` 模型名,在本地使用你提供的 key 实测并不能稳定通过 Codex CLI
118
+ - 本地实测能稳定跑通的组合是 `MiniMax-M2.7` + `m27` + `model: inherit` + Codex CLI `0.57.0`
119
+ - 当前最新版 `@openai/codex` 和 MiniMax 官方文档并不能稳定直接对齐
120
+
121
+ 如果你现在要走最稳的 DeepScientist + MiniMax 路径,建议直接使用 Codex CLI `0.57.0`。
122
+
103
123
  ### 需要准备什么
104
124
 
105
- - 已安装 Codex CLI
125
+ - 已安装 Codex CLI `0.57.0`
126
+ - 已创建 MiniMax `Coding Plan Key`
106
127
  - 在启动 Codex 和 DeepScientist 的 shell 中可见的 `MINIMAX_API_KEY`
128
+ - 当前 shell 已清理 `OPENAI_API_KEY` 和 `OPENAI_BASE_URL`
107
129
  - `~/.codex/config.toml` 中已经配置好的 Codex profile
108
130
 
131
+ ### 安装 Codex CLI `0.57.0`
132
+
133
+ 最直接的方式是把全局 Codex 安装固定到 `0.57.0`:
134
+
135
+ ```bash
136
+ npm install -g @openai/codex@0.57.0
137
+ codex --version
138
+ ```
139
+
140
+ 预期输出:
141
+
142
+ ```text
143
+ codex-cli 0.57.0
144
+ ```
145
+
146
+ 如果你还想保留另一个 Codex 版本,也可以单独写一个 wrapper 脚本,再把 `runners.codex.binary` 指向那个绝对路径。
147
+
109
148
  ### Codex 侧配置
110
149
 
111
- MiniMax 官方页面给了真实的 Codex custom provider 示例。profile 名称由你自己决定。下面用 `minimax` 作为示例;如果你已经配置成 `m27`,就继续使用 `m27`。
150
+ 请使用 `https://api.minimaxi.com/v1`,不要用 `https://api.minimax.io/v1`。
151
+
152
+ MiniMax 官方文档要求在配置前先清理 OpenAI 环境变量:
153
+
154
+ ```bash
155
+ unset OPENAI_API_KEY
156
+ unset OPENAI_BASE_URL
157
+ export MINIMAX_API_KEY="..."
158
+ ```
159
+
160
+ MiniMax 官方页面示例 profile 名是 `m21`。由于 profile 名只是本地别名,本仓库统一改写成 `m27`。
161
+
162
+ 先说明差异:
163
+
164
+ - 官方页面当前展示的是 `codex-MiniMax-M2.5`
165
+ - 但本地实测里,直接请求 MiniMax API 能稳定跑通的是 `MiniMax-M2.7`
166
+ - 同一把 key 下,`codex-MiniMax-M2.5` / `codex-MiniMax-M2.7` 通过 Codex CLI 都会失败
167
+
168
+ 因此,下面给的是当前 DeepScientist 推荐的可运行配置:
112
169
 
113
170
  ```toml
114
171
  [model_providers.minimax]
@@ -121,23 +178,50 @@ request_max_retries = 4
121
178
  stream_max_retries = 10
122
179
  stream_idle_timeout_ms = 300000
123
180
 
124
- [profiles.minimax]
125
- model = "codex-MiniMax-M2.5"
181
+ [profiles.m27]
182
+ model = "MiniMax-M2.7"
183
+ model_provider = "minimax"
184
+ ```
185
+
186
+ DeepScientist 现在对它的支持方式是:
187
+
188
+ - 如果你使用的是这类 profile-only MiniMax 配置,再配合 Codex CLI `0.57.0`,DeepScientist 会在自己的 probe / 运行时临时 `.codex/config.toml` 里,把所选 profile 的 `model_provider` 和 `model` 自动提升到顶层
189
+ - 这意味着即使终端里原样执行 `codex --profile m27` 还会失败,DeepScientist 也可以先兼容跑起来
190
+
191
+ 如果你还希望终端里的 `codex --profile <name>` 也直接可用,请使用显式顶层兼容写法:
192
+
193
+ ```toml
194
+ model = "MiniMax-M2.7"
195
+ model_provider = "minimax"
196
+ approval_policy = "never"
197
+ sandbox_mode = "workspace-write"
198
+
199
+ [model_providers.minimax]
200
+ name = "MiniMax Chat Completions API"
201
+ base_url = "https://api.minimaxi.com/v1"
202
+ env_key = "MINIMAX_API_KEY"
203
+ wire_api = "chat"
204
+ requires_openai_auth = false
205
+ request_max_retries = 4
206
+ stream_max_retries = 10
207
+ stream_idle_timeout_ms = 300000
208
+
209
+ [profiles.m27]
210
+ model = "MiniMax-M2.7"
126
211
  model_provider = "minimax"
127
212
  ```
128
213
 
129
214
  然后执行:
130
215
 
131
216
  ```bash
132
- export MINIMAX_API_KEY="..."
133
- codex --profile minimax
217
+ codex --profile m27
134
218
  ```
135
219
 
136
220
  ### DeepScientist 命令
137
221
 
138
222
  ```bash
139
- ds doctor --codex-profile minimax
140
- ds --codex-profile minimax
223
+ ds doctor --codex-profile m27
224
+ ds --codex-profile m27
141
225
  ```
142
226
 
143
227
  ### 持久化 runner 配置
@@ -145,12 +229,26 @@ ds --codex-profile minimax
145
229
  ```yaml
146
230
  codex:
147
231
  enabled: true
148
- binary: codex
232
+ binary: /tmp/codex057-wrapper
149
233
  config_dir: ~/.codex
150
- profile: minimax
234
+ profile: m27
151
235
  model: inherit
236
+ model_reasoning_effort: high
152
237
  ```
153
238
 
239
+ 如果你已经把全局 `codex` 固定到 `0.57.0`,也可以把 `binary` 写回 `codex`。这里写绝对路径只是为了明确避免误用系统里其他版本的 Codex。
240
+
241
+ 如果你不想把这个路径持久化写进 `runners.yaml`,也可以保留 `binary: codex`,然后在启动时临时加:
242
+
243
+ ```bash
244
+ ds --codex /absolute/path/to/codex --codex-profile m27
245
+ ```
246
+
247
+ DeepScientist 现在会为 MiniMax 的 `0.57.0` 路径额外做两层兼容:
248
+
249
+ - 当检测到旧版 Codex CLI 不支持 `xhigh` 时,自动把 `xhigh` 降级成 `high`
250
+ - 当检测到 MiniMax 使用 profile-only 的 `model_provider` / `model` 配置形态时,在临时 DeepScientist Codex home 里自动补齐顶层字段
251
+
154
252
  ## GLM
155
253
 
156
254
  GLM 的官方文档把 Coding Plan 描述成 OpenAI-compatible 的 coding endpoint,而不是单独的 Codex 登录流程。
package/docs/zh/README.md CHANGED
@@ -76,6 +76,8 @@ DeepScientist 灵活且易于使用,支持:
76
76
 
77
77
  - [00 快速开始](./00_QUICK_START.md)
78
78
  从安装、启动,到创建第一个项目,先看这一篇。
79
+ - [05 TUI 端到端指南](./05_TUI_GUIDE.md)
80
+ 如果你主要在服务器或终端里工作,这篇会带你从 `ds --tui` 一路走到 quest、connector 和跨端协作跑通。
79
81
  - [15 Codex Provider 配置](./15_CODEX_PROVIDER_SETUP.md)
80
82
  如果你准备通过 MiniMax、GLM、火山方舟、阿里百炼或其他 Codex profile 来运行 DeepScientist,先看这一篇。
81
83
  - [12 引导式工作流教程](./12_GUIDED_WORKFLOW_TOUR.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@researai/deepscientist",
3
- "version": "1.5.12",
3
+ "version": "1.5.14",
4
4
  "description": "DeepScientist is not just a fully open-source autonomous scientific discovery system. It is also a research map that keeps growing from every round.",
5
5
  "license": "Apache-2.0",
6
6
  "files": [
@@ -38,6 +38,7 @@
38
38
  "@openai/codex": "^0.114.0",
39
39
  "ink": "npm:@jrichman/ink@6.4.6",
40
40
  "ink-gradient": "^3.0.0",
41
+ "qrcode": "^1.5.4",
41
42
  "react": "^19.2.0",
42
43
  "react-dom": "^19.2.0",
43
44
  "string-width": "^8.1.0"
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepscientist"
7
- version = "1.5.12"
7
+ version = "1.5.14"
8
8
  description = "DeepScientist Core skeleton"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -5,4 +5,4 @@ __all__ = ["__version__"]
5
5
  try:
6
6
  __version__ = _package_version("deepscientist")
7
7
  except PackageNotFoundError: # pragma: no cover - source checkout fallback
8
- __version__ = "1.5.12"
8
+ __version__ = "1.5.14"
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  import shutil
5
- from pathlib import Path
5
+ from pathlib import Path, PurePosixPath
6
6
  from typing import Any
7
7
 
8
8
  from ..arxiv_library import ArxivLibraryService
@@ -693,6 +693,29 @@ class ArtifactService:
693
693
  except ValueError:
694
694
  return str(path)
695
695
 
696
+ def _paper_bundle_relative_path(
697
+ self,
698
+ quest_root: Path,
699
+ path: Path | None,
700
+ *,
701
+ workspace_root: Path | None = None,
702
+ ) -> str | None:
703
+ if path is None:
704
+ return None
705
+ resolved = path.resolve()
706
+ roots = [self._workspace_root_for(quest_root, workspace_root), quest_root]
707
+ seen: set[str] = set()
708
+ for root in roots:
709
+ key = str(root.resolve())
710
+ if key in seen:
711
+ continue
712
+ seen.add(key)
713
+ try:
714
+ return resolved.relative_to(root.resolve()).as_posix()
715
+ except ValueError:
716
+ continue
717
+ return str(path)
718
+
696
719
  @staticmethod
697
720
  def _branch_kind_from_name(branch_name: str | None) -> str:
698
721
  normalized = str(branch_name or "").strip()
@@ -1289,6 +1312,100 @@ class ArtifactService:
1289
1312
  def _paper_baseline_inventory_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
1290
1313
  return self._paper_root(quest_root, workspace_root=workspace_root, create=True) / "baseline_inventory.json"
1291
1314
 
1315
+ def _paper_bundle_path_candidates(
1316
+ self,
1317
+ quest_root: Path,
1318
+ raw_path: object,
1319
+ *,
1320
+ workspace_root: Path | None = None,
1321
+ ) -> list[Path]:
1322
+ text = str(raw_path or "").strip()
1323
+ if not text:
1324
+ return []
1325
+ candidate = Path(text).expanduser()
1326
+ roots = [self._workspace_root_for(quest_root, workspace_root), quest_root]
1327
+ resolved: list[Path] = []
1328
+ if candidate.is_absolute():
1329
+ try:
1330
+ resolved.append(candidate.resolve())
1331
+ except OSError:
1332
+ return []
1333
+ else:
1334
+ for root in roots:
1335
+ try:
1336
+ resolved.append((root / candidate).resolve())
1337
+ except OSError:
1338
+ continue
1339
+ deduped: list[Path] = []
1340
+ seen: set[str] = set()
1341
+ for item in resolved:
1342
+ key = str(item)
1343
+ if key in seen:
1344
+ continue
1345
+ seen.add(key)
1346
+ deduped.append(item)
1347
+ return deduped
1348
+
1349
+ def _paper_bundle_compile_report(
1350
+ self,
1351
+ quest_root: Path,
1352
+ *,
1353
+ workspace_root: Path | None = None,
1354
+ compile_report_path: object = None,
1355
+ ) -> dict[str, Any]:
1356
+ for candidate in self._paper_bundle_path_candidates(
1357
+ quest_root,
1358
+ compile_report_path,
1359
+ workspace_root=workspace_root,
1360
+ ):
1361
+ if not candidate.exists() or not candidate.is_file():
1362
+ continue
1363
+ payload = read_json(candidate, {})
1364
+ if isinstance(payload, dict):
1365
+ return payload
1366
+ return {}
1367
+
1368
+ def _normalize_paper_bundle_latex_root_path(
1369
+ self,
1370
+ quest_root: Path,
1371
+ *,
1372
+ workspace_root: Path | None = None,
1373
+ latex_root_path: object = None,
1374
+ compile_report_path: object = None,
1375
+ ) -> str | None:
1376
+ compile_report = self._paper_bundle_compile_report(
1377
+ quest_root,
1378
+ workspace_root=workspace_root,
1379
+ compile_report_path=compile_report_path,
1380
+ )
1381
+ for raw in (
1382
+ latex_root_path,
1383
+ compile_report.get("latex_root_path"),
1384
+ compile_report.get("main_file_path"),
1385
+ ):
1386
+ text = str(raw or "").strip()
1387
+ if not text:
1388
+ continue
1389
+ for candidate in self._paper_bundle_path_candidates(
1390
+ quest_root,
1391
+ text,
1392
+ workspace_root=workspace_root,
1393
+ ):
1394
+ if candidate.exists() and candidate.is_dir():
1395
+ return self._paper_bundle_relative_path(quest_root, candidate, workspace_root=workspace_root) or text
1396
+ if candidate.suffix.lower() == ".tex":
1397
+ return self._paper_bundle_relative_path(
1398
+ quest_root,
1399
+ candidate.parent,
1400
+ workspace_root=workspace_root,
1401
+ ) or PurePosixPath(text).parent.as_posix()
1402
+ if Path(text).suffix.lower() == ".tex":
1403
+ parent = PurePosixPath(text).parent.as_posix()
1404
+ if parent not in {"", "."}:
1405
+ return parent
1406
+ return text
1407
+ return None
1408
+
1292
1409
  def _open_source_root(
1293
1410
  self,
1294
1411
  quest_root: Path,
@@ -5696,6 +5813,12 @@ class ArtifactService:
5696
5813
  default_compile_report_path = (
5697
5814
  self._workspace_relative(quest_root, paper_root / "build" / "compile_report.json") or "paper/build/compile_report.json"
5698
5815
  )
5816
+ normalized_latex_root_path = self._normalize_paper_bundle_latex_root_path(
5817
+ quest_root,
5818
+ workspace_root=workspace_root,
5819
+ latex_root_path=latex_root_path,
5820
+ compile_report_path=compile_report_path or default_compile_report_path,
5821
+ )
5699
5822
  manifest = {
5700
5823
  "schema_version": 1,
5701
5824
  "title": str(
@@ -5717,7 +5840,7 @@ class ArtifactService:
5717
5840
  "claim_evidence_map_path": str(claim_evidence_map_path or default_claim_map_path).strip() or None,
5718
5841
  "compile_report_path": str(compile_report_path or default_compile_report_path).strip() or None,
5719
5842
  "pdf_path": str(pdf_path or "").strip() or None,
5720
- "latex_root_path": str(latex_root_path or "").strip() or None,
5843
+ "latex_root_path": normalized_latex_root_path,
5721
5844
  "baseline_inventory_path": paper_inventory_rel,
5722
5845
  "open_source_manifest_path": self._workspace_relative(
5723
5846
  quest_root,
@@ -39,6 +39,7 @@ def build_parser() -> argparse.ArgumentParser:
39
39
  parser = argparse.ArgumentParser(prog="ds", description="DeepScientist Core skeleton")
40
40
  parser.add_argument("--home", default=None, help="Override DeepScientist home")
41
41
  parser.add_argument("--proxy", default=None, help="Explicit outbound HTTP/WS proxy, for example `http://127.0.0.1:7890`.")
42
+ parser.add_argument("--codex", default=None, help="Override the Codex executable path for this invocation.")
42
43
 
43
44
  subparsers = parser.add_subparsers(dest="command", required=True)
44
45
 
@@ -475,6 +476,8 @@ def migrate_command(home: Path, target: str) -> int:
475
476
  def main(argv: list[str] | None = None) -> int:
476
477
  parser = build_parser()
477
478
  args = parser.parse_args(argv)
479
+ if args.codex:
480
+ os.environ["DEEPSCIENTIST_CODEX_BINARY"] = str(args.codex)
478
481
  configure_runtime_proxy(args.proxy)
479
482
  home = resolve_home(args)
480
483
 
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import subprocess
6
+ import tomllib
7
+ from functools import lru_cache
8
+
9
+ _MIN_XHIGH_SUPPORTED_VERSION = (0, 63, 0)
10
+ _CODEX_VERSION_PATTERN = re.compile(r"codex-cli\s+(\d+)\.(\d+)\.(\d+)", re.IGNORECASE)
11
+
12
+
13
+ def parse_codex_cli_version(text: str) -> tuple[int, int, int] | None:
14
+ match = _CODEX_VERSION_PATTERN.search(str(text or ""))
15
+ if not match:
16
+ return None
17
+ return tuple(int(part) for part in match.groups())
18
+
19
+
20
+ @lru_cache(maxsize=32)
21
+ def codex_cli_version(binary: str) -> tuple[int, int, int] | None:
22
+ normalized = str(binary or "").strip()
23
+ if not normalized:
24
+ return None
25
+ try:
26
+ result = subprocess.run(
27
+ [normalized, "--version"],
28
+ check=False,
29
+ capture_output=True,
30
+ text=True,
31
+ timeout=10,
32
+ )
33
+ except (OSError, subprocess.TimeoutExpired):
34
+ return None
35
+ return parse_codex_cli_version(f"{result.stdout}\n{result.stderr}")
36
+
37
+
38
+ def format_codex_cli_version(version: tuple[int, int, int] | None) -> str:
39
+ if version is None:
40
+ return ""
41
+ return ".".join(str(part) for part in version)
42
+
43
+
44
+ def normalize_codex_reasoning_effort(
45
+ reasoning_effort: str | None,
46
+ *,
47
+ resolved_binary: str | None,
48
+ ) -> tuple[str | None, str | None]:
49
+ normalized = str(reasoning_effort or "").strip()
50
+ if not normalized:
51
+ return None, None
52
+ if normalized.lower() != "xhigh":
53
+ return normalized, None
54
+
55
+ version = codex_cli_version(str(resolved_binary or ""))
56
+ if version is None or version >= _MIN_XHIGH_SUPPORTED_VERSION:
57
+ return normalized, None
58
+
59
+ version_text = format_codex_cli_version(version)
60
+ return (
61
+ "high",
62
+ (
63
+ f"Codex CLI {version_text} does not support `xhigh`; "
64
+ "DeepScientist downgraded reasoning effort to `high` automatically."
65
+ ),
66
+ )
67
+
68
+
69
+ def adapt_profile_only_provider_config(
70
+ config_text: str,
71
+ *,
72
+ profile: str,
73
+ ) -> tuple[str, str | None]:
74
+ normalized_profile = str(profile or "").strip()
75
+ if not normalized_profile or not str(config_text or "").strip():
76
+ return config_text, None
77
+ try:
78
+ parsed = tomllib.loads(config_text)
79
+ except tomllib.TOMLDecodeError:
80
+ return config_text, None
81
+
82
+ profiles = parsed.get("profiles")
83
+ if not isinstance(profiles, dict):
84
+ return config_text, None
85
+ profile_payload = profiles.get(normalized_profile)
86
+ if not isinstance(profile_payload, dict):
87
+ return config_text, None
88
+
89
+ prefix_lines: list[str] = []
90
+ injected_fields: list[str] = []
91
+ if "model_provider" not in parsed:
92
+ model_provider = str(profile_payload.get("model_provider") or "").strip()
93
+ if model_provider:
94
+ prefix_lines.append(f"model_provider = {json.dumps(model_provider, ensure_ascii=False)}")
95
+ injected_fields.append("model_provider")
96
+ if "model" not in parsed:
97
+ model = str(profile_payload.get("model") or "").strip()
98
+ if model:
99
+ prefix_lines.append(f"model = {json.dumps(model, ensure_ascii=False)}")
100
+ injected_fields.append("model")
101
+
102
+ if not prefix_lines:
103
+ return config_text, None
104
+
105
+ adapted = (
106
+ "# BEGIN DEEPSCIENTIST PROFILE COMPAT\n"
107
+ + "\n".join(prefix_lines)
108
+ + "\n# END DEEPSCIENTIST PROFILE COMPAT\n\n"
109
+ + config_text.lstrip()
110
+ )
111
+ return (
112
+ adapted,
113
+ (
114
+ f"DeepScientist promoted `{normalized_profile}` profile "
115
+ f"{', '.join(injected_fields)} to the top level for Codex compatibility."
116
+ ),
117
+ )
@@ -3,11 +3,14 @@ from __future__ import annotations
3
3
  import json
4
4
  import os
5
5
  import subprocess
6
+ import tempfile
7
+ from shutil import copy2
6
8
  from copy import deepcopy
7
9
  from pathlib import Path
8
10
  from urllib.error import URLError
9
11
  from urllib.request import Request
10
12
 
13
+ from ..codex_cli_compat import adapt_profile_only_provider_config, normalize_codex_reasoning_effort
11
14
  from ..connector.connector_profiles import PROFILEABLE_CONNECTOR_NAMES, list_connector_profiles, normalize_connector_config
12
15
  from ..connector_runtime import build_discovered_target, infer_connector_transport
13
16
  from ..home import repo_root
@@ -486,6 +489,7 @@ This page edits `{home_text}/config/runners.yaml`.
486
489
  - `claude` remains TODO / reserved in the current open-source release and is not runnable yet
487
490
  - set `codex.profile` only when your Codex CLI uses a named provider profile such as `m27`
488
491
  - when you launch DeepScientist ad hoc with a provider profile, you can also use `ds --codex-profile <name>`
492
+ - when you want a one-off Codex binary override, you can also use `ds --codex /absolute/path/to/codex`
489
493
  - keep `codex.model_reasoning_effort: xhigh` unless you explicitly want a lighter default
490
494
  - keep `codex.retry_on_failure: true` so transient Codex failures can resume automatically
491
495
  - keep retry timing near `10s / 6x / 1800s max` so Codex backs off exponentially and the last retry waits about 30 minutes
@@ -1206,6 +1210,31 @@ Use **Test** when the file exposes runtime dependencies.
1206
1210
  resolved[env_key] = str(value)
1207
1211
  return resolved
1208
1212
 
1213
+ def _prepare_codex_probe_home(
1214
+ self,
1215
+ *,
1216
+ config_dir: str,
1217
+ profile: str,
1218
+ ) -> tuple[str, str | None, tempfile.TemporaryDirectory[str] | None]:
1219
+ expanded = Path(config_dir).expanduser()
1220
+ config_path = expanded / "config.toml"
1221
+ if not config_path.exists():
1222
+ return str(expanded), None, None
1223
+
1224
+ original_text = read_text(config_path)
1225
+ adapted_text, warning = adapt_profile_only_provider_config(original_text, profile=profile)
1226
+ if warning is None:
1227
+ return str(expanded), None, None
1228
+
1229
+ temp_home = tempfile.TemporaryDirectory(prefix="ds-codex-probe-")
1230
+ temp_root = Path(temp_home.name)
1231
+ for filename in ("auth.json",):
1232
+ source_path = expanded / filename
1233
+ if source_path.exists():
1234
+ copy2(source_path, temp_root / filename)
1235
+ write_text(temp_root / "config.toml", adapted_text)
1236
+ return str(temp_root), warning, temp_home
1237
+
1209
1238
  def _codex_missing_binary_guidance(self, config: dict) -> list[str]:
1210
1239
  profile = self._codex_profile_name(config)
1211
1240
  guidance = [
@@ -1221,7 +1250,9 @@ Use **Test** when the file exposes runtime dependencies.
1221
1250
  )
1222
1251
  else:
1223
1252
  guidance.append("Run `codex --login` (or `codex`) once and finish authentication before starting DeepScientist.")
1224
- guidance.append("If you use a custom Codex path, set `runners.codex.binary` to that absolute executable path.")
1253
+ guidance.append(
1254
+ "If you use a custom Codex path, either set `runners.codex.binary` or launch with `ds --codex /absolute/path/to/codex`."
1255
+ )
1225
1256
  return guidance
1226
1257
 
1227
1258
  def _codex_probe_failure_guidance(self, config: dict) -> tuple[list[str], list[str]]:
@@ -1326,11 +1357,15 @@ Use **Test** when the file exposes runtime dependencies.
1326
1357
  profile = self._codex_profile_name(config)
1327
1358
  requested_model = self._codex_requested_model(config)
1328
1359
  raw_reasoning_effort = config.get("model_reasoning_effort")
1329
- reasoning_effort = (
1360
+ requested_reasoning_effort = (
1330
1361
  str(raw_reasoning_effort).strip()
1331
1362
  if raw_reasoning_effort is not None and str(raw_reasoning_effort).strip()
1332
1363
  else ("xhigh" if raw_reasoning_effort is None else None)
1333
1364
  )
1365
+ reasoning_effort, reasoning_effort_warning = normalize_codex_reasoning_effort(
1366
+ requested_reasoning_effort,
1367
+ resolved_binary=resolved_binary,
1368
+ )
1334
1369
  details: dict[str, object] = {
1335
1370
  "binary": binary,
1336
1371
  "resolved_binary": resolved_binary,
@@ -1342,6 +1377,7 @@ Use **Test** when the file exposes runtime dependencies.
1342
1377
  "approval_policy": str(config.get("approval_policy") or "on-request"),
1343
1378
  "sandbox_mode": str(config.get("sandbox_mode") or "workspace-write"),
1344
1379
  "reasoning_effort": reasoning_effort,
1380
+ "requested_reasoning_effort": requested_reasoning_effort,
1345
1381
  "model_fallback_attempted": False,
1346
1382
  "model_fallback_used": False,
1347
1383
  "checked_at": checked_at,
@@ -1365,9 +1401,20 @@ Use **Test** when the file exposes runtime dependencies.
1365
1401
  env = os.environ.copy()
1366
1402
  env.update(self._codex_runner_env(config))
1367
1403
  config_dir = str(config.get("config_dir") or "~/.codex").strip()
1404
+ probe_home_handle: tempfile.TemporaryDirectory[str] | None = None
1405
+ compatibility_warnings: list[str] = []
1368
1406
  if config_dir:
1369
- env["CODEX_HOME"] = str(Path(config_dir).expanduser())
1407
+ prepared_home, profile_config_warning, probe_home_handle = self._prepare_codex_probe_home(
1408
+ config_dir=config_dir,
1409
+ profile=profile,
1410
+ )
1411
+ env["CODEX_HOME"] = prepared_home
1412
+ if profile_config_warning:
1413
+ compatibility_warnings.append(profile_config_warning)
1370
1414
  prompt = "Reply with exactly HELLO."
1415
+ if reasoning_effort_warning:
1416
+ compatibility_warnings.append(reasoning_effort_warning)
1417
+ base_warnings: list[str] = list(compatibility_warnings)
1371
1418
 
1372
1419
  def run_probe_once(model_for_command: str) -> tuple[list[str], subprocess.CompletedProcess[str] | None, subprocess.TimeoutExpired | None]:
1373
1420
  command = self._build_codex_probe_command(
@@ -1406,7 +1453,7 @@ Use **Test** when the file exposes runtime dependencies.
1406
1453
  return {
1407
1454
  "ok": False,
1408
1455
  "summary": "Codex startup probe timed out.",
1409
- "warnings": [],
1456
+ "warnings": base_warnings,
1410
1457
  "errors": [
1411
1458
  "Codex did not answer the startup hello probe within 90 seconds.",
1412
1459
  *self._codex_probe_failure_guidance(config)[0],
@@ -1463,7 +1510,7 @@ Use **Test** when the file exposes runtime dependencies.
1463
1510
  return {
1464
1511
  "ok": True,
1465
1512
  "summary": "Codex startup probe completed with Codex default model fallback.",
1466
- "warnings": [fallback_warning],
1513
+ "warnings": [*base_warnings, fallback_warning],
1467
1514
  "errors": [],
1468
1515
  "details": details,
1469
1516
  "guidance": [
@@ -1483,7 +1530,7 @@ Use **Test** when the file exposes runtime dependencies.
1483
1530
  "probe_command": command,
1484
1531
  }
1485
1532
  )
1486
- warnings: list[str] = []
1533
+ warnings: list[str] = list(base_warnings)
1487
1534
  errors: list[str] = []
1488
1535
  if not ok:
1489
1536
  errors.append("Codex did not complete the startup hello probe successfully.")