@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
@@ -17,6 +17,18 @@ def _as_bool_env(name: str) -> bool:
17
17
  return value.lower() in {"1", "true", "yes", "on", "y"}
18
18
 
19
19
 
20
+ def _as_optional_bool_env(name: str) -> bool | None:
21
+ value = _as_text(os.environ.get(name))
22
+ if value is None:
23
+ return None
24
+ normalized = value.lower()
25
+ if normalized in {"1", "true", "yes", "on", "y"}:
26
+ return True
27
+ if normalized in {"0", "false", "no", "off", "n"}:
28
+ return False
29
+ return True
30
+
31
+
20
32
  def codex_runtime_overrides() -> dict[str, str]:
21
33
  binary = _as_text(os.environ.get("DEEPSCIENTIST_CODEX_BINARY") or os.environ.get("DS_CODEX_BINARY"))
22
34
  approval_policy = _as_text(os.environ.get("DEEPSCIENTIST_CODEX_APPROVAL_POLICY"))
@@ -24,9 +36,13 @@ def codex_runtime_overrides() -> dict[str, str]:
24
36
  profile = _as_text(os.environ.get("DEEPSCIENTIST_CODEX_PROFILE"))
25
37
  model = _as_text(os.environ.get("DEEPSCIENTIST_CODEX_MODEL"))
26
38
 
27
- if _as_bool_env("DEEPSCIENTIST_CODEX_YOLO"):
39
+ yolo_enabled = _as_optional_bool_env("DEEPSCIENTIST_CODEX_YOLO")
40
+ if yolo_enabled is True:
28
41
  approval_policy = approval_policy or "never"
29
42
  sandbox_mode = sandbox_mode or "danger-full-access"
43
+ elif yolo_enabled is False:
44
+ approval_policy = approval_policy or "on-request"
45
+ sandbox_mode = sandbox_mode or "workspace-write"
30
46
 
31
47
  overrides: dict[str, str] = {}
32
48
  if binary:
@@ -1,4 +1,4 @@
1
1
  from .installer import SkillInstaller
2
- from .registry import SkillBundle, discover_skill_bundles
2
+ from .registry import SkillBundle, companion_skill_ids, discover_skill_bundles, stage_skill_ids
3
3
 
4
- __all__ = ["SkillBundle", "SkillInstaller", "discover_skill_bundles"]
4
+ __all__ = ["SkillBundle", "SkillInstaller", "discover_skill_bundles", "stage_skill_ids", "companion_skill_ids"]
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hashlib
4
+ import re
3
5
  import shutil
4
6
  from pathlib import Path
5
7
  from uuid import uuid4
@@ -8,6 +10,10 @@ from ..memory.frontmatter import load_markdown_document
8
10
  from ..shared import ensure_dir, read_json, utc_now, write_json
9
11
  from .registry import discover_skill_bundles
10
12
 
13
+ _PROMPT_SYNC_STATE_FILENAME = ".deepscientist-prompt-sync.json"
14
+ _PROMPT_VERSIONS_DIRNAME = "prompt_versions"
15
+ _PROMPT_VERSIONS_INDEX_FILENAME = "index.json"
16
+
11
17
 
12
18
  class SkillInstaller:
13
19
  def __init__(self, repo_root: Path, home: Path) -> None:
@@ -40,9 +46,9 @@ class SkillInstaller:
40
46
  "notes": [],
41
47
  }
42
48
 
43
- def sync_quest(self, quest_root: Path) -> dict:
49
+ def sync_quest(self, quest_root: Path, *, installed_version: str | None = None) -> dict:
50
+ prompt_sync = self.sync_quest_prompts(quest_root, installed_version=installed_version)
44
51
  prompts_root = ensure_dir(quest_root / ".codex" / "prompts")
45
- self._sync_prompt_tree(prompts_root)
46
52
  codex_root = ensure_dir(quest_root / ".codex" / "skills")
47
53
  claude_root = ensure_dir(quest_root / ".claude" / "agents")
48
54
  copied_codex: list[str] = []
@@ -61,12 +67,13 @@ class SkillInstaller:
61
67
  self._prune_bundle_targets(claude_root, expected_claude)
62
68
  return {
63
69
  "prompts": [str(path) for path in sorted(prompts_root.rglob("*")) if path.is_file()],
70
+ "prompt_sync": prompt_sync,
64
71
  "codex": copied_codex,
65
72
  "claude": copied_claude,
66
73
  "notes": [],
67
74
  }
68
75
 
69
- def sync_existing_quests(self) -> dict:
76
+ def sync_existing_quests(self, *, installed_version: str | None = None) -> dict:
70
77
  quests_root = self.home / "quests"
71
78
  synced: list[dict[str, object]] = []
72
79
  if not quests_root.exists():
@@ -79,13 +86,15 @@ class SkillInstaller:
79
86
  continue
80
87
  if not (quest_root / "quest.yaml").exists():
81
88
  continue
82
- result = self.sync_quest(quest_root)
89
+ result = self.sync_quest(quest_root, installed_version=installed_version)
83
90
  synced.append(
84
91
  {
85
92
  "quest_id": quest_root.name,
86
93
  "quest_root": str(quest_root),
87
94
  "codex_count": len(result.get("codex") or []),
88
95
  "claude_count": len(result.get("claude") or []),
96
+ "prompt_backup_id": (result.get("prompt_sync") or {}).get("backup_id"),
97
+ "prompt_fingerprint": (result.get("prompt_sync") or {}).get("prompt_fingerprint"),
89
98
  }
90
99
  )
91
100
  return {
@@ -127,11 +136,193 @@ class SkillInstaller:
127
136
  summary["global"] = self.sync_global()
128
137
  summary["global_synced"] = True
129
138
  if sync_existing_quests_enabled:
130
- summary["existing_quests"] = self.sync_existing_quests()
139
+ summary["existing_quests"] = self.sync_existing_quests(installed_version=normalized_version)
131
140
  summary["existing_quests_synced"] = True
132
141
  self._write_release_sync_state(summary)
133
142
  return summary
134
143
 
144
+ def sync_quest_prompts(
145
+ self,
146
+ quest_root: Path,
147
+ *,
148
+ installed_version: str | None = None,
149
+ ) -> dict[str, object]:
150
+ prompts_root = ensure_dir(quest_root / ".codex" / "prompts")
151
+ source_root = self.repo_root / "src" / "prompts"
152
+ normalized_version = self._normalized_installed_version(installed_version)
153
+ previous_state = self._read_prompt_sync_state(prompts_root)
154
+ current_fingerprint = self._prompt_tree_fingerprint(prompts_root, exclude_state_file=True)
155
+ source_fingerprint = self._prompt_tree_fingerprint(source_root, exclude_state_file=False)
156
+ backup_id: str | None = None
157
+ updated = False
158
+
159
+ if current_fingerprint != source_fingerprint:
160
+ if current_fingerprint:
161
+ backup_id = self._backup_prompt_tree(
162
+ quest_root,
163
+ prompts_root=prompts_root,
164
+ installed_version=str(previous_state.get("installed_version") or normalized_version),
165
+ prompt_fingerprint=current_fingerprint,
166
+ )
167
+ self._sync_prompt_tree(prompts_root)
168
+ updated = True
169
+
170
+ prompt_state = {
171
+ "installed_version": normalized_version,
172
+ "prompt_fingerprint": self._prompt_tree_fingerprint(prompts_root, exclude_state_file=True),
173
+ "synced_at": utc_now(),
174
+ "backup_id": backup_id,
175
+ "source_root": str(source_root),
176
+ }
177
+ write_json(self._prompt_sync_state_path(prompts_root), prompt_state)
178
+ return {
179
+ "updated": updated,
180
+ "backup_id": backup_id,
181
+ "prompt_fingerprint": prompt_state["prompt_fingerprint"],
182
+ "installed_version": normalized_version,
183
+ "source_root": str(source_root),
184
+ "active_root": str(prompts_root),
185
+ "versions_root": str(self._prompt_versions_root(quest_root)),
186
+ }
187
+
188
+ def list_prompt_versions(self, quest_root: Path) -> list[dict[str, object]]:
189
+ payload = read_json(self._prompt_versions_index_path(quest_root), {})
190
+ versions = payload.get("versions") if isinstance(payload.get("versions"), list) else []
191
+ return [dict(item) for item in versions if isinstance(item, dict)]
192
+
193
+ def resolve_prompt_version_root(self, quest_root: Path, selection: str) -> Path | None:
194
+ normalized = str(selection or "").strip()
195
+ if not normalized:
196
+ return None
197
+ exact_root = self._prompt_versions_root(quest_root) / normalized
198
+ if exact_root.exists():
199
+ return exact_root
200
+ candidates = [
201
+ dict(item)
202
+ for item in self.list_prompt_versions(quest_root)
203
+ if str(item.get("installed_version") or "").strip() == normalized
204
+ ]
205
+ if not candidates:
206
+ return None
207
+ candidates.sort(key=lambda item: str(item.get("created_at") or ""))
208
+ selected_path = Path(str(candidates[-1].get("path") or "")).expanduser()
209
+ return selected_path if selected_path.exists() else None
210
+
211
+ @staticmethod
212
+ def _normalized_installed_version(installed_version: str | None) -> str:
213
+ normalized = str(installed_version or "").strip()
214
+ if normalized:
215
+ return normalized
216
+ from .. import __version__
217
+
218
+ return str(__version__ or "").strip() or "unknown"
219
+
220
+ def _backup_prompt_tree(
221
+ self,
222
+ quest_root: Path,
223
+ *,
224
+ prompts_root: Path,
225
+ installed_version: str,
226
+ prompt_fingerprint: str,
227
+ ) -> str:
228
+ versions_root = ensure_dir(self._prompt_versions_root(quest_root))
229
+ backup_id = ""
230
+ target_root: Path | None = None
231
+ for _attempt in range(8):
232
+ backup_id = self._unique_prompt_backup_id(
233
+ versions_root,
234
+ installed_version=installed_version,
235
+ prompt_fingerprint=prompt_fingerprint,
236
+ )
237
+ target_root = versions_root / backup_id
238
+ try:
239
+ shutil.copytree(prompts_root, target_root)
240
+ break
241
+ except FileExistsError:
242
+ # Another sync run may have created the same backup directory between
243
+ # name selection and copy. Regenerate a fresh id and retry.
244
+ continue
245
+ else:
246
+ raise FileExistsError(
247
+ f"Failed to allocate a unique prompt backup directory under `{versions_root}` after multiple attempts."
248
+ )
249
+ assert target_root is not None
250
+ entry = {
251
+ "backup_id": backup_id,
252
+ "installed_version": str(installed_version or "").strip() or "unknown",
253
+ "prompt_fingerprint": prompt_fingerprint,
254
+ "created_at": utc_now(),
255
+ "path": str(target_root),
256
+ }
257
+ versions = self.list_prompt_versions(quest_root)
258
+ versions = [item for item in versions if str(item.get("backup_id") or "").strip() != backup_id]
259
+ versions.append(entry)
260
+ versions.sort(key=lambda item: str(item.get("created_at") or ""))
261
+ write_json(self._prompt_versions_index_path(quest_root), {"versions": versions})
262
+ return backup_id
263
+
264
+ @staticmethod
265
+ def _prompt_versions_root(quest_root: Path) -> Path:
266
+ return ensure_dir(quest_root / ".codex" / _PROMPT_VERSIONS_DIRNAME)
267
+
268
+ @staticmethod
269
+ def _prompt_versions_index_path(quest_root: Path) -> Path:
270
+ return SkillInstaller._prompt_versions_root(quest_root) / _PROMPT_VERSIONS_INDEX_FILENAME
271
+
272
+ @staticmethod
273
+ def _prompt_sync_state_path(prompts_root: Path) -> Path:
274
+ return prompts_root / _PROMPT_SYNC_STATE_FILENAME
275
+
276
+ def _read_prompt_sync_state(self, prompts_root: Path) -> dict[str, object]:
277
+ payload = read_json(self._prompt_sync_state_path(prompts_root), {})
278
+ return payload if isinstance(payload, dict) else {}
279
+
280
+ @staticmethod
281
+ def _sanitize_prompt_label(value: str) -> str:
282
+ normalized = re.sub(r"[^A-Za-z0-9._-]+", "-", str(value or "").strip()).strip("-")
283
+ return normalized or "unknown"
284
+
285
+ def _unique_prompt_backup_id(
286
+ self,
287
+ versions_root: Path,
288
+ *,
289
+ installed_version: str,
290
+ prompt_fingerprint: str,
291
+ ) -> str:
292
+ version_label = self._sanitize_prompt_label(installed_version)
293
+ timestamp_label = self._sanitize_prompt_label(
294
+ utc_now().replace(":", "").replace("+00:00", "Z")
295
+ )
296
+ fingerprint_label = (str(prompt_fingerprint or "").strip() or "unknown")[:12]
297
+ base = f"{version_label}__prompts-{fingerprint_label}__{timestamp_label}"
298
+ candidate = base
299
+ counter = 2
300
+ while (versions_root / candidate).exists():
301
+ candidate = f"{base}__{counter}"
302
+ counter += 1
303
+ return candidate
304
+
305
+ @staticmethod
306
+ def _prompt_tree_fingerprint(root: Path, *, exclude_state_file: bool) -> str:
307
+ if not root.exists():
308
+ return ""
309
+ files = [
310
+ path
311
+ for path in sorted(root.rglob("*"))
312
+ if path.is_file()
313
+ and not (exclude_state_file and path.name == _PROMPT_SYNC_STATE_FILENAME)
314
+ ]
315
+ if not files:
316
+ return ""
317
+ hasher = hashlib.sha256()
318
+ for path in files:
319
+ relative = path.relative_to(root).as_posix()
320
+ hasher.update(relative.encode("utf-8"))
321
+ hasher.update(b"\0")
322
+ hasher.update(hashlib.sha256(path.read_bytes()).hexdigest().encode("ascii"))
323
+ hasher.update(b"\0")
324
+ return hasher.hexdigest()
325
+
135
326
  def _sync_claude_projection(self, bundle, target_root: Path) -> Path:
136
327
  target = target_root / f"deepscientist-{bundle.skill_id}.md"
137
328
  if bundle.claude_md and bundle.claude_md.exists():
@@ -2,9 +2,34 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from pathlib import Path
5
+ from typing import Any
5
6
 
6
7
  from ..memory.frontmatter import load_markdown_document
7
8
 
9
+ _DEFAULT_STAGE_SKILLS = (
10
+ "scout",
11
+ "baseline",
12
+ "idea",
13
+ "optimize",
14
+ "experiment",
15
+ "analysis-campaign",
16
+ "write",
17
+ "finalize",
18
+ "decision",
19
+ )
20
+
21
+ _DEFAULT_COMPANION_SKILLS = (
22
+ "figure-polish",
23
+ "intake-audit",
24
+ "review",
25
+ "rebuttal",
26
+ )
27
+
28
+ _SKILL_ROLE_FALLBACK_ORDER = {
29
+ **{skill_id: index for index, skill_id in enumerate(_DEFAULT_STAGE_SKILLS, start=10)},
30
+ **{skill_id: 100 + index for index, skill_id in enumerate(_DEFAULT_COMPANION_SKILLS, start=10)},
31
+ }
32
+
8
33
 
9
34
  @dataclass(frozen=True)
10
35
  class SkillBundle:
@@ -13,6 +38,8 @@ class SkillBundle:
13
38
  description: str
14
39
  root: Path
15
40
  skill_md: Path
41
+ role: str
42
+ metadata: dict[str, Any]
16
43
  openai_yaml: Path | None = None
17
44
  claude_md: Path | None = None
18
45
 
@@ -24,6 +51,29 @@ def _parse_frontmatter(path: Path) -> dict:
24
51
  return metadata
25
52
 
26
53
 
54
+ def _normalize_skill_role(skill_id: str, metadata: dict[str, Any]) -> str:
55
+ raw = str(metadata.get("skill_role") or metadata.get("role") or "").strip().lower()
56
+ if raw in {"stage", "companion", "custom"}:
57
+ return raw
58
+ if skill_id in _DEFAULT_STAGE_SKILLS:
59
+ return "stage"
60
+ if skill_id in _DEFAULT_COMPANION_SKILLS:
61
+ return "companion"
62
+ return "custom"
63
+
64
+
65
+ def _skill_order(skill_id: str, metadata: dict[str, Any]) -> tuple[int, str]:
66
+ raw = metadata.get("skill_order")
67
+ if isinstance(raw, int):
68
+ return raw, skill_id
69
+ if isinstance(raw, str):
70
+ try:
71
+ return int(raw.strip()), skill_id
72
+ except ValueError:
73
+ pass
74
+ return _SKILL_ROLE_FALLBACK_ORDER.get(skill_id, 10_000), skill_id
75
+
76
+
27
77
  def discover_skill_bundles(repo_root: Path) -> list[SkillBundle]:
28
78
  bundles: list[SkillBundle] = []
29
79
  skills_root = repo_root / "src" / "skills"
@@ -41,8 +91,24 @@ def discover_skill_bundles(repo_root: Path) -> list[SkillBundle]:
41
91
  description=metadata.get("description", ""),
42
92
  root=skill_md.parent,
43
93
  skill_md=skill_md,
94
+ role=_normalize_skill_role(skill_id, metadata),
95
+ metadata=metadata,
44
96
  openai_yaml=(skill_md.parent / "agents" / "openai.yaml") if (skill_md.parent / "agents" / "openai.yaml").exists() else None,
45
97
  claude_md=(skill_md.parent / "agents" / "claude.md") if (skill_md.parent / "agents" / "claude.md").exists() else None,
46
98
  )
47
99
  )
100
+ bundles.sort(key=lambda bundle: _skill_order(bundle.skill_id, bundle.metadata))
48
101
  return bundles
102
+
103
+
104
+ def skill_ids_for_role(repo_root: Path, role: str) -> tuple[str, ...]:
105
+ normalized = str(role or "").strip().lower()
106
+ return tuple(bundle.skill_id for bundle in discover_skill_bundles(repo_root) if bundle.role == normalized)
107
+
108
+
109
+ def stage_skill_ids(repo_root: Path) -> tuple[str, ...]:
110
+ return skill_ids_for_role(repo_root, "stage")
111
+
112
+
113
+ def companion_skill_ids(repo_root: Path) -> tuple[str, ...]:
114
+ return skill_ids_for_role(repo_root, "companion")
@@ -6,15 +6,25 @@
6
6
  - qq_runtime_ack_rule: the QQ bridge itself emits the immediate transport-level receipt acknowledgement before the model turn starts
7
7
  - qq_no_duplicate_ack_rule: do not waste your first model response or first `artifact.interact(...)` call on a redundant receipt-only acknowledgement such as "received", "已收到", or "I am processing" when the bridge already sent that
8
8
  - qq_reply_style: keep QQ replies concise, milestone-first, respectful, and easy to scan on a phone
9
+ - qq_report_style_rule: write QQ updates like a short operator report, not like an internal lab notebook; the user should understand the point from the first sentence
9
10
  - qq_reply_length_rule: for ordinary QQ progress updates, normally use only 2 to 4 short sentences, or 3 short bullets at most
10
11
  - qq_summary_first_rule: start with the conclusion the user cares about, then what it means, then the next action
11
12
  - qq_progress_shape_rule: make the current task, the main difficulty or latest real progress, and the next concrete measure explicit whenever possible
13
+ - qq_plain_chinese_rule: when the user is using Chinese, keep the whole QQ message in natural Chinese by default; avoid sudden full-English paragraphs or untranslated internal terms
14
+ - qq_jargon_ban_rule: avoid internal words or team black-talk such as `slice`, `taxonomy`, `claim boundary`, `route`, `surface`, `trace`, `sensitivity`, `checkpoint`, `pending/running/completed`, or similar control jargon unless the user explicitly asked for that layer of detail
15
+ - qq_milestone_tone_rule: for real wins, deliveries, or unblock moments, a short energetic opener such as `报告:`、`有结果了:`、`都搞定了:` is good, but only if the next sentence immediately gives the concrete result
16
+ - qq_energy_rule: keep QQ text lively and warm rather than bureaucratic; sound like a capable research buddy who proactively reports progress, not like a monitoring bot
17
+ - qq_cute_rule: a little cuteness is welcome in Chinese replies, but keep it lightweight and competent rather than overly sweet or role-play-heavy
18
+ - qq_emoji_rule: in Chinese QQ messages, you may use at most one light kaomoji or emoji for milestones, delivery, or encouraging progress, such as `(•̀ᴗ•́)و` or `✨`; avoid stacking multiple symbols, and avoid playful symbols on blockers or bad news
19
+ - qq_english_emoji_rule: in English QQ messages, use emoji instead of kaomoji when a light expressive touch helps, and keep it to at most one per message
20
+ - qq_user_value_rule: every QQ update should make one user-facing payoff explicit, such as whether the user needs to act, whether the result is trustworthy, or what will be delivered next
12
21
  - 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 or the next update; if uncertain, say that and still give the next check-in window
13
22
  - qq_tool_call_keepalive_rule: for ordinary active work, prefer one concise QQ progress update after roughly 6 tool calls when there is already a human-meaningful delta, and do not let work drift beyond roughly 12 tool calls or about 8 minutes without a user-visible checkpoint
14
23
  - qq_read_plan_keepalive_rule: if the active work is still mostly reading, comparison, or planning, do not wait too long for a "big result"; send a short QQ-facing checkpoint after about 5 consecutive tool calls if the user would otherwise see silence
15
24
  - qq_internal_detail_rule: omit worker names, heartbeat timestamps, retry counters, pending/running/completed counts, file names, and monitor-window narration unless the user asked for them or the detail changes the recommended action
16
25
  - qq_translation_rule: convert internal execution and file-management work into user value, such as saying the baseline record is now organized for easier later comparison instead of listing touched files
17
26
  - qq_preflight_rule: before sending a QQ progress update, rewrite it if it still sounds like a monitoring log, execution diary, or file inventory
27
+ - qq_report_template_rule: the default QQ template is `结论 / 当前判断 -> 一条最关键的结果或阻塞 -> 下一步和回报时间`; if one sentence does not help the user decide what happened, it is not ready to send
18
28
  - qq_operator_surface_rule: treat QQ as an operator surface for coordination and milestone delivery, not as a full artifact browser
19
29
  - qq_default_text_rule: plain text is the default and safest QQ mode
20
30
  - qq_absolute_path_rule: when you request native QQ image or file delivery via an attachment `path`, prefer an absolute path
@@ -68,7 +78,7 @@ Why bad:
68
78
  Good:
69
79
 
70
80
  ```text
71
- 公开 baseline 还在继续推进,暂时不需要额外修补。当前主要情况是整体在往前走,但其中一条线仍然更慢、更不稳定。接下来我会继续盯下一轮结果,预计 20 到 30 分钟内会有下一次关键判断;如果更早出现完成、再次卡住,或者需要干预,我会提前同步给您。
81
+ 先跟您报个平安:这轮 baseline 还在稳定推进,目前不用您额外处理。最新变化是主线结果已经开始收敛,只剩一条对照线还比较慢。接下来我会盯住这条慢线,预计 20 到 30 分钟内给您下一次关键判断;如果更早跑完或再次卡住,我会提前同步。
72
82
  ```
73
83
 
74
84
  Why good:
@@ -77,10 +87,10 @@ Why good:
77
87
  - it keeps the meaningful risk but removes unnecessary internal telemetry
78
88
  - it tells the user exactly what will happen next
79
89
 
80
- English-style reference shape:
90
+ Reference shape:
81
91
 
82
92
  ```text
83
- I'm working on {current task}. The main issue right now is {difficulty or risk}, but {latest real progress or current judgment}. Next I'll {concrete next measure}. You should hear from me again in about {ETA}, or sooner if {important condition} happens.
93
+ Conclusion first. Then say the one concrete result or blocker. Then say the next step and when the user should expect the next update.
84
94
  ```
85
95
 
86
96
  ### 1. Plain-text QQ progress update
@@ -88,7 +98,7 @@ I'm working on {current task}. The main issue right now is {difficulty or risk},
88
98
  ```python
89
99
  artifact.interact(
90
100
  kind="progress",
91
- message="主实验第一轮已经跑完,结果目前比较稳定。接下来我会继续补消融,确认这个提升是不是稳得住。下一次我只同步关键变化给您。",
101
+ message="有新进展啦:主实验第一轮已经跑完,而且结果目前比较稳定。接下来我会继续补关键消融,确认这个提升是不是稳得住;下一次我只同步真正影响判断的变化给您。",
92
102
  reply_mode="threaded",
93
103
  )
94
104
  ```
@@ -100,7 +110,7 @@ Use the normal `artifact.interact(...)` call. When DeepScientist already knows t
100
110
  ```python
101
111
  artifact.interact(
102
112
  kind="progress",
103
- message="我已经看完您刚才提到的那篇论文,也确认了它和当前 baseline 的核心差异。接下来我会把真正影响路线选择的部分整理出来,再给您一个更完整的结论。",
113
+ message="我已经看完您刚才提到的那篇论文,并确认了它和当前 baseline 的关键差异。接下来我会把真正影响路线选择的部分整理成一版清楚结论,再给您完整汇报。",
104
114
  reply_mode="threaded",
105
115
  )
106
116
  ```
@@ -112,7 +122,7 @@ Use this only when the active-surface block says `qq_enable_markdown_send: True`
112
122
  ```python
113
123
  artifact.interact(
114
124
  kind="milestone",
115
- message="## 主实验完成\n- 指标已稳定超过基线\n- 当前最主要风险是泛化边界仍需补充验证",
125
+ message="## 报告!主实验完成啦 ✨\n- 当前指标已稳定超过基线\n- 接下来只需要补一轮泛化验证,就能判断这条路线是否可以正式升级",
116
126
  reply_mode="threaded",
117
127
  connector_hints={"qq": {"render_mode": "markdown"}},
118
128
  )
@@ -125,7 +135,7 @@ Use this only when the active-surface block says `qq_enable_file_upload_experime
125
135
  ```python
126
136
  artifact.interact(
127
137
  kind="milestone",
128
- message="主实验已经完成。我发一张汇总图给您,便于手机上快速查看。",
138
+ message="报告!主实验已经完成啦 (•̀ᴗ•́)و 我发一张汇总图给您,方便直接在手机上快速看结论。",
129
139
  reply_mode="threaded",
130
140
  attachments=[
131
141
  {
@@ -144,7 +154,7 @@ artifact.interact(
144
154
  ```python
145
155
  artifact.interact(
146
156
  kind="milestone",
147
- message="论文初稿已整理完成。我把 PDF 一并发给您。",
157
+ message="都整理好啦 📄 论文初稿已经出炉,我把 PDF 一并发给您,您可以直接查看当前版本。",
148
158
  reply_mode="threaded",
149
159
  attachments=[
150
160
  {
@@ -6,15 +6,25 @@
6
6
  - weixin_runtime_ack_rule: the Weixin bridge itself emits the immediate transport-level receipt acknowledgement before the model turn starts
7
7
  - weixin_no_duplicate_ack_rule: do not waste your first model response or first `artifact.interact(...)` call on a second bare acknowledgement such as "received", "已收到", or "processing" when the bridge already sent that
8
8
  - weixin_reply_style_rule: keep Weixin replies concise, milestone-first, respectful, and easy to scan on a phone
9
+ - weixin_report_style_rule: write Weixin updates like a short report to the project owner, not like an internal execution diary
9
10
  - weixin_reply_length_rule: for ordinary Weixin progress replies, normally use only 2 to 4 short sentences, or 3 short bullets at most
10
11
  - weixin_summary_first_rule: start with the user-facing conclusion, then what it means, then the next action
11
12
  - weixin_progress_shape_rule: make the current task, the main difficulty or latest real progress, and the next concrete measure explicit whenever possible
13
+ - weixin_plain_chinese_rule: when the user is using Chinese, keep the whole Weixin message in natural Chinese by default; avoid sudden English paragraphs or untranslated internal terms
14
+ - weixin_jargon_ban_rule: avoid internal words or team black-talk such as `slice`, `taxonomy`, `claim boundary`, `route`, `surface`, `trace`, `sensitivity`, `checkpoint`, `pending/running/completed`, or similar control jargon unless the user explicitly asked for them
15
+ - weixin_milestone_tone_rule: for meaningful progress, delivery, or unblock moments, a short opener such as `报告:`、`有结果了:`、`都搞定了:` is welcome, but the next sentence must immediately state the concrete result
16
+ - weixin_energy_rule: keep Weixin text lively and warm rather than bureaucratic; sound like a capable research buddy who proactively reports progress
17
+ - weixin_cute_rule: a little cuteness is welcome in Chinese replies, but keep it light and competent rather than sugary or exaggerated
18
+ - weixin_emoji_rule: in Chinese Weixin messages, you may use at most one light kaomoji or emoji for milestones, delivery, or encouraging progress, such as `(•̀ᴗ•́)و` or `✨`; avoid stacking multiple symbols, and avoid playful symbols on blockers or bad news
19
+ - weixin_english_emoji_rule: in English Weixin messages, use emoji instead of kaomoji when a light expressive touch helps, and keep it to at most one per message
20
+ - weixin_user_value_rule: make the user payoff explicit in every Weixin update, such as whether action is needed, whether a result is already trustworthy, or what file/result will be delivered next
12
21
  - weixin_eta_rule: for important long-running phases such as baseline reproduction, main experiments, analysis, or paper packaging, include a rough ETA or next check-in window when you can
13
22
  - weixin_tool_call_keepalive_rule: for ordinary active work, prefer one concise Weixin progress update after roughly 6 tool calls when there is already a human-meaningful delta, and do not let work drift beyond roughly 12 tool calls or about 8 minutes without a user-visible checkpoint
14
23
  - weixin_read_plan_keepalive_rule: if the active work is still mostly reading, comparison, or planning, do not wait too long for a "big result"; send a short Weixin-facing checkpoint after about 5 consecutive tool calls if the user would otherwise see silence
15
24
  - weixin_internal_detail_rule: omit worker names, retry counters, pending/running/completed counts, low-level file listings, and monitor-window narration unless the user explicitly asked for them or they change the recommended action
16
25
  - weixin_translation_rule: translate internal execution and file-management work into user value instead of narrating tool or filesystem churn
17
26
  - weixin_preflight_rule: before sending a Weixin-facing progress update, rewrite it if it still reads like a monitor log, execution diary, or file inventory
27
+ - weixin_report_template_rule: the default Weixin template is `结论 / 当前判断 -> 一条最关键的结果或阻塞 -> 下一步和回报时间`; if the user still cannot tell what changed after the first sentence, rewrite it
18
28
  - weixin_operator_surface_rule: treat Weixin as an operator surface for concise coordination and milestone delivery, not as a full artifact browser
19
29
  - weixin_default_text_rule: plain text is the default and safest Weixin mode
20
30
  - weixin_context_token_rule: ordinary downstream replies rely on the runtime-managed `context_token`; do not invent your own reply token fields
@@ -85,7 +95,7 @@ Why bad:
85
95
  Good:
86
96
 
87
97
  ```text
88
- 主实验还在继续推进,当前不需要您额外处理。最新进展是核心结果已经基本稳定,但还有一条对照线比较慢。接下来我会补完这条对照,预计 20 分钟左右给您下一次关键更新。
98
+ 先跟您同步一下:主实验还在继续推进,目前不需要您额外处理。最新变化是核心结果已经基本稳定,只剩一条对照线还比较慢。接下来我会补完这条对照,预计 20 分钟左右给您下一次关键更新。
89
99
  ```
90
100
 
91
101
  Why good:
@@ -99,7 +109,7 @@ Why good:
99
109
  ```python
100
110
  artifact.interact(
101
111
  kind="progress",
102
- message="主实验第一轮已经跑完,当前结果基本稳定。接下来我会继续补关键对照,确认这个提升是不是稳得住。预计下一次关键更新在 20 分钟左右。",
112
+ message="有新进展啦:主实验第一轮已经跑完,而且当前结果基本稳定。接下来我会继续补关键对照,确认这个提升是不是稳得住;预计下一次关键更新在 20 分钟左右。",
103
113
  reply_mode="threaded",
104
114
  )
105
115
  ```
@@ -111,7 +121,7 @@ Use the normal `artifact.interact(...)` call. The runtime keeps continuity throu
111
121
  ```python
112
122
  artifact.interact(
113
123
  kind="progress",
114
- message="我已经看完您刚才发来的材料,也确认了它和当前 baseline 的关键差异。接下来我会把真正影响路线判断的部分整理出来,再给您一个更完整的结论。",
124
+ message="我已经看完您刚才发来的材料,并确认了它和当前 baseline 的关键差异。接下来我会把真正影响路线判断的部分整理成一版清楚结论,再给您完整汇报。",
115
125
  reply_mode="threaded",
116
126
  )
117
127
  ```
@@ -121,7 +131,7 @@ artifact.interact(
121
131
  ```python
122
132
  artifact.interact(
123
133
  kind="milestone",
124
- message="主实验已经完成。我发一张汇总图给您,方便直接在手机上看。",
134
+ message="报告!主实验已经完成啦 ✨ 我发一张汇总图给您,方便直接在手机上看结论。",
125
135
  reply_mode="threaded",
126
136
  attachments=[
127
137
  {
@@ -140,7 +150,7 @@ artifact.interact(
140
150
  ```python
141
151
  artifact.interact(
142
152
  kind="milestone",
143
- message="我把这段关键演示视频一起发给您。",
153
+ message="都整理好啦:我把这段关键演示视频一起发给您,方便直接确认效果。",
144
154
  reply_mode="threaded",
145
155
  attachments=[
146
156
  {
@@ -159,7 +169,7 @@ artifact.interact(
159
169
  ```python
160
170
  artifact.interact(
161
171
  kind="milestone",
162
- message="论文初稿已经整理完成,我把 PDF 一并发给您。",
172
+ message="都搞定啦 📄 论文初稿已经整理完成,我把 PDF 一并发给您,方便您直接查看当前版本。",
163
173
  reply_mode="threaded",
164
174
  attachments=[
165
175
  {
@@ -4,14 +4,34 @@ This shared contract is injected once per turn and applies across the stage and
4
4
 
5
5
  ## Shared interaction rules
6
6
 
7
+ - **Tool discipline rule: native `shell_command` / `command_execution` is forbidden across this workflow.**
8
+ - **All shell-like execution, including shell, CLI, Python, bash, node, git, npm, uv, package-manager, environment, and terminal-style file inspection work, must go through `bash_exec(...)`.**
9
+ - **Even if the runner or model surface exposes `shell_command`, ignore it and translate that action into `bash_exec(...)`.**
10
+ - **For git operations inside the current quest repository or worktree, prefer `artifact.git(...)` before raw shell git commands.**
11
+ - **Treat any attempt to use native `shell_command` / `command_execution` as a policy violation and immediately switch back to `bash_exec(...)`.**
7
12
  - Treat `artifact.interact(...)` as the main long-lived communication thread across TUI, web, and bound connectors.
8
13
  - If `artifact.interact(...)` returns queued user requirements, treat them as the highest-priority user instruction bundle before continuing the current stage or companion-skill task.
9
- - Immediately follow any non-empty mailbox poll with another `artifact.interact(...)` update that confirms receipt; if the request is directly answerable, answer there, otherwise say the current subtask is paused, give a short plan plus nearest report-back point, and handle that request first.
14
+ - Immediately follow any non-empty mailbox poll with another `artifact.interact(...)` update that gives a substantive receipt plus next action; if the request is directly answerable, answer there with `kind='answer'`, otherwise say the current subtask is paused, give a short plan plus nearest report-back point, and handle that request first. Do not send a second bare acknowledgement such as `received` or `已收到`.
15
+ - If you are explicitly answering or continuing a specific prior interaction thread, use `reply_to_interaction_id` instead of assuming the runtime will always infer the right target.
10
16
  - Stage-kickoff rule: after entering any stage or companion skill, send one `artifact.interact(kind='progress', reply_mode='threaded', ...)` update within the first 3 tool calls of substantial work.
11
17
  - Reading/planning keepalive rule: if you spend 5 consecutive tool calls on reading, searching, comparison, or planning without a user-visible update, send one concise checkpoint even if the route is not finalized yet.
18
+ - Visibility-bound rule: do not drift beyond roughly 12 tool calls or about 8 minutes without a user-visible update when the user-visible state has materially changed.
12
19
  - Subtask-boundary rule: send a user-visible update whenever the active subtask changes materially, especially across intake -> audit, audit -> experiment planning, experiment planning -> run launch, run result -> drafting, or drafting -> review/rebuttal.
13
- - Emit `artifact.interact(kind='progress', reply_mode='threaded', ...)` when there is real user-visible progress: a meaningful checkpoint, route-shaping update, or a concise keepalive once active work has crossed roughly 6 tool calls with a human-meaningful delta. Do not let ordinary active work drift beyond roughly 12 tool calls or about 8 minutes without a user-visible update.
20
+ - Emit `artifact.interact(kind='progress', reply_mode='threaded', ...)` when there is real user-visible progress: a meaningful checkpoint, route-shaping update, blocker, recovery, or a concise keepalive when silence would otherwise hide a meaningful change. Do not reflexively send another progress update if the user-visible state is unchanged.
14
21
  - Keep progress updates chat-like and easy to understand: say what changed, what it means, and what happens next.
15
- - Default to plain-language summaries. Do not mention file paths, artifact ids, branch/worktree ids, session ids, raw commands, or raw logs unless the user asks or needs them to act.
22
+ - Do not treat background monitoring as a reason for sub-minute chat churn. Long-running work should remain alive in detached `bash_exec` sessions; when those tasks are already active, auto-continue should serve as low-frequency inspection and recovery only, normally around `240` seconds between checks unless a real event demands sooner action.
23
+ - In autonomous mode, if no real long-running external task is active yet, the next turns should keep moving the quest toward that real unit of work instead of parking or pretending the quest is finished.
24
+ - For connector-facing progress in Chinese, default to a short report shape: first the conclusion or current judgment, then one concrete result or blocker, then the next action or next update window.
25
+ - Keep the tone respectful and easy to understand. In Chinese, natural respectful phrasing is good; in English, keep a polite professional tone.
26
+ - When the user is Chinese-speaking, keep the whole connector-facing update in natural Chinese by default instead of mixing in unexplained English sentences.
27
+ - Assume the user may not know the codebase or internal runtime objects. Explain progress in beginner-friendly task language before technical detail.
28
+ - If there are `2-3` options, tradeoffs, or next steps, prefer a short numbered list instead of a dense block of prose.
29
+ - If a key distinction is quantitative and the number is known, include the number or one short concrete example instead of only saying `better`, `slower`, or `more stable`.
30
+ - Default to plain-language summaries. Do not mention file paths, file names, artifact ids, branch/worktree ids, session ids, raw commands, or raw logs unless the user asks or needs them to act. First translate them into user-facing meaning such as baseline record, draft, experiment result, or supplementary run.
31
+ - Avoid internal research-control jargon or black-box team slang on connector surfaces unless the user explicitly asked for it. Rewrite terms such as `slice`, `taxonomy`, `claim boundary`, `route`, `surface`, `trace`, or `sensitivity` into plain task language first.
32
+ - If a draft update still reads like a monitor log, internal memo, or execution diary, rewrite it before sending so the user can immediately tell what changed, why it matters, and what happens next.
33
+ - When the user is plainly asking a direct question, answer it directly in plain language before resuming background stage work.
16
34
  - Use `reply_mode='blocking'` only for real user decisions that cannot be resolved from local evidence.
17
- - For any blocking decision request, provide 1 to 3 concrete options, put the recommended option first, explain each option's actual content plus pros and cons, and wait up to 1 day when feasible. If the blocker is a missing external credential or secret that only the user can provide, keep the quest waiting, ask the user to supply it or choose an alternative, and do not self-resolve; if resumed without that credential and no other work is possible, a long low-frequency wait such as `bash_exec(command='sleep 3600', mode='await', timeout_seconds=3700)` is acceptable. Otherwise choose the best option yourself and notify the user of the chosen option if the timeout expires.
35
+ - Keep `deliver_to_bound_conversations=True` for normal user-visible continuity. If `delivery_results` or `attachment_issues` show that requested delivery failed, treat that as a real failure and adapt instead of assuming the user already received the message or file.
36
+ - Use `dedupe_key`, `suppress_if_unchanged`, and `min_interval_seconds` only to suppress repeated unchanged `progress` updates, not to suppress a real answer or milestone.
37
+ - For any blocking decision request, provide 1 to 3 concrete options, put the recommended option first, and explain for each option: what it means, how strongly you recommend it, its likely impact on speed / quality / cost / risk, and when it is preferable. Make the user's reply format obvious and wait up to 1 day when feasible. If the blocker is a missing external credential or secret that only the user can provide, keep the quest waiting, ask the user to supply it or choose an alternative, and do not self-resolve; if resumed without that credential and no other work is possible, a long low-frequency wait such as `bash_exec(command='sleep 3600', mode='await', timeout_seconds=3700)` is acceptable. Otherwise choose the best option yourself and notify the user of the chosen option if the timeout expires.