@researai/deepscientist 1.5.14 → 1.5.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/assets/branding/logo-raster.png +0 -0
- package/bin/ds.js +134 -49
- package/docs/en/00_QUICK_START.md +2 -2
- package/docs/en/01_SETTINGS_REFERENCE.md +20 -4
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
- package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/en/README.md +6 -0
- package/docs/zh/00_QUICK_START.md +2 -2
- package/docs/zh/01_SETTINGS_REFERENCE.md +20 -4
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
- package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/zh/README.md +6 -0
- package/install.sh +2 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/artifact/charts.py +567 -0
- package/src/deepscientist/artifact/guidance.py +50 -10
- package/src/deepscientist/artifact/metrics.py +228 -5
- package/src/deepscientist/artifact/schemas.py +3 -0
- package/src/deepscientist/artifact/service.py +3534 -191
- package/src/deepscientist/bash_exec/models.py +23 -0
- package/src/deepscientist/bash_exec/monitor.py +147 -67
- package/src/deepscientist/bash_exec/runtime.py +218 -156
- package/src/deepscientist/bash_exec/service.py +79 -64
- package/src/deepscientist/bash_exec/shells.py +87 -0
- package/src/deepscientist/bridges/connectors.py +51 -2
- package/src/deepscientist/config/models.py +6 -3
- package/src/deepscientist/config/service.py +7 -2
- package/src/deepscientist/connector/weixin_support.py +122 -1
- package/src/deepscientist/daemon/api/handlers.py +75 -4
- package/src/deepscientist/daemon/api/router.py +1 -0
- package/src/deepscientist/daemon/app.py +758 -206
- package/src/deepscientist/doctor.py +51 -0
- package/src/deepscientist/file_lock.py +48 -0
- package/src/deepscientist/gitops/diff.py +167 -1
- package/src/deepscientist/mcp/server.py +173 -5
- package/src/deepscientist/process_control.py +161 -0
- package/src/deepscientist/prompts/builder.py +267 -442
- package/src/deepscientist/quest/service.py +2255 -163
- package/src/deepscientist/quest/stage_views.py +171 -0
- package/src/deepscientist/runners/base.py +2 -0
- package/src/deepscientist/runners/codex.py +88 -5
- package/src/deepscientist/runners/runtime_overrides.py +17 -1
- package/src/prompts/contracts/shared_interaction.md +13 -4
- package/src/prompts/system.md +916 -72
- package/src/skills/analysis-campaign/SKILL.md +31 -2
- package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
- package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
- package/src/skills/baseline/SKILL.md +2 -0
- package/src/skills/decision/SKILL.md +19 -2
- package/src/skills/experiment/SKILL.md +8 -2
- package/src/skills/finalize/SKILL.md +18 -0
- package/src/skills/idea/SKILL.md +78 -0
- package/src/skills/idea/references/idea-generation-playbook.md +100 -0
- package/src/skills/idea/references/outline-seeding-example.md +60 -0
- package/src/skills/intake-audit/SKILL.md +1 -1
- package/src/skills/optimize/SKILL.md +1644 -0
- package/src/skills/rebuttal/SKILL.md +2 -1
- package/src/skills/review/SKILL.md +2 -1
- package/src/skills/write/SKILL.md +80 -12
- package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
- package/src/tui/dist/app/AppContainer.js +3 -0
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-DaF9Nge_.js → AiManusChatView-DDjbFnbt.js} +12 -12
- package/src/ui/dist/assets/{AnalysisPlugin-BSVx6dXE.js → AnalysisPlugin-Yb5IdmaU.js} +1 -1
- package/src/ui/dist/assets/CliPlugin-e64sreyu.js +31037 -0
- package/src/ui/dist/assets/{CodeEditorPlugin-DU9G0Tox.js → CodeEditorPlugin-C4D2TIkU.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DoX_fI9l.js → CodeViewerPlugin-BVoNZIvC.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-C4FWIXuU.js → DocViewerPlugin-CLChbllo.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-BgfFMgtf.js → GitDiffViewerPlugin-C4xeFyFQ.js} +20 -20
- package/src/ui/dist/assets/{ImageViewerPlugin-tcPkfY_x.js → ImageViewerPlugin-OiMUAcLi.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-_dKV60Bf.js → LabCopilotPanel-BjD2ThQF.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-Bje0ayoC.js → LabPlugin-DQPg-NrB.js} +2 -2
- package/src/ui/dist/assets/{LatexPlugin-CVsBzAln.js → LatexPlugin-CI05XAV9.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-xjmrqv_8.js → MarkdownViewerPlugin-DpeBLYZf.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-mMM2A8wP.js → MarketplacePlugin-DolE58Q2.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-3kVDSOBo.js → NotebookEditor-7Qm2rSWD.js} +11 -11
- package/src/ui/dist/assets/{NotebookEditor-SoJ8X-MO.js → NotebookEditor-C1kWaxKi.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DElVuHl9.js → PdfLoader-BfOHw8Zw.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-Bq88XT4G.js → PdfMarkdownPlugin-BulDREv1.js} +2 -2
- package/src/ui/dist/assets/{PdfViewerPlugin-CsCXMo9S.js → PdfViewerPlugin-C-daaOaL.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-oUPvy19k.js → SearchPlugin-CjpaiJ3A.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-CRkT9yNy.js → TextViewerPlugin-BxIyqPQC.js} +5 -5
- package/src/ui/dist/assets/{VNCViewer-BgbuvWhR.js → VNCViewer-HAg9mF7M.js} +10 -10
- package/src/ui/dist/assets/{bot-v_RASACv.js → bot-0DYntytV.js} +1 -1
- package/src/ui/dist/assets/{code-5hC9d0VH.js → code-B20Slj_w.js} +1 -1
- package/src/ui/dist/assets/{file-content-D1PxfOrp.js → file-content-DT24KFma.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DG1oT_Hj.js → file-diff-panel-DK13YPql.js} +1 -1
- package/src/ui/dist/assets/{file-socket-BmdFYQlk.js → file-socket-B4T2o4nR.js} +1 -1
- package/src/ui/dist/assets/{image-Dqe2X2tW.js → image-DSeR_sDS.js} +1 -1
- package/src/ui/dist/assets/{index-RDlNXXx1.js → index-BrFje2Uk.js} +2 -2
- package/src/ui/dist/assets/{index-DVsMKK_y.js → index-BwRJaoTl.js} +1 -1
- package/src/ui/dist/assets/{index-Nt9hS4ck.js → index-D_E4281X.js} +5007 -28514
- package/src/ui/dist/assets/{index-Duvz8Ip0.js → index-DnYB3xb1.js} +12 -12
- package/src/ui/dist/assets/{index-BQG-1s2o.css → index-G7AcWcMu.css} +43 -2
- package/src/ui/dist/assets/{monaco-DIXge1CP.js → monaco-LExaAN3Y.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-BBTTQaO-.js → pdf-effect-queue-BJk5okWJ.js} +1 -1
- package/src/ui/dist/assets/{popover-BWlolyxo.js → popover-D3Gg_FoV.js} +1 -1
- package/src/ui/dist/assets/{project-sync-BM5PkFH4.js → project-sync-C_ygLlVU.js} +1 -1
- package/src/ui/dist/assets/{select-D4dAtrA8.js → select-CpAK6uWm.js} +2 -2
- package/src/ui/dist/assets/{sigma-CKbE5jJT.js → sigma-DEccaSgk.js} +1 -1
- package/src/ui/dist/assets/{square-check-big-CZNGMgiB.js → square-check-big-uUfyVsbD.js} +1 -1
- package/src/ui/dist/assets/{trash-DaB37xAz.js → trash-CXvwwSe8.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-C2OmAcWe.js → useCliAccess-Bnop4mgR.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-Dowd1Ij4.js → useFileDiffOverlay-B8eUAX0I.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-BGjAhAUq.js → wrap-text-9vbOBpkW.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-dMZQMXzc.js → zoom-out-BgVMmOW4.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
|
@@ -11,6 +11,7 @@ from typing import Any
|
|
|
11
11
|
from urllib.error import URLError
|
|
12
12
|
from urllib.request import Request, urlopen
|
|
13
13
|
|
|
14
|
+
from .bash_exec.shells import build_exec_shell_launch, build_terminal_shell_launch
|
|
14
15
|
from .config import ConfigManager
|
|
15
16
|
from .home import ensure_home_layout
|
|
16
17
|
from .runtime_tools import RuntimeToolService
|
|
@@ -321,6 +322,55 @@ def _check_bundles(repo_root: Path) -> dict[str, Any]:
|
|
|
321
322
|
)
|
|
322
323
|
|
|
323
324
|
|
|
325
|
+
def _check_shell_backend() -> dict[str, Any]:
|
|
326
|
+
exec_launch = build_exec_shell_launch("echo ok")
|
|
327
|
+
terminal_launch = build_terminal_shell_launch(Path("doctor-terminal-probe"))
|
|
328
|
+
|
|
329
|
+
def resolve_binary(binary: str) -> str | None:
|
|
330
|
+
candidate = str(binary or "").strip()
|
|
331
|
+
if not candidate:
|
|
332
|
+
return None
|
|
333
|
+
if os.path.isabs(candidate) or os.path.sep in candidate or (os.path.altsep and os.path.altsep in candidate):
|
|
334
|
+
return candidate if Path(candidate).exists() else None
|
|
335
|
+
return which(candidate)
|
|
336
|
+
|
|
337
|
+
details = {
|
|
338
|
+
"exec_shell": exec_launch.shell_name,
|
|
339
|
+
"exec_shell_family": exec_launch.family,
|
|
340
|
+
"exec_argv": exec_launch.argv,
|
|
341
|
+
"terminal_shell": terminal_launch.shell_name,
|
|
342
|
+
"terminal_shell_family": terminal_launch.family,
|
|
343
|
+
"terminal_argv": terminal_launch.argv,
|
|
344
|
+
}
|
|
345
|
+
warnings: list[str] = []
|
|
346
|
+
guidance: list[str] = []
|
|
347
|
+
errors: list[str] = []
|
|
348
|
+
|
|
349
|
+
exec_binary = resolve_binary(exec_launch.argv[0])
|
|
350
|
+
terminal_binary = resolve_binary(terminal_launch.argv[0])
|
|
351
|
+
details["exec_resolved_binary"] = exec_binary
|
|
352
|
+
details["terminal_resolved_binary"] = terminal_binary
|
|
353
|
+
|
|
354
|
+
if sys.platform == "win32":
|
|
355
|
+
warnings.append("Native Windows support is currently experimental; WSL2 remains the most battle-tested path.")
|
|
356
|
+
if not exec_binary:
|
|
357
|
+
errors.append("DeepScientist could not resolve a Windows command shell for bash_exec.")
|
|
358
|
+
guidance.append("Install PowerShell (`pwsh`) or ensure `powershell.exe` is available on PATH.")
|
|
359
|
+
if not terminal_binary:
|
|
360
|
+
errors.append("DeepScientist could not resolve a Windows interactive shell backend.")
|
|
361
|
+
guidance.append("Ensure `powershell.exe` is available on PATH for the interactive terminal surface.")
|
|
362
|
+
return _make_check(
|
|
363
|
+
check_id="shell_backend",
|
|
364
|
+
label="Shell backend",
|
|
365
|
+
ok=len(errors) == 0,
|
|
366
|
+
summary="DeepScientist resolved platform shell backends for command and terminal sessions." if len(errors) == 0 else "DeepScientist could not resolve a required shell backend.",
|
|
367
|
+
warnings=warnings,
|
|
368
|
+
errors=errors,
|
|
369
|
+
guidance=guidance,
|
|
370
|
+
details=details,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
324
374
|
def _check_latex_runtime(home: Path) -> dict[str, Any]:
|
|
325
375
|
runtime = RuntimeToolService(home).status("tinytex")
|
|
326
376
|
pdflatex = runtime.get("binaries", {}).get("pdflatex") or {}
|
|
@@ -436,6 +486,7 @@ def run_doctor(home: Path, *, repo_root: Path) -> dict[str, Any]:
|
|
|
436
486
|
_check_python_runtime(),
|
|
437
487
|
_check_home_writable(home),
|
|
438
488
|
_check_uv(home),
|
|
489
|
+
_check_shell_backend(),
|
|
439
490
|
_check_git(config_manager),
|
|
440
491
|
_check_config_validation(config_manager),
|
|
441
492
|
_check_runner_support(config_manager),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterator, TextIO
|
|
7
|
+
|
|
8
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows
|
|
9
|
+
import msvcrt
|
|
10
|
+
else: # pragma: no cover - exercised on POSIX
|
|
11
|
+
import fcntl
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _ensure_lockable_file(handle: TextIO) -> None:
|
|
15
|
+
handle.seek(0, os.SEEK_END)
|
|
16
|
+
if handle.tell() > 0:
|
|
17
|
+
handle.seek(0)
|
|
18
|
+
return
|
|
19
|
+
handle.write("\0")
|
|
20
|
+
handle.flush()
|
|
21
|
+
handle.seek(0)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _lock_handle(handle: TextIO) -> None:
|
|
25
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows
|
|
26
|
+
_ensure_lockable_file(handle)
|
|
27
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
|
|
28
|
+
return
|
|
29
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _unlock_handle(handle: TextIO) -> None:
|
|
33
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows
|
|
34
|
+
handle.seek(0)
|
|
35
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
36
|
+
return
|
|
37
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@contextmanager
|
|
41
|
+
def advisory_file_lock(path: Path) -> Iterator[TextIO]:
|
|
42
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
with path.open("a+", encoding="utf-8") as handle:
|
|
44
|
+
_lock_handle(handle)
|
|
45
|
+
try:
|
|
46
|
+
yield handle
|
|
47
|
+
finally:
|
|
48
|
+
_unlock_handle(handle)
|
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from ..artifact.metrics import extract_latest_metric
|
|
8
|
-
from ..shared import read_json, run_command
|
|
8
|
+
from ..shared import read_json, run_command, slugify
|
|
9
9
|
from .service import branch_exists, current_branch, head_commit
|
|
10
10
|
|
|
11
11
|
|
|
@@ -68,6 +68,12 @@ def list_branch_canvas(repo: Path, *, quest_id: str) -> dict[str, Any]:
|
|
|
68
68
|
"run_id": state.get("run_id"),
|
|
69
69
|
"run_kind": state.get("run_kind"),
|
|
70
70
|
"idea_id": state.get("idea_id"),
|
|
71
|
+
"paper_line_id": state.get("paper_line_id"),
|
|
72
|
+
"paper_line_branch": state.get("paper_line_branch"),
|
|
73
|
+
"selected_outline_ref": state.get("selected_outline_ref"),
|
|
74
|
+
"source_branch": state.get("source_branch"),
|
|
75
|
+
"source_run_id": state.get("source_run_id"),
|
|
76
|
+
"source_idea_id": state.get("source_idea_id"),
|
|
71
77
|
"parent_branch_recorded": state.get("parent_branch"),
|
|
72
78
|
"worktree_root": state.get("worktree_root"),
|
|
73
79
|
"latest_metric": state.get("latest_metric"),
|
|
@@ -356,9 +362,158 @@ def _collect_branch_state(repo: Path) -> dict[str, dict[str, Any]]:
|
|
|
356
362
|
for item in state.get("recent_artifacts", []):
|
|
357
363
|
if isinstance(item, dict):
|
|
358
364
|
item.pop("_sort_key", None)
|
|
365
|
+
for workspace_root in _canvas_workspace_roots(repo):
|
|
366
|
+
state_path = workspace_root / "paper" / "paper_line_state.json"
|
|
367
|
+
if not state_path.exists():
|
|
368
|
+
continue
|
|
369
|
+
payload = read_json(state_path, {})
|
|
370
|
+
if not isinstance(payload, dict) or not payload:
|
|
371
|
+
continue
|
|
372
|
+
paper_branch = str(payload.get("paper_branch") or "").strip() or current_branch(workspace_root)
|
|
373
|
+
if not paper_branch:
|
|
374
|
+
continue
|
|
375
|
+
state = branch_state[paper_branch]
|
|
376
|
+
state.setdefault("branch", paper_branch)
|
|
377
|
+
state["worktree_root"] = str(workspace_root)
|
|
378
|
+
state["paper_line_id"] = str(payload.get("paper_line_id") or "").strip() or state.get("paper_line_id")
|
|
379
|
+
state["paper_line_branch"] = paper_branch
|
|
380
|
+
state["selected_outline_ref"] = str(payload.get("selected_outline_ref") or "").strip() or state.get("selected_outline_ref")
|
|
381
|
+
state["source_branch"] = str(payload.get("source_branch") or "").strip() or state.get("source_branch")
|
|
382
|
+
state["source_run_id"] = str(payload.get("source_run_id") or "").strip() or state.get("source_run_id")
|
|
383
|
+
state["source_idea_id"] = str(payload.get("source_idea_id") or "").strip() or state.get("source_idea_id")
|
|
384
|
+
state["updated_at"] = str(payload.get("updated_at") or state.get("updated_at") or "")
|
|
385
|
+
if not state.get("parent_branch") and state.get("source_branch"):
|
|
386
|
+
state["parent_branch"] = state.get("source_branch")
|
|
387
|
+
for workspace_root in _canvas_workspace_roots(repo):
|
|
388
|
+
paper_root = workspace_root / "paper"
|
|
389
|
+
if not paper_root.exists():
|
|
390
|
+
continue
|
|
391
|
+
state_path = paper_root / "paper_line_state.json"
|
|
392
|
+
if state_path.exists():
|
|
393
|
+
continue
|
|
394
|
+
selected_outline = read_json(paper_root / "selected_outline.json", {})
|
|
395
|
+
bundle_manifest = read_json(paper_root / "paper_bundle_manifest.json", {})
|
|
396
|
+
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
397
|
+
bundle_manifest = bundle_manifest if isinstance(bundle_manifest, dict) else {}
|
|
398
|
+
if not selected_outline and not bundle_manifest:
|
|
399
|
+
continue
|
|
400
|
+
paper_branch = str(bundle_manifest.get("paper_branch") or "").strip() or current_branch(workspace_root)
|
|
401
|
+
if not paper_branch:
|
|
402
|
+
continue
|
|
403
|
+
selected_outline_ref = str(
|
|
404
|
+
selected_outline.get("outline_id") or bundle_manifest.get("selected_outline_ref") or ""
|
|
405
|
+
).strip() or None
|
|
406
|
+
source_run_id = str(bundle_manifest.get("source_run_id") or "").strip() or None
|
|
407
|
+
state = branch_state[paper_branch]
|
|
408
|
+
state.setdefault("branch", paper_branch)
|
|
409
|
+
state["worktree_root"] = str(workspace_root)
|
|
410
|
+
state["paper_line_id"] = state.get("paper_line_id") or slugify(
|
|
411
|
+
"::".join([paper_branch or "paper", selected_outline_ref or "outline", source_run_id or "run"]),
|
|
412
|
+
"paper-line",
|
|
413
|
+
)
|
|
414
|
+
state["paper_line_branch"] = paper_branch
|
|
415
|
+
state["selected_outline_ref"] = selected_outline_ref or state.get("selected_outline_ref")
|
|
416
|
+
state["source_branch"] = str(bundle_manifest.get("source_branch") or "").strip() or state.get("source_branch")
|
|
417
|
+
state["source_run_id"] = source_run_id or state.get("source_run_id")
|
|
418
|
+
state["source_idea_id"] = str(bundle_manifest.get("source_idea_id") or "").strip() or state.get("source_idea_id")
|
|
419
|
+
if not state.get("parent_branch") and state.get("source_branch"):
|
|
420
|
+
state["parent_branch"] = state.get("source_branch")
|
|
421
|
+
campaigns_root = repo / ".ds" / "analysis_campaigns"
|
|
422
|
+
if campaigns_root.exists():
|
|
423
|
+
for path in sorted(campaigns_root.glob("*.json")):
|
|
424
|
+
manifest = read_json(path, {})
|
|
425
|
+
if not isinstance(manifest, dict) or not manifest:
|
|
426
|
+
continue
|
|
427
|
+
campaign_id = str(manifest.get("campaign_id") or path.stem).strip() or path.stem
|
|
428
|
+
paper_line_id = str(manifest.get("paper_line_id") or "").strip() or None
|
|
429
|
+
paper_line_branch = str(manifest.get("paper_line_branch") or "").strip() or None
|
|
430
|
+
analysis_parent_branch = str(manifest.get("parent_branch") or "").strip() or None
|
|
431
|
+
selected_outline_ref = str(manifest.get("selected_outline_ref") or "").strip() or None
|
|
432
|
+
source_idea_id = str(manifest.get("active_idea_id") or "").strip() or None
|
|
433
|
+
if not paper_line_branch:
|
|
434
|
+
paper_line_branch = _infer_paper_line_branch_for_campaign(
|
|
435
|
+
manifest,
|
|
436
|
+
branch_state=branch_state,
|
|
437
|
+
)
|
|
438
|
+
for item in manifest.get("slices") or []:
|
|
439
|
+
if not isinstance(item, dict):
|
|
440
|
+
continue
|
|
441
|
+
branch_name = str(item.get("branch") or "").strip()
|
|
442
|
+
if not branch_name:
|
|
443
|
+
continue
|
|
444
|
+
state = branch_state[branch_name]
|
|
445
|
+
state.setdefault("branch", branch_name)
|
|
446
|
+
state["campaign_id"] = campaign_id
|
|
447
|
+
state["paper_line_id"] = paper_line_id or state.get("paper_line_id")
|
|
448
|
+
state["paper_line_branch"] = paper_line_branch or state.get("paper_line_branch")
|
|
449
|
+
state["analysis_parent_branch"] = analysis_parent_branch or state.get("analysis_parent_branch")
|
|
450
|
+
state["selected_outline_ref"] = selected_outline_ref or state.get("selected_outline_ref")
|
|
451
|
+
state["source_idea_id"] = source_idea_id or state.get("source_idea_id")
|
|
452
|
+
if item.get("worktree_root"):
|
|
453
|
+
state["worktree_root"] = item.get("worktree_root")
|
|
359
454
|
return branch_state
|
|
360
455
|
|
|
361
456
|
|
|
457
|
+
def _canvas_workspace_roots(repo: Path) -> list[Path]:
|
|
458
|
+
roots: list[Path] = [repo]
|
|
459
|
+
research_state = read_json(repo / ".ds" / "research_state.json", {})
|
|
460
|
+
preferred_raw = str((research_state or {}).get("research_head_worktree_root") or "").strip()
|
|
461
|
+
if preferred_raw:
|
|
462
|
+
preferred = Path(preferred_raw)
|
|
463
|
+
if preferred.exists():
|
|
464
|
+
roots.append(preferred)
|
|
465
|
+
worktrees_root = repo / ".ds" / "worktrees"
|
|
466
|
+
if worktrees_root.exists():
|
|
467
|
+
roots.extend(path for path in sorted(worktrees_root.iterdir()) if path.is_dir())
|
|
468
|
+
deduped: list[Path] = []
|
|
469
|
+
seen: set[str] = set()
|
|
470
|
+
for root in roots:
|
|
471
|
+
key = str(root.resolve())
|
|
472
|
+
if key in seen:
|
|
473
|
+
continue
|
|
474
|
+
seen.add(key)
|
|
475
|
+
deduped.append(root)
|
|
476
|
+
return deduped
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _infer_paper_line_branch_for_campaign(
|
|
480
|
+
manifest: dict[str, Any],
|
|
481
|
+
*,
|
|
482
|
+
branch_state: dict[str, dict[str, Any]],
|
|
483
|
+
) -> str | None:
|
|
484
|
+
selected_outline_ref = str(manifest.get("selected_outline_ref") or "").strip() or None
|
|
485
|
+
source_idea_id = str(manifest.get("active_idea_id") or "").strip() or None
|
|
486
|
+
source_run_id = str(manifest.get("parent_run_id") or "").strip() or None
|
|
487
|
+
source_branch = str(manifest.get("parent_branch") or "").strip() or None
|
|
488
|
+
ranked: list[tuple[int, str]] = []
|
|
489
|
+
for branch_name, state in branch_state.items():
|
|
490
|
+
if not branch_name.startswith("paper/"):
|
|
491
|
+
continue
|
|
492
|
+
score = 0
|
|
493
|
+
candidate_outline = str(state.get("selected_outline_ref") or "").strip() or None
|
|
494
|
+
candidate_idea = str(state.get("source_idea_id") or "").strip() or None
|
|
495
|
+
candidate_run = str(state.get("source_run_id") or "").strip() or None
|
|
496
|
+
candidate_branch = str(state.get("source_branch") or "").strip() or None
|
|
497
|
+
if selected_outline_ref:
|
|
498
|
+
if candidate_outline != selected_outline_ref:
|
|
499
|
+
continue
|
|
500
|
+
score += 2
|
|
501
|
+
if source_idea_id and candidate_idea == source_idea_id:
|
|
502
|
+
score += 4
|
|
503
|
+
if source_run_id and candidate_run == source_run_id:
|
|
504
|
+
score += 3
|
|
505
|
+
if source_branch and candidate_branch == source_branch:
|
|
506
|
+
score += 2
|
|
507
|
+
if score > 0:
|
|
508
|
+
ranked.append((score, branch_name))
|
|
509
|
+
if not ranked:
|
|
510
|
+
return None
|
|
511
|
+
ranked.sort(key=lambda item: (item[0], item[1]), reverse=True)
|
|
512
|
+
if len(ranked) > 1 and ranked[0][0] == ranked[1][0]:
|
|
513
|
+
return None
|
|
514
|
+
return ranked[0][1]
|
|
515
|
+
|
|
516
|
+
|
|
362
517
|
def _resolve_run_result_payload(repo: Path, record: dict[str, Any]) -> dict[str, Any]:
|
|
363
518
|
details = dict(record.get("details") or {}) if isinstance(record.get("details"), dict) else {}
|
|
364
519
|
paths = dict(record.get("paths") or {}) if isinstance(record.get("paths"), dict) else {}
|
|
@@ -469,9 +624,17 @@ def _infer_parent_ref(
|
|
|
469
624
|
) -> str | None:
|
|
470
625
|
if ref == default_ref:
|
|
471
626
|
return None
|
|
627
|
+
if classifications[ref]["branch_kind"] == "analysis":
|
|
628
|
+
paper_line_branch = str(state.get("paper_line_branch") or "").strip()
|
|
629
|
+
if paper_line_branch and paper_line_branch in refs and paper_line_branch != ref:
|
|
630
|
+
return paper_line_branch
|
|
472
631
|
parent_branch = str(state.get("parent_branch") or "").strip()
|
|
473
632
|
if parent_branch and parent_branch in refs and parent_branch != ref:
|
|
474
633
|
return parent_branch
|
|
634
|
+
if classifications[ref]["branch_kind"] == "paper":
|
|
635
|
+
source_branch = str(state.get("source_branch") or "").strip()
|
|
636
|
+
if source_branch and source_branch in refs and source_branch != ref:
|
|
637
|
+
return source_branch
|
|
475
638
|
if ref.startswith("idea/"):
|
|
476
639
|
return default_ref
|
|
477
640
|
if state.get("idea_id"):
|
|
@@ -479,6 +642,9 @@ def _infer_parent_ref(
|
|
|
479
642
|
if candidate in refs:
|
|
480
643
|
return candidate
|
|
481
644
|
if classifications[ref]["branch_kind"] == "analysis":
|
|
645
|
+
analysis_parent_branch = str(state.get("analysis_parent_branch") or "").strip()
|
|
646
|
+
if analysis_parent_branch and analysis_parent_branch in refs and analysis_parent_branch != ref:
|
|
647
|
+
return analysis_parent_branch
|
|
482
648
|
major_refs = [
|
|
483
649
|
candidate
|
|
484
650
|
for candidate, meta in classifications.items()
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from mcp.server.fastmcp import FastMCP
|
|
9
|
+
from mcp.types import ToolAnnotations
|
|
9
10
|
|
|
10
11
|
from ..artifact import ArtifactService
|
|
11
12
|
from ..artifact.metrics import MetricContractValidationError
|
|
@@ -54,6 +55,16 @@ ARTIFACT_STATE_CHANGE_WATCHDOG_NOTES = {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
def _read_only_tool_annotations(*, title: str | None = None) -> ToolAnnotations:
|
|
59
|
+
return ToolAnnotations(
|
|
60
|
+
title=title,
|
|
61
|
+
readOnlyHint=True,
|
|
62
|
+
destructiveHint=False,
|
|
63
|
+
idempotentHint=True,
|
|
64
|
+
openWorldHint=False,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
57
68
|
def _metric_validation_error_payload(exc: MetricContractValidationError) -> dict[str, Any]:
|
|
58
69
|
return exc.as_payload()
|
|
59
70
|
|
|
@@ -62,7 +73,7 @@ def _progress_watchdog_note(tool_call_count: int) -> str:
|
|
|
62
73
|
return (
|
|
63
74
|
"By the way, you have gone "
|
|
64
75
|
f"{tool_call_count} tool calls without notifying the user via artifact.interact(...). "
|
|
65
|
-
"
|
|
76
|
+
"Inspect whether the user-visible state actually changed; only send a progress update if there is a real new checkpoint, blocker, or route change."
|
|
66
77
|
)
|
|
67
78
|
|
|
68
79
|
|
|
@@ -71,7 +82,7 @@ def _visibility_watchdog_note(seconds_since_last_update: int) -> str:
|
|
|
71
82
|
return (
|
|
72
83
|
"By the way, it has been "
|
|
73
84
|
f"{minutes} minutes since the last user-visible artifact.interact(...). "
|
|
74
|
-
"
|
|
85
|
+
"Inspect the current run or task state now. Only send a new user-visible update if the frontier materially changed or the user explicitly needs a fresh checkpoint."
|
|
75
86
|
)
|
|
76
87
|
|
|
77
88
|
|
|
@@ -114,11 +125,16 @@ def _attach_interaction_watchdog(
|
|
|
114
125
|
state_change_note: str | None = None,
|
|
115
126
|
) -> dict[str, Any]:
|
|
116
127
|
enriched = dict(payload)
|
|
117
|
-
|
|
128
|
+
interaction_watchdog = dict(watchdog or {})
|
|
118
129
|
notes = _collect_interaction_watchdog_notes(
|
|
119
130
|
watchdog,
|
|
120
131
|
state_change_note=state_change_note,
|
|
121
132
|
)
|
|
133
|
+
interaction_watchdog["user_update_due"] = bool(
|
|
134
|
+
interaction_watchdog.get("user_update_due")
|
|
135
|
+
or any(str(item.get("kind") or "") == "state_change" for item in notes)
|
|
136
|
+
)
|
|
137
|
+
enriched["interaction_watchdog"] = interaction_watchdog
|
|
122
138
|
if not notes:
|
|
123
139
|
return enriched
|
|
124
140
|
enriched["watchdog_notes"] = notes
|
|
@@ -377,6 +393,7 @@ def build_memory_server(context: McpContext) -> FastMCP:
|
|
|
377
393
|
"Read a memory card by id or path. "
|
|
378
394
|
"Use after list_recent or search surfaced a specific card worth reusing now."
|
|
379
395
|
),
|
|
396
|
+
annotations=_read_only_tool_annotations(title="Read memory card"),
|
|
380
397
|
)
|
|
381
398
|
def read(
|
|
382
399
|
card_id: str | None = None,
|
|
@@ -394,6 +411,7 @@ def build_memory_server(context: McpContext) -> FastMCP:
|
|
|
394
411
|
"Search memory cards by metadata or body text. "
|
|
395
412
|
"Use before broad literature search, retries, route decisions, or repeated debugging."
|
|
396
413
|
),
|
|
414
|
+
annotations=_read_only_tool_annotations(title="Search memory cards"),
|
|
397
415
|
)
|
|
398
416
|
def search(
|
|
399
417
|
query: str,
|
|
@@ -413,6 +431,7 @@ def build_memory_server(context: McpContext) -> FastMCP:
|
|
|
413
431
|
"List the most recently updated memory cards. "
|
|
414
432
|
"Use to recover quest context at turn start, after resume, or after a long pause."
|
|
415
433
|
),
|
|
434
|
+
annotations=_read_only_tool_annotations(title="List recent memory cards"),
|
|
416
435
|
)
|
|
417
436
|
def list_recent(
|
|
418
437
|
scope: str = "quest",
|
|
@@ -557,19 +576,26 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
557
576
|
name="submit_idea",
|
|
558
577
|
description=(
|
|
559
578
|
"Create or revise the active research idea. "
|
|
560
|
-
"Normal research flow should use mode=create together with lineage_intent=continue_line or branch_alternative, so each durable idea submission becomes a new branch/worktree and a new user-visible research node. "
|
|
579
|
+
"Normal research flow should use mode=create together with submission_mode='line' and lineage_intent=continue_line or branch_alternative, so each durable idea submission becomes a new branch/worktree and a new user-visible research node. "
|
|
580
|
+
"submission_mode='candidate' records a candidate idea brief without opening a new branch yet. "
|
|
561
581
|
"mode=revise is maintenance-only for refining the current active idea.md in place. "
|
|
562
582
|
"When foundation_ref is omitted, lineage_intent infers the parent and default foundation from the active research line."
|
|
563
583
|
),
|
|
564
584
|
)
|
|
565
585
|
def submit_idea(
|
|
566
586
|
mode: str = "create",
|
|
587
|
+
submission_mode: str = "line",
|
|
567
588
|
idea_id: str | None = None,
|
|
568
589
|
lineage_intent: str | None = None,
|
|
569
590
|
title: str = "",
|
|
570
591
|
problem: str = "",
|
|
571
592
|
hypothesis: str = "",
|
|
572
593
|
mechanism: str = "",
|
|
594
|
+
method_brief: str = "",
|
|
595
|
+
selection_scores: dict[str, Any] | None = None,
|
|
596
|
+
mechanism_family: str = "",
|
|
597
|
+
change_layer: str = "",
|
|
598
|
+
source_lens: str = "",
|
|
573
599
|
expected_gain: str = "",
|
|
574
600
|
evidence_paths: list[str] | None = None,
|
|
575
601
|
risks: list[str] | None = None,
|
|
@@ -578,17 +604,24 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
578
604
|
foundation_reason: str = "",
|
|
579
605
|
next_target: str = "experiment",
|
|
580
606
|
draft_markdown: str = "",
|
|
607
|
+
source_candidate_id: str | None = None,
|
|
581
608
|
comment: str | dict[str, Any] | None = None,
|
|
582
609
|
) -> dict[str, Any]:
|
|
583
610
|
return service.submit_idea(
|
|
584
611
|
context.require_quest_root(),
|
|
585
612
|
mode=mode,
|
|
613
|
+
submission_mode=submission_mode,
|
|
586
614
|
idea_id=idea_id,
|
|
587
615
|
lineage_intent=lineage_intent,
|
|
588
616
|
title=title,
|
|
589
617
|
problem=problem,
|
|
590
618
|
hypothesis=hypothesis,
|
|
591
619
|
mechanism=mechanism,
|
|
620
|
+
method_brief=method_brief,
|
|
621
|
+
selection_scores=selection_scores,
|
|
622
|
+
mechanism_family=mechanism_family,
|
|
623
|
+
change_layer=change_layer,
|
|
624
|
+
source_lens=source_lens,
|
|
592
625
|
expected_gain=expected_gain,
|
|
593
626
|
evidence_paths=evidence_paths,
|
|
594
627
|
risks=risks,
|
|
@@ -597,6 +630,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
597
630
|
foundation_reason=foundation_reason,
|
|
598
631
|
next_target=next_target,
|
|
599
632
|
draft_markdown=draft_markdown,
|
|
633
|
+
source_candidate_id=source_candidate_id,
|
|
600
634
|
)
|
|
601
635
|
|
|
602
636
|
@server.tool(
|
|
@@ -605,6 +639,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
605
639
|
"List research branches with branch number, active idea, foundation info, and corresponding main-experiment results. "
|
|
606
640
|
"Use before creating the next idea when you need to compare possible foundations."
|
|
607
641
|
),
|
|
642
|
+
annotations=_read_only_tool_annotations(title="List research branches"),
|
|
608
643
|
)
|
|
609
644
|
def list_research_branches(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
|
|
610
645
|
return service.list_research_branches(context.require_quest_root())
|
|
@@ -615,16 +650,135 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
615
650
|
"Resolve the current canonical research ids and refs. "
|
|
616
651
|
"Use this before supplementary work when you need the active idea, latest main run, active campaign, outline, or reply-thread ids without guessing."
|
|
617
652
|
),
|
|
653
|
+
annotations=_read_only_tool_annotations(title="Resolve runtime refs"),
|
|
618
654
|
)
|
|
619
655
|
def resolve_runtime_refs(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
|
|
620
656
|
return service.resolve_runtime_refs(context.require_quest_root())
|
|
621
657
|
|
|
658
|
+
@server.tool(
|
|
659
|
+
name="get_paper_contract_health",
|
|
660
|
+
description=(
|
|
661
|
+
"Inspect whether the active paper line is actually unblocked for writing or finalize work. "
|
|
662
|
+
"Use detail='summary' for a compact decision surface or detail='full' for exact blocking items."
|
|
663
|
+
),
|
|
664
|
+
annotations=_read_only_tool_annotations(title="Get paper contract health"),
|
|
665
|
+
)
|
|
666
|
+
def get_paper_contract_health(
|
|
667
|
+
detail: str = "summary",
|
|
668
|
+
comment: str | dict[str, Any] | None = None,
|
|
669
|
+
) -> dict[str, Any]:
|
|
670
|
+
return service.get_paper_contract_health(
|
|
671
|
+
context.require_quest_root(),
|
|
672
|
+
detail=detail,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
@server.tool(
|
|
676
|
+
name="get_quest_state",
|
|
677
|
+
description=(
|
|
678
|
+
"Read the current quest runtime state without mutating anything. "
|
|
679
|
+
"Use detail='summary' for a compact operational view or detail='full' for recent artifacts, runs, and active interactions."
|
|
680
|
+
),
|
|
681
|
+
annotations=_read_only_tool_annotations(title="Get quest state"),
|
|
682
|
+
)
|
|
683
|
+
def get_quest_state(
|
|
684
|
+
detail: str = "summary",
|
|
685
|
+
comment: str | dict[str, Any] | None = None,
|
|
686
|
+
) -> dict[str, Any]:
|
|
687
|
+
return service.get_quest_state(
|
|
688
|
+
context.require_quest_root(),
|
|
689
|
+
detail=detail,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
@server.tool(
|
|
693
|
+
name="get_global_status",
|
|
694
|
+
description=(
|
|
695
|
+
"Read a concise quest-global status summary for direct user questions such as overall progress, paper readiness, or the latest measured result. "
|
|
696
|
+
"Use detail='brief' for a compact answer surface or detail='full' for more structured context."
|
|
697
|
+
),
|
|
698
|
+
annotations=_read_only_tool_annotations(title="Get global status"),
|
|
699
|
+
)
|
|
700
|
+
def get_global_status(
|
|
701
|
+
detail: str = "brief",
|
|
702
|
+
locale: str = "zh",
|
|
703
|
+
comment: str | dict[str, Any] | None = None,
|
|
704
|
+
) -> dict[str, Any]:
|
|
705
|
+
return service.get_global_status(
|
|
706
|
+
context.require_quest_root(),
|
|
707
|
+
detail=detail,
|
|
708
|
+
locale=locale,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
@server.tool(
|
|
712
|
+
name="get_method_scoreboard",
|
|
713
|
+
description=(
|
|
714
|
+
"Read or refresh the quest-level method scoreboard so overall experiment history and the current incumbent line are explicit."
|
|
715
|
+
),
|
|
716
|
+
)
|
|
717
|
+
def get_method_scoreboard(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
|
|
718
|
+
return service.refresh_method_scoreboard(context.require_quest_root())
|
|
719
|
+
|
|
720
|
+
@server.tool(
|
|
721
|
+
name="get_optimization_frontier",
|
|
722
|
+
description=(
|
|
723
|
+
"Read a compact optimization-frontier summary for algorithm-first quests. "
|
|
724
|
+
"It summarizes candidate briefs, promoted lines, recent implementation candidates, stagnant branches, fusion opportunities, and the recommended next mode."
|
|
725
|
+
),
|
|
726
|
+
annotations=_read_only_tool_annotations(title="Get optimization frontier"),
|
|
727
|
+
)
|
|
728
|
+
def get_optimization_frontier(
|
|
729
|
+
comment: str | dict[str, Any] | None = None,
|
|
730
|
+
) -> dict[str, Any]:
|
|
731
|
+
return service.get_optimization_frontier(
|
|
732
|
+
context.require_quest_root(),
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
@server.tool(
|
|
736
|
+
name="read_quest_documents",
|
|
737
|
+
description=(
|
|
738
|
+
"Read durable quest documents such as brief, plan, status, summary, and active user requirements. "
|
|
739
|
+
"Use mode='excerpt' for compact recovery or mode='full' when exact document wording matters."
|
|
740
|
+
),
|
|
741
|
+
annotations=_read_only_tool_annotations(title="Read quest documents"),
|
|
742
|
+
)
|
|
743
|
+
def read_quest_documents(
|
|
744
|
+
names: list[str] | None = None,
|
|
745
|
+
mode: str = "excerpt",
|
|
746
|
+
max_lines: int = 12,
|
|
747
|
+
comment: str | dict[str, Any] | None = None,
|
|
748
|
+
) -> dict[str, Any]:
|
|
749
|
+
return service.read_quest_documents(
|
|
750
|
+
context.require_quest_root(),
|
|
751
|
+
names=names,
|
|
752
|
+
mode=mode,
|
|
753
|
+
max_lines=max_lines,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
@server.tool(
|
|
757
|
+
name="get_conversation_context",
|
|
758
|
+
description=(
|
|
759
|
+
"Read a recent window of quest conversation history. "
|
|
760
|
+
"Use this when earlier user/assistant continuity matters and the current prompt intentionally keeps only a compact turn launcher."
|
|
761
|
+
),
|
|
762
|
+
annotations=_read_only_tool_annotations(title="Get conversation context"),
|
|
763
|
+
)
|
|
764
|
+
def get_conversation_context(
|
|
765
|
+
limit: int = 12,
|
|
766
|
+
include_attachments: bool = False,
|
|
767
|
+
comment: str | dict[str, Any] | None = None,
|
|
768
|
+
) -> dict[str, Any]:
|
|
769
|
+
return service.get_conversation_context(
|
|
770
|
+
context.require_quest_root(),
|
|
771
|
+
limit=limit,
|
|
772
|
+
include_attachments=include_attachments,
|
|
773
|
+
)
|
|
774
|
+
|
|
622
775
|
@server.tool(
|
|
623
776
|
name="get_analysis_campaign",
|
|
624
777
|
description=(
|
|
625
778
|
"Get one analysis campaign manifest with todo items, slice status, and next pending slice. "
|
|
626
779
|
"Pass campaign_id='active' or omit it to recover the active campaign."
|
|
627
780
|
),
|
|
781
|
+
annotations=_read_only_tool_annotations(title="Get analysis campaign"),
|
|
628
782
|
)
|
|
629
783
|
def get_analysis_campaign(
|
|
630
784
|
campaign_id: str | None = "active",
|
|
@@ -762,6 +916,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
762
916
|
"List candidate/revised paper outlines and the selected outline reference. "
|
|
763
917
|
"Use this before writing-facing analysis campaigns or when you need a valid outline_id."
|
|
764
918
|
),
|
|
919
|
+
annotations=_read_only_tool_annotations(title="List paper outlines"),
|
|
765
920
|
)
|
|
766
921
|
def list_paper_outlines(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
|
|
767
922
|
return service.list_paper_outlines(context.require_quest_root())
|
|
@@ -957,7 +1112,14 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
957
1112
|
def render_git_graph(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
|
|
958
1113
|
return service.render_git_graph(context.require_quest_root())
|
|
959
1114
|
|
|
960
|
-
@server.tool(
|
|
1115
|
+
@server.tool(
|
|
1116
|
+
name="interact",
|
|
1117
|
+
description=(
|
|
1118
|
+
"Send a structured user-facing interaction and optionally fetch new inbound messages. "
|
|
1119
|
+
"Use kind='answer' for direct user questions, kind='progress' for long-running checkpoint updates, "
|
|
1120
|
+
"kind='milestone' for material state changes, and kind='decision_request' only for true blocking decisions."
|
|
1121
|
+
),
|
|
1122
|
+
)
|
|
961
1123
|
def interact(
|
|
962
1124
|
kind: str = "progress",
|
|
963
1125
|
message: str = "",
|
|
@@ -977,6 +1139,9 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
977
1139
|
reply_schema: dict[str, Any] | None = None,
|
|
978
1140
|
reply_to_interaction_id: str | None = None,
|
|
979
1141
|
supersede_open_requests: bool = True,
|
|
1142
|
+
dedupe_key: str | None = None,
|
|
1143
|
+
suppress_if_unchanged: bool | None = None,
|
|
1144
|
+
min_interval_seconds: int | None = None,
|
|
980
1145
|
comment: str | dict[str, Any] | None = None,
|
|
981
1146
|
) -> dict[str, Any]:
|
|
982
1147
|
result = service.interact(
|
|
@@ -999,6 +1164,9 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
999
1164
|
reply_schema=reply_schema,
|
|
1000
1165
|
reply_to_interaction_id=reply_to_interaction_id,
|
|
1001
1166
|
supersede_open_requests=supersede_open_requests,
|
|
1167
|
+
dedupe_key=dedupe_key,
|
|
1168
|
+
suppress_if_unchanged=suppress_if_unchanged,
|
|
1169
|
+
min_interval_seconds=min_interval_seconds,
|
|
1002
1170
|
)
|
|
1003
1171
|
result["interaction_watchdog"] = quest_service.artifact_interaction_watchdog_status(context.require_quest_root())
|
|
1004
1172
|
return result
|