@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
@@ -4,13 +4,17 @@ import json
4
4
  import os
5
5
  import subprocess
6
6
  import tempfile
7
- from shutil import copy2
8
7
  from copy import deepcopy
9
8
  from pathlib import Path
10
9
  from urllib.error import URLError
11
10
  from urllib.request import Request
12
11
 
13
- from ..codex_cli_compat import adapt_profile_only_provider_config, normalize_codex_reasoning_effort
12
+ from ..codex_cli_compat import (
13
+ adapt_profile_only_provider_config,
14
+ materialize_codex_runtime_home,
15
+ normalize_codex_reasoning_effort,
16
+ provider_profile_metadata_from_home,
17
+ )
14
18
  from ..connector.connector_profiles import PROFILEABLE_CONNECTOR_NAMES, list_connector_profiles, normalize_connector_config
15
19
  from ..connector_runtime import build_discovered_target, infer_connector_transport
16
20
  from ..home import repo_root
@@ -48,6 +52,7 @@ from .models import (
48
52
  ConfigFileInfo,
49
53
  SYSTEM_CONNECTOR_NAMES,
50
54
  config_filename,
55
+ default_system_enabled_connectors,
51
56
  default_payload,
52
57
  )
53
58
 
@@ -112,8 +117,9 @@ class ConfigManager:
112
117
  config = self.load_runtime_config()
113
118
  connectors = config.get("connectors") if isinstance(config.get("connectors"), dict) else {}
114
119
  system_enabled = connectors.get("system_enabled") if isinstance(connectors.get("system_enabled"), dict) else {}
120
+ defaults = default_system_enabled_connectors()
115
121
  return {
116
- name: self._coerce_bool(system_enabled.get(name), default=name in {"qq", "weixin"})
122
+ name: self._coerce_bool(system_enabled.get(name), default=defaults.get(name, False))
117
123
  for name in SYSTEM_CONNECTOR_NAMES
118
124
  }
119
125
 
@@ -1190,6 +1196,14 @@ Use **Test** when the file exposes runtime dependencies.
1190
1196
  return "gpt-5.4"
1191
1197
  return str(raw_model).strip()
1192
1198
 
1199
+ @staticmethod
1200
+ def _codex_effective_model(config: dict) -> str:
1201
+ requested = ConfigManager._codex_requested_model(config)
1202
+ profile = ConfigManager._codex_profile_name(config)
1203
+ if profile and not ConfigManager._codex_should_inherit_model(requested):
1204
+ return "inherit"
1205
+ return requested
1206
+
1193
1207
  @staticmethod
1194
1208
  def _codex_profile_name(config: dict) -> str:
1195
1209
  raw_profile = config.get("profile")
@@ -1207,7 +1221,10 @@ Use **Test** when the file exposes runtime dependencies.
1207
1221
  env_key = str(key or "").strip()
1208
1222
  if not env_key or value is None:
1209
1223
  continue
1210
- resolved[env_key] = str(value)
1224
+ env_value = str(value)
1225
+ if env_value == "":
1226
+ continue
1227
+ resolved[env_key] = env_value
1211
1228
  return resolved
1212
1229
 
1213
1230
  def _prepare_codex_probe_home(
@@ -1228,10 +1245,11 @@ Use **Test** when the file exposes runtime dependencies.
1228
1245
 
1229
1246
  temp_home = tempfile.TemporaryDirectory(prefix="ds-codex-probe-")
1230
1247
  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)
1248
+ materialize_codex_runtime_home(
1249
+ source_home=expanded,
1250
+ target_home=temp_root,
1251
+ profile=profile,
1252
+ )
1235
1253
  write_text(temp_root / "config.toml", adapted_text)
1236
1254
  return str(temp_root), warning, temp_home
1237
1255
 
@@ -1356,6 +1374,7 @@ Use **Test** when the file exposes runtime dependencies.
1356
1374
  resolved_binary = resolve_runner_binary(binary, runner_name="codex")
1357
1375
  profile = self._codex_profile_name(config)
1358
1376
  requested_model = self._codex_requested_model(config)
1377
+ effective_model = self._codex_effective_model(config)
1359
1378
  raw_reasoning_effort = config.get("model_reasoning_effort")
1360
1379
  requested_reasoning_effort = (
1361
1380
  str(raw_reasoning_effort).strip()
@@ -1371,9 +1390,9 @@ Use **Test** when the file exposes runtime dependencies.
1371
1390
  "resolved_binary": resolved_binary,
1372
1391
  "config_dir": str(config.get("config_dir") or "~/.codex"),
1373
1392
  "profile": profile,
1374
- "model": requested_model or "inherit",
1393
+ "model": effective_model or "inherit",
1375
1394
  "requested_model": requested_model or "inherit",
1376
- "effective_model": requested_model or "inherit",
1395
+ "effective_model": effective_model or "inherit",
1377
1396
  "approval_policy": str(config.get("approval_policy") or "on-request"),
1378
1397
  "sandbox_mode": str(config.get("sandbox_mode") or "workspace-write"),
1379
1398
  "reasoning_effort": reasoning_effort,
@@ -1411,9 +1430,17 @@ Use **Test** when the file exposes runtime dependencies.
1411
1430
  env["CODEX_HOME"] = prepared_home
1412
1431
  if profile_config_warning:
1413
1432
  compatibility_warnings.append(profile_config_warning)
1433
+ metadata = provider_profile_metadata_from_home(env.get("CODEX_HOME") or config_dir, profile=profile)
1434
+ if metadata.get("requires_openai_auth") is False:
1435
+ env.pop("OPENAI_API_KEY", None)
1436
+ env.pop("OPENAI_BASE_URL", None)
1414
1437
  prompt = "Reply with exactly HELLO."
1415
1438
  if reasoning_effort_warning:
1416
1439
  compatibility_warnings.append(reasoning_effort_warning)
1440
+ if profile and effective_model == "inherit" and not self._codex_should_inherit_model(requested_model):
1441
+ compatibility_warnings.append(
1442
+ f"Codex profile `{profile}` is provider-backed. DeepScientist is probing it with `model: inherit`."
1443
+ )
1417
1444
  base_warnings: list[str] = list(compatibility_warnings)
1418
1445
 
1419
1446
  def run_probe_once(model_for_command: str) -> tuple[list[str], subprocess.CompletedProcess[str] | None, subprocess.TimeoutExpired | None]:
@@ -1440,7 +1467,7 @@ Use **Test** when the file exposes runtime dependencies.
1440
1467
  return command, None, exc
1441
1468
  return command, result, None
1442
1469
 
1443
- command, result, timeout_error = run_probe_once(requested_model)
1470
+ command, result, timeout_error = run_probe_once(effective_model)
1444
1471
  if timeout_error is not None:
1445
1472
  details.update(
1446
1473
  {
@@ -13,7 +13,7 @@ from urllib.request import Request
13
13
 
14
14
  from .. import __version__ as DEEPSCIENTIST_VERSION
15
15
  from ..network import urlopen_with_proxy as urlopen
16
- from ..shared import ensure_dir, read_json, write_json
16
+ from ..shared import ensure_dir, read_json, utc_now, write_json
17
17
 
18
18
  DEFAULT_WEIXIN_BASE_URL = "https://ilinkai.weixin.qq.com"
19
19
  DEFAULT_WEIXIN_CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c"
@@ -611,6 +611,22 @@ def save_weixin_context_tokens(root: Path, items: dict[str, dict[str, Any]]) ->
611
611
  write_json(weixin_context_tokens_path(root), {"tokens": items})
612
612
 
613
613
 
614
+ def _weixin_replay_cursor(value: Any) -> int:
615
+ try:
616
+ return max(0, int(value or 0))
617
+ except (TypeError, ValueError):
618
+ return 0
619
+
620
+
621
+ def get_weixin_context_entry(root: Path, user_id: str) -> dict[str, Any]:
622
+ normalized_user_id = str(user_id or "").strip()
623
+ if not normalized_user_id:
624
+ return {}
625
+ items = load_weixin_context_tokens(root)
626
+ current = items.get(normalized_user_id)
627
+ return dict(current) if isinstance(current, dict) else {}
628
+
629
+
614
630
  def remember_weixin_context_token(
615
631
  root: Path,
616
632
  *,
@@ -635,6 +651,16 @@ def remember_weixin_context_token(
635
651
  "conversation_id": str(conversation_id or current.get("conversation_id") or "").strip() or None,
636
652
  "message_id": str(message_id or current.get("message_id") or "").strip() or None,
637
653
  "updated_at": str(updated_at or current.get("updated_at") or "").strip() or None,
654
+ "stale_context": False,
655
+ "stale_since": None,
656
+ "last_ret_minus_2_at": None,
657
+ "last_outbound_error": None,
658
+ "last_outbound_kind": None,
659
+ "queued_replay_cursor": _weixin_replay_cursor(current.get("queued_replay_cursor")),
660
+ "last_replay_at": str(current.get("last_replay_at") or "").strip() or None,
661
+ "last_replay_trigger_message_id": str(current.get("last_replay_trigger_message_id") or "").strip() or None,
662
+ "last_replayed_count": _weixin_replay_cursor(current.get("last_replayed_count")) or None,
663
+ "last_replay_dropped_count": _weixin_replay_cursor(current.get("last_replay_dropped_count")) or None,
638
664
  }
639
665
  save_weixin_context_tokens(root, items)
640
666
 
@@ -648,6 +674,101 @@ def get_weixin_context_token(root: Path, user_id: str) -> str | None:
648
674
  return token or None
649
675
 
650
676
 
677
+ def get_weixin_replay_cursor(root: Path, user_id: str) -> int:
678
+ entry = get_weixin_context_entry(root, user_id)
679
+ return _weixin_replay_cursor(entry.get("queued_replay_cursor"))
680
+
681
+
682
+ def update_weixin_replay_cursor(
683
+ root: Path,
684
+ *,
685
+ user_id: str,
686
+ queued_replay_cursor: int,
687
+ last_replay_at: str | None = None,
688
+ last_replay_trigger_message_id: str | None = None,
689
+ last_replayed_count: int | None = None,
690
+ last_replay_dropped_count: int | None = None,
691
+ ) -> dict[str, Any]:
692
+ normalized_user_id = str(user_id or "").strip()
693
+ if not normalized_user_id:
694
+ return {}
695
+ items = load_weixin_context_tokens(root)
696
+ current = dict(items.get(normalized_user_id) or {})
697
+ current.update(
698
+ {
699
+ "user_id": normalized_user_id,
700
+ "queued_replay_cursor": _weixin_replay_cursor(queued_replay_cursor),
701
+ "last_replay_at": str(last_replay_at or utc_now()).strip() or utc_now(),
702
+ "last_replay_trigger_message_id": str(last_replay_trigger_message_id or "").strip() or None,
703
+ "last_replayed_count": _weixin_replay_cursor(last_replayed_count) if last_replayed_count is not None else None,
704
+ "last_replay_dropped_count": _weixin_replay_cursor(last_replay_dropped_count)
705
+ if last_replay_dropped_count is not None
706
+ else None,
707
+ }
708
+ )
709
+ items[normalized_user_id] = current
710
+ save_weixin_context_tokens(root, items)
711
+ return current
712
+
713
+
714
+ def mark_weixin_context_stale(
715
+ root: Path,
716
+ *,
717
+ user_id: str,
718
+ error: str,
719
+ kind: str | None = None,
720
+ updated_at: str | None = None,
721
+ ) -> dict[str, Any]:
722
+ normalized_user_id = str(user_id or "").strip()
723
+ if not normalized_user_id:
724
+ return {}
725
+ timestamp = str(updated_at or utc_now()).strip() or utc_now()
726
+ items = load_weixin_context_tokens(root)
727
+ current = dict(items.get(normalized_user_id) or {})
728
+ current.update(
729
+ {
730
+ "user_id": normalized_user_id,
731
+ "stale_context": True,
732
+ "stale_since": str(current.get("stale_since") or "").strip() or timestamp,
733
+ "last_ret_minus_2_at": timestamp,
734
+ "last_outbound_error": str(error or "").strip() or None,
735
+ "last_outbound_kind": str(kind or "").strip() or None,
736
+ }
737
+ )
738
+ items[normalized_user_id] = current
739
+ save_weixin_context_tokens(root, items)
740
+ return current
741
+
742
+
743
+ def clear_weixin_context_send_state(
744
+ root: Path,
745
+ *,
746
+ user_id: str,
747
+ kind: str | None = None,
748
+ updated_at: str | None = None,
749
+ ) -> dict[str, Any]:
750
+ normalized_user_id = str(user_id or "").strip()
751
+ if not normalized_user_id:
752
+ return {}
753
+ timestamp = str(updated_at or utc_now()).strip() or utc_now()
754
+ items = load_weixin_context_tokens(root)
755
+ current = dict(items.get(normalized_user_id) or {})
756
+ current.update(
757
+ {
758
+ "user_id": normalized_user_id,
759
+ "stale_context": False,
760
+ "stale_since": None,
761
+ "last_ret_minus_2_at": None,
762
+ "last_outbound_error": None,
763
+ "last_outbound_kind": str(kind or "").strip() or None,
764
+ "last_success_at": timestamp,
765
+ }
766
+ )
767
+ items[normalized_user_id] = current
768
+ save_weixin_context_tokens(root, items)
769
+ return current
770
+
771
+
651
772
  def weixin_sync_state_path(root: Path) -> Path:
652
773
  return ensure_dir(root) / "sync_state.json"
653
774
 
@@ -11,12 +11,17 @@ from urllib.parse import parse_qs, unquote
11
11
  from ...acp import OptionalACPBridge, build_session_descriptor, build_session_update, get_acp_bridge_status
12
12
  from ...bash_exec.service import DEFAULT_TERMINAL_SESSION_ID
13
13
  from ... import __version__ as DEEPSCIENTIST_VERSION
14
- from ...gitops import commit_detail, compare_refs, diff_file_between_refs, diff_file_for_commit, export_git_graph, list_branch_canvas, log_ref_history
14
+ from ...gitops import commit_detail, compare_refs, diff_file_between_refs, diff_file_for_commit, export_git_graph, log_ref_history
15
15
  from ...memory import MemoryService
16
16
  from ...quest import QuestService
17
17
  from ...shared import generate_id, read_json, read_text, resolve_within, run_command, sha256_text, utc_now
18
18
  from ...runners import RunRequest
19
19
 
20
+ _COPILOT_LEAD_MESSAGE = (
21
+ "我是 DeepScientist,任何事情都可以找我帮忙。"
22
+ "你可以让我读论文、改代码、看实验、整理思路,或者直接开始执行一个任务。"
23
+ )
24
+
20
25
 
21
26
  class ApiHandlers:
22
27
  def __init__(self, app: "DaemonApp") -> None:
@@ -73,6 +78,8 @@ class ApiHandlers:
73
78
  runtime_payload = {
74
79
  "surface": "quest",
75
80
  "version": DEEPSCIENTIST_VERSION,
81
+ "homePath": str(self.app.home),
82
+ "auth": self.app.browser_auth_runtime_payload(),
76
83
  "supports": {
77
84
  "productApis": False,
78
85
  "socketIo": False,
@@ -158,9 +165,87 @@ npm --prefix src/ui run build</pre>
158
165
  "daemon_id": self.app.daemon_id,
159
166
  "managed_by": self.app.daemon_managed_by,
160
167
  "pid": os.getpid(),
168
+ "auth_enabled": self.app.browser_auth_enabled,
161
169
  "sessions": self.app.sessions.snapshot(),
162
170
  }
163
171
 
172
+ def auth_login(self, body: dict | None = None) -> tuple[int, dict, str] | tuple[int, dict]:
173
+ if not self.app.browser_auth_enabled:
174
+ payload = {
175
+ "ok": True,
176
+ "authenticated": True,
177
+ "auth_enabled": False,
178
+ }
179
+ return 200, {"Content-Type": "application/json; charset=utf-8"}, json.dumps(payload, ensure_ascii=False)
180
+
181
+ candidate = str(((body or {}) if isinstance(body, dict) else {}).get("token") or "").strip()
182
+ if not candidate:
183
+ return 400, {
184
+ "ok": False,
185
+ "message": "Token is required.",
186
+ "auth_required": True,
187
+ "auth_enabled": True,
188
+ }
189
+ if not self.app.browser_auth_matches(candidate):
190
+ return 401, {
191
+ "ok": False,
192
+ "message": "Invalid token.",
193
+ "auth_required": True,
194
+ "auth_enabled": True,
195
+ }
196
+ payload = {
197
+ "ok": True,
198
+ "authenticated": True,
199
+ "auth_enabled": True,
200
+ "token_masked": self.app.masked_browser_auth_token(),
201
+ }
202
+ return (
203
+ 200,
204
+ {
205
+ "Content-Type": "application/json; charset=utf-8",
206
+ "Cache-Control": "no-store, max-age=0, must-revalidate",
207
+ "Set-Cookie": self.app._browser_auth_cookie_header(candidate),
208
+ },
209
+ json.dumps(payload, ensure_ascii=False),
210
+ )
211
+
212
+ def auth_token(self) -> dict:
213
+ return {
214
+ "ok": True,
215
+ "auth_enabled": self.app.browser_auth_enabled,
216
+ "token": self.app.browser_auth_token,
217
+ "token_masked": self.app.masked_browser_auth_token(),
218
+ }
219
+
220
+ def auth_rotate(self, body: dict | None = None) -> tuple[int, dict, str] | tuple[int, dict]:
221
+ if not self.app.browser_auth_enabled:
222
+ payload = {
223
+ "ok": True,
224
+ "auth_enabled": False,
225
+ "rotated": False,
226
+ "token": None,
227
+ "token_masked": None,
228
+ }
229
+ return 200, {"Content-Type": "application/json; charset=utf-8"}, json.dumps(payload, ensure_ascii=False)
230
+
231
+ rotated = self.app.rotate_browser_auth_token()
232
+ payload = {
233
+ "ok": True,
234
+ "auth_enabled": True,
235
+ "rotated": True,
236
+ "token": rotated,
237
+ "token_masked": self.app.masked_browser_auth_token(),
238
+ }
239
+ return (
240
+ 200,
241
+ {
242
+ "Content-Type": "application/json; charset=utf-8",
243
+ "Cache-Control": "no-store, max-age=0, must-revalidate",
244
+ "Set-Cookie": self.app._browser_auth_cookie_header(rotated),
245
+ },
246
+ json.dumps(payload, ensure_ascii=False),
247
+ )
248
+
164
249
  def system_update(self) -> dict:
165
250
  return self.app.system_update_status()
166
251
 
@@ -324,6 +409,33 @@ npm --prefix src/ui run build</pre>
324
409
  return 400, {"ok": False, "message": str(exc)}
325
410
  except RuntimeError as exc:
326
411
  return 409, {"ok": False, "message": str(exc)}
412
+ workspace_mode = (
413
+ str(startup_contract.get("workspace_mode") or "").strip().lower()
414
+ if isinstance(startup_contract, dict)
415
+ else ""
416
+ )
417
+ if workspace_mode in {"copilot", "autonomous"}:
418
+ quest_root = self.app.quest_service._quest_root(snapshot["quest_id"])
419
+ self.app.quest_service.update_research_state(quest_root, workspace_mode=workspace_mode)
420
+ if workspace_mode == "copilot":
421
+ self.app.quest_service.append_message(
422
+ snapshot["quest_id"],
423
+ "assistant",
424
+ _COPILOT_LEAD_MESSAGE,
425
+ source="deepscientist",
426
+ )
427
+ self.app.quest_service.update_runtime_state(
428
+ quest_root=quest_root,
429
+ status="idle",
430
+ display_status="idle",
431
+ )
432
+ self.app.quest_service.set_continuation_state(
433
+ quest_root,
434
+ policy="wait_for_user_or_resume",
435
+ anchor="decision",
436
+ reason="copilot_mode",
437
+ )
438
+ snapshot = self.app.quest_service.snapshot(snapshot["quest_id"])
327
439
  payload: dict[str, object] = {"ok": True, "snapshot": snapshot}
328
440
  if auto_start:
329
441
  startup = self.app.submit_user_message(
@@ -474,6 +586,12 @@ npm --prefix src/ui run build</pre>
474
586
 
475
587
  def quest_session(self, quest_id: str) -> dict:
476
588
  snapshot = self.app.quest_service.snapshot_fast(quest_id)
589
+ for kind in ("details", "canvas", "git_canvas"):
590
+ try:
591
+ self.app.quest_service.prime_projection(quest_id, kind)
592
+ except Exception:
593
+ continue
594
+ self.app.schedule_latest_quest_terminal_prewarm(quest_id)
477
595
  return {
478
596
  "ok": True,
479
597
  "quest_id": quest_id,
@@ -491,7 +609,7 @@ npm --prefix src/ui run build</pre>
491
609
  tail = tail_raw in {"1", "true", "yes", "on"}
492
610
  format_name = ((query.get("format") or ["both"])[0] or "both").lower()
493
611
  session_id = ((query.get("session_id") or [f"quest:{quest_id}"])[0] or f"quest:{quest_id}")
494
- payload = self._fresh_quest_service().events(
612
+ payload = self.app.quest_service.events(
495
613
  quest_id,
496
614
  after=after,
497
615
  before=before,
@@ -768,7 +886,20 @@ npm --prefix src/ui run build</pre>
768
886
  return self.app.control_quest(quest_id, action=action, source=source)
769
887
 
770
888
  def workflow(self, quest_id: str) -> dict:
771
- return self.app.quest_service.workflow(quest_id)
889
+ payload = self.app.quest_service.workflow(quest_id)
890
+ projection_state = str(((payload or {}).get("projection_status") or {}).get("state") or "").strip().lower()
891
+ if projection_state and projection_state != "ready":
892
+ if isinstance(payload, dict):
893
+ payload["optimization_frontier"] = None
894
+ return payload
895
+ quest_root = self._fresh_quest_service()._quest_root(quest_id)
896
+ try:
897
+ frontier = self.app.artifact_service.get_optimization_frontier(quest_root)
898
+ except Exception:
899
+ frontier = {"ok": False}
900
+ if isinstance(payload, dict):
901
+ payload["optimization_frontier"] = frontier.get("optimization_frontier") if isinstance(frontier, dict) else None
902
+ return payload
772
903
 
773
904
  def quest_layout(self, quest_id: str) -> dict:
774
905
  quest_root = self._fresh_quest_service()._quest_root(quest_id)
@@ -816,15 +947,21 @@ npm --prefix src/ui run build</pre>
816
947
  def metrics_timeline(self, quest_id: str) -> dict:
817
948
  return self.app.quest_service.metrics_timeline(quest_id)
818
949
 
950
+ def baseline_compare(self, quest_id: str) -> dict:
951
+ return self.app.quest_service.baseline_compare(quest_id)
952
+
819
953
  def git_branches(self, quest_id: str) -> dict:
820
954
  quest_root = self._fresh_quest_service()._quest_root(quest_id)
821
- payload = list_branch_canvas(quest_root, quest_id=quest_id)
955
+ payload = self.app.quest_service.git_branch_canvas(quest_id)
822
956
  research_state = self.app.quest_service.read_research_state(quest_root)
823
957
  active_workspace_branch = str(research_state.get("current_workspace_branch") or "").strip() or None
824
958
  research_head_branch = str(research_state.get("research_head_branch") or "").strip() or None
825
959
  payload["active_workspace_ref"] = active_workspace_branch
826
960
  payload["research_head_ref"] = research_head_branch
827
961
  payload["workspace_mode"] = str(research_state.get("workspace_mode") or "quest").strip() or "quest"
962
+ projection_state = str(((payload or {}).get("projection_status") or {}).get("state") or "").strip().lower()
963
+ if projection_state and projection_state != "ready" and not (payload.get("nodes") or []):
964
+ return payload
828
965
  quest_data = self.app.quest_service.read_quest_yaml(quest_root)
829
966
  active_anchor = str(quest_data.get("active_anchor") or "").strip().lower()
830
967
  active_analysis_campaign_id = str(research_state.get("active_analysis_campaign_id") or "").strip() or None
@@ -837,11 +974,40 @@ npm --prefix src/ui run build</pre>
837
974
  branch_summary = self.app.artifact_service.list_research_branches(quest_root)
838
975
  except Exception:
839
976
  branch_summary = {"branches": []}
977
+ try:
978
+ optimization_frontier = self.app.artifact_service.get_optimization_frontier(quest_root)
979
+ except Exception:
980
+ optimization_frontier = {"ok": False}
840
981
  branch_summary_by_name = {
841
982
  str(item.get("branch_name") or "").strip(): item
842
983
  for item in (branch_summary.get("branches") or [])
843
984
  if str(item.get("branch_name") or "").strip()
844
985
  }
986
+ frontier_payload = (
987
+ dict(optimization_frontier.get("optimization_frontier") or {})
988
+ if isinstance(optimization_frontier, dict)
989
+ and isinstance(optimization_frontier.get("optimization_frontier"), dict)
990
+ else {}
991
+ )
992
+ best_branch_name = str(((frontier_payload.get("best_branch") or {}) if isinstance(frontier_payload.get("best_branch"), dict) else {}).get("branch_name") or "").strip() or None
993
+ stagnant_branch_names = {
994
+ str(item.get("branch_name") or "").strip()
995
+ for item in (frontier_payload.get("stagnant_branches") or [])
996
+ if isinstance(item, dict) and str(item.get("branch_name") or "").strip()
997
+ }
998
+ fusion_candidate_names = {
999
+ str(item.get("branch_name") or "").strip()
1000
+ for item in (frontier_payload.get("fusion_candidates") or [])
1001
+ if isinstance(item, dict) and str(item.get("branch_name") or "").strip()
1002
+ }
1003
+ candidate_count_by_branch: dict[str, int] = {}
1004
+ for item in frontier_payload.get("implementation_candidates") or []:
1005
+ if not isinstance(item, dict):
1006
+ continue
1007
+ branch_name = str(item.get("branch") or "").strip()
1008
+ if not branch_name:
1009
+ continue
1010
+ candidate_count_by_branch[branch_name] = candidate_count_by_branch.get(branch_name, 0) + 1
845
1011
  active_campaign = {}
846
1012
  if active_analysis_campaign_id:
847
1013
  try:
@@ -856,6 +1022,11 @@ npm --prefix src/ui run build</pre>
856
1022
  if isinstance(active_campaign, dict)
857
1023
  else None
858
1024
  )
1025
+ campaign_paper_line_branch = (
1026
+ str(active_campaign.get("paper_line_branch") or "").strip() or None
1027
+ if isinstance(active_campaign, dict)
1028
+ else None
1029
+ )
859
1030
  campaign_slices = [
860
1031
  dict(item)
861
1032
  for item in ((active_campaign or {}).get("slices") or [])
@@ -903,6 +1074,14 @@ npm --prefix src/ui run build</pre>
903
1074
  workflow_state["status_reason"] = "Analysis slice pending."
904
1075
  return workflow_state
905
1076
  if branch_kind == "paper":
1077
+ if campaign_paper_line_branch and ref == campaign_paper_line_branch and next_pending_slice_id is not None:
1078
+ workflow_state["analysis_state"] = "active"
1079
+ workflow_state["writing_state"] = "blocked_by_analysis"
1080
+ workflow_state["status_reason"] = (
1081
+ f"Analysis {campaign_completed_slices}/{campaign_total_slices} done"
1082
+ + (f" · next: {next_pending_slice_id}" if next_pending_slice_id else "")
1083
+ )
1084
+ return workflow_state
906
1085
  if ref == current_workspace_branch and workspace_mode == "paper":
907
1086
  workflow_state["writing_state"] = "completed" if active_anchor == "finalize" else "active"
908
1087
  workflow_state["status_reason"] = (
@@ -912,7 +1091,7 @@ npm --prefix src/ui run build</pre>
912
1091
  workflow_state["writing_state"] = "ready"
913
1092
  workflow_state["status_reason"] = "Writing workspace prepared."
914
1093
  return workflow_state
915
- if campaign_parent_branch and ref == campaign_parent_branch:
1094
+ if campaign_parent_branch and not campaign_paper_line_branch and ref == campaign_parent_branch:
916
1095
  workflow_state["analysis_state"] = "completed" if next_pending_slice_id is None else "active"
917
1096
  if has_main_result:
918
1097
  workflow_state["writing_state"] = "ready" if next_pending_slice_id is None else "blocked_by_analysis"
@@ -955,6 +1134,20 @@ npm --prefix src/ui run build</pre>
955
1134
  node["latest_main_experiment"] = summary.get("latest_main_experiment")
956
1135
  node["experiment_count"] = summary.get("experiment_count")
957
1136
  node["has_main_result"] = summary.get("has_main_result")
1137
+ node["optimization_mode"] = frontier_payload.get("mode")
1138
+ node["optimization_best"] = ref == best_branch_name
1139
+ node["optimization_stagnant"] = ref in stagnant_branch_names
1140
+ node["optimization_fusion_candidate"] = ref in fusion_candidate_names
1141
+ node["optimization_candidate_count"] = candidate_count_by_branch.get(ref, 0)
1142
+ return payload
1143
+
1144
+ def git_canvas(self, quest_id: str) -> dict:
1145
+ quest_root = self._fresh_quest_service()._quest_root(quest_id)
1146
+ payload = self.app.quest_service.git_commit_canvas(quest_id)
1147
+ research_state = self.app.quest_service.read_research_state(quest_root)
1148
+ active_workspace_branch = str(research_state.get("current_workspace_branch") or "").strip() or None
1149
+ payload["active_workspace_ref"] = active_workspace_branch
1150
+ payload["workspace_mode"] = str(research_state.get("workspace_mode") or "copilot").strip() or "copilot"
958
1151
  return payload
959
1152
 
960
1153
  def git_log(self, quest_id: str, path: str) -> dict:
@@ -1319,10 +1512,7 @@ npm --prefix src/ui run build</pre>
1319
1512
  mime_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
1320
1513
  content = quest_service._read_git_bytes(quest_root, revision, relative)
1321
1514
  return 200, self._asset_headers(mime_type), content
1322
- path, _writable, _scope, _source_kind = quest_service._resolve_document(
1323
- quest_service._quest_root(quest_id),
1324
- document_id,
1325
- )
1515
+ path, _writable, _scope, _source_kind = quest_service.resolve_document(quest_id, document_id)
1326
1516
  if not path.exists() or not path.is_file():
1327
1517
  return 404, {"Content-Type": "text/plain; charset=utf-8"}, b"Not Found"
1328
1518
  mime_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
@@ -9,6 +9,9 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
9
9
  ("GET", re.compile(r"^/metis/agent/api/health$"), "lingzhu_health"),
10
10
  ("POST", re.compile(r"^/metis/agent/api/sse$"), "lingzhu_sse"),
11
11
  ("GET", re.compile(r"^/(?P<spa_path>(?!api(?:/|$)|metis(?:/|$)|ui(?:/|$)|assets(?:/|$)).+)$"), "spa_root"),
12
+ ("POST", re.compile(r"^/api/auth/login$"), "auth_login"),
13
+ ("GET", re.compile(r"^/api/auth/token$"), "auth_token"),
14
+ ("POST", re.compile(r"^/api/auth/rotate$"), "auth_rotate"),
12
15
  ("GET", re.compile(r"^/api/health$"), "health"),
13
16
  ("GET", re.compile(r"^/api/system/update$"), "system_update"),
14
17
  ("POST", re.compile(r"^/api/system/update$"), "system_update_action"),
@@ -61,7 +64,9 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
61
64
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/graph$"), "graph"),
62
65
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/graph/(?P<kind>svg|png|json)$"), "graph_asset"),
63
66
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/metrics/timeline$"), "metrics_timeline"),
67
+ ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/baselines/compare$"), "baseline_compare"),
64
68
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/branches$"), "git_branches"),
69
+ ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/canvas$"), "git_canvas"),
65
70
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/log$"), "git_log"),
66
71
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/compare$"), "git_compare"),
67
72
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/commit$"), "git_commit"),