@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.
- package/README.md +69 -1
- package/bin/ds.js +2239 -153
- package/docs/en/00_QUICK_START.md +60 -20
- package/docs/en/01_SETTINGS_REFERENCE.md +20 -20
- package/docs/en/02_START_RESEARCH_GUIDE.md +11 -11
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +10 -10
- package/docs/en/05_TUI_GUIDE.md +1 -1
- package/docs/en/09_DOCTOR.md +48 -4
- package/docs/en/90_ARCHITECTURE.md +4 -2
- package/docs/zh/00_QUICK_START.md +60 -20
- package/docs/zh/01_SETTINGS_REFERENCE.md +21 -21
- package/docs/zh/02_START_RESEARCH_GUIDE.md +19 -19
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +10 -10
- package/docs/zh/05_TUI_GUIDE.md +1 -1
- package/docs/zh/09_DOCTOR.md +46 -4
- package/install.sh +125 -8
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +6 -1
- package/src/deepscientist/artifact/service.py +553 -26
- package/src/deepscientist/bash_exec/monitor.py +23 -4
- package/src/deepscientist/bash_exec/runtime.py +3 -0
- package/src/deepscientist/bash_exec/service.py +132 -4
- package/src/deepscientist/bridges/base.py +10 -19
- package/src/deepscientist/channels/discord_gateway.py +25 -2
- package/src/deepscientist/channels/feishu_long_connection.py +41 -3
- package/src/deepscientist/channels/qq.py +524 -64
- package/src/deepscientist/channels/qq_gateway.py +22 -3
- package/src/deepscientist/channels/relay.py +429 -90
- package/src/deepscientist/channels/slack_socket.py +29 -5
- package/src/deepscientist/channels/telegram_polling.py +25 -2
- package/src/deepscientist/channels/whatsapp_local_session.py +32 -4
- package/src/deepscientist/cli.py +27 -0
- package/src/deepscientist/config/models.py +6 -40
- package/src/deepscientist/config/service.py +165 -156
- package/src/deepscientist/connector_profiles.py +346 -0
- package/src/deepscientist/connector_runtime.py +88 -43
- package/src/deepscientist/daemon/api/handlers.py +65 -11
- package/src/deepscientist/daemon/api/router.py +4 -2
- package/src/deepscientist/daemon/app.py +772 -219
- package/src/deepscientist/doctor.py +69 -2
- package/src/deepscientist/gitops/diff.py +3 -0
- package/src/deepscientist/home.py +25 -2
- package/src/deepscientist/mcp/context.py +3 -1
- package/src/deepscientist/mcp/server.py +66 -7
- package/src/deepscientist/migration.py +114 -0
- package/src/deepscientist/prompts/builder.py +71 -3
- package/src/deepscientist/qq_profiles.py +186 -0
- package/src/deepscientist/quest/layout.py +1 -0
- package/src/deepscientist/quest/service.py +70 -12
- package/src/deepscientist/quest/stage_views.py +46 -0
- package/src/deepscientist/runners/codex.py +2 -0
- package/src/deepscientist/shared.py +44 -17
- package/src/prompts/connectors/lingzhu.md +3 -0
- package/src/prompts/connectors/qq.md +42 -2
- package/src/prompts/system.md +123 -10
- package/src/skills/analysis-campaign/SKILL.md +35 -6
- package/src/skills/baseline/SKILL.md +73 -32
- package/src/skills/decision/SKILL.md +4 -3
- package/src/skills/experiment/SKILL.md +28 -6
- package/src/skills/finalize/SKILL.md +5 -2
- package/src/skills/idea/SKILL.md +2 -2
- package/src/skills/intake-audit/SKILL.md +2 -2
- package/src/skills/rebuttal/SKILL.md +4 -2
- package/src/skills/review/SKILL.md +4 -2
- package/src/skills/scout/SKILL.md +2 -2
- package/src/skills/write/SKILL.md +2 -2
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-w5lF2Ttt.js → AiManusChatView-qzChi9uh.js} +67 -94
- package/src/ui/dist/assets/{AnalysisPlugin-DJOED79I.js → AnalysisPlugin-CcC_-UqN.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-DaG61Y0M.js → AutoFigurePlugin-DD8LkJLe.js} +5 -5
- package/src/ui/dist/assets/{CliPlugin-CV4LqUB_.js → CliPlugin-DJJFfVmW.js} +17 -110
- package/src/ui/dist/assets/{CodeEditorPlugin-DylfAea4.js → CodeEditorPlugin-CrjkHNLh.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-F7saY0LM.js → CodeViewerPlugin-obnD6G5R.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-COP0c7jf.js → DocViewerPlugin-DB9SUQVd.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-CAS05pT9.js → GitDiffViewerPlugin-DZLlNlD2.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-Bco1CN_w.js → ImageViewerPlugin-BGwfDZ0Y.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-CvMlCD99.js → LabCopilotPanel-dfLptQcR.js} +10 -10
- package/src/ui/dist/assets/{LabPlugin-BYankkE4.js → LabPlugin-CeGjAl3A.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-LDSMR-t-.js → LatexPlugin-BBJ7kd1V.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-B7o80jgm.js → MarkdownViewerPlugin-DKZi7BcB.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-CM6ZOcpC.js → MarketplacePlugin-C_k-9jD0.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-Dc61cXmK.js → NotebookEditor-4R88_BMO.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DWowuQwx.js → PdfLoader-DwEFQLrw.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-BsJM1q_a.js → PdfMarkdownPlugin-D-jdsqF8.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-DB2eEEFQ.js → PdfViewerPlugin-CmeBGDY0.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-CraThSvt.js → SearchPlugin-Dlz2WKJ4.js} +1 -1
- package/src/ui/dist/assets/{Stepper-CgocRTPq.js → Stepper-ClOgzWM3.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-B1JGhKtd.js → TextViewerPlugin-DDQWxibk.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-CclFC7FM.js → VNCViewer-CJXT0Nm8.js} +9 -9
- package/src/ui/dist/assets/{bibtex-D3IKsMl7.js → bibtex-DLr4Rtk4.js} +1 -1
- package/src/ui/dist/assets/{code-BP37Xx0p.js → code-DgKK408Y.js} +1 -1
- package/src/ui/dist/assets/{file-content-BAJSu-9r.js → file-content-6HBqQnvQ.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DUGeCTuy.js → file-diff-panel-Dhu0TbBM.js} +1 -1
- package/src/ui/dist/assets/{file-socket-CXc1Ojf7.js → file-socket-CP3iwVZG.js} +1 -1
- package/src/ui/dist/assets/{file-utils-2J21jt7M.js → file-utils-BsS-Aw68.js} +1 -1
- package/src/ui/dist/assets/{image-CMMmgvcn.js → image-ByeK-Zcv.js} +1 -1
- package/src/ui/dist/assets/{index-DmwmJmbW.js → index-BLjo5--a.js} +33610 -31016
- package/src/ui/dist/assets/{index-CWgMgpow.js → index-BdsE0uRz.js} +11 -11
- package/src/ui/dist/assets/{index-s7aHnNQ4.js → index-C-eX-N6A.js} +1 -1
- package/src/ui/dist/assets/{index-KGt-z-dD.css → index-CuQhlrR-.css} +2747 -2
- package/src/ui/dist/assets/{index-BaVumsQT.js → index-DyremSIv.js} +2 -2
- package/src/ui/dist/assets/{message-square-CQRfX0Am.js → message-square-DnagiLnc.js} +1 -1
- package/src/ui/dist/assets/{monaco-B4TbdsrF.js → monaco-4kBFeprs.js} +1 -1
- package/src/ui/dist/assets/{popover-B8Rokodk.js → popover-hRCXZzs2.js} +1 -1
- package/src/ui/dist/assets/{project-sync-D_i96KH4.js → project-sync-O_85YuP6.js} +1 -1
- package/src/ui/dist/assets/{sigma-D12PnzCN.js → sigma-DvKopSnL.js} +1 -1
- package/src/ui/dist/assets/{tooltip-B6YrI4aJ.js → tooltip-BmlPc6kc.js} +1 -1
- package/src/ui/dist/assets/{trash-Bc8jGp0V.js → trash-n-UvdZFR.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-mXVCYSZ-.js → useCliAccess-WDd3_wIh.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-Bg6b9H9K.js → useFileDiffOverlay-rXLIL2NF.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-Drh5GEnL.js → wrap-text-qIYQ4a_W.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-CJj9DZLn.js → zoom-out-fZXCEFsy.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/uv.lock +1155 -0
- 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
|
|
264
|
+
summary="Codex CLI is not available to DeepScientist.",
|
|
199
265
|
errors=[f"Runner binary `{binary}` could not be resolved."],
|
|
200
266
|
guidance=[
|
|
201
|
-
"
|
|
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
|
-
|
|
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("
|
|
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 `
|
|
685
|
-
if normalized_mode
|
|
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=
|
|
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
|
-
|
|
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.
|
|
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', ...)
|
|
737
|
-
"-
|
|
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 []
|