@researai/deepscientist 1.5.14 → 1.5.15

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 (119) hide show
  1. package/README.md +8 -0
  2. package/assets/branding/logo-raster.png +0 -0
  3. package/bin/ds.js +134 -49
  4. package/docs/en/00_QUICK_START.md +2 -2
  5. package/docs/en/01_SETTINGS_REFERENCE.md +20 -4
  6. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
  7. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  8. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
  9. package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  10. package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  11. package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  12. package/docs/en/README.md +6 -0
  13. package/docs/zh/00_QUICK_START.md +2 -2
  14. package/docs/zh/01_SETTINGS_REFERENCE.md +20 -4
  15. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
  16. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  17. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
  18. package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  19. package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  20. package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  21. package/docs/zh/README.md +6 -0
  22. package/install.sh +2 -0
  23. package/package.json +1 -1
  24. package/pyproject.toml +1 -1
  25. package/src/deepscientist/__init__.py +1 -1
  26. package/src/deepscientist/artifact/charts.py +567 -0
  27. package/src/deepscientist/artifact/guidance.py +50 -10
  28. package/src/deepscientist/artifact/metrics.py +228 -5
  29. package/src/deepscientist/artifact/schemas.py +3 -0
  30. package/src/deepscientist/artifact/service.py +3534 -191
  31. package/src/deepscientist/bash_exec/models.py +23 -0
  32. package/src/deepscientist/bash_exec/monitor.py +147 -67
  33. package/src/deepscientist/bash_exec/runtime.py +218 -156
  34. package/src/deepscientist/bash_exec/service.py +79 -64
  35. package/src/deepscientist/bash_exec/shells.py +87 -0
  36. package/src/deepscientist/bridges/connectors.py +51 -2
  37. package/src/deepscientist/config/models.py +6 -3
  38. package/src/deepscientist/config/service.py +7 -2
  39. package/src/deepscientist/connector/weixin_support.py +122 -1
  40. package/src/deepscientist/daemon/api/handlers.py +75 -4
  41. package/src/deepscientist/daemon/api/router.py +1 -0
  42. package/src/deepscientist/daemon/app.py +758 -206
  43. package/src/deepscientist/doctor.py +51 -0
  44. package/src/deepscientist/file_lock.py +48 -0
  45. package/src/deepscientist/gitops/diff.py +167 -1
  46. package/src/deepscientist/mcp/server.py +173 -5
  47. package/src/deepscientist/process_control.py +161 -0
  48. package/src/deepscientist/prompts/builder.py +267 -442
  49. package/src/deepscientist/quest/service.py +2255 -163
  50. package/src/deepscientist/quest/stage_views.py +171 -0
  51. package/src/deepscientist/runners/base.py +2 -0
  52. package/src/deepscientist/runners/codex.py +88 -5
  53. package/src/deepscientist/runners/runtime_overrides.py +17 -1
  54. package/src/prompts/contracts/shared_interaction.md +13 -4
  55. package/src/prompts/system.md +916 -72
  56. package/src/skills/analysis-campaign/SKILL.md +31 -2
  57. package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
  58. package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
  59. package/src/skills/baseline/SKILL.md +2 -0
  60. package/src/skills/decision/SKILL.md +19 -2
  61. package/src/skills/experiment/SKILL.md +8 -2
  62. package/src/skills/finalize/SKILL.md +18 -0
  63. package/src/skills/idea/SKILL.md +78 -0
  64. package/src/skills/idea/references/idea-generation-playbook.md +100 -0
  65. package/src/skills/idea/references/outline-seeding-example.md +60 -0
  66. package/src/skills/intake-audit/SKILL.md +1 -1
  67. package/src/skills/optimize/SKILL.md +1644 -0
  68. package/src/skills/rebuttal/SKILL.md +2 -1
  69. package/src/skills/review/SKILL.md +2 -1
  70. package/src/skills/write/SKILL.md +80 -12
  71. package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
  72. package/src/tui/dist/app/AppContainer.js +3 -0
  73. package/src/tui/package.json +1 -1
  74. package/src/ui/dist/assets/{AiManusChatView-DaF9Nge_.js → AiManusChatView-DDjbFnbt.js} +12 -12
  75. package/src/ui/dist/assets/{AnalysisPlugin-BSVx6dXE.js → AnalysisPlugin-Yb5IdmaU.js} +1 -1
  76. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +31037 -0
  77. package/src/ui/dist/assets/{CodeEditorPlugin-DU9G0Tox.js → CodeEditorPlugin-C4D2TIkU.js} +8 -8
  78. package/src/ui/dist/assets/{CodeViewerPlugin-DoX_fI9l.js → CodeViewerPlugin-BVoNZIvC.js} +5 -5
  79. package/src/ui/dist/assets/{DocViewerPlugin-C4FWIXuU.js → DocViewerPlugin-CLChbllo.js} +3 -3
  80. package/src/ui/dist/assets/{GitDiffViewerPlugin-BgfFMgtf.js → GitDiffViewerPlugin-C4xeFyFQ.js} +20 -20
  81. package/src/ui/dist/assets/{ImageViewerPlugin-tcPkfY_x.js → ImageViewerPlugin-OiMUAcLi.js} +5 -5
  82. package/src/ui/dist/assets/{LabCopilotPanel-_dKV60Bf.js → LabCopilotPanel-BjD2ThQF.js} +11 -11
  83. package/src/ui/dist/assets/{LabPlugin-Bje0ayoC.js → LabPlugin-DQPg-NrB.js} +2 -2
  84. package/src/ui/dist/assets/{LatexPlugin-CVsBzAln.js → LatexPlugin-CI05XAV9.js} +7 -7
  85. package/src/ui/dist/assets/{MarkdownViewerPlugin-xjmrqv_8.js → MarkdownViewerPlugin-DpeBLYZf.js} +4 -4
  86. package/src/ui/dist/assets/{MarketplacePlugin-mMM2A8wP.js → MarketplacePlugin-DolE58Q2.js} +3 -3
  87. package/src/ui/dist/assets/{NotebookEditor-3kVDSOBo.js → NotebookEditor-7Qm2rSWD.js} +11 -11
  88. package/src/ui/dist/assets/{NotebookEditor-SoJ8X-MO.js → NotebookEditor-C1kWaxKi.js} +1 -1
  89. package/src/ui/dist/assets/{PdfLoader-DElVuHl9.js → PdfLoader-BfOHw8Zw.js} +1 -1
  90. package/src/ui/dist/assets/{PdfMarkdownPlugin-Bq88XT4G.js → PdfMarkdownPlugin-BulDREv1.js} +2 -2
  91. package/src/ui/dist/assets/{PdfViewerPlugin-CsCXMo9S.js → PdfViewerPlugin-C-daaOaL.js} +10 -10
  92. package/src/ui/dist/assets/{SearchPlugin-oUPvy19k.js → SearchPlugin-CjpaiJ3A.js} +1 -1
  93. package/src/ui/dist/assets/{TextViewerPlugin-CRkT9yNy.js → TextViewerPlugin-BxIyqPQC.js} +5 -5
  94. package/src/ui/dist/assets/{VNCViewer-BgbuvWhR.js → VNCViewer-HAg9mF7M.js} +10 -10
  95. package/src/ui/dist/assets/{bot-v_RASACv.js → bot-0DYntytV.js} +1 -1
  96. package/src/ui/dist/assets/{code-5hC9d0VH.js → code-B20Slj_w.js} +1 -1
  97. package/src/ui/dist/assets/{file-content-D1PxfOrp.js → file-content-DT24KFma.js} +1 -1
  98. package/src/ui/dist/assets/{file-diff-panel-DG1oT_Hj.js → file-diff-panel-DK13YPql.js} +1 -1
  99. package/src/ui/dist/assets/{file-socket-BmdFYQlk.js → file-socket-B4T2o4nR.js} +1 -1
  100. package/src/ui/dist/assets/{image-Dqe2X2tW.js → image-DSeR_sDS.js} +1 -1
  101. package/src/ui/dist/assets/{index-RDlNXXx1.js → index-BrFje2Uk.js} +2 -2
  102. package/src/ui/dist/assets/{index-DVsMKK_y.js → index-BwRJaoTl.js} +1 -1
  103. package/src/ui/dist/assets/{index-Nt9hS4ck.js → index-D_E4281X.js} +5007 -28514
  104. package/src/ui/dist/assets/{index-Duvz8Ip0.js → index-DnYB3xb1.js} +12 -12
  105. package/src/ui/dist/assets/{index-BQG-1s2o.css → index-G7AcWcMu.css} +43 -2
  106. package/src/ui/dist/assets/{monaco-DIXge1CP.js → monaco-LExaAN3Y.js} +1 -1
  107. package/src/ui/dist/assets/{pdf-effect-queue-BBTTQaO-.js → pdf-effect-queue-BJk5okWJ.js} +1 -1
  108. package/src/ui/dist/assets/{popover-BWlolyxo.js → popover-D3Gg_FoV.js} +1 -1
  109. package/src/ui/dist/assets/{project-sync-BM5PkFH4.js → project-sync-C_ygLlVU.js} +1 -1
  110. package/src/ui/dist/assets/{select-D4dAtrA8.js → select-CpAK6uWm.js} +2 -2
  111. package/src/ui/dist/assets/{sigma-CKbE5jJT.js → sigma-DEccaSgk.js} +1 -1
  112. package/src/ui/dist/assets/{square-check-big-CZNGMgiB.js → square-check-big-uUfyVsbD.js} +1 -1
  113. package/src/ui/dist/assets/{trash-DaB37xAz.js → trash-CXvwwSe8.js} +1 -1
  114. package/src/ui/dist/assets/{useCliAccess-C2OmAcWe.js → useCliAccess-Bnop4mgR.js} +1 -1
  115. package/src/ui/dist/assets/{useFileDiffOverlay-Dowd1Ij4.js → useFileDiffOverlay-B8eUAX0I.js} +1 -1
  116. package/src/ui/dist/assets/{wrap-text-BGjAhAUq.js → wrap-text-9vbOBpkW.js} +1 -1
  117. package/src/ui/dist/assets/{zoom-out-dMZQMXzc.js → zoom-out-BgVMmOW4.js} +1 -1
  118. package/src/ui/dist/index.html +2 -2
  119. package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
@@ -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 {}
@@ -215,6 +235,8 @@ class QuestStageViewBuilder:
215
235
  def build(self) -> dict[str, Any]:
216
236
  selection_type = str(self.selection.get("selection_type") or "").strip()
217
237
  self.stage_key = self._resolve_effective_stage_key()
238
+ if selection_type == "idea_candidate":
239
+ return self._build_idea_candidate()
218
240
  if selection_type == "branch_node" and self.stage_key not in {"experiment", "analysis", "paper"}:
219
241
  return self._build_branch()
220
242
  if self.stage_key == "baseline":
@@ -280,6 +302,8 @@ class QuestStageViewBuilder:
280
302
  normalized = [str(item).strip() for item in raw if str(item).strip()]
281
303
  if normalized:
282
304
  return normalized
305
+ if str(self.selection.get("selection_type") or "").strip() == "idea_candidate":
306
+ return self._idea_candidate_scope_paths()
283
307
  if str(self.selection.get("selection_type") or "").strip() == "branch_node":
284
308
  return self._branch_scope_paths()
285
309
  defaults = {
@@ -693,6 +717,13 @@ class QuestStageViewBuilder:
693
717
  "artifacts/reports",
694
718
  ]
695
719
 
720
+ def _idea_candidate_scope_paths(self) -> list[str]:
721
+ candidate_id = str(self.selection.get("selection_ref") or self.selection.get("idea_id") or "").strip()
722
+ return [
723
+ *( [f"memory/ideas/_candidates/{candidate_id}"] if candidate_id else []),
724
+ "artifacts/reports",
725
+ ]
726
+
696
727
  def _experiment_scope_paths(self, run_id: str | None) -> list[str]:
697
728
  return [
698
729
  *( [f"experiments/main/{run_id}"] if run_id else []),
@@ -917,6 +948,25 @@ class QuestStageViewBuilder:
917
948
  items.append(item)
918
949
  return items
919
950
 
951
+ def _idea_candidate_stage_items(self) -> list[dict[str, Any]]:
952
+ candidate_id = str(self.selection.get("selection_ref") or self.selection.get("idea_id") or "").strip()
953
+ if not candidate_id:
954
+ return []
955
+ items: list[dict[str, Any]] = []
956
+ for item in self.artifacts:
957
+ payload = self._payload(item)
958
+ if str(payload.get("kind") or "").strip() != "idea":
959
+ continue
960
+ if str(payload.get("idea_id") or "").strip() != candidate_id:
961
+ continue
962
+ flow_type = str(payload.get("flow_type") or "").strip()
963
+ protocol_step = str(payload.get("protocol_step") or "").strip()
964
+ details = dict(payload.get("details") or {}) if isinstance(payload.get("details"), dict) else {}
965
+ submission_mode = str(details.get("submission_mode") or payload.get("submission_mode") or "").strip().lower()
966
+ if flow_type == "idea_submission" and (protocol_step == "candidate" or submission_mode == "candidate"):
967
+ items.append(item)
968
+ return items
969
+
920
970
  def _build_idea(self) -> dict[str, Any]:
921
971
  idea_items = self._idea_stage_items()
922
972
  latest = idea_items[-1] if idea_items else None
@@ -944,6 +994,8 @@ class QuestStageViewBuilder:
944
994
  draft_md_rel_path = self._relative_path_or_raw(draft_md_path)
945
995
  draft_markdown = self._markdown_body_for_path(draft_md_path)
946
996
  lineage_intent = str(payload.get("lineage_intent") or details.get("lineage_intent") or "").strip() or None
997
+ selection_scores = details.get("selection_scores")
998
+ selection_score_summary = _selection_score_summary(selection_scores)
947
999
  note = (
948
1000
  str(payload.get("summary") or payload.get("reason") or "").strip()
949
1001
  or "No durable idea submission has been recorded yet."
@@ -987,6 +1039,11 @@ class QuestStageViewBuilder:
987
1039
  _field("Problem", details.get("problem") or "Not recorded"),
988
1040
  _field("Hypothesis", details.get("hypothesis") or "Not recorded"),
989
1041
  _field("Mechanism", details.get("mechanism") or "Not recorded"),
1042
+ _field("Method Brief", details.get("method_brief") or "Not recorded"),
1043
+ _field("Selection Scores", selection_score_summary or "Not recorded"),
1044
+ _field("Mechanism Family", details.get("mechanism_family") or "Not recorded"),
1045
+ _field("Change Layer", details.get("change_layer") or "Not recorded"),
1046
+ _field("Source Lens", details.get("source_lens") or "Not recorded"),
990
1047
  _field("Expected Gain", details.get("expected_gain") or "Not recorded"),
991
1048
  _field("Risks", details.get("risks") or "Not recorded"),
992
1049
  _field("Evidence Paths", details.get("evidence_paths") or "Not recorded"),
@@ -1009,6 +1066,11 @@ class QuestStageViewBuilder:
1009
1066
  "problem": details.get("problem"),
1010
1067
  "hypothesis": details.get("hypothesis"),
1011
1068
  "mechanism": details.get("mechanism"),
1069
+ "method_brief": details.get("method_brief"),
1070
+ "selection_scores": selection_scores or None,
1071
+ "mechanism_family": details.get("mechanism_family"),
1072
+ "change_layer": details.get("change_layer"),
1073
+ "source_lens": details.get("source_lens"),
1012
1074
  "expected_gain": details.get("expected_gain"),
1013
1075
  "risks": details.get("risks") or [],
1014
1076
  "evidence_paths": details.get("evidence_paths") or [],
@@ -1028,6 +1090,101 @@ class QuestStageViewBuilder:
1028
1090
  subviews=["overview", "details", "draft"] if draft_markdown else ["overview", "details"],
1029
1091
  )
1030
1092
 
1093
+ def _build_idea_candidate(self) -> dict[str, Any]:
1094
+ candidate_items = self._idea_candidate_stage_items()
1095
+ latest = candidate_items[-1] if candidate_items else None
1096
+ payload = self._payload(latest or {})
1097
+ details = dict(payload.get("details") or {}) if isinstance(payload.get("details"), dict) else {}
1098
+ candidate_id = str(self.selection.get("selection_ref") or payload.get("idea_id") or "candidate").strip() or "candidate"
1099
+ title_text = (
1100
+ str(details.get("title") or self.selection.get("label") or candidate_id).strip() or candidate_id
1101
+ )
1102
+ paths = dict(payload.get("paths") or {}) if isinstance(payload.get("paths"), dict) else {}
1103
+ candidate_root = paths.get("candidate_root") or str(self.quest_root / "memory" / "ideas" / "_candidates" / candidate_id)
1104
+ idea_md_path = paths.get("idea_md") or str(Path(candidate_root) / "idea.md")
1105
+ draft_md_path = paths.get("idea_draft_md") or details.get("idea_draft_path") or str(Path(candidate_root) / "draft.md")
1106
+ idea_markdown = self._markdown_body_for_path(idea_md_path)
1107
+ draft_markdown = self._markdown_body_for_path(draft_md_path)
1108
+ idea_md_rel_path = self._relative_path_or_raw(idea_md_path)
1109
+ draft_md_rel_path = self._relative_path_or_raw(draft_md_path)
1110
+ candidate_root_rel_path = self._relative_path_or_raw(candidate_root)
1111
+ selection_scores = details.get("selection_scores")
1112
+ selection_score_summary = _selection_score_summary(selection_scores)
1113
+ note = (
1114
+ str(payload.get("summary") or payload.get("reason") or self.selection.get("summary") or "").strip()
1115
+ or "No durable candidate brief summary has been recorded yet."
1116
+ )
1117
+ lineage_intent = str(payload.get("lineage_intent") or details.get("lineage_intent") or "").strip() or None
1118
+ parent_branch = str(payload.get("parent_branch") or details.get("parent_branch") or self.selection.get("branch_name") or "").strip() or None
1119
+ foundation_reason = str(payload.get("foundation_reason") or details.get("foundation_reason") or "").strip() or None
1120
+ return self._base_payload(
1121
+ title=f"Candidate Brief · {title_text}",
1122
+ note=note,
1123
+ status=str(payload.get("status") or "candidate").strip() or "candidate",
1124
+ tags=[
1125
+ "candidate-brief",
1126
+ details.get("mechanism_family") or "",
1127
+ details.get("change_layer") or "",
1128
+ details.get("source_lens") or "",
1129
+ lineage_intent or "",
1130
+ ],
1131
+ overview=[
1132
+ _field("Candidate ID", candidate_id),
1133
+ _field("Parent Branch", parent_branch or "Not recorded"),
1134
+ _field("Next Target", details.get("next_target") or "optimize"),
1135
+ _field("Candidate Root", candidate_root_rel_path or candidate_root),
1136
+ ],
1137
+ key_facts=[
1138
+ _field("Problem", details.get("problem") or "Not recorded"),
1139
+ _field("Hypothesis", details.get("hypothesis") or "Not recorded"),
1140
+ _field("Mechanism", details.get("mechanism") or "Not recorded"),
1141
+ _field("Method Brief", details.get("method_brief") or "Not recorded"),
1142
+ _field("Selection Scores", selection_score_summary or "Not recorded"),
1143
+ _field("Mechanism Family", details.get("mechanism_family") or "Not recorded"),
1144
+ _field("Change Layer", details.get("change_layer") or "Not recorded"),
1145
+ _field("Source Lens", details.get("source_lens") or "Not recorded"),
1146
+ _field("Expected Gain", details.get("expected_gain") or "Not recorded"),
1147
+ _field("Foundation Reason", foundation_reason or "Not recorded"),
1148
+ ],
1149
+ key_files=self._dedupe_files(
1150
+ [
1151
+ self._file_entry(candidate_root, label="Candidate Root", description="Branchless candidate brief workspace.", expected_kind="directory"),
1152
+ self._file_entry(idea_md_path, label="Candidate Markdown", description="Durable candidate brief document."),
1153
+ self._file_entry(draft_md_path, label="Candidate Draft", description="Long-form candidate brief draft."),
1154
+ ]
1155
+ ),
1156
+ history=self._artifact_history(candidate_items),
1157
+ details={
1158
+ "idea": {
1159
+ "idea_id": candidate_id,
1160
+ "title": title_text,
1161
+ "problem": details.get("problem"),
1162
+ "hypothesis": details.get("hypothesis"),
1163
+ "mechanism": details.get("mechanism"),
1164
+ "method_brief": details.get("method_brief"),
1165
+ "selection_scores": selection_scores or None,
1166
+ "mechanism_family": details.get("mechanism_family"),
1167
+ "change_layer": details.get("change_layer"),
1168
+ "source_lens": details.get("source_lens"),
1169
+ "expected_gain": details.get("expected_gain"),
1170
+ "next_target": details.get("next_target") or "optimize",
1171
+ "lineage_intent": lineage_intent,
1172
+ "parent_branch": parent_branch,
1173
+ "candidate_root": candidate_root_rel_path or candidate_root,
1174
+ "idea_path": idea_md_rel_path,
1175
+ "idea_markdown": idea_markdown,
1176
+ "draft_path": draft_md_rel_path,
1177
+ "draft_markdown": draft_markdown,
1178
+ "decision_reason": payload.get("reason"),
1179
+ },
1180
+ "latest_artifact": self._artifact_detail(latest, payload),
1181
+ },
1182
+ lineage_intent=lineage_intent,
1183
+ idea_draft_path=draft_md_rel_path,
1184
+ draft_available=bool(draft_markdown),
1185
+ subviews=["overview", "details", "draft"] if draft_markdown else ["overview", "details"],
1186
+ )
1187
+
1031
1188
  def _build_branch(self) -> dict[str, Any]:
1032
1189
  idea_items = [
1033
1190
  item
@@ -1050,6 +1207,8 @@ class QuestStageViewBuilder:
1050
1207
  idea_title = str(latest_idea_details.get("title") or "").strip() or None
1051
1208
  idea_problem = str(latest_idea_details.get("problem") or "").strip() or None
1052
1209
  next_target = str(latest_idea_details.get("next_target") or "").strip() or None
1210
+ selection_scores = latest_idea_details.get("selection_scores")
1211
+ selection_score_summary = _selection_score_summary(selection_scores)
1053
1212
  lineage_intent = str(
1054
1213
  latest_idea_payload.get("lineage_intent")
1055
1214
  or latest_idea_details.get("lineage_intent")
@@ -1169,6 +1328,11 @@ class QuestStageViewBuilder:
1169
1328
  key_facts=[
1170
1329
  _field("Idea Title", idea_title or "Not recorded"),
1171
1330
  _field("Idea Problem", idea_problem or "Not recorded"),
1331
+ _field("Method Brief", latest_idea_details.get("method_brief") or "Not recorded"),
1332
+ _field("Selection Scores", selection_score_summary or "Not recorded"),
1333
+ _field("Mechanism Family", latest_idea_details.get("mechanism_family") or "Not recorded"),
1334
+ _field("Change Layer", latest_idea_details.get("change_layer") or "Not recorded"),
1335
+ _field("Source Lens", latest_idea_details.get("source_lens") or "Not recorded"),
1172
1336
  _field("Foundation", foundation_label or "Current head"),
1173
1337
  _field("Foundation Reason", foundation_reason or "Not recorded"),
1174
1338
  _field("Next Target", next_target or "Not recorded"),
@@ -1224,6 +1388,11 @@ class QuestStageViewBuilder:
1224
1388
  "lineage_intent": lineage_intent,
1225
1389
  "idea_title": idea_title,
1226
1390
  "idea_problem": idea_problem,
1391
+ "method_brief": latest_idea_details.get("method_brief"),
1392
+ "selection_scores": selection_scores or None,
1393
+ "mechanism_family": latest_idea_details.get("mechanism_family"),
1394
+ "change_layer": latest_idea_details.get("change_layer"),
1395
+ "source_lens": latest_idea_details.get("source_lens"),
1227
1396
  "next_target": next_target,
1228
1397
  "idea_draft_path": idea_draft_rel_path,
1229
1398
  "idea_draft_markdown": idea_draft_markdown,
@@ -1579,11 +1748,13 @@ class QuestStageViewBuilder:
1579
1748
  for path in candidates
1580
1749
  ],
1581
1750
  self._file_entry(paper_root / "selected_outline.json", label="Selected Outline", description="Chosen paper outline."),
1751
+ self._file_entry(paper_root / "outline" / "manifest.json", label="Outline Manifest", description="Author-facing paper outline manifest."),
1582
1752
  self._file_entry(paper_root / "outline_selection.md", label="Outline Selection Note", description="Outline selection rationale."),
1583
1753
  self._file_entry(paper_root / "draft.md", label="Draft Markdown", description="Current paper draft."),
1584
1754
  self._file_entry(paper_root / "writing_plan.md", label="Writing Plan", description="Paper writing plan."),
1585
1755
  self._file_entry(paper_root / "references.bib", label="References", description="Bibliography file."),
1586
1756
  self._file_entry(paper_root / "claim_evidence_map.json", label="Claim-Evidence Map", description="Claim to evidence mapping."),
1757
+ self._file_entry(paper_root / "paper_line_state.json", label="Paper Line State", description="Derived summary state for the active paper line."),
1587
1758
  self._file_entry(paper_root / "baseline_inventory.json", label="Baseline Inventory", description="Canonical and supplementary baseline inventory for writing."),
1588
1759
  self._file_entry(paper_root / "build" / "compile_report.json", label="Compile Report", description="Paper build/compile report."),
1589
1760
  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
@@ -24,6 +24,50 @@ _TOOL_EVENT_ARGS_TEXT_LIMIT = 8_000
24
24
  _TOOL_EVENT_OUTPUT_TEXT_LIMIT = 16_000
25
25
  _MAX_QUEST_EVENT_JSON_BYTES = 2_000_000
26
26
  _OVERSIZED_EVENT_PREVIEW_TEXT_LIMIT = 12_000
27
+ _BUILTIN_MCP_TOOL_APPROVALS: dict[str, tuple[str, ...]] = {
28
+ "memory": (
29
+ "write",
30
+ "read",
31
+ "search",
32
+ "list_recent",
33
+ "promote_to_global",
34
+ ),
35
+ "artifact": (
36
+ "record",
37
+ "checkpoint",
38
+ "prepare_branch",
39
+ "activate_branch",
40
+ "submit_idea",
41
+ "list_research_branches",
42
+ "resolve_runtime_refs",
43
+ "get_paper_contract_health",
44
+ "get_quest_state",
45
+ "get_global_status",
46
+ "get_method_scoreboard",
47
+ "get_optimization_frontier",
48
+ "read_quest_documents",
49
+ "get_conversation_context",
50
+ "get_analysis_campaign",
51
+ "record_main_experiment",
52
+ "create_analysis_campaign",
53
+ "submit_paper_outline",
54
+ "list_paper_outlines",
55
+ "submit_paper_bundle",
56
+ "record_analysis_slice",
57
+ "publish_baseline",
58
+ "attach_baseline",
59
+ "confirm_baseline",
60
+ "waive_baseline",
61
+ "arxiv",
62
+ "refresh_summary",
63
+ "render_git_graph",
64
+ "interact",
65
+ "complete_quest",
66
+ ),
67
+ "bash_exec": (
68
+ "bash_exec",
69
+ ),
70
+ }
27
71
 
28
72
 
29
73
  def _compact_text(value: object, *, limit: int = 1200) -> str:
@@ -322,9 +366,11 @@ def _tool_output(event: dict[str, Any], item: dict[str, Any]) -> str:
322
366
  item.get("result"),
323
367
  item.get("output"),
324
368
  item.get("content"),
369
+ item.get("error"),
325
370
  event.get("result"),
326
371
  event.get("output"),
327
372
  event.get("content"),
373
+ event.get("error"),
328
374
  item.get("aggregated_output"),
329
375
  event.get("aggregated_output"),
330
376
  ):
@@ -338,11 +384,13 @@ def _tool_output(event: dict[str, Any], item: dict[str, Any]) -> str:
338
384
  item.get("output"),
339
385
  item.get("result"),
340
386
  item.get("content"),
387
+ item.get("error"),
341
388
  event.get("aggregated_output"),
342
389
  event.get("changes"),
343
390
  event.get("output"),
344
391
  event.get("result"),
345
392
  event.get("content"),
393
+ event.get("error"),
346
394
  ):
347
395
  text = _compact_text(value, limit=1200)
348
396
  if text:
@@ -642,6 +690,8 @@ class CodexRunner:
642
690
  user_message=request.message,
643
691
  model=request.model,
644
692
  turn_reason=request.turn_reason,
693
+ turn_intent=request.turn_intent,
694
+ turn_mode=request.turn_mode,
645
695
  retry_context=request.retry_context,
646
696
  )
647
697
  write_text(run_root / "prompt.md", prompt)
@@ -663,6 +713,8 @@ class CodexRunner:
663
713
  "workspace_root": str(workspace_root),
664
714
  "cwd": str(workspace_root),
665
715
  "turn_reason": request.turn_reason,
716
+ "turn_intent": request.turn_intent,
717
+ "turn_mode": request.turn_mode,
666
718
  },
667
719
  )
668
720
 
@@ -672,7 +724,10 @@ class CodexRunner:
672
724
  env_key = str(key or "").strip()
673
725
  if not env_key or value is None:
674
726
  continue
675
- env[env_key] = str(value)
727
+ env_value = str(value)
728
+ if env_value == "":
729
+ continue
730
+ env[env_key] = env_value
676
731
  env["CODEX_HOME"] = str(codex_home)
677
732
  env["DEEPSCIENTIST_HOME"] = str(self.home)
678
733
  env["DS_HOME"] = str(self.home)
@@ -681,6 +736,8 @@ class CodexRunner:
681
736
  env["DS_WORKTREE_ROOT"] = str(workspace_root)
682
737
  env["DS_RUN_ID"] = request.run_id
683
738
  env["DS_TURN_REASON"] = request.turn_reason
739
+ env["DS_TURN_INTENT"] = request.turn_intent
740
+ env["DS_TURN_MODE"] = request.turn_mode
684
741
  quest_yaml = read_yaml(request.quest_root / "quest.yaml", {})
685
742
  env["DS_ACTIVE_ANCHOR"] = str(quest_yaml.get("active_anchor", "baseline"))
686
743
  env["DS_CONVERSATION_ID"] = f"quest:{request.quest_id}"
@@ -740,6 +797,13 @@ class CodexRunner:
740
797
  timestamp = utc_now()
741
798
  append_jsonl(history_events, {"timestamp": timestamp, "event": payload})
742
799
  append_jsonl(stdout_events, {"timestamp": timestamp, "line": line})
800
+ try:
801
+ self.artifact_service.quest_service.schedule_projection_refresh(
802
+ request.quest_root,
803
+ kinds=("details",),
804
+ )
805
+ except Exception:
806
+ pass
743
807
  tool_event = _tool_event(
744
808
  payload,
745
809
  quest_id=request.quest_id,
@@ -810,6 +874,14 @@ class CodexRunner:
810
874
  }
811
875
  write_json(run_root / "result.json", result_payload)
812
876
  write_json(history_root / "meta.json", result_payload)
877
+ try:
878
+ self.artifact_service.quest_service.schedule_projection_refresh(
879
+ request.quest_root,
880
+ kinds=("details",),
881
+ throttle_seconds=0.0,
882
+ )
883
+ except Exception:
884
+ pass
813
885
  self.logger.log(
814
886
  "info",
815
887
  "runner.codex.completed",
@@ -822,6 +894,7 @@ class CodexRunner:
822
894
  request.quest_root,
823
895
  {
824
896
  "kind": "run",
897
+ "status": "completed" if exit_code == 0 else "failed",
825
898
  "run_id": request.run_id,
826
899
  "run_kind": request.skill_id,
827
900
  "model": request.model,
@@ -954,10 +1027,11 @@ class CodexRunner:
954
1027
  for filename in ("config.toml", "auth.json"):
955
1028
  source_path = source / filename
956
1029
  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)
1030
+ if not source_path.exists():
1031
+ continue
1032
+ if source_path.resolve() == target_path.resolve():
1033
+ continue
1034
+ shutil.copy2(source_path, target_path)
961
1035
  config_path = target / "config.toml"
962
1036
  if profile and config_path.exists():
963
1037
  adapted_text, _ = adapt_profile_only_provider_config(read_text(config_path), profile=profile)
@@ -1043,6 +1117,7 @@ class CodexRunner:
1043
1117
  args = ["-m", "deepscientist.mcp.server", "--namespace", name]
1044
1118
  lines = [
1045
1119
  f"[mcp_servers.{name}]",
1120
+ 'transport = "stdio"',
1046
1121
  f'command = "{sys.executable}"',
1047
1122
  f"args = [{', '.join(json.dumps(item) for item in args)}]",
1048
1123
  ]
@@ -1057,6 +1132,14 @@ class CodexRunner:
1057
1132
  )
1058
1133
  for key, value in env.items():
1059
1134
  lines.append(f"{key} = {json.dumps(value)}")
1135
+ for tool_name in _BUILTIN_MCP_TOOL_APPROVALS.get(name, ()):
1136
+ lines.extend(
1137
+ [
1138
+ "",
1139
+ f"[mcp_servers.{name}.tools.{tool_name}]",
1140
+ 'approval_mode = "approve"',
1141
+ ]
1142
+ )
1060
1143
  return "\n".join(lines)
1061
1144
 
1062
1145
  def _load_runner_config(self) -> dict[str, Any]:
@@ -17,6 +17,18 @@ def _as_bool_env(name: str) -> bool:
17
17
  return value.lower() in {"1", "true", "yes", "on", "y"}
18
18
 
19
19
 
20
+ def _as_optional_bool_env(name: str) -> bool | None:
21
+ value = _as_text(os.environ.get(name))
22
+ if value is None:
23
+ return None
24
+ normalized = value.lower()
25
+ if normalized in {"1", "true", "yes", "on", "y"}:
26
+ return True
27
+ if normalized in {"0", "false", "no", "off", "n"}:
28
+ return False
29
+ return True
30
+
31
+
20
32
  def codex_runtime_overrides() -> dict[str, str]:
21
33
  binary = _as_text(os.environ.get("DEEPSCIENTIST_CODEX_BINARY") or os.environ.get("DS_CODEX_BINARY"))
22
34
  approval_policy = _as_text(os.environ.get("DEEPSCIENTIST_CODEX_APPROVAL_POLICY"))
@@ -24,9 +36,13 @@ def codex_runtime_overrides() -> dict[str, str]:
24
36
  profile = _as_text(os.environ.get("DEEPSCIENTIST_CODEX_PROFILE"))
25
37
  model = _as_text(os.environ.get("DEEPSCIENTIST_CODEX_MODEL"))
26
38
 
27
- if _as_bool_env("DEEPSCIENTIST_CODEX_YOLO"):
39
+ yolo_enabled = _as_optional_bool_env("DEEPSCIENTIST_CODEX_YOLO")
40
+ if yolo_enabled is True:
28
41
  approval_policy = approval_policy or "never"
29
42
  sandbox_mode = sandbox_mode or "danger-full-access"
43
+ elif yolo_enabled is False:
44
+ approval_policy = approval_policy or "on-request"
45
+ sandbox_mode = sandbox_mode or "workspace-write"
30
46
 
31
47
  overrides: dict[str, str] = {}
32
48
  if binary:
@@ -6,12 +6,21 @@ This shared contract is injected once per turn and applies across the stage and
6
6
 
7
7
  - Treat `artifact.interact(...)` as the main long-lived communication thread across TUI, web, and bound connectors.
8
8
  - If `artifact.interact(...)` returns queued user requirements, treat them as the highest-priority user instruction bundle before continuing the current stage or companion-skill task.
9
- - Immediately follow any non-empty mailbox poll with another `artifact.interact(...)` update that confirms receipt; if the request is directly answerable, answer there, otherwise say the current subtask is paused, give a short plan plus nearest report-back point, and handle that request first.
9
+ - Immediately follow any non-empty mailbox poll with another `artifact.interact(...)` update that confirms receipt; if the request is directly answerable, answer there with `kind='answer'`, otherwise say the current subtask is paused, give a short plan plus nearest report-back point, and handle that request first.
10
+ - If you are explicitly answering or continuing a specific prior interaction thread, use `reply_to_interaction_id` instead of assuming the runtime will always infer the right target.
10
11
  - Stage-kickoff rule: after entering any stage or companion skill, send one `artifact.interact(kind='progress', reply_mode='threaded', ...)` update within the first 3 tool calls of substantial work.
11
12
  - Reading/planning keepalive rule: if you spend 5 consecutive tool calls on reading, searching, comparison, or planning without a user-visible update, send one concise checkpoint even if the route is not finalized yet.
13
+ - Visibility-bound rule: do not drift beyond roughly 12 tool calls or about 8 minutes without a user-visible update when the user-visible state has materially changed.
12
14
  - Subtask-boundary rule: send a user-visible update whenever the active subtask changes materially, especially across intake -> audit, audit -> experiment planning, experiment planning -> run launch, run result -> drafting, or drafting -> review/rebuttal.
13
- - Emit `artifact.interact(kind='progress', reply_mode='threaded', ...)` when there is real user-visible progress: a meaningful checkpoint, route-shaping update, or a concise keepalive once active work has crossed roughly 6 tool calls with a human-meaningful delta. Do not let ordinary active work drift beyond roughly 12 tool calls or about 8 minutes without a user-visible update.
15
+ - Emit `artifact.interact(kind='progress', reply_mode='threaded', ...)` when there is real user-visible progress: a meaningful checkpoint, route-shaping update, blocker, recovery, or a concise keepalive when silence would otherwise hide a meaningful change. Do not reflexively send another progress update if the user-visible state is unchanged.
14
16
  - Keep progress updates chat-like and easy to understand: say what changed, what it means, and what happens next.
15
- - Default to plain-language summaries. Do not mention file paths, artifact ids, branch/worktree ids, session ids, raw commands, or raw logs unless the user asks or needs them to act.
17
+ - Keep the tone respectful and easy to understand. In Chinese, natural respectful phrasing is good; in English, keep a polite professional tone.
18
+ - Assume the user may not know the codebase or internal runtime objects. Explain progress in beginner-friendly task language before technical detail.
19
+ - If there are `2-3` options, tradeoffs, or next steps, prefer a short numbered list instead of a dense block of prose.
20
+ - If a key distinction is quantitative and the number is known, include the number or one short concrete example instead of only saying `better`, `slower`, or `more stable`.
21
+ - Default to plain-language summaries. Do not mention file paths, file names, artifact ids, branch/worktree ids, session ids, raw commands, or raw logs unless the user asks or needs them to act. First translate them into user-facing meaning such as baseline record, draft, experiment result, or supplementary run.
22
+ - When the user is plainly asking a direct question, answer it directly in plain language before resuming background stage work.
16
23
  - Use `reply_mode='blocking'` only for real user decisions that cannot be resolved from local evidence.
17
- - For any blocking decision request, provide 1 to 3 concrete options, put the recommended option first, explain each option's actual content plus pros and cons, and wait up to 1 day when feasible. If the blocker is a missing external credential or secret that only the user can provide, keep the quest waiting, ask the user to supply it or choose an alternative, and do not self-resolve; if resumed without that credential and no other work is possible, a long low-frequency wait such as `bash_exec(command='sleep 3600', mode='await', timeout_seconds=3700)` is acceptable. Otherwise choose the best option yourself and notify the user of the chosen option if the timeout expires.
24
+ - Keep `deliver_to_bound_conversations=True` for normal user-visible continuity. If `delivery_results` or `attachment_issues` show that requested delivery failed, treat that as a real failure and adapt instead of assuming the user already received the message or file.
25
+ - Use `dedupe_key`, `suppress_if_unchanged`, and `min_interval_seconds` only to suppress repeated unchanged `progress` updates, not to suppress a real answer or milestone.
26
+ - For any blocking decision request, provide 1 to 3 concrete options, put the recommended option first, and explain for each option: what it means, how strongly you recommend it, its likely impact on speed / quality / cost / risk, and when it is preferable. Make the user's reply format obvious and wait up to 1 day when feasible. If the blocker is a missing external credential or secret that only the user can provide, keep the quest waiting, ask the user to supply it or choose an alternative, and do not self-resolve; if resumed without that credential and no other work is possible, a long low-frequency wait such as `bash_exec(command='sleep 3600', mode='await', timeout_seconds=3700)` is acceptable. Otherwise choose the best option yourself and notify the user of the chosen option if the timeout expires.