@researai/deepscientist 1.5.1 → 1.5.3

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 (116) hide show
  1. package/README.md +69 -1
  2. package/bin/ds.js +2239 -153
  3. package/docs/en/00_QUICK_START.md +60 -20
  4. package/docs/en/01_SETTINGS_REFERENCE.md +20 -20
  5. package/docs/en/02_START_RESEARCH_GUIDE.md +11 -11
  6. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +10 -10
  7. package/docs/en/05_TUI_GUIDE.md +1 -1
  8. package/docs/en/09_DOCTOR.md +48 -4
  9. package/docs/en/90_ARCHITECTURE.md +4 -2
  10. package/docs/zh/00_QUICK_START.md +60 -20
  11. package/docs/zh/01_SETTINGS_REFERENCE.md +21 -21
  12. package/docs/zh/02_START_RESEARCH_GUIDE.md +19 -19
  13. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +10 -10
  14. package/docs/zh/05_TUI_GUIDE.md +1 -1
  15. package/docs/zh/09_DOCTOR.md +46 -4
  16. package/install.sh +125 -8
  17. package/package.json +2 -1
  18. package/pyproject.toml +1 -1
  19. package/src/deepscientist/__init__.py +6 -1
  20. package/src/deepscientist/artifact/service.py +553 -26
  21. package/src/deepscientist/bash_exec/monitor.py +23 -4
  22. package/src/deepscientist/bash_exec/runtime.py +3 -0
  23. package/src/deepscientist/bash_exec/service.py +132 -4
  24. package/src/deepscientist/bridges/base.py +10 -19
  25. package/src/deepscientist/channels/discord_gateway.py +25 -2
  26. package/src/deepscientist/channels/feishu_long_connection.py +41 -3
  27. package/src/deepscientist/channels/qq.py +524 -64
  28. package/src/deepscientist/channels/qq_gateway.py +22 -3
  29. package/src/deepscientist/channels/relay.py +429 -90
  30. package/src/deepscientist/channels/slack_socket.py +29 -5
  31. package/src/deepscientist/channels/telegram_polling.py +25 -2
  32. package/src/deepscientist/channels/whatsapp_local_session.py +32 -4
  33. package/src/deepscientist/cli.py +27 -0
  34. package/src/deepscientist/config/models.py +6 -40
  35. package/src/deepscientist/config/service.py +165 -156
  36. package/src/deepscientist/connector_profiles.py +346 -0
  37. package/src/deepscientist/connector_runtime.py +88 -43
  38. package/src/deepscientist/daemon/api/handlers.py +65 -11
  39. package/src/deepscientist/daemon/api/router.py +4 -2
  40. package/src/deepscientist/daemon/app.py +772 -219
  41. package/src/deepscientist/doctor.py +69 -2
  42. package/src/deepscientist/gitops/diff.py +3 -0
  43. package/src/deepscientist/home.py +25 -2
  44. package/src/deepscientist/mcp/context.py +3 -1
  45. package/src/deepscientist/mcp/server.py +66 -7
  46. package/src/deepscientist/migration.py +114 -0
  47. package/src/deepscientist/prompts/builder.py +71 -3
  48. package/src/deepscientist/qq_profiles.py +186 -0
  49. package/src/deepscientist/quest/layout.py +1 -0
  50. package/src/deepscientist/quest/service.py +70 -12
  51. package/src/deepscientist/quest/stage_views.py +46 -0
  52. package/src/deepscientist/runners/codex.py +2 -0
  53. package/src/deepscientist/shared.py +44 -17
  54. package/src/prompts/connectors/lingzhu.md +3 -0
  55. package/src/prompts/connectors/qq.md +42 -2
  56. package/src/prompts/system.md +123 -10
  57. package/src/skills/analysis-campaign/SKILL.md +35 -6
  58. package/src/skills/baseline/SKILL.md +73 -32
  59. package/src/skills/decision/SKILL.md +4 -3
  60. package/src/skills/experiment/SKILL.md +28 -6
  61. package/src/skills/finalize/SKILL.md +5 -2
  62. package/src/skills/idea/SKILL.md +2 -2
  63. package/src/skills/intake-audit/SKILL.md +2 -2
  64. package/src/skills/rebuttal/SKILL.md +4 -2
  65. package/src/skills/review/SKILL.md +4 -2
  66. package/src/skills/scout/SKILL.md +2 -2
  67. package/src/skills/write/SKILL.md +2 -2
  68. package/src/tui/package.json +1 -1
  69. package/src/ui/dist/assets/{AiManusChatView-w5lF2Ttt.js → AiManusChatView-qzChi9uh.js} +67 -94
  70. package/src/ui/dist/assets/{AnalysisPlugin-DJOED79I.js → AnalysisPlugin-CcC_-UqN.js} +1 -1
  71. package/src/ui/dist/assets/{AutoFigurePlugin-DaG61Y0M.js → AutoFigurePlugin-DD8LkJLe.js} +5 -5
  72. package/src/ui/dist/assets/{CliPlugin-CV4LqUB_.js → CliPlugin-DJJFfVmW.js} +17 -110
  73. package/src/ui/dist/assets/{CodeEditorPlugin-DylfAea4.js → CodeEditorPlugin-CrjkHNLh.js} +8 -8
  74. package/src/ui/dist/assets/{CodeViewerPlugin-F7saY0LM.js → CodeViewerPlugin-obnD6G5R.js} +5 -5
  75. package/src/ui/dist/assets/{DocViewerPlugin-COP0c7jf.js → DocViewerPlugin-DB9SUQVd.js} +3 -3
  76. package/src/ui/dist/assets/{GitDiffViewerPlugin-CAS05pT9.js → GitDiffViewerPlugin-DZLlNlD2.js} +1 -1
  77. package/src/ui/dist/assets/{ImageViewerPlugin-Bco1CN_w.js → ImageViewerPlugin-BGwfDZ0Y.js} +5 -5
  78. package/src/ui/dist/assets/{LabCopilotPanel-CvMlCD99.js → LabCopilotPanel-dfLptQcR.js} +10 -10
  79. package/src/ui/dist/assets/{LabPlugin-BYankkE4.js → LabPlugin-CeGjAl3A.js} +1 -1
  80. package/src/ui/dist/assets/{LatexPlugin-LDSMR-t-.js → LatexPlugin-BBJ7kd1V.js} +7 -7
  81. package/src/ui/dist/assets/{MarkdownViewerPlugin-B7o80jgm.js → MarkdownViewerPlugin-DKZi7BcB.js} +4 -4
  82. package/src/ui/dist/assets/{MarketplacePlugin-CM6ZOcpC.js → MarketplacePlugin-C_k-9jD0.js} +3 -3
  83. package/src/ui/dist/assets/{NotebookEditor-Dc61cXmK.js → NotebookEditor-4R88_BMO.js} +1 -1
  84. package/src/ui/dist/assets/{PdfLoader-DWowuQwx.js → PdfLoader-DwEFQLrw.js} +1 -1
  85. package/src/ui/dist/assets/{PdfMarkdownPlugin-BsJM1q_a.js → PdfMarkdownPlugin-D-jdsqF8.js} +3 -3
  86. package/src/ui/dist/assets/{PdfViewerPlugin-DB2eEEFQ.js → PdfViewerPlugin-CmeBGDY0.js} +10 -10
  87. package/src/ui/dist/assets/{SearchPlugin-CraThSvt.js → SearchPlugin-Dlz2WKJ4.js} +1 -1
  88. package/src/ui/dist/assets/{Stepper-CgocRTPq.js → Stepper-ClOgzWM3.js} +1 -1
  89. package/src/ui/dist/assets/{TextViewerPlugin-B1JGhKtd.js → TextViewerPlugin-DDQWxibk.js} +4 -4
  90. package/src/ui/dist/assets/{VNCViewer-CclFC7FM.js → VNCViewer-CJXT0Nm8.js} +9 -9
  91. package/src/ui/dist/assets/{bibtex-D3IKsMl7.js → bibtex-DLr4Rtk4.js} +1 -1
  92. package/src/ui/dist/assets/{code-BP37Xx0p.js → code-DgKK408Y.js} +1 -1
  93. package/src/ui/dist/assets/{file-content-BAJSu-9r.js → file-content-6HBqQnvQ.js} +1 -1
  94. package/src/ui/dist/assets/{file-diff-panel-DUGeCTuy.js → file-diff-panel-Dhu0TbBM.js} +1 -1
  95. package/src/ui/dist/assets/{file-socket-CXc1Ojf7.js → file-socket-CP3iwVZG.js} +1 -1
  96. package/src/ui/dist/assets/{file-utils-2J21jt7M.js → file-utils-BsS-Aw68.js} +1 -1
  97. package/src/ui/dist/assets/{image-CMMmgvcn.js → image-ByeK-Zcv.js} +1 -1
  98. package/src/ui/dist/assets/{index-DmwmJmbW.js → index-BLjo5--a.js} +33610 -31016
  99. package/src/ui/dist/assets/{index-CWgMgpow.js → index-BdsE0uRz.js} +11 -11
  100. package/src/ui/dist/assets/{index-s7aHnNQ4.js → index-C-eX-N6A.js} +1 -1
  101. package/src/ui/dist/assets/{index-KGt-z-dD.css → index-CuQhlrR-.css} +2747 -2
  102. package/src/ui/dist/assets/{index-BaVumsQT.js → index-DyremSIv.js} +2 -2
  103. package/src/ui/dist/assets/{message-square-CQRfX0Am.js → message-square-DnagiLnc.js} +1 -1
  104. package/src/ui/dist/assets/{monaco-B4TbdsrF.js → monaco-4kBFeprs.js} +1 -1
  105. package/src/ui/dist/assets/{popover-B8Rokodk.js → popover-hRCXZzs2.js} +1 -1
  106. package/src/ui/dist/assets/{project-sync-D_i96KH4.js → project-sync-O_85YuP6.js} +1 -1
  107. package/src/ui/dist/assets/{sigma-D12PnzCN.js → sigma-DvKopSnL.js} +1 -1
  108. package/src/ui/dist/assets/{tooltip-B6YrI4aJ.js → tooltip-BmlPc6kc.js} +1 -1
  109. package/src/ui/dist/assets/{trash-Bc8jGp0V.js → trash-n-UvdZFR.js} +1 -1
  110. package/src/ui/dist/assets/{useCliAccess-mXVCYSZ-.js → useCliAccess-WDd3_wIh.js} +1 -1
  111. package/src/ui/dist/assets/{useFileDiffOverlay-Bg6b9H9K.js → useFileDiffOverlay-rXLIL2NF.js} +1 -1
  112. package/src/ui/dist/assets/{wrap-text-Drh5GEnL.js → wrap-text-qIYQ4a_W.js} +1 -1
  113. package/src/ui/dist/assets/{zoom-out-CJj9DZLn.js → zoom-out-fZXCEFsy.js} +1 -1
  114. package/src/ui/dist/index.html +2 -2
  115. package/uv.lock +1155 -0
  116. package/src/ui/dist/assets/LabPlugin-D9jVIo0A.css +0 -2698
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  import socket
5
+ import subprocess
4
6
  import sys
5
7
  import tempfile
8
+ from shutil import which
6
9
  from pathlib import Path
7
10
  from typing import Any
8
11
  from urllib.error import URLError
@@ -108,6 +111,69 @@ def _check_home_writable(home: Path) -> dict[str, Any]:
108
111
  )
109
112
 
110
113
 
114
+ def _resolve_uv_binary(home: Path) -> str | None:
115
+ for env_name in ("DEEPSCIENTIST_UV", "UV_BIN"):
116
+ override = str(os.environ.get(env_name) or "").strip()
117
+ if not override:
118
+ continue
119
+ override_path = Path(override).expanduser()
120
+ if override_path.exists():
121
+ return str(override_path)
122
+ resolved_override = which(override)
123
+ if resolved_override:
124
+ return resolved_override
125
+
126
+ local_candidates = [
127
+ home / "runtime" / "tools" / "uv" / "bin" / "uv",
128
+ home / "runtime" / "tools" / "uv" / "bin" / "uv.exe",
129
+ ]
130
+ for candidate in local_candidates:
131
+ if candidate.exists():
132
+ return str(candidate)
133
+ return which("uv")
134
+
135
+
136
+ def _check_uv(home: Path) -> dict[str, Any]:
137
+ resolved = _resolve_uv_binary(home)
138
+ if not resolved:
139
+ guidance = [
140
+ "Run `ds` once so DeepScientist can bootstrap a local uv runtime manager automatically.",
141
+ ]
142
+ if sys.platform == "win32":
143
+ guidance.append('PowerShell: `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`')
144
+ else:
145
+ guidance.append("macOS/Linux: `curl -LsSf https://astral.sh/uv/install.sh | sh`")
146
+ return _make_check(
147
+ check_id="uv",
148
+ label="uv runtime manager",
149
+ ok=False,
150
+ summary="uv is not available to DeepScientist.",
151
+ errors=["DeepScientist cannot provision or repair its local Python runtime without `uv`."],
152
+ guidance=guidance,
153
+ )
154
+
155
+ version = ""
156
+ try:
157
+ result = subprocess.run(
158
+ [resolved, "--version"],
159
+ check=False,
160
+ capture_output=True,
161
+ text=True,
162
+ )
163
+ if result.returncode == 0:
164
+ version = (result.stdout or result.stderr or "").strip()
165
+ except OSError:
166
+ version = ""
167
+
168
+ return _make_check(
169
+ check_id="uv",
170
+ label="uv runtime manager",
171
+ ok=True,
172
+ summary="uv is available for locked Python runtime management.",
173
+ details={"resolved_binary": resolved, "version": version or None},
174
+ )
175
+
176
+
111
177
  def _check_git(config_manager: ConfigManager) -> dict[str, Any]:
112
178
  readiness = config_manager.git_readiness()
113
179
  return _make_check(
@@ -195,10 +261,10 @@ def _check_codex(config_manager: ConfigManager) -> dict[str, Any]:
195
261
  check_id="codex",
196
262
  label="Codex CLI",
197
263
  ok=False,
198
- summary="Codex CLI is not available on PATH.",
264
+ summary="Codex CLI is not available to DeepScientist.",
199
265
  errors=[f"Runner binary `{binary}` could not be resolved."],
200
266
  guidance=[
201
- "Install Codex first: `npm install -g @openai/codex`.",
267
+ "Run `npm install -g @researai/deepscientist` again so the bundled Codex dependency is installed.",
202
268
  "Then run `codex` once and complete login.",
203
269
  ],
204
270
  details={"binary": binary},
@@ -370,6 +436,7 @@ def run_doctor(home: Path, *, repo_root: Path) -> dict[str, Any]:
370
436
  checks = [
371
437
  _check_python_runtime(),
372
438
  _check_home_writable(home),
439
+ _check_uv(home),
373
440
  _check_git(config_manager),
374
441
  _check_config_validation(config_manager),
375
442
  _check_runner_support(config_manager),
@@ -333,6 +333,9 @@ def _collect_branch_state(repo: Path) -> dict[str, dict[str, Any]]:
333
333
  "baseline_ref": record.get("baseline_ref") or {},
334
334
  "baseline_comparisons": record.get("baseline_comparisons") or {},
335
335
  "progress_eval": record.get("progress_eval") or {},
336
+ "evaluation_summary": record.get("evaluation_summary")
337
+ or ((record.get("details") or {}) if isinstance(record.get("details"), dict) else {}).get("evaluation_summary")
338
+ or {},
336
339
  "files_changed": record.get("files_changed") or [],
337
340
  "evidence_paths": record.get("evidence_paths") or [],
338
341
  "updated_at": record.get("updated_at"),
@@ -1,12 +1,34 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  from pathlib import Path
4
5
 
5
6
  from .shared import ensure_dir
6
7
 
7
8
 
9
+ def _looks_like_repo_root(path: Path) -> bool:
10
+ return (
11
+ (path / "pyproject.toml").exists()
12
+ and (path / "src" / "deepscientist").exists()
13
+ and (path / "src" / "skills").exists()
14
+ )
15
+
16
+
8
17
  def repo_root() -> Path:
9
- return Path(__file__).resolve().parents[2]
18
+ configured = str(os.environ.get("DEEPSCIENTIST_REPO_ROOT") or "").strip()
19
+ if configured:
20
+ candidate = Path(configured).expanduser().resolve()
21
+ if _looks_like_repo_root(candidate):
22
+ return candidate
23
+
24
+ cwd = Path.cwd().resolve()
25
+ if _looks_like_repo_root(cwd):
26
+ return cwd
27
+
28
+ candidate = Path(__file__).resolve().parents[2]
29
+ if _looks_like_repo_root(candidate):
30
+ return candidate
31
+ return candidate
10
32
 
11
33
 
12
34
  def default_home() -> Path:
@@ -15,9 +37,10 @@ def default_home() -> Path:
15
37
 
16
38
  def ensure_home_layout(home: Path) -> dict[str, Path]:
17
39
  runtime = ensure_dir(home / "runtime")
18
- ensure_dir(runtime / "venv")
19
40
  ensure_dir(runtime / "bundle")
20
41
  ensure_dir(runtime / "tools")
42
+ ensure_dir(runtime / "python")
43
+ ensure_dir(runtime / "uv-cache")
21
44
 
22
45
  config = ensure_dir(home / "config")
23
46
  ensure_dir(config / "baselines")
@@ -4,6 +4,8 @@ import os
4
4
  from dataclasses import dataclass
5
5
  from pathlib import Path
6
6
 
7
+ from ..home import default_home
8
+
7
9
 
8
10
  @dataclass(frozen=True)
9
11
  class McpContext:
@@ -24,7 +26,7 @@ class McpContext:
24
26
  value = os.environ.get(name, "").strip()
25
27
  return Path(value).expanduser() if value else None
26
28
 
27
- home = _path("DS_HOME") or (Path.home() / "DeepScientist")
29
+ home = _path("DEEPSCIENTIST_HOME") or _path("DS_HOME") or default_home()
28
30
  return cls(
29
31
  home=home,
30
32
  quest_id=os.environ.get("DS_QUEST_ID") or None,
@@ -307,6 +307,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
307
307
  status: str = "completed",
308
308
  baseline_id: str | None = None,
309
309
  baseline_variant_id: str | None = None,
310
+ evaluation_summary: dict[str, Any] | None = None,
310
311
  comment: str | dict[str, Any] | None = None,
311
312
  ) -> dict[str, Any]:
312
313
  return service.record_main_experiment(
@@ -330,6 +331,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
330
331
  status=status,
331
332
  baseline_id=baseline_id,
332
333
  baseline_variant_id=baseline_variant_id,
334
+ evaluation_summary=evaluation_summary,
333
335
  )
334
336
 
335
337
  @server.tool(
@@ -462,6 +464,8 @@ def build_artifact_server(context: McpContext) -> FastMCP:
462
464
  next_recommendation: str | None = None,
463
465
  dataset_scope: str = "full",
464
466
  subset_approval_ref: str | None = None,
467
+ comparison_baselines: list[dict[str, Any]] | None = None,
468
+ evaluation_summary: dict[str, Any] | None = None,
465
469
  comment: str | dict[str, Any] | None = None,
466
470
  ) -> dict[str, Any]:
467
471
  return service.record_analysis_slice(
@@ -481,6 +485,8 @@ def build_artifact_server(context: McpContext) -> FastMCP:
481
485
  next_recommendation=next_recommendation,
482
486
  dataset_scope=dataset_scope,
483
487
  subset_approval_ref=subset_approval_ref,
488
+ comparison_baselines=comparison_baselines,
489
+ evaluation_summary=evaluation_summary,
484
490
  )
485
491
 
486
492
  @server.tool(name="publish_baseline", description="Publish a quest baseline to the global baseline registry.")
@@ -655,8 +661,8 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
655
661
  description=(
656
662
  "Execute a bash command inside the current quest. "
657
663
  "mode=detach returns immediately. mode=await/create waits for completion. "
658
- "mode=read returns the saved log. mode=kill requests termination. "
659
- "mode=list shows known quest-local bash sessions."
664
+ "mode=read returns the saved log or a tailed log window. mode=kill requests termination. "
665
+ "mode=list shows known quest-local bash sessions. mode=history shows a compact reverse-chronological bash id list."
660
666
  ),
661
667
  )
662
668
  def bash_exec(
@@ -670,39 +676,88 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
670
676
  export_log_to: str | None = None,
671
677
  timeout_seconds: int | None = None,
672
678
  status: str | None = None,
679
+ kind: str | None = None,
673
680
  agent_ids: list[str] | None = None,
674
681
  agent_instance_ids: list[str] | None = None,
675
682
  chat_session_id: str | None = None,
676
683
  limit: int = 20,
684
+ tail_limit: int | None = None,
685
+ before_seq: int | None = None,
686
+ after_seq: int | None = None,
687
+ order: str = "asc",
688
+ include_log: bool = False,
689
+ wait: bool = False,
690
+ force: bool = False,
677
691
  comment: str | dict[str, Any] | None = None,
678
692
  ) -> dict[str, Any]:
679
693
  quest_root = context.require_quest_root().resolve()
680
694
  normalized_mode = (mode or "detach").strip().lower()
681
695
  if normalized_mode == "create":
682
696
  normalized_mode = "await"
683
- if normalized_mode not in {"detach", "await", "read", "kill", "list"}:
684
- raise ValueError("Mode must be one of `detach`, `await`, `create`, `read`, `kill`, or `list`.")
685
- if normalized_mode == "list":
697
+ if normalized_mode not in {"detach", "await", "read", "kill", "list", "history"}:
698
+ raise ValueError("Mode must be one of `detach`, `await`, `create`, `read`, `kill`, `list`, or `history`.")
699
+ if normalized_mode in {"list", "history"}:
700
+ resolved_limit = 500 if normalized_mode == "history" and limit == 20 else max(1, min(limit, 500))
686
701
  items = service.list_sessions(
687
702
  quest_root,
688
703
  status=status,
704
+ kind=kind,
689
705
  agent_ids=agent_ids,
690
706
  agent_instance_ids=agent_instance_ids,
691
707
  chat_session_id=chat_session_id,
692
- limit=max(1, min(limit, 500)),
708
+ limit=resolved_limit,
693
709
  )
710
+ history_lines = [service.format_history_line(item) for item in items]
694
711
  counts: dict[str, int] = {}
695
712
  for item in items:
696
713
  item_status = str(item.get("status") or "unknown")
697
714
  counts[item_status] = counts.get(item_status, 0) + 1
698
- return {
715
+ payload = {
699
716
  "count": len(items),
700
717
  "items": items,
701
718
  "status_counts": counts,
719
+ "summary": service.summary(quest_root),
720
+ "history_lines": history_lines,
702
721
  }
722
+ if normalized_mode == "history":
723
+ return {
724
+ "count": len(items),
725
+ "lines": history_lines,
726
+ "items": items,
727
+ }
728
+ return payload
703
729
  if normalized_mode == "read":
704
730
  bash_id = service.resolve_session_id(quest_root, id)
705
731
  session = service.get_session(quest_root, bash_id)
732
+ normalized_order = (order or "asc").strip().lower()
733
+ if normalized_order not in {"asc", "desc"}:
734
+ normalized_order = "asc"
735
+ use_tail = tail_limit is not None or before_seq is not None or after_seq is not None or normalized_order != "asc"
736
+ if use_tail:
737
+ resolved_tail_limit = max(1, min(int(tail_limit or 200), 1000))
738
+ entries, tail_meta = service.read_log_entries(
739
+ quest_root,
740
+ bash_id,
741
+ limit=resolved_tail_limit,
742
+ before_seq=before_seq,
743
+ after_seq=after_seq,
744
+ order=normalized_order,
745
+ )
746
+ payload = service.build_tool_result(
747
+ context,
748
+ session=session,
749
+ include_log=include_log,
750
+ export_log=export_log,
751
+ export_log_to=export_log_to,
752
+ )
753
+ payload["tail"] = entries
754
+ payload["tail_limit"] = tail_meta.get("tail_limit")
755
+ payload["tail_start_seq"] = tail_meta.get("tail_start_seq")
756
+ payload["latest_seq"] = tail_meta.get("latest_seq")
757
+ payload["after_seq"] = tail_meta.get("after_seq")
758
+ payload["before_seq"] = tail_meta.get("before_seq")
759
+ payload["order"] = normalized_order
760
+ return payload
706
761
  return service.build_tool_result(
707
762
  context,
708
763
  session=session,
@@ -717,7 +772,10 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
717
772
  bash_id,
718
773
  reason=reason,
719
774
  user_id=f"agent:{context.agent_role or 'pi'}",
775
+ force=force,
720
776
  )
777
+ if wait:
778
+ session = service.wait_for_session(quest_root, bash_id, timeout_seconds=timeout_seconds)
721
779
  return service.build_tool_result(context, session=session, include_log=False)
722
780
  if normalized_mode == "await" and not command:
723
781
  bash_id = service.resolve_session_id(quest_root, id)
@@ -738,6 +796,7 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
738
796
  workdir=workdir,
739
797
  env=env,
740
798
  timeout_seconds=timeout_seconds,
799
+ comment=comment,
741
800
  )
742
801
  if normalized_mode == "detach":
743
802
  return service.build_tool_result(context, session=session, include_log=False)
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ import uuid
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ HOME_SIGNATURES = (
11
+ "runtime",
12
+ "config",
13
+ "memory",
14
+ "quests",
15
+ "plugins",
16
+ "logs",
17
+ "cache",
18
+ "cli",
19
+ )
20
+
21
+
22
+ def looks_like_deepscientist_root(path: Path) -> bool:
23
+ if not path.exists() or not path.is_dir():
24
+ return False
25
+ if (path / "cli" / "bin" / "ds.js").exists():
26
+ return True
27
+ return any((path / name).exists() for name in HOME_SIGNATURES)
28
+
29
+
30
+ def _is_relative_to(candidate: Path, other: Path) -> bool:
31
+ try:
32
+ candidate.relative_to(other)
33
+ return True
34
+ except ValueError:
35
+ return False
36
+
37
+
38
+ def _collect_manifest(root: Path) -> dict[str, Any]:
39
+ manifest: dict[str, Any] = {}
40
+ file_count = 0
41
+ dir_count = 0
42
+ symlink_count = 0
43
+ total_bytes = 0
44
+ stack = [Path("")]
45
+ while stack:
46
+ rel_root = stack.pop()
47
+ current_root = root / rel_root
48
+ for child in sorted(current_root.iterdir(), key=lambda item: item.name):
49
+ rel_path = (rel_root / child.name).as_posix()
50
+ if child.is_symlink():
51
+ manifest[rel_path] = {"kind": "symlink", "target": os.readlink(child)}
52
+ symlink_count += 1
53
+ continue
54
+ if child.is_dir():
55
+ manifest[rel_path] = {"kind": "dir"}
56
+ dir_count += 1
57
+ stack.append(rel_root / child.name)
58
+ continue
59
+ size = child.stat().st_size
60
+ manifest[rel_path] = {"kind": "file", "size": size}
61
+ file_count += 1
62
+ total_bytes += size
63
+ return {
64
+ "entries": manifest,
65
+ "stats": {
66
+ "file_count": file_count,
67
+ "dir_count": dir_count,
68
+ "symlink_count": symlink_count,
69
+ "total_bytes": total_bytes,
70
+ "entry_count": len(manifest),
71
+ },
72
+ }
73
+
74
+
75
+ def migrate_deepscientist_root(source: Path, target: Path) -> dict[str, Any]:
76
+ source = source.expanduser().resolve()
77
+ target = target.expanduser().resolve()
78
+ if not source.exists():
79
+ raise ValueError(f"Source path does not exist: {source}")
80
+ if not source.is_dir():
81
+ raise ValueError(f"Source path is not a directory: {source}")
82
+ if not looks_like_deepscientist_root(source):
83
+ raise ValueError(f"Source path does not look like a DeepScientist home or install root: {source}")
84
+ if source == target:
85
+ raise ValueError("Source path and target path must be different.")
86
+ if _is_relative_to(target, source):
87
+ raise ValueError("Target path cannot be placed inside the current DeepScientist root.")
88
+ if _is_relative_to(source, target):
89
+ raise ValueError("Target path cannot be a parent of the current DeepScientist root.")
90
+ if target.exists():
91
+ raise ValueError(f"Target path already exists: {target}")
92
+ target.parent.mkdir(parents=True, exist_ok=True)
93
+
94
+ staging = target.parent / f".{target.name}.migrating-{uuid.uuid4().hex[:10]}"
95
+ if staging.exists():
96
+ shutil.rmtree(staging, ignore_errors=True)
97
+ try:
98
+ shutil.copytree(source, staging, symlinks=True, copy_function=shutil.copy2)
99
+ source_manifest = _collect_manifest(source)
100
+ staging_manifest = _collect_manifest(staging)
101
+ if source_manifest["entries"] != staging_manifest["entries"]:
102
+ raise ValueError("Copied tree validation failed: source and target contents do not match.")
103
+ staging.rename(target)
104
+ return {
105
+ "ok": True,
106
+ "source": str(source),
107
+ "target": str(target),
108
+ "staging": str(staging),
109
+ "stats": source_manifest["stats"],
110
+ "summary": "DeepScientist root copied and verified successfully.",
111
+ }
112
+ except Exception:
113
+ shutil.rmtree(staging, ignore_errors=True)
114
+ raise
@@ -87,7 +87,7 @@ class PromptBuilder:
87
87
  ) -> str:
88
88
  snapshot = self.quest_service.snapshot(quest_id)
89
89
  runtime_config = self.config_manager.load_named("config")
90
- connectors_config = self.config_manager.load_named("connectors")
90
+ connectors_config = self.config_manager.load_named_normalized("connectors")
91
91
  quest_root = Path(snapshot["quest_root"])
92
92
  active_anchor = str(snapshot.get("active_anchor") or skill_id)
93
93
  default_locale = str(runtime_config.get("default_locale") or "zh-CN")
@@ -260,6 +260,11 @@ class PromptBuilder:
260
260
  "- qq_surface_rule: QQ is a milestone-report surface, not a full artifact browser.",
261
261
  "- qq_default_mode: keep outbound replies concise, respectful, text-first, and progress-aware.",
262
262
  "- qq_detail_rule: do not proactively dump file inventories, path lists, or low-level file details unless the user explicitly asked for them.",
263
+ "- qq_length_rule: for ordinary QQ progress replies, normally use only 2 to 4 short sentences, or 3 very short bullets at most.",
264
+ "- qq_summary_first_rule: start with the user-facing conclusion, then the immediate meaning, then the next action; do not make the user reverse-engineer the status from telemetry.",
265
+ "- qq_internal_signal_rule: omit worker names, heartbeat timestamps, retry counters, pending/running/completed counts, file names, and monitor-window narration unless that detail is necessary for a user decision or to explain a real risk.",
266
+ "- qq_translation_rule: translate internal actions into user value, for example say that you organized the baseline record for easier comparison later instead of listing the files you touched.",
267
+ "- qq_eta_rule: for baseline reproduction, main experiments, analysis experiments, and other important long-running research phases, include a rough ETA for the next meaningful result, next step, or next update; if the runtime is uncertain, say that directly and still give the next check-in window.",
263
268
  f"- qq_auto_send_main_experiment_png: {bool(qq_config.get('auto_send_main_experiment_png', True))}",
264
269
  f"- qq_auto_send_analysis_summary_png: {bool(qq_config.get('auto_send_analysis_summary_png', True))}",
265
270
  f"- qq_auto_send_slice_png: {bool(qq_config.get('auto_send_slice_png', False))}",
@@ -387,6 +392,14 @@ class PromptBuilder:
387
392
  "- must_continue_rule: unless there is a real blocking user decision, keep advancing the quest automatically from durable state",
388
393
  ]
389
394
  )
395
+ bash_running_count = int(((snapshot.get("counts") or {}).get("bash_running_count")) or 0)
396
+ if bash_running_count > 0:
397
+ lines.extend(
398
+ [
399
+ f"- active_bash_run_count: {bash_running_count}",
400
+ "- long_run_watchdog_rule: while an important long-running bash_exec session is active, never let more than 30 minutes pass without inspecting real logs/status and sending a concise artifact.interact progress update if the run is still ongoing",
401
+ ]
402
+ )
390
403
  if str(turn_reason or "").strip() == "auto_continue":
391
404
  lines.append(
392
405
  "- auto_continue_rule: this turn has no new user message; continue from the active requirements, durable artifacts, and current quest state instead of replaying the previous user message"
@@ -733,13 +746,30 @@ class PromptBuilder:
733
746
  "- interaction_protocol: first message may be plain conversation; after that, treat artifact.interact threads and mailbox polls as the main continuity spine across TUI, web, and connectors",
734
747
  "- mailbox_protocol: artifact.interact(include_recent_inbound_messages=True) is the queued human-message mailbox; when it returns user text, treat that input as higher priority than background subtasks until it has been acknowledged",
735
748
  "- acknowledgment_protocol: after artifact.interact returns any human message, immediately call artifact.interact(...) again to confirm receipt; if answerable, answer directly, otherwise state the short plan, nearest checkpoint, and that the current background subtask is paused",
736
- "- progress_protocol: emit artifact.interact(kind='progress', reply_mode='threaded', ...) only at real human-meaningful checkpoints, after the first meaningful signal from long-running work, and then only occasional keepalives during truly long work, usually about every 20 to 30 minutes",
737
- "- long_run_reporting_protocol: for long-running bash_exec monitoring loops, report after each completed sleep/await cycle with real evidence plus the next planned check time and estimated next reply time",
749
+ "- progress_protocol: emit artifact.interact(kind='progress', reply_mode='threaded', ...) at real human-meaningful checkpoints; if no natural checkpoint appears during active user-relevant work, send a concise keepalive before you drift beyond roughly 10 to 30 tool calls without a user-visible update",
750
+ "- smoke_then_detach_protocol: for baseline reproduction, main experiments, and analysis experiments, first validate the command path with a bounded smoke test; once the smoke test passes, launch the real long run with bash_exec(mode='detach', ...) and usually leave timeout_seconds unset rather than guessing a fake deadline",
751
+ "- long_run_reporting_protocol: for long-running bash_exec monitoring loops, inspect real logs or status after each completed sleep/await cycle and at least once every 30 minutes at worst, then report real evidence plus the next planned check time and estimated next reply time",
752
+ "- long_run_watchdog_protocol: for baseline reproduction, baseline-running stages, main experiments, and other important detached runs, do not let more than 30 minutes pass without a real progress inspection and, if the run is still active, a user-visible artifact.interact progress update",
753
+ "- tail_monitoring_protocol: when monitoring a detached run, prefer bash_exec(mode='read', id=..., tail_limit=..., order='desc') so you inspect the newest evidence first instead of re-reading full logs every time",
754
+ "- managed_recovery_protocol: if a detached baseline, main-experiment, or analysis run is clearly invalid, wedged, or superseded, stop it with bash_exec(mode='kill', id=...), document the reason, fix the issue, and relaunch cleanly instead of letting a bad run linger",
755
+ "- timeout_protocol: before using bash_exec(mode='await', ...), estimate whether the command can finish within the selected wait window; if runtime is uncertain or likely longer, use bash_exec(mode='detach', ...) and monitor, or set timeout_seconds intentionally",
738
756
  "- blocking_protocol: use reply_mode='blocking' only for true unresolved user decisions; ordinary progress updates should stay threaded and non-blocking",
757
+ "- credential_blocking_protocol: if continuation requires user-supplied external credentials or secrets such as an API key, GitHub key/token, or Hugging Face key/token, emit one structured blocking decision request that asks the user to provide the credential or choose an alternative route; do not invent placeholders or silently skip the blocked step",
758
+ "- credential_wait_protocol: if that credential request remains unanswered, keep the quest waiting rather than self-resolving; if you are resumed without new credentials and no other work is possible, a long low-frequency park such as `bash_exec(command='sleep 3600', mode='await', timeout_seconds=3700)` is acceptable to avoid busy-looping",
739
759
  f"- standby_prefix_rule: when you intentionally leave one blocking standby interaction after task completion, prefix it with {'[等待决策]' if chinese_turn else '[Waiting for decision]'} and wait for a new user reply before continuing",
740
760
  "- stop_notice_protocol: if work must pause or stop, send a user-visible notice that explains why, confirms preserved context, and states that any new message or `/resume` will continue from the same quest",
741
761
  "- respect_protocol: write user-facing updates as natural, respectful, easy-to-follow chat; do not sound like a formal status report or internal tool log",
742
762
  "- omission_protocol: for ordinary user-facing updates, omit file paths, artifact ids, branch/worktree ids, session ids, raw commands, raw logs, and internal tool names unless the user asked for them or needs them to act",
763
+ "- compaction_protocol: ordinary artifact.interact progress updates should usually fit in 2 to 4 short sentences and should not read like a monitoring transcript or execution diary",
764
+ "- tool_call_keepalive_protocol: for active multi-step work outside long detached experiment waits, if you have spent roughly 10 to 30 tool calls without a user-visible checkpoint, send one concise artifact.interact progress update before continuing",
765
+ "- human_progress_shape_protocol: ordinary progress updates should usually make three things explicit in human language: the current task, the main difficulty or latest real progress, and the concrete next measure you will take",
766
+ "- eta_visibility_protocol: for baseline reproduction, main experiments, analysis experiments, and other important long-running phases, progress updates should also make the expected time to the next meaningful result, next milestone, or next user-visible update explicit; use roughly 10 to 30 minutes as the normal update window, and if the ETA is unreliable, say that and give a realistic next check-in window instead",
767
+ "- teammate_voice_protocol: write like a calm capable teammate using natural first-person phrasing when helpful, for example 'I'm working on ...', 'The main issue right now is ...', 'Next I'll ...'; do not sound like a dashboard or incident log",
768
+ "- tqdm_progress_protocol: when you control the experiment code for baseline reproduction, main experiments, or analysis experiments, instrument long loops with a throttled tqdm-style progress reporter when feasible and also prefer periodic __DS_PROGRESS__ JSON markers so monitoring stays both human-readable and machine-usable",
769
+ "- translation_protocol: convert internal actions into user-facing meaning; describe what was finished and why it matters instead of naming every touched file, counter, timestamp, or subprocess",
770
+ "- detail_gate_protocol: include exact counters, worker labels, timestamps, retry counts, or file names only when the user explicitly asked for them, when they change the recommended action, or when they are the only honest way to explain a real blocker",
771
+ "- monitoring_summary_protocol: for long-running monitoring loops, summarize the frontier state in plain language such as still progressing, temporarily stalled, recovered, or needs intervention; do not narrate each watch window unless the boundary itself matters",
772
+ "- preflight_rewrite_protocol: before sending artifact.interact, quickly self-check whether the draft reads like a monitoring log, file inventory, or internal diary; if it mentions watch windows, heartbeats, retry counters, raw counts, timestamps, or multiple file names without being necessary for user action, rewrite it into conclusion -> meaning -> next step first",
743
773
  "- non_research_mode_protocol: if the user message looks like a non-research request, ask for a second confirmation before engaging stage skills or research workflow; after completion, leave one blocking standby interaction instead of repeatedly pinging",
744
774
  "- workspace_discipline: read and modify code inside current_workspace_root; treat quest_root as the canonical repo identity and durable runtime root",
745
775
  "- binary_safety: do not open or rewrite large binary assets unless truly necessary; prefer summaries, metadata, and targeted inspection first",
@@ -913,6 +943,26 @@ class PromptBuilder:
913
943
  "- active_baseline_metric_contract_rule: before planning or running `experiment` or `analysis-campaign`, read this JSON file and treat it as the canonical baseline comparison contract unless a newer confirmed baseline explicitly replaces it.",
914
944
  ]
915
945
  )
946
+ analysis_baseline_inventory = read_json(quest_root / "artifacts" / "baselines" / "analysis_inventory.json", {})
947
+ analysis_baseline_inventory = analysis_baseline_inventory if isinstance(analysis_baseline_inventory, dict) else {}
948
+ analysis_inventory_entries = (
949
+ analysis_baseline_inventory.get("entries") if isinstance(analysis_baseline_inventory.get("entries"), list) else []
950
+ )
951
+ registered_count = sum(
952
+ 1
953
+ for item in analysis_inventory_entries
954
+ if isinstance(item, dict) and str(item.get("status") or "").strip().lower() == "registered"
955
+ )
956
+ if analysis_inventory_entries:
957
+ lines.extend(
958
+ [
959
+ f"- supplementary_baseline_inventory_status: artifacts/baselines/analysis_inventory.json [exists]",
960
+ f"- supplementary_baseline_count: {len(analysis_inventory_entries)}",
961
+ f"- supplementary_baseline_registered_count: {registered_count}",
962
+ ]
963
+ )
964
+ else:
965
+ lines.append("- supplementary_baseline_inventory_status: artifacts/baselines/analysis_inventory.json [missing]")
916
966
  lines.extend(["", "Active interactions:"])
917
967
  active_interactions = snapshot.get("active_interactions") or []
918
968
  if active_interactions:
@@ -1001,10 +1051,14 @@ class PromptBuilder:
1001
1051
  )
1002
1052
  bundle_manifest = read_json(paper_root / "paper_bundle_manifest.json", {})
1003
1053
  bundle_manifest = bundle_manifest if isinstance(bundle_manifest, dict) else {}
1054
+ paper_baseline_inventory = read_json(paper_root / "baseline_inventory.json", {})
1055
+ paper_baseline_inventory = paper_baseline_inventory if isinstance(paper_baseline_inventory, dict) else {}
1004
1056
  claim_evidence_map = read_json(paper_root / "claim_evidence_map.json", {})
1005
1057
  claim_evidence_map = claim_evidence_map if isinstance(claim_evidence_map, dict) else {}
1006
1058
  compile_report = read_json(paper_root / "build" / "compile_report.json", {})
1007
1059
  compile_report = compile_report if isinstance(compile_report, dict) else {}
1060
+ open_source_manifest = read_json(quest_root / "release" / "open_source" / "manifest.json", {})
1061
+ open_source_manifest = open_source_manifest if isinstance(open_source_manifest, dict) else {}
1008
1062
 
1009
1063
  selected_outline_ref = str(
1010
1064
  selected_outline.get("outline_id") or bundle_manifest.get("selected_outline_ref") or ""
@@ -1045,6 +1099,7 @@ class PromptBuilder:
1045
1099
  f"- draft_status: {_path_status(bundle_manifest.get('draft_path'), fallback='paper/draft.md')}",
1046
1100
  f"- references_status: {_path_status(bundle_manifest.get('references_path'), fallback='paper/references.bib')}",
1047
1101
  f"- claim_evidence_map_status: {_path_status(bundle_manifest.get('claim_evidence_map_path'), fallback='paper/claim_evidence_map.json')}",
1102
+ f"- baseline_inventory_status: {_path_status(bundle_manifest.get('baseline_inventory_path'), fallback='paper/baseline_inventory.json')}",
1048
1103
  f"- review_status: {'paper/review/review.md [exists]' if (paper_root / 'review' / 'review.md').exists() else 'paper/review/review.md [missing]'}",
1049
1104
  f"- proofing_report_status: {'paper/proofing/proofing_report.md [exists]' if (paper_root / 'proofing' / 'proofing_report.md').exists() else 'paper/proofing/proofing_report.md [missing]'}",
1050
1105
  f"- page_images_manifest_status: {'paper/proofing/page_images_manifest.json [exists]' if (paper_root / 'proofing' / 'page_images_manifest.json').exists() else 'paper/proofing/page_images_manifest.json [missing]'}",
@@ -1061,6 +1116,8 @@ class PromptBuilder:
1061
1116
  f"- bundle_pdf_status: {_path_status(pdf_rel_path, fallback='paper/paper.pdf')}",
1062
1117
  f"- bundle_compile_report_status: {_path_status(compile_rel_path, fallback='paper/build/compile_report.json')}",
1063
1118
  f"- bundle_latex_root: {latex_root_path or 'none'}",
1119
+ f"- open_source_manifest_status: {_path_status(bundle_manifest.get('open_source_manifest_path'), fallback='release/open_source/manifest.json')}",
1120
+ f"- open_source_cleanup_plan_status: {_path_status(bundle_manifest.get('open_source_cleanup_plan_path'), fallback='release/open_source/cleanup_plan.md')}",
1064
1121
  ]
1065
1122
  )
1066
1123
  else:
@@ -1089,6 +1146,17 @@ class PromptBuilder:
1089
1146
 
1090
1147
  if compile_report:
1091
1148
  lines.append(f"- compile_report_ok: {compile_report.get('ok') if 'ok' in compile_report else 'unknown'}")
1149
+ supplementary_baselines = (
1150
+ paper_baseline_inventory.get("supplementary_baselines")
1151
+ if isinstance(paper_baseline_inventory.get("supplementary_baselines"), list)
1152
+ else []
1153
+ )
1154
+ if paper_baseline_inventory:
1155
+ lines.append(f"- paper_supplementary_baseline_count: {len(supplementary_baselines)}")
1156
+ if open_source_manifest:
1157
+ lines.append(
1158
+ f"- open_source_release_branch: {str(open_source_manifest.get('release_branch') or '').strip() or 'none'}"
1159
+ )
1092
1160
 
1093
1161
  lines.extend(["", "Recent supporting runs:"])
1094
1162
  recent_runs = snapshot.get("recent_runs") or []