@researai/deepscientist 1.5.14 → 1.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +336 -90
  2. package/assets/branding/logo-raster.png +0 -0
  3. package/bin/ds.js +816 -131
  4. package/docs/en/00_QUICK_START.md +36 -15
  5. package/docs/en/01_SETTINGS_REFERENCE.md +53 -4
  6. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  7. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
  8. package/docs/en/05_TUI_GUIDE.md +6 -0
  9. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  10. package/docs/en/09_DOCTOR.md +11 -5
  11. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  12. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
  13. package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
  14. package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  15. package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  16. package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  17. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  18. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  19. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  20. package/docs/en/README.md +24 -0
  21. package/docs/zh/00_QUICK_START.md +36 -15
  22. package/docs/zh/01_SETTINGS_REFERENCE.md +53 -4
  23. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  24. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
  25. package/docs/zh/05_TUI_GUIDE.md +6 -0
  26. package/docs/zh/09_DOCTOR.md +11 -5
  27. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  28. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
  29. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
  30. package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  31. package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  32. package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  33. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  34. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  35. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  36. package/docs/zh/README.md +24 -0
  37. package/install.sh +2 -0
  38. package/package.json +1 -1
  39. package/pyproject.toml +1 -1
  40. package/src/deepscientist/__init__.py +1 -1
  41. package/src/deepscientist/acp/envelope.py +6 -0
  42. package/src/deepscientist/artifact/charts.py +567 -0
  43. package/src/deepscientist/artifact/guidance.py +50 -10
  44. package/src/deepscientist/artifact/metrics.py +228 -5
  45. package/src/deepscientist/artifact/schemas.py +3 -0
  46. package/src/deepscientist/artifact/service.py +4276 -308
  47. package/src/deepscientist/bash_exec/models.py +23 -0
  48. package/src/deepscientist/bash_exec/monitor.py +147 -67
  49. package/src/deepscientist/bash_exec/runtime.py +218 -156
  50. package/src/deepscientist/bash_exec/service.py +309 -69
  51. package/src/deepscientist/bash_exec/shells.py +87 -0
  52. package/src/deepscientist/bridges/connectors.py +51 -2
  53. package/src/deepscientist/cli.py +115 -19
  54. package/src/deepscientist/codex_cli_compat.py +232 -0
  55. package/src/deepscientist/config/models.py +8 -4
  56. package/src/deepscientist/config/service.py +38 -11
  57. package/src/deepscientist/connector/weixin_support.py +122 -1
  58. package/src/deepscientist/daemon/api/handlers.py +199 -9
  59. package/src/deepscientist/daemon/api/router.py +5 -0
  60. package/src/deepscientist/daemon/app.py +1458 -289
  61. package/src/deepscientist/doctor.py +51 -0
  62. package/src/deepscientist/file_lock.py +48 -0
  63. package/src/deepscientist/gitops/__init__.py +10 -1
  64. package/src/deepscientist/gitops/diff.py +296 -1
  65. package/src/deepscientist/gitops/service.py +4 -1
  66. package/src/deepscientist/mcp/server.py +212 -5
  67. package/src/deepscientist/process_control.py +161 -0
  68. package/src/deepscientist/prompts/builder.py +501 -453
  69. package/src/deepscientist/quest/layout.py +15 -2
  70. package/src/deepscientist/quest/service.py +2539 -195
  71. package/src/deepscientist/quest/stage_views.py +177 -1
  72. package/src/deepscientist/runners/base.py +2 -0
  73. package/src/deepscientist/runners/codex.py +169 -31
  74. package/src/deepscientist/runners/runtime_overrides.py +17 -1
  75. package/src/deepscientist/skills/__init__.py +2 -2
  76. package/src/deepscientist/skills/installer.py +196 -5
  77. package/src/deepscientist/skills/registry.py +66 -0
  78. package/src/prompts/connectors/qq.md +18 -8
  79. package/src/prompts/connectors/weixin.md +16 -6
  80. package/src/prompts/contracts/shared_interaction.md +24 -4
  81. package/src/prompts/system.md +921 -72
  82. package/src/prompts/system_copilot.md +43 -0
  83. package/src/skills/analysis-campaign/SKILL.md +32 -2
  84. package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
  85. package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
  86. package/src/skills/baseline/SKILL.md +10 -0
  87. package/src/skills/decision/SKILL.md +27 -2
  88. package/src/skills/experiment/SKILL.md +16 -2
  89. package/src/skills/figure-polish/SKILL.md +1 -0
  90. package/src/skills/finalize/SKILL.md +19 -0
  91. package/src/skills/idea/SKILL.md +79 -0
  92. package/src/skills/idea/references/idea-generation-playbook.md +100 -0
  93. package/src/skills/idea/references/outline-seeding-example.md +60 -0
  94. package/src/skills/intake-audit/SKILL.md +9 -1
  95. package/src/skills/mentor/SKILL.md +217 -0
  96. package/src/skills/mentor/references/correction-rules.md +210 -0
  97. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  98. package/src/skills/mentor/references/persona-profile.md +138 -0
  99. package/src/skills/mentor/references/taste-profile.md +128 -0
  100. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  101. package/src/skills/mentor/references/work-profile.md +289 -0
  102. package/src/skills/mentor/references/workflow-profile.md +240 -0
  103. package/src/skills/optimize/SKILL.md +1645 -0
  104. package/src/skills/rebuttal/SKILL.md +3 -1
  105. package/src/skills/review/SKILL.md +3 -1
  106. package/src/skills/scout/SKILL.md +8 -0
  107. package/src/skills/write/SKILL.md +81 -12
  108. package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
  109. package/src/tui/dist/app/AppContainer.js +22 -11
  110. package/src/tui/dist/index.js +4 -1
  111. package/src/tui/dist/lib/api.js +33 -3
  112. package/src/tui/package.json +1 -1
  113. package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
  114. package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
  115. package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
  116. package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
  117. package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
  118. package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
  119. package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
  120. package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
  121. package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
  122. package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
  123. package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
  124. package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
  125. package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
  126. package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
  127. package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
  128. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  129. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  130. package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
  131. package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
  132. package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
  133. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  134. package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
  135. package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
  136. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  137. package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
  138. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  139. package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
  140. package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
  141. package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
  142. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  143. package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
  144. package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
  145. package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
  146. package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
  147. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  148. package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
  149. package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
  150. package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
  151. package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
  152. package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
  153. package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
  154. package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
  155. package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
  156. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  157. package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
  158. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  159. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  160. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  161. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  162. package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
  163. package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
  164. package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
  165. package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
  166. package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
  167. package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
  168. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  169. package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
  170. package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
  171. package/src/ui/dist/index.html +5 -2
  172. package/src/ui/dist/assets/AiManusChatView-DaF9Nge_.js +0 -26597
  173. package/src/ui/dist/assets/AnalysisPlugin-BSVx6dXE.js +0 -123
  174. package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
  175. package/src/ui/dist/assets/CodeEditorPlugin-DU9G0Tox.js +0 -427
  176. package/src/ui/dist/assets/CodeViewerPlugin-DoX_fI9l.js +0 -905
  177. package/src/ui/dist/assets/DocViewerPlugin-C4FWIXuU.js +0 -278
  178. package/src/ui/dist/assets/GitDiffViewerPlugin-BgfFMgtf.js +0 -2661
  179. package/src/ui/dist/assets/ImageViewerPlugin-tcPkfY_x.js +0 -500
  180. package/src/ui/dist/assets/LabCopilotPanel-_dKV60Bf.js +0 -4104
  181. package/src/ui/dist/assets/LabPlugin-Bje0ayoC.js +0 -2677
  182. package/src/ui/dist/assets/LatexPlugin-CVsBzAln.js +0 -1792
  183. package/src/ui/dist/assets/MarkdownViewerPlugin-xjmrqv_8.js +0 -308
  184. package/src/ui/dist/assets/MarketplacePlugin-mMM2A8wP.js +0 -413
  185. package/src/ui/dist/assets/NotebookEditor-3kVDSOBo.js +0 -4214
  186. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  187. package/src/ui/dist/assets/NotebookEditor-SoJ8X-MO.js +0 -84873
  188. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  189. package/src/ui/dist/assets/PdfLoader-DElVuHl9.js +0 -25468
  190. package/src/ui/dist/assets/PdfMarkdownPlugin-Bq88XT4G.js +0 -409
  191. package/src/ui/dist/assets/PdfViewerPlugin-CsCXMo9S.js +0 -3095
  192. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  193. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  194. package/src/ui/dist/assets/SearchPlugin-oUPvy19k.js +0 -741
  195. package/src/ui/dist/assets/TextViewerPlugin-CRkT9yNy.js +0 -472
  196. package/src/ui/dist/assets/VNCViewer-BgbuvWhR.js +0 -18821
  197. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  198. package/src/ui/dist/assets/bot-v_RASACv.js +0 -21
  199. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  200. package/src/ui/dist/assets/code-5hC9d0VH.js +0 -17
  201. package/src/ui/dist/assets/file-content-D1PxfOrp.js +0 -377
  202. package/src/ui/dist/assets/file-diff-panel-DG1oT_Hj.js +0 -92
  203. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  204. package/src/ui/dist/assets/file-socket-BmdFYQlk.js +0 -58
  205. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  206. package/src/ui/dist/assets/image-Dqe2X2tW.js +0 -18
  207. package/src/ui/dist/assets/index-BQG-1s2o.css +0 -12553
  208. package/src/ui/dist/assets/index-DVsMKK_y.js +0 -25
  209. package/src/ui/dist/assets/index-Duvz8Ip0.js +0 -159
  210. package/src/ui/dist/assets/index-Nt9hS4ck.js +0 -244829
  211. package/src/ui/dist/assets/index-RDlNXXx1.js +0 -120
  212. package/src/ui/dist/assets/monaco-DIXge1CP.js +0 -623
  213. package/src/ui/dist/assets/pdf-effect-queue-BBTTQaO-.js +0 -47
  214. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  215. package/src/ui/dist/assets/popover-BWlolyxo.js +0 -476
  216. package/src/ui/dist/assets/project-sync-BM5PkFH4.js +0 -297
  217. package/src/ui/dist/assets/select-D4dAtrA8.js +0 -1690
  218. package/src/ui/dist/assets/sigma-CKbE5jJT.js +0 -22
  219. package/src/ui/dist/assets/square-check-big-CZNGMgiB.js +0 -17
  220. package/src/ui/dist/assets/trash-DaB37xAz.js +0 -32
  221. package/src/ui/dist/assets/useCliAccess-C2OmAcWe.js +0 -957
  222. package/src/ui/dist/assets/useFileDiffOverlay-Dowd1Ij4.js +0 -53
  223. package/src/ui/dist/assets/wrap-text-BGjAhAUq.js +0 -35
  224. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  225. package/src/ui/dist/assets/zoom-out-dMZQMXzc.js +0 -34
@@ -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
- "Please report your latest progress now."
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
- "Send one concise progress update now before continuing with background work."
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
- enriched["interaction_watchdog"] = dict(watchdog or {})
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",
@@ -507,6 +526,45 @@ def build_artifact_server(context: McpContext) -> FastMCP:
507
526
  allow_empty=allow_empty,
508
527
  )
509
528
 
529
+ @server.tool(
530
+ name="git",
531
+ description=(
532
+ "Run git-oriented workspace operations for Copilot mode and general quest maintenance. "
533
+ "Use action=status|commit|branch|checkout|log|show|diff|graph."
534
+ ),
535
+ )
536
+ def git(
537
+ action: str,
538
+ message: str | None = None,
539
+ ref: str | None = None,
540
+ base: str | None = None,
541
+ head: str | None = None,
542
+ sha: str | None = None,
543
+ path: str | None = None,
544
+ branch: str | None = None,
545
+ create_from: str | None = None,
546
+ limit: int = 30,
547
+ allow_empty: bool = False,
548
+ checkout_new_branch: bool = False,
549
+ comment: str | dict[str, Any] | None = None,
550
+ ) -> dict[str, Any]:
551
+ return service.git_action(
552
+ context.require_quest_root(),
553
+ action=action,
554
+ workspace_root=context.worktree_root,
555
+ message=message,
556
+ ref=ref,
557
+ base=base,
558
+ head=head,
559
+ sha=sha,
560
+ path=path,
561
+ branch=branch,
562
+ create_from=create_from,
563
+ limit=limit,
564
+ allow_empty=allow_empty,
565
+ checkout_new_branch=checkout_new_branch,
566
+ )
567
+
510
568
  @server.tool(name="prepare_branch", description="Prepare an idea or run branch and optional worktree.")
511
569
  def prepare_branch(
512
570
  run_id: str | None = None,
@@ -557,19 +615,26 @@ def build_artifact_server(context: McpContext) -> FastMCP:
557
615
  name="submit_idea",
558
616
  description=(
559
617
  "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. "
618
+ "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. "
619
+ "submission_mode='candidate' records a candidate idea brief without opening a new branch yet. "
561
620
  "mode=revise is maintenance-only for refining the current active idea.md in place. "
562
621
  "When foundation_ref is omitted, lineage_intent infers the parent and default foundation from the active research line."
563
622
  ),
564
623
  )
565
624
  def submit_idea(
566
625
  mode: str = "create",
626
+ submission_mode: str = "line",
567
627
  idea_id: str | None = None,
568
628
  lineage_intent: str | None = None,
569
629
  title: str = "",
570
630
  problem: str = "",
571
631
  hypothesis: str = "",
572
632
  mechanism: str = "",
633
+ method_brief: str = "",
634
+ selection_scores: dict[str, Any] | None = None,
635
+ mechanism_family: str = "",
636
+ change_layer: str = "",
637
+ source_lens: str = "",
573
638
  expected_gain: str = "",
574
639
  evidence_paths: list[str] | None = None,
575
640
  risks: list[str] | None = None,
@@ -578,17 +643,24 @@ def build_artifact_server(context: McpContext) -> FastMCP:
578
643
  foundation_reason: str = "",
579
644
  next_target: str = "experiment",
580
645
  draft_markdown: str = "",
646
+ source_candidate_id: str | None = None,
581
647
  comment: str | dict[str, Any] | None = None,
582
648
  ) -> dict[str, Any]:
583
649
  return service.submit_idea(
584
650
  context.require_quest_root(),
585
651
  mode=mode,
652
+ submission_mode=submission_mode,
586
653
  idea_id=idea_id,
587
654
  lineage_intent=lineage_intent,
588
655
  title=title,
589
656
  problem=problem,
590
657
  hypothesis=hypothesis,
591
658
  mechanism=mechanism,
659
+ method_brief=method_brief,
660
+ selection_scores=selection_scores,
661
+ mechanism_family=mechanism_family,
662
+ change_layer=change_layer,
663
+ source_lens=source_lens,
592
664
  expected_gain=expected_gain,
593
665
  evidence_paths=evidence_paths,
594
666
  risks=risks,
@@ -597,6 +669,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
597
669
  foundation_reason=foundation_reason,
598
670
  next_target=next_target,
599
671
  draft_markdown=draft_markdown,
672
+ source_candidate_id=source_candidate_id,
600
673
  )
601
674
 
602
675
  @server.tool(
@@ -605,6 +678,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
605
678
  "List research branches with branch number, active idea, foundation info, and corresponding main-experiment results. "
606
679
  "Use before creating the next idea when you need to compare possible foundations."
607
680
  ),
681
+ annotations=_read_only_tool_annotations(title="List research branches"),
608
682
  )
609
683
  def list_research_branches(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
610
684
  return service.list_research_branches(context.require_quest_root())
@@ -615,16 +689,135 @@ def build_artifact_server(context: McpContext) -> FastMCP:
615
689
  "Resolve the current canonical research ids and refs. "
616
690
  "Use this before supplementary work when you need the active idea, latest main run, active campaign, outline, or reply-thread ids without guessing."
617
691
  ),
692
+ annotations=_read_only_tool_annotations(title="Resolve runtime refs"),
618
693
  )
619
694
  def resolve_runtime_refs(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
620
695
  return service.resolve_runtime_refs(context.require_quest_root())
621
696
 
697
+ @server.tool(
698
+ name="get_paper_contract_health",
699
+ description=(
700
+ "Inspect whether the active paper line is actually unblocked for writing or finalize work. "
701
+ "Use detail='summary' for a compact decision surface or detail='full' for exact blocking items."
702
+ ),
703
+ annotations=_read_only_tool_annotations(title="Get paper contract health"),
704
+ )
705
+ def get_paper_contract_health(
706
+ detail: str = "summary",
707
+ comment: str | dict[str, Any] | None = None,
708
+ ) -> dict[str, Any]:
709
+ return service.get_paper_contract_health(
710
+ context.require_quest_root(),
711
+ detail=detail,
712
+ )
713
+
714
+ @server.tool(
715
+ name="get_quest_state",
716
+ description=(
717
+ "Read the current quest runtime state without mutating anything. "
718
+ "Use detail='summary' for a compact operational view or detail='full' for recent artifacts, runs, and active interactions."
719
+ ),
720
+ annotations=_read_only_tool_annotations(title="Get quest state"),
721
+ )
722
+ def get_quest_state(
723
+ detail: str = "summary",
724
+ comment: str | dict[str, Any] | None = None,
725
+ ) -> dict[str, Any]:
726
+ return service.get_quest_state(
727
+ context.require_quest_root(),
728
+ detail=detail,
729
+ )
730
+
731
+ @server.tool(
732
+ name="get_global_status",
733
+ description=(
734
+ "Read a concise quest-global status summary for direct user questions such as overall progress, paper readiness, or the latest measured result. "
735
+ "Use detail='brief' for a compact answer surface or detail='full' for more structured context."
736
+ ),
737
+ annotations=_read_only_tool_annotations(title="Get global status"),
738
+ )
739
+ def get_global_status(
740
+ detail: str = "brief",
741
+ locale: str = "zh",
742
+ comment: str | dict[str, Any] | None = None,
743
+ ) -> dict[str, Any]:
744
+ return service.get_global_status(
745
+ context.require_quest_root(),
746
+ detail=detail,
747
+ locale=locale,
748
+ )
749
+
750
+ @server.tool(
751
+ name="get_method_scoreboard",
752
+ description=(
753
+ "Read or refresh the quest-level method scoreboard so overall experiment history and the current incumbent line are explicit."
754
+ ),
755
+ )
756
+ def get_method_scoreboard(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
757
+ return service.refresh_method_scoreboard(context.require_quest_root())
758
+
759
+ @server.tool(
760
+ name="get_optimization_frontier",
761
+ description=(
762
+ "Read a compact optimization-frontier summary for algorithm-first quests. "
763
+ "It summarizes candidate briefs, promoted lines, recent implementation candidates, stagnant branches, fusion opportunities, and the recommended next mode."
764
+ ),
765
+ annotations=_read_only_tool_annotations(title="Get optimization frontier"),
766
+ )
767
+ def get_optimization_frontier(
768
+ comment: str | dict[str, Any] | None = None,
769
+ ) -> dict[str, Any]:
770
+ return service.get_optimization_frontier(
771
+ context.require_quest_root(),
772
+ )
773
+
774
+ @server.tool(
775
+ name="read_quest_documents",
776
+ description=(
777
+ "Read durable quest documents such as brief, plan, status, summary, and active user requirements. "
778
+ "Use mode='excerpt' for compact recovery or mode='full' when exact document wording matters."
779
+ ),
780
+ annotations=_read_only_tool_annotations(title="Read quest documents"),
781
+ )
782
+ def read_quest_documents(
783
+ names: list[str] | None = None,
784
+ mode: str = "excerpt",
785
+ max_lines: int = 12,
786
+ comment: str | dict[str, Any] | None = None,
787
+ ) -> dict[str, Any]:
788
+ return service.read_quest_documents(
789
+ context.require_quest_root(),
790
+ names=names,
791
+ mode=mode,
792
+ max_lines=max_lines,
793
+ )
794
+
795
+ @server.tool(
796
+ name="get_conversation_context",
797
+ description=(
798
+ "Read a recent window of quest conversation history. "
799
+ "Use this when earlier user/assistant continuity matters and the current prompt intentionally keeps only a compact turn launcher."
800
+ ),
801
+ annotations=_read_only_tool_annotations(title="Get conversation context"),
802
+ )
803
+ def get_conversation_context(
804
+ limit: int = 12,
805
+ include_attachments: bool = False,
806
+ comment: str | dict[str, Any] | None = None,
807
+ ) -> dict[str, Any]:
808
+ return service.get_conversation_context(
809
+ context.require_quest_root(),
810
+ limit=limit,
811
+ include_attachments=include_attachments,
812
+ )
813
+
622
814
  @server.tool(
623
815
  name="get_analysis_campaign",
624
816
  description=(
625
817
  "Get one analysis campaign manifest with todo items, slice status, and next pending slice. "
626
818
  "Pass campaign_id='active' or omit it to recover the active campaign."
627
819
  ),
820
+ annotations=_read_only_tool_annotations(title="Get analysis campaign"),
628
821
  )
629
822
  def get_analysis_campaign(
630
823
  campaign_id: str | None = "active",
@@ -762,6 +955,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
762
955
  "List candidate/revised paper outlines and the selected outline reference. "
763
956
  "Use this before writing-facing analysis campaigns or when you need a valid outline_id."
764
957
  ),
958
+ annotations=_read_only_tool_annotations(title="List paper outlines"),
765
959
  )
766
960
  def list_paper_outlines(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
767
961
  return service.list_paper_outlines(context.require_quest_root())
@@ -957,7 +1151,14 @@ def build_artifact_server(context: McpContext) -> FastMCP:
957
1151
  def render_git_graph(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
958
1152
  return service.render_git_graph(context.require_quest_root())
959
1153
 
960
- @server.tool(name="interact", description="Send a structured user-facing update and optionally fetch new inbound messages.")
1154
+ @server.tool(
1155
+ name="interact",
1156
+ description=(
1157
+ "Send a structured user-facing interaction and optionally fetch new inbound messages. "
1158
+ "Use kind='answer' for direct user questions, kind='progress' for long-running checkpoint updates, "
1159
+ "kind='milestone' for material state changes, and kind='decision_request' only for true blocking decisions."
1160
+ ),
1161
+ )
961
1162
  def interact(
962
1163
  kind: str = "progress",
963
1164
  message: str = "",
@@ -977,6 +1178,9 @@ def build_artifact_server(context: McpContext) -> FastMCP:
977
1178
  reply_schema: dict[str, Any] | None = None,
978
1179
  reply_to_interaction_id: str | None = None,
979
1180
  supersede_open_requests: bool = True,
1181
+ dedupe_key: str | None = None,
1182
+ suppress_if_unchanged: bool | None = None,
1183
+ min_interval_seconds: int | None = None,
980
1184
  comment: str | dict[str, Any] | None = None,
981
1185
  ) -> dict[str, Any]:
982
1186
  result = service.interact(
@@ -999,6 +1203,9 @@ def build_artifact_server(context: McpContext) -> FastMCP:
999
1203
  reply_schema=reply_schema,
1000
1204
  reply_to_interaction_id=reply_to_interaction_id,
1001
1205
  supersede_open_requests=supersede_open_requests,
1206
+ dedupe_key=dedupe_key,
1207
+ suppress_if_unchanged=suppress_if_unchanged,
1208
+ min_interval_seconds=min_interval_seconds,
1002
1209
  )
1003
1210
  result["interaction_watchdog"] = quest_service.artifact_interaction_watchdog_status(context.require_quest_root())
1004
1211
  return result
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ import signal
6
+ import subprocess
7
+ import time
8
+ from typing import Any
9
+
10
+
11
+ def process_session_popen_kwargs(
12
+ *,
13
+ hide_window: bool = False,
14
+ new_process_group: bool = True,
15
+ ) -> dict[str, Any]:
16
+ if os.name == "nt": # pragma: no cover - exercised on Windows
17
+ creationflags = 0
18
+ if new_process_group and hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"):
19
+ creationflags |= getattr(subprocess, "CREATE_NEW_PROCESS_GROUP")
20
+ if hide_window and hasattr(subprocess, "CREATE_NO_WINDOW"):
21
+ creationflags |= getattr(subprocess, "CREATE_NO_WINDOW")
22
+ payload: dict[str, Any] = {}
23
+ if creationflags:
24
+ payload["creationflags"] = creationflags
25
+ if hide_window and hasattr(subprocess, "STARTUPINFO") and hasattr(subprocess, "STARTF_USESHOWWINDOW"):
26
+ startupinfo = subprocess.STARTUPINFO()
27
+ startupinfo.dwFlags |= getattr(subprocess, "STARTF_USESHOWWINDOW")
28
+ startupinfo.wShowWindow = getattr(subprocess, "SW_HIDE", 0)
29
+ payload["startupinfo"] = startupinfo
30
+ return payload
31
+ if new_process_group:
32
+ return {
33
+ "start_new_session": True,
34
+ }
35
+ return {}
36
+
37
+
38
+ def is_process_alive(pid: object) -> bool:
39
+ if not isinstance(pid, int) or pid <= 0:
40
+ return False
41
+ if os.name != "nt":
42
+ proc_stat_path = Path("/proc") / str(pid) / "stat"
43
+ if proc_stat_path.exists():
44
+ try:
45
+ parts = proc_stat_path.read_text(encoding="utf-8").split()
46
+ except OSError:
47
+ parts = []
48
+ if len(parts) >= 3 and parts[2] == "Z":
49
+ return False
50
+ try:
51
+ os.kill(pid, 0)
52
+ except ProcessLookupError:
53
+ return False
54
+ except PermissionError:
55
+ return True
56
+ except OSError:
57
+ return False
58
+ return True
59
+
60
+
61
+ def terminate_process_ids(
62
+ *,
63
+ process_pid: int | None,
64
+ process_group_id: int | None,
65
+ force: bool = False,
66
+ ) -> None:
67
+ if os.name == "nt": # pragma: no cover - exercised on Windows
68
+ if isinstance(process_pid, int) and process_pid > 0:
69
+ taskkill_args = ["taskkill", "/PID", str(process_pid), "/T"]
70
+ if force:
71
+ taskkill_args.append("/F")
72
+ subprocess.run(taskkill_args, check=False, capture_output=True, text=True)
73
+ if not force and is_process_alive(process_pid):
74
+ try:
75
+ os.kill(process_pid, signal.SIGTERM)
76
+ except OSError:
77
+ pass
78
+ return
79
+ if isinstance(process_group_id, int) and process_group_id > 0:
80
+ try:
81
+ os.killpg(process_group_id, signal.SIGKILL if force else signal.SIGTERM)
82
+ except ProcessLookupError:
83
+ return
84
+ return
85
+ if isinstance(process_pid, int) and process_pid > 0:
86
+ try:
87
+ os.kill(process_pid, signal.SIGKILL if force else signal.SIGTERM)
88
+ except ProcessLookupError:
89
+ return
90
+
91
+
92
+ def terminate_subprocess(
93
+ process: subprocess.Popen[Any],
94
+ *,
95
+ process_group_id: int | None = None,
96
+ force: bool = False,
97
+ prefer_ctrl_break: bool = False,
98
+ grace_seconds: float = 5.0,
99
+ ) -> None:
100
+ if process.poll() is not None:
101
+ return
102
+
103
+ if os.name == "nt": # pragma: no cover - exercised on Windows
104
+ if not force and prefer_ctrl_break and hasattr(signal, "CTRL_BREAK_EVENT"):
105
+ try:
106
+ process.send_signal(signal.CTRL_BREAK_EVENT) # type: ignore[attr-defined]
107
+ except (AttributeError, OSError, ValueError):
108
+ try:
109
+ process.terminate()
110
+ except OSError:
111
+ return
112
+ elif not force:
113
+ try:
114
+ process.terminate()
115
+ except OSError:
116
+ return
117
+ else:
118
+ try:
119
+ process.kill()
120
+ except OSError:
121
+ return
122
+ if force:
123
+ return
124
+ deadline = time.monotonic() + max(grace_seconds, 0.1)
125
+ while time.monotonic() < deadline:
126
+ if process.poll() is not None:
127
+ return
128
+ time.sleep(0.05)
129
+ try:
130
+ process.kill()
131
+ except OSError:
132
+ return
133
+ return
134
+
135
+ if isinstance(process_group_id, int) and process_group_id > 0:
136
+ try:
137
+ os.killpg(process_group_id, signal.SIGKILL if force else signal.SIGTERM)
138
+ except ProcessLookupError:
139
+ return
140
+ else:
141
+ try:
142
+ process.kill() if force else process.terminate()
143
+ except OSError:
144
+ return
145
+ if force:
146
+ return
147
+ deadline = time.monotonic() + max(grace_seconds, 0.1)
148
+ while time.monotonic() < deadline:
149
+ if process.poll() is not None:
150
+ return
151
+ time.sleep(0.05)
152
+ if isinstance(process_group_id, int) and process_group_id > 0:
153
+ try:
154
+ os.killpg(process_group_id, signal.SIGKILL)
155
+ except ProcessLookupError:
156
+ return
157
+ elif process.poll() is None:
158
+ try:
159
+ process.kill()
160
+ except OSError:
161
+ return