@researai/deepscientist 1.5.14 → 1.5.16

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 (225) hide show
  1. package/README.md +336 -90
  2. package/assets/branding/logo-raster.png +0 -0
  3. package/bin/ds.js +816 -131
  4. package/docs/en/00_QUICK_START.md +36 -15
  5. package/docs/en/01_SETTINGS_REFERENCE.md +53 -4
  6. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  7. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
  8. package/docs/en/05_TUI_GUIDE.md +6 -0
  9. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  10. package/docs/en/09_DOCTOR.md +11 -5
  11. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  12. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
  13. package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
  14. package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  15. package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  16. package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  17. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  18. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  19. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  20. package/docs/en/README.md +24 -0
  21. package/docs/zh/00_QUICK_START.md +36 -15
  22. package/docs/zh/01_SETTINGS_REFERENCE.md +53 -4
  23. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  24. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
  25. package/docs/zh/05_TUI_GUIDE.md +6 -0
  26. package/docs/zh/09_DOCTOR.md +11 -5
  27. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  28. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
  29. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
  30. package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  31. package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  32. package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  33. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  34. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  35. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  36. package/docs/zh/README.md +24 -0
  37. package/install.sh +2 -0
  38. package/package.json +1 -1
  39. package/pyproject.toml +1 -1
  40. package/src/deepscientist/__init__.py +1 -1
  41. package/src/deepscientist/acp/envelope.py +6 -0
  42. package/src/deepscientist/artifact/charts.py +567 -0
  43. package/src/deepscientist/artifact/guidance.py +50 -10
  44. package/src/deepscientist/artifact/metrics.py +228 -5
  45. package/src/deepscientist/artifact/schemas.py +3 -0
  46. package/src/deepscientist/artifact/service.py +4276 -308
  47. package/src/deepscientist/bash_exec/models.py +23 -0
  48. package/src/deepscientist/bash_exec/monitor.py +147 -67
  49. package/src/deepscientist/bash_exec/runtime.py +218 -156
  50. package/src/deepscientist/bash_exec/service.py +309 -69
  51. package/src/deepscientist/bash_exec/shells.py +87 -0
  52. package/src/deepscientist/bridges/connectors.py +51 -2
  53. package/src/deepscientist/cli.py +115 -19
  54. package/src/deepscientist/codex_cli_compat.py +232 -0
  55. package/src/deepscientist/config/models.py +8 -4
  56. package/src/deepscientist/config/service.py +38 -11
  57. package/src/deepscientist/connector/weixin_support.py +122 -1
  58. package/src/deepscientist/daemon/api/handlers.py +199 -9
  59. package/src/deepscientist/daemon/api/router.py +5 -0
  60. package/src/deepscientist/daemon/app.py +1458 -289
  61. package/src/deepscientist/doctor.py +51 -0
  62. package/src/deepscientist/file_lock.py +48 -0
  63. package/src/deepscientist/gitops/__init__.py +10 -1
  64. package/src/deepscientist/gitops/diff.py +296 -1
  65. package/src/deepscientist/gitops/service.py +4 -1
  66. package/src/deepscientist/mcp/server.py +212 -5
  67. package/src/deepscientist/process_control.py +161 -0
  68. package/src/deepscientist/prompts/builder.py +501 -453
  69. package/src/deepscientist/quest/layout.py +15 -2
  70. package/src/deepscientist/quest/service.py +2539 -195
  71. package/src/deepscientist/quest/stage_views.py +177 -1
  72. package/src/deepscientist/runners/base.py +2 -0
  73. package/src/deepscientist/runners/codex.py +169 -31
  74. package/src/deepscientist/runners/runtime_overrides.py +17 -1
  75. package/src/deepscientist/skills/__init__.py +2 -2
  76. package/src/deepscientist/skills/installer.py +196 -5
  77. package/src/deepscientist/skills/registry.py +66 -0
  78. package/src/prompts/connectors/qq.md +18 -8
  79. package/src/prompts/connectors/weixin.md +16 -6
  80. package/src/prompts/contracts/shared_interaction.md +24 -4
  81. package/src/prompts/system.md +921 -72
  82. package/src/prompts/system_copilot.md +43 -0
  83. package/src/skills/analysis-campaign/SKILL.md +32 -2
  84. package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
  85. package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
  86. package/src/skills/baseline/SKILL.md +10 -0
  87. package/src/skills/decision/SKILL.md +27 -2
  88. package/src/skills/experiment/SKILL.md +16 -2
  89. package/src/skills/figure-polish/SKILL.md +1 -0
  90. package/src/skills/finalize/SKILL.md +19 -0
  91. package/src/skills/idea/SKILL.md +79 -0
  92. package/src/skills/idea/references/idea-generation-playbook.md +100 -0
  93. package/src/skills/idea/references/outline-seeding-example.md +60 -0
  94. package/src/skills/intake-audit/SKILL.md +9 -1
  95. package/src/skills/mentor/SKILL.md +217 -0
  96. package/src/skills/mentor/references/correction-rules.md +210 -0
  97. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  98. package/src/skills/mentor/references/persona-profile.md +138 -0
  99. package/src/skills/mentor/references/taste-profile.md +128 -0
  100. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  101. package/src/skills/mentor/references/work-profile.md +289 -0
  102. package/src/skills/mentor/references/workflow-profile.md +240 -0
  103. package/src/skills/optimize/SKILL.md +1645 -0
  104. package/src/skills/rebuttal/SKILL.md +3 -1
  105. package/src/skills/review/SKILL.md +3 -1
  106. package/src/skills/scout/SKILL.md +8 -0
  107. package/src/skills/write/SKILL.md +81 -12
  108. package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
  109. package/src/tui/dist/app/AppContainer.js +22 -11
  110. package/src/tui/dist/index.js +4 -1
  111. package/src/tui/dist/lib/api.js +33 -3
  112. package/src/tui/package.json +1 -1
  113. package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
  114. package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
  115. package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
  116. package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
  117. package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
  118. package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
  119. package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
  120. package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
  121. package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
  122. package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
  123. package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
  124. package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
  125. package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
  126. package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
  127. package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
  128. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  129. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  130. package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
  131. package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
  132. package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
  133. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  134. package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
  135. package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
  136. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  137. package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
  138. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  139. package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
  140. package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
  141. package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
  142. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  143. package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
  144. package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
  145. package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
  146. package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
  147. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  148. package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
  149. package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
  150. package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
  151. package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
  152. package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
  153. package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
  154. package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
  155. package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
  156. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  157. package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
  158. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  159. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  160. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  161. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  162. package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
  163. package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
  164. package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
  165. package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
  166. package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
  167. package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
  168. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  169. package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
  170. package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
  171. package/src/ui/dist/index.html +5 -2
  172. package/src/ui/dist/assets/AiManusChatView-DaF9Nge_.js +0 -26597
  173. package/src/ui/dist/assets/AnalysisPlugin-BSVx6dXE.js +0 -123
  174. package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
  175. package/src/ui/dist/assets/CodeEditorPlugin-DU9G0Tox.js +0 -427
  176. package/src/ui/dist/assets/CodeViewerPlugin-DoX_fI9l.js +0 -905
  177. package/src/ui/dist/assets/DocViewerPlugin-C4FWIXuU.js +0 -278
  178. package/src/ui/dist/assets/GitDiffViewerPlugin-BgfFMgtf.js +0 -2661
  179. package/src/ui/dist/assets/ImageViewerPlugin-tcPkfY_x.js +0 -500
  180. package/src/ui/dist/assets/LabCopilotPanel-_dKV60Bf.js +0 -4104
  181. package/src/ui/dist/assets/LabPlugin-Bje0ayoC.js +0 -2677
  182. package/src/ui/dist/assets/LatexPlugin-CVsBzAln.js +0 -1792
  183. package/src/ui/dist/assets/MarkdownViewerPlugin-xjmrqv_8.js +0 -308
  184. package/src/ui/dist/assets/MarketplacePlugin-mMM2A8wP.js +0 -413
  185. package/src/ui/dist/assets/NotebookEditor-3kVDSOBo.js +0 -4214
  186. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  187. package/src/ui/dist/assets/NotebookEditor-SoJ8X-MO.js +0 -84873
  188. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  189. package/src/ui/dist/assets/PdfLoader-DElVuHl9.js +0 -25468
  190. package/src/ui/dist/assets/PdfMarkdownPlugin-Bq88XT4G.js +0 -409
  191. package/src/ui/dist/assets/PdfViewerPlugin-CsCXMo9S.js +0 -3095
  192. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  193. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  194. package/src/ui/dist/assets/SearchPlugin-oUPvy19k.js +0 -741
  195. package/src/ui/dist/assets/TextViewerPlugin-CRkT9yNy.js +0 -472
  196. package/src/ui/dist/assets/VNCViewer-BgbuvWhR.js +0 -18821
  197. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  198. package/src/ui/dist/assets/bot-v_RASACv.js +0 -21
  199. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  200. package/src/ui/dist/assets/code-5hC9d0VH.js +0 -17
  201. package/src/ui/dist/assets/file-content-D1PxfOrp.js +0 -377
  202. package/src/ui/dist/assets/file-diff-panel-DG1oT_Hj.js +0 -92
  203. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  204. package/src/ui/dist/assets/file-socket-BmdFYQlk.js +0 -58
  205. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  206. package/src/ui/dist/assets/image-Dqe2X2tW.js +0 -18
  207. package/src/ui/dist/assets/index-BQG-1s2o.css +0 -12553
  208. package/src/ui/dist/assets/index-DVsMKK_y.js +0 -25
  209. package/src/ui/dist/assets/index-Duvz8Ip0.js +0 -159
  210. package/src/ui/dist/assets/index-Nt9hS4ck.js +0 -244829
  211. package/src/ui/dist/assets/index-RDlNXXx1.js +0 -120
  212. package/src/ui/dist/assets/monaco-DIXge1CP.js +0 -623
  213. package/src/ui/dist/assets/pdf-effect-queue-BBTTQaO-.js +0 -47
  214. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  215. package/src/ui/dist/assets/popover-BWlolyxo.js +0 -476
  216. package/src/ui/dist/assets/project-sync-BM5PkFH4.js +0 -297
  217. package/src/ui/dist/assets/select-D4dAtrA8.js +0 -1690
  218. package/src/ui/dist/assets/sigma-CKbE5jJT.js +0 -22
  219. package/src/ui/dist/assets/square-check-big-CZNGMgiB.js +0 -17
  220. package/src/ui/dist/assets/trash-DaB37xAz.js +0 -32
  221. package/src/ui/dist/assets/useCliAccess-C2OmAcWe.js +0 -957
  222. package/src/ui/dist/assets/useFileDiffOverlay-Dowd1Ij4.js +0 -53
  223. package/src/ui/dist/assets/wrap-text-BGjAhAUq.js +0 -35
  224. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  225. package/src/ui/dist/assets/zoom-out-dMZQMXzc.js +0 -34
@@ -62,6 +62,26 @@ def _field(label: str, value: object, *, tone: str = "default") -> dict[str, Any
62
62
  }
63
63
 
64
64
 
65
+ def _selection_score_summary(value: object) -> str | None:
66
+ if not isinstance(value, dict):
67
+ return None
68
+ parts: list[str] = []
69
+ for key, raw in value.items():
70
+ name = str(key or "").strip()
71
+ if not name:
72
+ continue
73
+ if isinstance(raw, float):
74
+ rendered = f"{raw:.4f}".rstrip("0").rstrip(".")
75
+ else:
76
+ rendered = str(raw).strip()
77
+ if not rendered:
78
+ continue
79
+ parts.append(f"{name}={rendered}")
80
+ if len(parts) >= 4:
81
+ break
82
+ return " · ".join(parts) or None
83
+
84
+
65
85
  def _evaluation_summary(value: object) -> dict[str, Any]:
66
86
  if not isinstance(value, dict):
67
87
  return {}
@@ -214,8 +234,11 @@ class QuestStageViewBuilder:
214
234
 
215
235
  def build(self) -> dict[str, Any]:
216
236
  selection_type = str(self.selection.get("selection_type") or "").strip()
237
+ explicit_stage_key = str(self.selection.get("stage_key") or "").strip()
217
238
  self.stage_key = self._resolve_effective_stage_key()
218
- if selection_type == "branch_node" and self.stage_key not in {"experiment", "analysis", "paper"}:
239
+ if selection_type == "idea_candidate":
240
+ return self._build_idea_candidate()
241
+ if selection_type == "branch_node" and not explicit_stage_key:
219
242
  return self._build_branch()
220
243
  if self.stage_key == "baseline":
221
244
  return self._build_baseline()
@@ -280,6 +303,8 @@ class QuestStageViewBuilder:
280
303
  normalized = [str(item).strip() for item in raw if str(item).strip()]
281
304
  if normalized:
282
305
  return normalized
306
+ if str(self.selection.get("selection_type") or "").strip() == "idea_candidate":
307
+ return self._idea_candidate_scope_paths()
283
308
  if str(self.selection.get("selection_type") or "").strip() == "branch_node":
284
309
  return self._branch_scope_paths()
285
310
  defaults = {
@@ -693,6 +718,13 @@ class QuestStageViewBuilder:
693
718
  "artifacts/reports",
694
719
  ]
695
720
 
721
+ def _idea_candidate_scope_paths(self) -> list[str]:
722
+ candidate_id = str(self.selection.get("selection_ref") or self.selection.get("idea_id") or "").strip()
723
+ return [
724
+ *( [f"memory/ideas/_candidates/{candidate_id}"] if candidate_id else []),
725
+ "artifacts/reports",
726
+ ]
727
+
696
728
  def _experiment_scope_paths(self, run_id: str | None) -> list[str]:
697
729
  return [
698
730
  *( [f"experiments/main/{run_id}"] if run_id else []),
@@ -917,6 +949,25 @@ class QuestStageViewBuilder:
917
949
  items.append(item)
918
950
  return items
919
951
 
952
+ def _idea_candidate_stage_items(self) -> list[dict[str, Any]]:
953
+ candidate_id = str(self.selection.get("selection_ref") or self.selection.get("idea_id") or "").strip()
954
+ if not candidate_id:
955
+ return []
956
+ items: list[dict[str, Any]] = []
957
+ for item in self.artifacts:
958
+ payload = self._payload(item)
959
+ if str(payload.get("kind") or "").strip() != "idea":
960
+ continue
961
+ if str(payload.get("idea_id") or "").strip() != candidate_id:
962
+ continue
963
+ flow_type = str(payload.get("flow_type") or "").strip()
964
+ protocol_step = str(payload.get("protocol_step") or "").strip()
965
+ details = dict(payload.get("details") or {}) if isinstance(payload.get("details"), dict) else {}
966
+ submission_mode = str(details.get("submission_mode") or payload.get("submission_mode") or "").strip().lower()
967
+ if flow_type == "idea_submission" and (protocol_step == "candidate" or submission_mode == "candidate"):
968
+ items.append(item)
969
+ return items
970
+
920
971
  def _build_idea(self) -> dict[str, Any]:
921
972
  idea_items = self._idea_stage_items()
922
973
  latest = idea_items[-1] if idea_items else None
@@ -944,6 +995,8 @@ class QuestStageViewBuilder:
944
995
  draft_md_rel_path = self._relative_path_or_raw(draft_md_path)
945
996
  draft_markdown = self._markdown_body_for_path(draft_md_path)
946
997
  lineage_intent = str(payload.get("lineage_intent") or details.get("lineage_intent") or "").strip() or None
998
+ selection_scores = details.get("selection_scores")
999
+ selection_score_summary = _selection_score_summary(selection_scores)
947
1000
  note = (
948
1001
  str(payload.get("summary") or payload.get("reason") or "").strip()
949
1002
  or "No durable idea submission has been recorded yet."
@@ -987,6 +1040,11 @@ class QuestStageViewBuilder:
987
1040
  _field("Problem", details.get("problem") or "Not recorded"),
988
1041
  _field("Hypothesis", details.get("hypothesis") or "Not recorded"),
989
1042
  _field("Mechanism", details.get("mechanism") or "Not recorded"),
1043
+ _field("Method Brief", details.get("method_brief") or "Not recorded"),
1044
+ _field("Selection Scores", selection_score_summary or "Not recorded"),
1045
+ _field("Mechanism Family", details.get("mechanism_family") or "Not recorded"),
1046
+ _field("Change Layer", details.get("change_layer") or "Not recorded"),
1047
+ _field("Source Lens", details.get("source_lens") or "Not recorded"),
990
1048
  _field("Expected Gain", details.get("expected_gain") or "Not recorded"),
991
1049
  _field("Risks", details.get("risks") or "Not recorded"),
992
1050
  _field("Evidence Paths", details.get("evidence_paths") or "Not recorded"),
@@ -1009,6 +1067,11 @@ class QuestStageViewBuilder:
1009
1067
  "problem": details.get("problem"),
1010
1068
  "hypothesis": details.get("hypothesis"),
1011
1069
  "mechanism": details.get("mechanism"),
1070
+ "method_brief": details.get("method_brief"),
1071
+ "selection_scores": selection_scores or None,
1072
+ "mechanism_family": details.get("mechanism_family"),
1073
+ "change_layer": details.get("change_layer"),
1074
+ "source_lens": details.get("source_lens"),
1012
1075
  "expected_gain": details.get("expected_gain"),
1013
1076
  "risks": details.get("risks") or [],
1014
1077
  "evidence_paths": details.get("evidence_paths") or [],
@@ -1028,6 +1091,101 @@ class QuestStageViewBuilder:
1028
1091
  subviews=["overview", "details", "draft"] if draft_markdown else ["overview", "details"],
1029
1092
  )
1030
1093
 
1094
+ def _build_idea_candidate(self) -> dict[str, Any]:
1095
+ candidate_items = self._idea_candidate_stage_items()
1096
+ latest = candidate_items[-1] if candidate_items else None
1097
+ payload = self._payload(latest or {})
1098
+ details = dict(payload.get("details") or {}) if isinstance(payload.get("details"), dict) else {}
1099
+ candidate_id = str(self.selection.get("selection_ref") or payload.get("idea_id") or "candidate").strip() or "candidate"
1100
+ title_text = (
1101
+ str(details.get("title") or self.selection.get("label") or candidate_id).strip() or candidate_id
1102
+ )
1103
+ paths = dict(payload.get("paths") or {}) if isinstance(payload.get("paths"), dict) else {}
1104
+ candidate_root = paths.get("candidate_root") or str(self.quest_root / "memory" / "ideas" / "_candidates" / candidate_id)
1105
+ idea_md_path = paths.get("idea_md") or str(Path(candidate_root) / "idea.md")
1106
+ draft_md_path = paths.get("idea_draft_md") or details.get("idea_draft_path") or str(Path(candidate_root) / "draft.md")
1107
+ idea_markdown = self._markdown_body_for_path(idea_md_path)
1108
+ draft_markdown = self._markdown_body_for_path(draft_md_path)
1109
+ idea_md_rel_path = self._relative_path_or_raw(idea_md_path)
1110
+ draft_md_rel_path = self._relative_path_or_raw(draft_md_path)
1111
+ candidate_root_rel_path = self._relative_path_or_raw(candidate_root)
1112
+ selection_scores = details.get("selection_scores")
1113
+ selection_score_summary = _selection_score_summary(selection_scores)
1114
+ note = (
1115
+ str(payload.get("summary") or payload.get("reason") or self.selection.get("summary") or "").strip()
1116
+ or "No durable candidate brief summary has been recorded yet."
1117
+ )
1118
+ lineage_intent = str(payload.get("lineage_intent") or details.get("lineage_intent") or "").strip() or None
1119
+ parent_branch = str(payload.get("parent_branch") or details.get("parent_branch") or self.selection.get("branch_name") or "").strip() or None
1120
+ foundation_reason = str(payload.get("foundation_reason") or details.get("foundation_reason") or "").strip() or None
1121
+ return self._base_payload(
1122
+ title=f"Candidate Brief · {title_text}",
1123
+ note=note,
1124
+ status=str(payload.get("status") or "candidate").strip() or "candidate",
1125
+ tags=[
1126
+ "candidate-brief",
1127
+ details.get("mechanism_family") or "",
1128
+ details.get("change_layer") or "",
1129
+ details.get("source_lens") or "",
1130
+ lineage_intent or "",
1131
+ ],
1132
+ overview=[
1133
+ _field("Candidate ID", candidate_id),
1134
+ _field("Parent Branch", parent_branch or "Not recorded"),
1135
+ _field("Next Target", details.get("next_target") or "optimize"),
1136
+ _field("Candidate Root", candidate_root_rel_path or candidate_root),
1137
+ ],
1138
+ key_facts=[
1139
+ _field("Problem", details.get("problem") or "Not recorded"),
1140
+ _field("Hypothesis", details.get("hypothesis") or "Not recorded"),
1141
+ _field("Mechanism", details.get("mechanism") or "Not recorded"),
1142
+ _field("Method Brief", details.get("method_brief") or "Not recorded"),
1143
+ _field("Selection Scores", selection_score_summary or "Not recorded"),
1144
+ _field("Mechanism Family", details.get("mechanism_family") or "Not recorded"),
1145
+ _field("Change Layer", details.get("change_layer") or "Not recorded"),
1146
+ _field("Source Lens", details.get("source_lens") or "Not recorded"),
1147
+ _field("Expected Gain", details.get("expected_gain") or "Not recorded"),
1148
+ _field("Foundation Reason", foundation_reason or "Not recorded"),
1149
+ ],
1150
+ key_files=self._dedupe_files(
1151
+ [
1152
+ self._file_entry(candidate_root, label="Candidate Root", description="Branchless candidate brief workspace.", expected_kind="directory"),
1153
+ self._file_entry(idea_md_path, label="Candidate Markdown", description="Durable candidate brief document."),
1154
+ self._file_entry(draft_md_path, label="Candidate Draft", description="Long-form candidate brief draft."),
1155
+ ]
1156
+ ),
1157
+ history=self._artifact_history(candidate_items),
1158
+ details={
1159
+ "idea": {
1160
+ "idea_id": candidate_id,
1161
+ "title": title_text,
1162
+ "problem": details.get("problem"),
1163
+ "hypothesis": details.get("hypothesis"),
1164
+ "mechanism": details.get("mechanism"),
1165
+ "method_brief": details.get("method_brief"),
1166
+ "selection_scores": selection_scores or None,
1167
+ "mechanism_family": details.get("mechanism_family"),
1168
+ "change_layer": details.get("change_layer"),
1169
+ "source_lens": details.get("source_lens"),
1170
+ "expected_gain": details.get("expected_gain"),
1171
+ "next_target": details.get("next_target") or "optimize",
1172
+ "lineage_intent": lineage_intent,
1173
+ "parent_branch": parent_branch,
1174
+ "candidate_root": candidate_root_rel_path or candidate_root,
1175
+ "idea_path": idea_md_rel_path,
1176
+ "idea_markdown": idea_markdown,
1177
+ "draft_path": draft_md_rel_path,
1178
+ "draft_markdown": draft_markdown,
1179
+ "decision_reason": payload.get("reason"),
1180
+ },
1181
+ "latest_artifact": self._artifact_detail(latest, payload),
1182
+ },
1183
+ lineage_intent=lineage_intent,
1184
+ idea_draft_path=draft_md_rel_path,
1185
+ draft_available=bool(draft_markdown),
1186
+ subviews=["overview", "details", "draft"] if draft_markdown else ["overview", "details"],
1187
+ )
1188
+
1031
1189
  def _build_branch(self) -> dict[str, Any]:
1032
1190
  idea_items = [
1033
1191
  item
@@ -1050,6 +1208,8 @@ class QuestStageViewBuilder:
1050
1208
  idea_title = str(latest_idea_details.get("title") or "").strip() or None
1051
1209
  idea_problem = str(latest_idea_details.get("problem") or "").strip() or None
1052
1210
  next_target = str(latest_idea_details.get("next_target") or "").strip() or None
1211
+ selection_scores = latest_idea_details.get("selection_scores")
1212
+ selection_score_summary = _selection_score_summary(selection_scores)
1053
1213
  lineage_intent = str(
1054
1214
  latest_idea_payload.get("lineage_intent")
1055
1215
  or latest_idea_details.get("lineage_intent")
@@ -1129,11 +1289,15 @@ class QuestStageViewBuilder:
1129
1289
  for item in self.artifacts
1130
1290
  if self._branch_matches(self._payload(item), allow_parent=True, include_unscoped=False)
1131
1291
  ]
1292
+ latest_branch_payload = self._payload(branch_items[-1] if branch_items else {})
1132
1293
  note = (
1133
1294
  str(
1134
1295
  latest_experiment_payload.get("summary")
1135
1296
  or latest_idea_payload.get("summary")
1136
1297
  or latest_idea_payload.get("reason")
1298
+ or latest_branch_payload.get("summary")
1299
+ or latest_branch_payload.get("message")
1300
+ or latest_branch_payload.get("reason")
1137
1301
  or self.trace.get("summary")
1138
1302
  or self.selection.get("summary")
1139
1303
  or ""
@@ -1169,6 +1333,11 @@ class QuestStageViewBuilder:
1169
1333
  key_facts=[
1170
1334
  _field("Idea Title", idea_title or "Not recorded"),
1171
1335
  _field("Idea Problem", idea_problem or "Not recorded"),
1336
+ _field("Method Brief", latest_idea_details.get("method_brief") or "Not recorded"),
1337
+ _field("Selection Scores", selection_score_summary or "Not recorded"),
1338
+ _field("Mechanism Family", latest_idea_details.get("mechanism_family") or "Not recorded"),
1339
+ _field("Change Layer", latest_idea_details.get("change_layer") or "Not recorded"),
1340
+ _field("Source Lens", latest_idea_details.get("source_lens") or "Not recorded"),
1172
1341
  _field("Foundation", foundation_label or "Current head"),
1173
1342
  _field("Foundation Reason", foundation_reason or "Not recorded"),
1174
1343
  _field("Next Target", next_target or "Not recorded"),
@@ -1224,6 +1393,11 @@ class QuestStageViewBuilder:
1224
1393
  "lineage_intent": lineage_intent,
1225
1394
  "idea_title": idea_title,
1226
1395
  "idea_problem": idea_problem,
1396
+ "method_brief": latest_idea_details.get("method_brief"),
1397
+ "selection_scores": selection_scores or None,
1398
+ "mechanism_family": latest_idea_details.get("mechanism_family"),
1399
+ "change_layer": latest_idea_details.get("change_layer"),
1400
+ "source_lens": latest_idea_details.get("source_lens"),
1227
1401
  "next_target": next_target,
1228
1402
  "idea_draft_path": idea_draft_rel_path,
1229
1403
  "idea_draft_markdown": idea_draft_markdown,
@@ -1579,11 +1753,13 @@ class QuestStageViewBuilder:
1579
1753
  for path in candidates
1580
1754
  ],
1581
1755
  self._file_entry(paper_root / "selected_outline.json", label="Selected Outline", description="Chosen paper outline."),
1756
+ self._file_entry(paper_root / "outline" / "manifest.json", label="Outline Manifest", description="Author-facing paper outline manifest."),
1582
1757
  self._file_entry(paper_root / "outline_selection.md", label="Outline Selection Note", description="Outline selection rationale."),
1583
1758
  self._file_entry(paper_root / "draft.md", label="Draft Markdown", description="Current paper draft."),
1584
1759
  self._file_entry(paper_root / "writing_plan.md", label="Writing Plan", description="Paper writing plan."),
1585
1760
  self._file_entry(paper_root / "references.bib", label="References", description="Bibliography file."),
1586
1761
  self._file_entry(paper_root / "claim_evidence_map.json", label="Claim-Evidence Map", description="Claim to evidence mapping."),
1762
+ self._file_entry(paper_root / "paper_line_state.json", label="Paper Line State", description="Derived summary state for the active paper line."),
1587
1763
  self._file_entry(paper_root / "baseline_inventory.json", label="Baseline Inventory", description="Canonical and supplementary baseline inventory for writing."),
1588
1764
  self._file_entry(paper_root / "build" / "compile_report.json", label="Compile Report", description="Paper build/compile report."),
1589
1765
  self._file_entry(paper_root / "paper_bundle_manifest.json", label="Bundle Manifest", description="Final paper bundle manifest."),
@@ -17,6 +17,8 @@ class RunRequest:
17
17
  approval_policy: str
18
18
  sandbox_mode: str
19
19
  turn_reason: str = "user_message"
20
+ turn_intent: str = "continue_stage"
21
+ turn_mode: str = "stage_execution"
20
22
  reasoning_effort: str | None = None
21
23
  turn_id: str | None = None
22
24
  attempt_index: int = 1
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  import json
4
4
  import os
5
5
  import signal
6
- import shutil
7
6
  import subprocess
8
7
  import sys
9
8
  import threading
@@ -11,12 +10,16 @@ from pathlib import Path
11
10
  from typing import Any
12
11
 
13
12
  from ..artifact import ArtifactService
14
- from ..codex_cli_compat import adapt_profile_only_provider_config, normalize_codex_reasoning_effort
13
+ from ..codex_cli_compat import (
14
+ materialize_codex_runtime_home,
15
+ normalize_codex_reasoning_effort,
16
+ provider_profile_metadata_from_home,
17
+ )
15
18
  from ..config import ConfigManager
16
19
  from ..gitops import export_git_graph
17
20
  from ..prompts import PromptBuilder
18
21
  from ..runtime_logs import JsonlLogger
19
- from ..shared import append_jsonl, ensure_dir, generate_id, read_text, read_yaml, resolve_runner_binary, utc_now, write_json, write_text
22
+ from ..shared import append_jsonl, ensure_dir, generate_id, read_yaml, resolve_runner_binary, utc_now, write_json, write_text
20
23
  from ..web_search import extract_web_search_payload
21
24
  from .base import RunRequest, RunResult
22
25
 
@@ -24,6 +27,55 @@ _TOOL_EVENT_ARGS_TEXT_LIMIT = 8_000
24
27
  _TOOL_EVENT_OUTPUT_TEXT_LIMIT = 16_000
25
28
  _MAX_QUEST_EVENT_JSON_BYTES = 2_000_000
26
29
  _OVERSIZED_EVENT_PREVIEW_TEXT_LIMIT = 12_000
30
+ _BUILTIN_MCP_TOOL_APPROVALS: dict[str, tuple[str, ...]] = {
31
+ "memory": (
32
+ "write",
33
+ "read",
34
+ "search",
35
+ "list_recent",
36
+ "promote_to_global",
37
+ ),
38
+ "artifact": (
39
+ "record",
40
+ "checkpoint",
41
+ "prepare_branch",
42
+ "activate_branch",
43
+ "submit_idea",
44
+ "list_research_branches",
45
+ "resolve_runtime_refs",
46
+ "get_paper_contract_health",
47
+ "get_quest_state",
48
+ "get_global_status",
49
+ "get_method_scoreboard",
50
+ "get_optimization_frontier",
51
+ "read_quest_documents",
52
+ "get_conversation_context",
53
+ "get_analysis_campaign",
54
+ "record_main_experiment",
55
+ "create_analysis_campaign",
56
+ "submit_paper_outline",
57
+ "list_paper_outlines",
58
+ "submit_paper_bundle",
59
+ "record_analysis_slice",
60
+ "publish_baseline",
61
+ "attach_baseline",
62
+ "confirm_baseline",
63
+ "waive_baseline",
64
+ "arxiv",
65
+ "refresh_summary",
66
+ "render_git_graph",
67
+ "interact",
68
+ "complete_quest",
69
+ ),
70
+ "bash_exec": (
71
+ "bash_exec",
72
+ ),
73
+ }
74
+
75
+ _PROVIDER_ENV_CONFLICT_KEYS = (
76
+ "OPENAI_API_KEY",
77
+ "OPENAI_BASE_URL",
78
+ )
27
79
 
28
80
 
29
81
  def _compact_text(value: object, *, limit: int = 1200) -> str:
@@ -151,7 +203,9 @@ def _iter_event_texts(event: dict[str, Any]) -> list[str]:
151
203
  if isinstance(value, str) and value.strip():
152
204
  texts.append(value)
153
205
  delta = event.get("delta")
154
- if isinstance(delta, dict):
206
+ if isinstance(delta, str) and delta.strip():
207
+ texts.append(delta)
208
+ elif isinstance(delta, dict):
155
209
  for key in ("text", "content"):
156
210
  value = delta.get(key)
157
211
  if isinstance(value, str) and value.strip():
@@ -178,6 +232,36 @@ def _web_search_text_payload(item: dict[str, Any]) -> str:
178
232
  return _compact_text(payload, limit=2400)
179
233
 
180
234
 
235
+ def _message_stream_id(event: dict[str, Any], item: dict[str, Any], *, run_id: str, kind: str) -> str:
236
+ for value in (
237
+ event.get("stream_id"),
238
+ item.get("stream_id"),
239
+ event.get("message_id"),
240
+ item.get("message_id"),
241
+ event.get("item_id"),
242
+ item.get("id"),
243
+ event.get("output_item_id"),
244
+ event.get("response_id"),
245
+ ):
246
+ if value:
247
+ return str(value)
248
+ normalized_kind = str(kind or "message").strip().lower() or "message"
249
+ return f"{run_id}:{normalized_kind}"
250
+
251
+
252
+ def _message_id(event: dict[str, Any], item: dict[str, Any], *, stream_id: str) -> str:
253
+ for value in (
254
+ event.get("message_id"),
255
+ item.get("message_id"),
256
+ event.get("item_id"),
257
+ item.get("id"),
258
+ event.get("output_item_id"),
259
+ ):
260
+ if value:
261
+ return str(value)
262
+ return stream_id
263
+
264
+
181
265
  def _message_events(
182
266
  event: dict[str, Any],
183
267
  *,
@@ -194,6 +278,8 @@ def _message_events(
194
278
 
195
279
  if item_type == "agent_message":
196
280
  texts = _dedupe_texts(_iter_event_texts(event))
281
+ stream_id = _message_stream_id(event, item, run_id=run_id, kind="assistant")
282
+ message_id = _message_id(event, item, stream_id=stream_id)
197
283
  for text in texts:
198
284
  quest_events.append(
199
285
  {
@@ -204,6 +290,8 @@ def _message_events(
204
290
  "source": "codex",
205
291
  "skill_id": skill_id,
206
292
  "text": text,
293
+ "stream_id": stream_id,
294
+ "message_id": message_id,
207
295
  "created_at": created_at,
208
296
  }
209
297
  )
@@ -211,6 +299,8 @@ def _message_events(
211
299
 
212
300
  if item_type in {"reasoning", "reasoning_summary"} or "reasoning" in event_type:
213
301
  texts = _dedupe_texts(_iter_event_texts(event))
302
+ stream_id = _message_stream_id(event, item, run_id=run_id, kind=item_type or "reasoning")
303
+ message_id = _message_id(event, item, stream_id=stream_id)
214
304
  for text in texts:
215
305
  quest_events.append(
216
306
  {
@@ -221,6 +311,8 @@ def _message_events(
221
311
  "source": "codex",
222
312
  "skill_id": skill_id,
223
313
  "text": text,
314
+ "stream_id": stream_id,
315
+ "message_id": message_id,
224
316
  "kind": item_type or "reasoning",
225
317
  "created_at": created_at,
226
318
  }
@@ -234,6 +326,8 @@ def _message_events(
234
326
  return [], []
235
327
 
236
328
  texts = _dedupe_texts(_iter_event_texts(event))
329
+ stream_id = _message_stream_id(event, item, run_id=run_id, kind="assistant")
330
+ message_id = _message_id(event, item, stream_id=stream_id)
237
331
  for text in texts:
238
332
  quest_events.append(
239
333
  {
@@ -244,6 +338,8 @@ def _message_events(
244
338
  "source": "codex",
245
339
  "skill_id": skill_id,
246
340
  "text": text,
341
+ "stream_id": stream_id,
342
+ "message_id": message_id,
247
343
  "created_at": created_at,
248
344
  }
249
345
  )
@@ -322,9 +418,11 @@ def _tool_output(event: dict[str, Any], item: dict[str, Any]) -> str:
322
418
  item.get("result"),
323
419
  item.get("output"),
324
420
  item.get("content"),
421
+ item.get("error"),
325
422
  event.get("result"),
326
423
  event.get("output"),
327
424
  event.get("content"),
425
+ event.get("error"),
328
426
  item.get("aggregated_output"),
329
427
  event.get("aggregated_output"),
330
428
  ):
@@ -338,11 +436,13 @@ def _tool_output(event: dict[str, Any], item: dict[str, Any]) -> str:
338
436
  item.get("output"),
339
437
  item.get("result"),
340
438
  item.get("content"),
439
+ item.get("error"),
341
440
  event.get("aggregated_output"),
342
441
  event.get("changes"),
343
442
  event.get("output"),
344
443
  event.get("result"),
345
444
  event.get("content"),
445
+ event.get("error"),
346
446
  ):
347
447
  text = _compact_text(value, limit=1200)
348
448
  if text:
@@ -642,6 +742,8 @@ class CodexRunner:
642
742
  user_message=request.message,
643
743
  model=request.model,
644
744
  turn_reason=request.turn_reason,
745
+ turn_intent=request.turn_intent,
746
+ turn_mode=request.turn_mode,
645
747
  retry_context=request.retry_context,
646
748
  )
647
749
  write_text(run_root / "prompt.md", prompt)
@@ -663,6 +765,8 @@ class CodexRunner:
663
765
  "workspace_root": str(workspace_root),
664
766
  "cwd": str(workspace_root),
665
767
  "turn_reason": request.turn_reason,
768
+ "turn_intent": request.turn_intent,
769
+ "turn_mode": request.turn_mode,
666
770
  },
667
771
  )
668
772
 
@@ -672,8 +776,12 @@ class CodexRunner:
672
776
  env_key = str(key or "").strip()
673
777
  if not env_key or value is None:
674
778
  continue
675
- env[env_key] = str(value)
779
+ env_value = str(value)
780
+ if env_value == "":
781
+ continue
782
+ env[env_key] = env_value
676
783
  env["CODEX_HOME"] = str(codex_home)
784
+ env = self._sanitize_provider_env(env, runner_config=runner_config)
677
785
  env["DEEPSCIENTIST_HOME"] = str(self.home)
678
786
  env["DS_HOME"] = str(self.home)
679
787
  env["DS_QUEST_ID"] = request.quest_id
@@ -681,6 +789,8 @@ class CodexRunner:
681
789
  env["DS_WORKTREE_ROOT"] = str(workspace_root)
682
790
  env["DS_RUN_ID"] = request.run_id
683
791
  env["DS_TURN_REASON"] = request.turn_reason
792
+ env["DS_TURN_INTENT"] = request.turn_intent
793
+ env["DS_TURN_MODE"] = request.turn_mode
684
794
  quest_yaml = read_yaml(request.quest_root / "quest.yaml", {})
685
795
  env["DS_ACTIVE_ANCHOR"] = str(quest_yaml.get("active_anchor", "baseline"))
686
796
  env["DS_CONVERSATION_ID"] = f"quest:{request.quest_id}"
@@ -740,6 +850,13 @@ class CodexRunner:
740
850
  timestamp = utc_now()
741
851
  append_jsonl(history_events, {"timestamp": timestamp, "event": payload})
742
852
  append_jsonl(stdout_events, {"timestamp": timestamp, "line": line})
853
+ try:
854
+ self.artifact_service.quest_service.schedule_projection_refresh(
855
+ request.quest_root,
856
+ kinds=("details",),
857
+ )
858
+ except Exception:
859
+ pass
743
860
  tool_event = _tool_event(
744
861
  payload,
745
862
  quest_id=request.quest_id,
@@ -810,6 +927,14 @@ class CodexRunner:
810
927
  }
811
928
  write_json(run_root / "result.json", result_payload)
812
929
  write_json(history_root / "meta.json", result_payload)
930
+ try:
931
+ self.artifact_service.quest_service.schedule_projection_refresh(
932
+ request.quest_root,
933
+ kinds=("details",),
934
+ throttle_seconds=0.0,
935
+ )
936
+ except Exception:
937
+ pass
813
938
  self.logger.log(
814
939
  "info",
815
940
  "runner.codex.completed",
@@ -822,6 +947,7 @@ class CodexRunner:
822
947
  request.quest_root,
823
948
  {
824
949
  "kind": "run",
950
+ "status": "completed" if exit_code == 0 else "failed",
825
951
  "run_id": request.run_id,
826
952
  "run_kind": request.skill_id,
827
953
  "model": request.model,
@@ -902,6 +1028,8 @@ class CodexRunner:
902
1028
  resolved_runner_config = runner_config if isinstance(runner_config, dict) else self._load_runner_config()
903
1029
  profile = str(resolved_runner_config.get("profile") or "").strip()
904
1030
  normalized_model = str(request.model or "").strip()
1031
+ if profile and normalized_model.lower() not in {"", "inherit", "default", "codex-default"}:
1032
+ normalized_model = "inherit"
905
1033
  command = [
906
1034
  resolved_binary or self.binary,
907
1035
  "--search",
@@ -946,35 +1074,16 @@ class CodexRunner:
946
1074
  run_id: str,
947
1075
  runner_config: dict[str, Any] | None = None,
948
1076
  ) -> Path:
949
- target = ensure_dir(workspace_root / ".codex")
1077
+ target = ensure_dir(workspace_root / ".ds" / "codex-home")
950
1078
  resolved_runner_config = runner_config if isinstance(runner_config, dict) else self._load_runner_config()
951
1079
  configured_home = str(resolved_runner_config.get("config_dir") or os.environ.get("CODEX_HOME") or str(Path.home() / ".codex"))
952
1080
  profile = str(resolved_runner_config.get("profile") or "").strip()
953
- source = Path(configured_home).expanduser()
954
- for filename in ("config.toml", "auth.json"):
955
- source_path = source / filename
956
- target_path = target / filename
957
- if source_path.exists() and not target_path.exists():
958
- if source_path.resolve() == target_path.resolve():
959
- continue
960
- shutil.copy2(source_path, target_path)
961
- config_path = target / "config.toml"
962
- if profile and config_path.exists():
963
- adapted_text, _ = adapt_profile_only_provider_config(read_text(config_path), profile=profile)
964
- write_text(config_path, adapted_text)
965
- ensure_dir(target / "skills")
966
- quest_skills_root = quest_root / ".codex" / "skills"
967
- if quest_skills_root.exists():
968
- for source_path in sorted(quest_skills_root.rglob("*")):
969
- relative = source_path.relative_to(quest_skills_root)
970
- target_path = target / "skills" / relative
971
- if source_path.is_dir():
972
- ensure_dir(target_path)
973
- continue
974
- if source_path.resolve() == target_path.resolve():
975
- continue
976
- ensure_dir(target_path.parent)
977
- shutil.copy2(source_path, target_path)
1081
+ materialize_codex_runtime_home(
1082
+ source_home=configured_home,
1083
+ target_home=target,
1084
+ profile=profile,
1085
+ quest_codex_root=quest_root / ".codex",
1086
+ )
978
1087
  self._inject_built_in_mcp(
979
1088
  target,
980
1089
  quest_root=quest_root,
@@ -1043,6 +1152,7 @@ class CodexRunner:
1043
1152
  args = ["-m", "deepscientist.mcp.server", "--namespace", name]
1044
1153
  lines = [
1045
1154
  f"[mcp_servers.{name}]",
1155
+ 'transport = "stdio"',
1046
1156
  f'command = "{sys.executable}"',
1047
1157
  f"args = [{', '.join(json.dumps(item) for item in args)}]",
1048
1158
  ]
@@ -1057,6 +1167,14 @@ class CodexRunner:
1057
1167
  )
1058
1168
  for key, value in env.items():
1059
1169
  lines.append(f"{key} = {json.dumps(value)}")
1170
+ for tool_name in _BUILTIN_MCP_TOOL_APPROVALS.get(name, ()):
1171
+ lines.extend(
1172
+ [
1173
+ "",
1174
+ f"[mcp_servers.{name}.tools.{tool_name}]",
1175
+ 'approval_mode = "approve"',
1176
+ ]
1177
+ )
1060
1178
  return "\n".join(lines)
1061
1179
 
1062
1180
  def _load_runner_config(self) -> dict[str, Any]:
@@ -1074,3 +1192,23 @@ class CodexRunner:
1074
1192
  except (TypeError, ValueError):
1075
1193
  return None
1076
1194
  return timeout if timeout > 0 else None
1195
+
1196
+ @staticmethod
1197
+ def _sanitize_provider_env(
1198
+ env: dict[str, str],
1199
+ *,
1200
+ runner_config: dict[str, Any] | None = None,
1201
+ ) -> dict[str, str]:
1202
+ resolved_runner_config = runner_config if isinstance(runner_config, dict) else {}
1203
+ profile = str(resolved_runner_config.get("profile") or "").strip()
1204
+ config_home = str(resolved_runner_config.get("config_dir") or env.get("CODEX_HOME") or "").strip()
1205
+ if not profile or not config_home:
1206
+ return env
1207
+ metadata = provider_profile_metadata_from_home(config_home, profile=profile)
1208
+ requires_openai_auth = metadata.get("requires_openai_auth")
1209
+ if requires_openai_auth is not False:
1210
+ return env
1211
+ sanitized = dict(env)
1212
+ for key in _PROVIDER_ENV_CONFLICT_KEYS:
1213
+ sanitized.pop(key, None)
1214
+ return sanitized