@researai/deepscientist 1.5.0 → 1.5.1

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 (163) hide show
  1. package/AGENTS.md +26 -0
  2. package/README.md +19 -179
  3. package/assets/connectors/lingzhu/openclaw-bridge/README.md +124 -0
  4. package/assets/connectors/lingzhu/openclaw-bridge/index.ts +162 -0
  5. package/assets/connectors/lingzhu/openclaw-bridge/openclaw.plugin.json +145 -0
  6. package/assets/connectors/lingzhu/openclaw-bridge/package.json +35 -0
  7. package/assets/connectors/lingzhu/openclaw-bridge/src/cli.ts +180 -0
  8. package/assets/connectors/lingzhu/openclaw-bridge/src/config.ts +196 -0
  9. package/assets/connectors/lingzhu/openclaw-bridge/src/debug-log.ts +111 -0
  10. package/assets/connectors/lingzhu/openclaw-bridge/src/events.ts +4 -0
  11. package/assets/connectors/lingzhu/openclaw-bridge/src/http-handler.ts +1133 -0
  12. package/assets/connectors/lingzhu/openclaw-bridge/src/image-cache.ts +75 -0
  13. package/assets/connectors/lingzhu/openclaw-bridge/src/lingzhu-tools.ts +246 -0
  14. package/assets/connectors/lingzhu/openclaw-bridge/src/transform.ts +541 -0
  15. package/assets/connectors/lingzhu/openclaw-bridge/src/types.ts +131 -0
  16. package/assets/connectors/lingzhu/openclaw-bridge/tsconfig.json +14 -0
  17. package/assets/connectors/lingzhu/openclaw.lingzhu.config.template.json +39 -0
  18. package/bin/ds.js +233 -53
  19. package/docs/en/00_QUICK_START.md +134 -0
  20. package/docs/en/01_SETTINGS_REFERENCE.md +1104 -0
  21. package/docs/en/02_START_RESEARCH_GUIDE.md +404 -0
  22. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +325 -0
  23. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +216 -0
  24. package/docs/en/05_TUI_GUIDE.md +141 -0
  25. package/docs/en/06_RUNTIME_AND_CANVAS.md +679 -0
  26. package/docs/en/07_MEMORY_AND_MCP.md +253 -0
  27. package/docs/en/08_FIGURE_STYLE_GUIDE.md +97 -0
  28. package/docs/en/09_DOCTOR.md +108 -0
  29. package/docs/en/90_ARCHITECTURE.md +245 -0
  30. package/docs/en/91_DEVELOPMENT.md +195 -0
  31. package/docs/en/99_ACKNOWLEDGEMENTS.md +29 -0
  32. package/docs/zh/00_QUICK_START.md +134 -0
  33. package/docs/zh/01_SETTINGS_REFERENCE.md +1137 -0
  34. package/docs/zh/02_START_RESEARCH_GUIDE.md +414 -0
  35. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +324 -0
  36. package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +230 -0
  37. package/docs/zh/05_TUI_GUIDE.md +128 -0
  38. package/docs/zh/06_RUNTIME_AND_CANVAS.md +271 -0
  39. package/docs/zh/07_MEMORY_AND_MCP.md +235 -0
  40. package/docs/zh/08_FIGURE_STYLE_GUIDE.md +97 -0
  41. package/docs/zh/09_DOCTOR.md +112 -0
  42. package/docs/zh/99_ACKNOWLEDGEMENTS.md +29 -0
  43. package/install.sh +32 -8
  44. package/package.json +4 -2
  45. package/pyproject.toml +1 -1
  46. package/src/deepscientist/artifact/guidance.py +9 -2
  47. package/src/deepscientist/artifact/service.py +482 -22
  48. package/src/deepscientist/bash_exec/monitor.py +27 -5
  49. package/src/deepscientist/bash_exec/runtime.py +639 -0
  50. package/src/deepscientist/bash_exec/service.py +99 -16
  51. package/src/deepscientist/bridges/base.py +3 -0
  52. package/src/deepscientist/bridges/connectors.py +292 -13
  53. package/src/deepscientist/channels/qq.py +19 -2
  54. package/src/deepscientist/channels/relay.py +1 -0
  55. package/src/deepscientist/cli.py +32 -25
  56. package/src/deepscientist/config/models.py +28 -2
  57. package/src/deepscientist/config/service.py +201 -6
  58. package/src/deepscientist/connector_runtime.py +2 -0
  59. package/src/deepscientist/daemon/api/handlers.py +50 -5
  60. package/src/deepscientist/daemon/api/router.py +1 -0
  61. package/src/deepscientist/daemon/app.py +442 -15
  62. package/src/deepscientist/doctor.py +444 -0
  63. package/src/deepscientist/home.py +1 -0
  64. package/src/deepscientist/latex_runtime.py +17 -4
  65. package/src/deepscientist/lingzhu_support.py +182 -0
  66. package/src/deepscientist/mcp/server.py +49 -2
  67. package/src/deepscientist/prompts/builder.py +181 -58
  68. package/src/deepscientist/quest/layout.py +1 -0
  69. package/src/deepscientist/quest/service.py +63 -2
  70. package/src/deepscientist/quest/stage_views.py +19 -1
  71. package/src/deepscientist/runtime_tools/__init__.py +16 -0
  72. package/src/deepscientist/runtime_tools/builtins.py +19 -0
  73. package/src/deepscientist/runtime_tools/models.py +29 -0
  74. package/src/deepscientist/runtime_tools/registry.py +40 -0
  75. package/src/deepscientist/runtime_tools/service.py +59 -0
  76. package/src/deepscientist/runtime_tools/tinytex.py +25 -0
  77. package/src/deepscientist/tinytex.py +276 -0
  78. package/src/prompts/connectors/lingzhu.md +12 -0
  79. package/src/prompts/connectors/qq.md +121 -0
  80. package/src/prompts/system.md +177 -33
  81. package/src/skills/analysis-campaign/SKILL.md +22 -6
  82. package/src/skills/baseline/SKILL.md +5 -4
  83. package/src/skills/decision/SKILL.md +4 -3
  84. package/src/skills/experiment/SKILL.md +5 -4
  85. package/src/skills/finalize/SKILL.md +5 -4
  86. package/src/skills/idea/SKILL.md +5 -4
  87. package/src/skills/intake-audit/SKILL.md +277 -0
  88. package/src/skills/intake-audit/references/state-audit-template.md +41 -0
  89. package/src/skills/rebuttal/SKILL.md +407 -0
  90. package/src/skills/rebuttal/references/action-plan-template.md +63 -0
  91. package/src/skills/rebuttal/references/evidence-update-template.md +30 -0
  92. package/src/skills/rebuttal/references/response-letter-template.md +113 -0
  93. package/src/skills/rebuttal/references/review-matrix-template.md +55 -0
  94. package/src/skills/review/SKILL.md +293 -0
  95. package/src/skills/review/references/experiment-todo-template.md +29 -0
  96. package/src/skills/review/references/review-report-template.md +83 -0
  97. package/src/skills/review/references/revision-log-template.md +40 -0
  98. package/src/skills/scout/SKILL.md +5 -4
  99. package/src/skills/write/SKILL.md +7 -3
  100. package/src/tui/dist/components/WelcomePanel.js +17 -43
  101. package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -2
  102. package/src/tui/package.json +1 -1
  103. package/src/ui/dist/assets/{AiManusChatView-7v-dHngU.js → AiManusChatView-w5lF2Ttt.js} +109 -575
  104. package/src/ui/dist/assets/{AnalysisPlugin-B_Xmz-KE.js → AnalysisPlugin-DJOED79I.js} +1 -1
  105. package/src/ui/dist/assets/{AutoFigurePlugin-Cko-0tm1.js → AutoFigurePlugin-DaG61Y0M.js} +63 -8
  106. package/src/ui/dist/assets/{CliPlugin-BsU0ht7q.js → CliPlugin-CV4LqUB_.js} +43 -609
  107. package/src/ui/dist/assets/{CodeEditorPlugin-DcMMP0Rt.js → CodeEditorPlugin-DylfAea4.js} +8 -8
  108. package/src/ui/dist/assets/{CodeViewerPlugin-BqoQ5QyY.js → CodeViewerPlugin-F7saY0LM.js} +5 -5
  109. package/src/ui/dist/assets/{DocViewerPlugin-D7eHNhU6.js → DocViewerPlugin-COP0c7jf.js} +3 -3
  110. package/src/ui/dist/assets/{GitDiffViewerPlugin-DLJN42T5.js → GitDiffViewerPlugin-CAS05pT9.js} +1 -1
  111. package/src/ui/dist/assets/{ImageViewerPlugin-gJMV7MOu.js → ImageViewerPlugin-Bco1CN_w.js} +5 -6
  112. package/src/ui/dist/assets/{LabCopilotPanel-B857sfxP.js → LabCopilotPanel-CvMlCD99.js} +12 -15
  113. package/src/ui/dist/assets/LabPlugin-BYankkE4.js +2676 -0
  114. package/src/ui/dist/assets/LabPlugin-D9jVIo0A.css +2698 -0
  115. package/src/ui/dist/assets/{LatexPlugin-DWKEo-Wj.js → LatexPlugin-LDSMR-t-.js} +16 -16
  116. package/src/ui/dist/assets/{MarkdownViewerPlugin-DBzoEmhv.js → MarkdownViewerPlugin-B7o80jgm.js} +4 -4
  117. package/src/ui/dist/assets/{MarketplacePlugin-DoHc-8vo.js → MarketplacePlugin-CM6ZOcpC.js} +3 -3
  118. package/src/ui/dist/assets/{NotebookEditor-CKjKH-yS.js → NotebookEditor-Dc61cXmK.js} +3 -3
  119. package/src/ui/dist/assets/{PdfLoader-zFoL0VPo.js → PdfLoader-DWowuQwx.js} +1 -1
  120. package/src/ui/dist/assets/{PdfMarkdownPlugin-DXPaL9Nt.js → PdfMarkdownPlugin-BsJM1q_a.js} +3 -3
  121. package/src/ui/dist/assets/{PdfViewerPlugin-DhK8qCFp.js → PdfViewerPlugin-DB2eEEFQ.js} +10 -10
  122. package/src/ui/dist/assets/{SearchPlugin-CdSi6krf.js → SearchPlugin-CraThSvt.js} +1 -1
  123. package/src/ui/dist/assets/{Stepper-V-WiDQJl.js → Stepper-CgocRTPq.js} +1 -1
  124. package/src/ui/dist/assets/{TextViewerPlugin-hIs1Efiu.js → TextViewerPlugin-B1JGhKtd.js} +4 -4
  125. package/src/ui/dist/assets/{VNCViewer-DG8b0q2X.js → VNCViewer-CclFC7FM.js} +9 -10
  126. package/src/ui/dist/assets/{bibtex-HDac6fVW.js → bibtex-D3IKsMl7.js} +1 -1
  127. package/src/ui/dist/assets/{code-BnBeNxBc.js → code-BP37Xx0p.js} +1 -1
  128. package/src/ui/dist/assets/{file-content-IRQ3jHb8.js → file-content-BAJSu-9r.js} +1 -1
  129. package/src/ui/dist/assets/{file-diff-panel-DZoQ9I6r.js → file-diff-panel-DUGeCTuy.js} +1 -1
  130. package/src/ui/dist/assets/{file-socket-BMCdLc-P.js → file-socket-CXc1Ojf7.js} +1 -1
  131. package/src/ui/dist/assets/{file-utils-CltILB3w.js → file-utils-2J21jt7M.js} +1 -1
  132. package/src/ui/dist/assets/{image-Boe6ffhu.js → image-CMMmgvcn.js} +1 -1
  133. package/src/ui/dist/assets/{index-BlplpvE1.js → index-BaVumsQT.js} +2 -2
  134. package/src/ui/dist/assets/{index-DZqJ-qAM.js → index-CWgMgpow.js} +60 -2154
  135. package/src/ui/dist/assets/{index-DO43pFZP.js → index-DmwmJmbW.js} +6372 -8434
  136. package/src/ui/dist/assets/{index-Bq2bvfkl.css → index-KGt-z-dD.css} +225 -2920
  137. package/src/ui/dist/assets/{index-2Zf65FZt.js → index-s7aHnNQ4.js} +1 -1
  138. package/src/ui/dist/assets/{message-square-mUHn_Ssb.js → message-square-CQRfX0Am.js} +1 -1
  139. package/src/ui/dist/assets/{monaco-fe0arNEU.js → monaco-B4TbdsrF.js} +1 -1
  140. package/src/ui/dist/assets/{popover-D_7i19qU.js → popover-B8Rokodk.js} +1 -1
  141. package/src/ui/dist/assets/{project-sync-DyVGrU7H.js → project-sync-D_i96KH4.js} +2 -8
  142. package/src/ui/dist/assets/{sigma-BzazRyxQ.js → sigma-D12PnzCN.js} +1 -1
  143. package/src/ui/dist/assets/{tooltip-DN_yjHFH.js → tooltip-B6YrI4aJ.js} +1 -1
  144. package/src/ui/dist/assets/trash-Bc8jGp0V.js +32 -0
  145. package/src/ui/dist/assets/{useCliAccess-DV2L2Qxy.js → useCliAccess-mXVCYSZ-.js} +12 -42
  146. package/src/ui/dist/assets/{useFileDiffOverlay-DyTj-p_V.js → useFileDiffOverlay-Bg6b9H9K.js} +1 -1
  147. package/src/ui/dist/assets/{wrap-text-ozYHtUwq.js → wrap-text-Drh5GEnL.js} +1 -1
  148. package/src/ui/dist/assets/{zoom-out-BN9MUyCQ.js → zoom-out-CJj9DZLn.js} +1 -1
  149. package/src/ui/dist/index.html +2 -2
  150. package/assets/fonts/Inter-Variable.ttf +0 -0
  151. package/assets/fonts/NotoSerifSC-Regular-C94HN_ZN.ttf +0 -0
  152. package/assets/fonts/NunitoSans-Variable.ttf +0 -0
  153. package/assets/fonts/Satoshi-Medium-ByP-Zb-9.woff2 +0 -0
  154. package/assets/fonts/SourceSans3-Variable.ttf +0 -0
  155. package/assets/fonts/ds-fonts.css +0 -83
  156. package/src/ui/dist/assets/Inter-Variable-VF2RPR_K.ttf +0 -0
  157. package/src/ui/dist/assets/LabPlugin-bL7rpic8.js +0 -43
  158. package/src/ui/dist/assets/NotoSerifSC-Regular-C94HN_ZN-C94HN_ZN.ttf +0 -0
  159. package/src/ui/dist/assets/NunitoSans-Variable-B_ZymHAd.ttf +0 -0
  160. package/src/ui/dist/assets/Satoshi-Medium-ByP-Zb-9-GkA34YXu.woff2 +0 -0
  161. package/src/ui/dist/assets/SourceSans3-Variable-CD-WOsSK.ttf +0 -0
  162. package/src/ui/dist/assets/info-CcsK_htA.js +0 -18
  163. package/src/ui/dist/assets/user-plus-BusDx-hF.js +0 -79
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import secrets
5
+ from typing import Any
6
+ from urllib.parse import urlparse
7
+
8
+
9
+ DEFAULT_LINGZHU_GATEWAY_PORT = 18789
10
+ DEFAULT_LINGZHU_LOCAL_HOST = "127.0.0.1"
11
+ DEFAULT_LINGZHU_AGENT_ID = "main"
12
+ DEFAULT_LINGZHU_SESSION_NAMESPACE = "lingzhu"
13
+
14
+ _AUTH_AK_SEGMENTS = (8, 4, 4, 4, 12)
15
+ _AUTH_AK_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"
16
+
17
+
18
+ def generate_lingzhu_auth_ak() -> str:
19
+ parts: list[str] = []
20
+ for segment_length in _AUTH_AK_SEGMENTS:
21
+ parts.append("".join(secrets.choice(_AUTH_AK_CHARS) for _ in range(segment_length)))
22
+ return "-".join(parts)
23
+
24
+
25
+ def lingzhu_local_host(config: dict[str, Any] | None) -> str:
26
+ value = str((config or {}).get("local_host") or DEFAULT_LINGZHU_LOCAL_HOST).strip()
27
+ return value or DEFAULT_LINGZHU_LOCAL_HOST
28
+
29
+
30
+ def lingzhu_gateway_port(config: dict[str, Any] | None) -> int:
31
+ raw = (config or {}).get("gateway_port")
32
+ try:
33
+ value = int(raw)
34
+ except (TypeError, ValueError):
35
+ return DEFAULT_LINGZHU_GATEWAY_PORT
36
+ if value < 1 or value > 65535:
37
+ return DEFAULT_LINGZHU_GATEWAY_PORT
38
+ return value
39
+
40
+
41
+ def normalize_public_base_url(value: Any) -> str | None:
42
+ text = str(value or "").strip()
43
+ if not text:
44
+ return None
45
+ parsed = urlparse(text)
46
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
47
+ return None
48
+ return text.rstrip("/")
49
+
50
+
51
+ def lingzhu_local_base_url(config: dict[str, Any] | None) -> str:
52
+ return f"http://{lingzhu_local_host(config)}:{lingzhu_gateway_port(config)}"
53
+
54
+
55
+ def lingzhu_public_base_url(config: dict[str, Any] | None) -> str | None:
56
+ return normalize_public_base_url((config or {}).get("public_base_url"))
57
+
58
+
59
+ def lingzhu_health_url(config: dict[str, Any] | None, *, public: bool = False) -> str | None:
60
+ base = lingzhu_public_base_url(config) if public else lingzhu_local_base_url(config)
61
+ if not base:
62
+ return None
63
+ return f"{base}/metis/agent/api/health"
64
+
65
+
66
+ def lingzhu_sse_url(config: dict[str, Any] | None, *, public: bool = False) -> str | None:
67
+ base = lingzhu_public_base_url(config) if public else lingzhu_local_base_url(config)
68
+ if not base:
69
+ return None
70
+ return f"{base}/metis/agent/api/sse"
71
+
72
+
73
+ def lingzhu_agent_id(config: dict[str, Any] | None) -> str:
74
+ value = str((config or {}).get("agent_id") or DEFAULT_LINGZHU_AGENT_ID).strip()
75
+ return value or DEFAULT_LINGZHU_AGENT_ID
76
+
77
+
78
+ def lingzhu_probe_payload(
79
+ config: dict[str, Any] | None,
80
+ *,
81
+ message_id: str = "ds-lingzhu-probe-001",
82
+ text: str = "你好",
83
+ ) -> dict[str, Any]:
84
+ return {
85
+ "message_id": message_id,
86
+ "agent_id": lingzhu_agent_id(config),
87
+ "message": [
88
+ {
89
+ "role": "user",
90
+ "type": "text",
91
+ "text": text,
92
+ }
93
+ ],
94
+ }
95
+
96
+
97
+ def lingzhu_generated_openclaw_config(config: dict[str, Any] | None) -> dict[str, Any]:
98
+ resolved = dict(config or {})
99
+ return {
100
+ "gateway": {
101
+ "port": lingzhu_gateway_port(resolved),
102
+ "http": {
103
+ "endpoints": {
104
+ "chatCompletions": {
105
+ "enabled": True,
106
+ }
107
+ }
108
+ },
109
+ },
110
+ "plugins": {
111
+ "entries": {
112
+ "lingzhu": {
113
+ "enabled": bool(resolved.get("enabled", False)),
114
+ "config": {
115
+ "authAk": str(resolved.get("auth_ak") or "").strip(),
116
+ "agentId": lingzhu_agent_id(resolved),
117
+ "includeMetadata": bool(resolved.get("include_metadata", True)),
118
+ "requestTimeoutMs": int(resolved.get("request_timeout_ms") or 60000),
119
+ "systemPrompt": str(resolved.get("system_prompt") or ""),
120
+ "defaultNavigationMode": str(resolved.get("default_navigation_mode") or "0"),
121
+ "enableFollowUp": bool(resolved.get("enable_follow_up", True)),
122
+ "followUpMaxCount": int(resolved.get("follow_up_max_count") or 3),
123
+ "maxImageBytes": int(resolved.get("max_image_bytes") or 5 * 1024 * 1024),
124
+ "sessionMode": str(resolved.get("session_mode") or "per_user"),
125
+ "sessionNamespace": str(
126
+ resolved.get("session_namespace") or DEFAULT_LINGZHU_SESSION_NAMESPACE
127
+ ),
128
+ "autoReceiptAck": bool(resolved.get("auto_receipt_ack", True)),
129
+ "visibleProgressHeartbeat": bool(
130
+ resolved.get("visible_progress_heartbeat", True)
131
+ ),
132
+ "visibleProgressHeartbeatSec": int(
133
+ resolved.get("visible_progress_heartbeat_sec") or 10
134
+ ),
135
+ "debugLogging": bool(resolved.get("debug_logging", False)),
136
+ "debugLogPayloads": bool(resolved.get("debug_log_payloads", False)),
137
+ "debugLogDir": str(resolved.get("debug_log_dir") or ""),
138
+ "enableExperimentalNativeActions": bool(
139
+ resolved.get("enable_experimental_native_actions", False)
140
+ ),
141
+ },
142
+ }
143
+ }
144
+ },
145
+ }
146
+
147
+
148
+ def lingzhu_generated_openclaw_config_text(config: dict[str, Any] | None) -> str:
149
+ return json.dumps(lingzhu_generated_openclaw_config(config), indent=2, ensure_ascii=False)
150
+
151
+
152
+ def lingzhu_generated_curl(config: dict[str, Any] | None, *, text: str = "你好") -> str:
153
+ auth_ak = str((config or {}).get("auth_ak") or "").strip()
154
+ payload = lingzhu_probe_payload(config, text=text)
155
+ endpoint_url = lingzhu_sse_url(config) or ""
156
+ return (
157
+ f"curl -X POST '{endpoint_url}' \\\n"
158
+ f" --header 'Authorization: Bearer {auth_ak}' \\\n"
159
+ " --header 'Content-Type: application/json' \\\n"
160
+ f" --data '{json.dumps(payload, ensure_ascii=False)}'"
161
+ )
162
+
163
+
164
+ def lingzhu_supported_commands(*, experimental_enabled: bool) -> list[str]:
165
+ commands = [
166
+ "take_photo",
167
+ "take_navigation",
168
+ "control_calendar",
169
+ "notify_agent_off",
170
+ ]
171
+ if experimental_enabled:
172
+ commands.extend(
173
+ [
174
+ "send_notification",
175
+ "send_toast",
176
+ "speak_tts",
177
+ "start_video_record",
178
+ "stop_video_record",
179
+ "open_custom_view",
180
+ ]
181
+ )
182
+ return commands
@@ -256,6 +256,29 @@ def build_artifact_server(context: McpContext) -> FastMCP:
256
256
  def list_research_branches(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
257
257
  return service.list_research_branches(context.require_quest_root())
258
258
 
259
+ @server.tool(
260
+ name="resolve_runtime_refs",
261
+ description=(
262
+ "Resolve the current canonical research ids and refs. "
263
+ "Use this before supplementary work when you need the active idea, latest main run, active campaign, outline, or reply-thread ids without guessing."
264
+ ),
265
+ )
266
+ def resolve_runtime_refs(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
267
+ return service.resolve_runtime_refs(context.require_quest_root())
268
+
269
+ @server.tool(
270
+ name="get_analysis_campaign",
271
+ description=(
272
+ "Get one analysis campaign manifest with todo items, slice status, and next pending slice. "
273
+ "Pass campaign_id='active' or omit it to recover the active campaign."
274
+ ),
275
+ )
276
+ def get_analysis_campaign(
277
+ campaign_id: str | None = "active",
278
+ comment: str | dict[str, Any] | None = None,
279
+ ) -> dict[str, Any]:
280
+ return service.get_analysis_campaign(context.require_quest_root(), campaign_id=campaign_id)
281
+
259
282
  @server.tool(
260
283
  name="record_main_experiment",
261
284
  description=(
@@ -312,8 +335,8 @@ def build_artifact_server(context: McpContext) -> FastMCP:
312
335
  @server.tool(
313
336
  name="create_analysis_campaign",
314
337
  description=(
315
- "Create a structured analysis campaign from the active idea branch. "
316
- "Each slice receives its own branch/worktree and explicit requirements."
338
+ "Create a structured analysis campaign from the current workspace/result node. "
339
+ "Use this for one or more extra experiments; each slice receives its own child branch/worktree and explicit requirements."
317
340
  ),
318
341
  )
319
342
  def create_analysis_campaign(
@@ -321,6 +344,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
321
344
  campaign_goal: str,
322
345
  slices: list[dict[str, Any]],
323
346
  parent_run_id: str | None = None,
347
+ campaign_origin: dict[str, Any] | None = None,
324
348
  selected_outline_ref: str | None = None,
325
349
  research_questions: list[str] | None = None,
326
350
  experimental_designs: list[str] | None = None,
@@ -333,6 +357,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
333
357
  campaign_goal=campaign_goal,
334
358
  parent_run_id=parent_run_id or context.run_id,
335
359
  slices=slices,
360
+ campaign_origin=campaign_origin,
336
361
  selected_outline_ref=selected_outline_ref,
337
362
  research_questions=research_questions,
338
363
  experimental_designs=experimental_designs,
@@ -371,6 +396,16 @@ def build_artifact_server(context: McpContext) -> FastMCP:
371
396
  selected_reason=selected_reason,
372
397
  )
373
398
 
399
+ @server.tool(
400
+ name="list_paper_outlines",
401
+ description=(
402
+ "List candidate/revised paper outlines and the selected outline reference. "
403
+ "Use this before writing-facing analysis campaigns or when you need a valid outline_id."
404
+ ),
405
+ )
406
+ def list_paper_outlines(comment: str | dict[str, Any] | None = None) -> dict[str, Any]:
407
+ return service.list_paper_outlines(context.require_quest_root())
408
+
374
409
  @server.tool(
375
410
  name="submit_paper_bundle",
376
411
  description=(
@@ -421,6 +456,10 @@ def build_artifact_server(context: McpContext) -> FastMCP:
421
456
  evidence_paths: list[str] | None = None,
422
457
  metric_rows: list[dict[str, Any]] | None = None,
423
458
  deviations: list[str] | None = None,
459
+ claim_impact: str | None = None,
460
+ reviewer_resolution: str | None = None,
461
+ manuscript_update_hint: str | None = None,
462
+ next_recommendation: str | None = None,
424
463
  dataset_scope: str = "full",
425
464
  subset_approval_ref: str | None = None,
426
465
  comment: str | dict[str, Any] | None = None,
@@ -436,6 +475,10 @@ def build_artifact_server(context: McpContext) -> FastMCP:
436
475
  evidence_paths=evidence_paths,
437
476
  metric_rows=metric_rows,
438
477
  deviations=deviations,
478
+ claim_impact=claim_impact,
479
+ reviewer_resolution=reviewer_resolution,
480
+ manuscript_update_hint=manuscript_update_hint,
481
+ next_recommendation=next_recommendation,
439
482
  dataset_scope=dataset_scope,
440
483
  subset_approval_ref=subset_approval_ref,
441
484
  )
@@ -547,6 +590,8 @@ def build_artifact_server(context: McpContext) -> FastMCP:
547
590
  expects_reply: bool | None = None,
548
591
  reply_mode: str | None = None,
549
592
  options: list[dict[str, Any]] | None = None,
593
+ surface_actions: list[dict[str, Any]] | None = None,
594
+ connector_hints: dict[str, Any] | None = None,
550
595
  allow_free_text: bool = True,
551
596
  reply_schema: dict[str, Any] | None = None,
552
597
  reply_to_interaction_id: str | None = None,
@@ -567,6 +612,8 @@ def build_artifact_server(context: McpContext) -> FastMCP:
567
612
  expects_reply=expects_reply,
568
613
  reply_mode=reply_mode,
569
614
  options=options,
615
+ surface_actions=surface_actions,
616
+ connector_hints=connector_hints,
570
617
  allow_free_text=allow_free_text,
571
618
  reply_schema=reply_schema,
572
619
  reply_to_interaction_id=reply_to_interaction_id,
@@ -4,7 +4,7 @@ import json
4
4
  import re
5
5
  from pathlib import Path
6
6
 
7
- from ..connector_runtime import parse_conversation_id
7
+ from ..connector_runtime import normalize_conversation_id, parse_conversation_id
8
8
  from ..config import ConfigManager
9
9
  from ..memory import MemoryService
10
10
  from ..memory.frontmatter import load_markdown_document
@@ -25,6 +25,9 @@ STANDARD_SKILLS = (
25
25
 
26
26
  COMPANION_SKILLS = (
27
27
  "figure-polish",
28
+ "intake-audit",
29
+ "review",
30
+ "rebuttal",
28
31
  )
29
32
 
30
33
  STAGE_MEMORY_PLAN = {
@@ -89,46 +92,57 @@ class PromptBuilder:
89
92
  active_anchor = str(snapshot.get("active_anchor") or skill_id)
90
93
  default_locale = str(runtime_config.get("default_locale") or "zh-CN")
91
94
  system_block = self._prompt_fragment("src/prompts/system.md")
92
- return "\n\n".join(
95
+ connector_contract_block = self._connector_contract_block(quest_id=quest_id, snapshot=snapshot)
96
+ sections = [
97
+ system_block,
98
+ "",
99
+ "## Runtime Context",
100
+ f"ds_home: {self.home.resolve()}",
101
+ f"quest_id: {quest_id}",
102
+ f"quest_root: {quest_root}",
103
+ f"research_head_branch: {snapshot.get('research_head_branch') or 'none'}",
104
+ f"research_head_worktree_root: {snapshot.get('research_head_worktree_root') or 'none'}",
105
+ f"current_workspace_branch: {snapshot.get('current_workspace_branch') or 'none'}",
106
+ f"current_workspace_root: {snapshot.get('current_workspace_root') or 'none'}",
107
+ f"active_idea_id: {snapshot.get('active_idea_id') or 'none'}",
108
+ f"active_analysis_campaign_id: {snapshot.get('active_analysis_campaign_id') or 'none'}",
109
+ f"active_anchor: {active_anchor}",
110
+ f"active_branch: {snapshot.get('branch')}",
111
+ f"requested_skill: {skill_id}",
112
+ f"runner_name: codex",
113
+ f"model: {model}",
114
+ f"conversation_id: quest:{quest_id}",
115
+ f"default_locale: {default_locale}",
116
+ "built_in_mcp_namespaces: memory, artifact, bash_exec",
117
+ "mcp_namespace_note: any shell-like command execution must use bash_exec, including curl/python/bash/node and similar CLI tools; do not use transient shell snippets.",
118
+ "",
119
+ "Canonical stage skills root:",
120
+ str((self.repo_root / "src" / "skills").resolve()),
121
+ "",
122
+ "Standard stage skill paths:",
123
+ self._skill_paths_block(),
124
+ "",
125
+ "Companion skill paths:",
126
+ self._companion_skill_paths_block(),
127
+ "",
128
+ "## Active Communication Surface",
129
+ self._active_communication_surface_block(
130
+ quest_id=quest_id,
131
+ snapshot=snapshot,
132
+ runtime_config=runtime_config,
133
+ connectors_config=connectors_config,
134
+ ),
135
+ ]
136
+ if connector_contract_block:
137
+ sections.extend(
138
+ [
139
+ "",
140
+ "## Connector Contract",
141
+ connector_contract_block,
142
+ ]
143
+ )
144
+ sections.extend(
93
145
  [
94
- system_block,
95
- "",
96
- "## Runtime Context",
97
- f"ds_home: {self.home.resolve()}",
98
- f"quest_id: {quest_id}",
99
- f"quest_root: {quest_root}",
100
- f"research_head_branch: {snapshot.get('research_head_branch') or 'none'}",
101
- f"research_head_worktree_root: {snapshot.get('research_head_worktree_root') or 'none'}",
102
- f"current_workspace_branch: {snapshot.get('current_workspace_branch') or 'none'}",
103
- f"current_workspace_root: {snapshot.get('current_workspace_root') or 'none'}",
104
- f"active_idea_id: {snapshot.get('active_idea_id') or 'none'}",
105
- f"active_analysis_campaign_id: {snapshot.get('active_analysis_campaign_id') or 'none'}",
106
- f"active_anchor: {active_anchor}",
107
- f"active_branch: {snapshot.get('branch')}",
108
- f"requested_skill: {skill_id}",
109
- f"runner_name: codex",
110
- f"model: {model}",
111
- f"conversation_id: quest:{quest_id}",
112
- f"default_locale: {default_locale}",
113
- "built_in_mcp_namespaces: memory, artifact, bash_exec",
114
- "mcp_namespace_note: any shell-like command execution must use bash_exec, including curl/python/bash/node and similar CLI tools; do not use transient shell snippets.",
115
- "",
116
- "Canonical stage skills root:",
117
- str((self.repo_root / "src" / "skills").resolve()),
118
- "",
119
- "Standard stage skill paths:",
120
- self._skill_paths_block(),
121
- "",
122
- "Companion skill paths:",
123
- self._companion_skill_paths_block(),
124
- "",
125
- "## Active Communication Surface",
126
- self._active_communication_surface_block(
127
- quest_id=quest_id,
128
- snapshot=snapshot,
129
- runtime_config=runtime_config,
130
- connectors_config=connectors_config,
131
- ),
132
146
  "",
133
147
  "## Turn Driver",
134
148
  self._turn_driver_block(turn_reason=turn_reason, user_message=user_message),
@@ -183,7 +197,8 @@ class PromptBuilder:
183
197
  "## Current User Message",
184
198
  self._current_user_message_block(turn_reason=turn_reason, user_message=user_message),
185
199
  ]
186
- ).strip() + "\n"
200
+ )
201
+ return "\n\n".join(sections).strip() + "\n"
187
202
 
188
203
  def _turn_driver_block(self, *, turn_reason: str, user_message: str) -> str:
189
204
  normalized_reason = str(turn_reason or "user_message").strip() or "user_message"
@@ -218,30 +233,25 @@ class PromptBuilder:
218
233
  runtime_config: dict,
219
234
  connectors_config: dict,
220
235
  ) -> str:
221
- latest_user = self._latest_user_message(quest_id)
222
- source = str((latest_user or {}).get("source") or "local:default").strip() or "local:default"
223
- parsed = parse_conversation_id(source)
236
+ surface_context = self._surface_context(quest_id=quest_id, snapshot=snapshot)
237
+ source = surface_context["latest_user_source"]
238
+ surface = surface_context["active_surface"]
239
+ connector = surface_context["active_connector"]
240
+ chat_type = surface_context["active_chat_type"]
241
+ chat_id = surface_context["active_chat_id"]
224
242
  qq_config = connectors_config.get("qq") if isinstance(connectors_config.get("qq"), dict) else {}
225
243
 
226
- if parsed is None:
227
- surface = "local"
228
- connector = "local"
229
- chat_type = "local"
230
- chat_id = "default"
231
- else:
232
- surface = "connector"
233
- connector = str(parsed.get("connector") or "connector")
234
- chat_type = str(parsed.get("chat_type") or "direct")
235
- chat_id = str(parsed.get("chat_id") or "unknown")
236
-
237
244
  lines = [
238
245
  f"- latest_user_source: {source}",
239
246
  f"- active_surface: {surface}",
240
247
  f"- active_connector: {connector}",
241
248
  f"- active_chat_type: {chat_type}",
242
249
  f"- active_chat_id: {chat_id}",
250
+ f"- active_connector_origin: {surface_context['active_connector_origin']}",
251
+ f"- bound_external_connector_count: {surface_context['bound_external_connector_count']}",
243
252
  "- surface_rule: treat web, TUI, and connector threads as one continuous quest, but adapt the amount of detail to the active surface.",
244
253
  "- surface_reply_rule: use artifact.interact(...) for durable user-visible continuity; do not dump raw internal tool chatter into connector replies.",
254
+ "- connector_contract_rule: load connector-specific prompt fragments only for the active or bound external connector; do not load unused connector contracts.",
245
255
  ]
246
256
 
247
257
  if connector == "qq":
@@ -254,10 +264,12 @@ class PromptBuilder:
254
264
  f"- qq_auto_send_analysis_summary_png: {bool(qq_config.get('auto_send_analysis_summary_png', True))}",
255
265
  f"- qq_auto_send_slice_png: {bool(qq_config.get('auto_send_slice_png', False))}",
256
266
  f"- qq_auto_send_paper_pdf: {bool(qq_config.get('auto_send_paper_pdf', True))}",
267
+ f"- qq_enable_markdown_send: {bool(qq_config.get('enable_markdown_send', False))}",
257
268
  f"- qq_enable_file_upload_experimental: {bool(qq_config.get('enable_file_upload_experimental', False))}",
258
269
  "- qq_visual_rule: follow the fixed Morandi palette guide defined in the system prompt and active stage skill; do not assume per-install palette config exists.",
259
270
  "- qq_media_rule: auto-send only high-value milestone media such as a main-experiment summary PNG, an aggregated analysis summary PNG, or the final paper PDF when available and configured.",
260
271
  "- qq_media_rule_2: do not auto-send every slice image, every debug plot, or draft paper figures unless the user explicitly asked for them.",
272
+ "- qq_structured_delivery_rule: when you want native QQ markdown or native QQ image/file delivery, request it through artifact.interact(connector_hints=..., attachments=[...]) instead of inventing connector-specific inline tag syntax.",
261
273
  ]
262
274
  )
263
275
  else:
@@ -265,6 +277,61 @@ class PromptBuilder:
265
277
 
266
278
  return "\n".join(lines)
267
279
 
280
+ def _surface_context(self, *, quest_id: str, snapshot: dict) -> dict[str, str | int]:
281
+ latest_user = self._latest_user_message(quest_id)
282
+ latest_user_source = str((latest_user or {}).get("source") or "local:default").strip() or "local:default"
283
+ latest_user_parsed = parse_conversation_id(normalize_conversation_id(latest_user_source))
284
+ bound_sources = snapshot.get("bound_conversations") or []
285
+ bound_external: list[dict[str, str]] = []
286
+ for raw in bound_sources:
287
+ parsed = parse_conversation_id(normalize_conversation_id(raw))
288
+ if parsed is None:
289
+ continue
290
+ if str(parsed.get("connector") or "").strip().lower() == "local":
291
+ continue
292
+ bound_external.append(parsed)
293
+ active = bound_external[0] if bound_external else None
294
+ origin = "bound_external_binding" if active is not None else "latest_user_source"
295
+ if active is None and latest_user_parsed is not None:
296
+ latest_connector = str(latest_user_parsed.get("connector") or "").strip().lower()
297
+ if latest_connector and latest_connector != "local":
298
+ active = latest_user_parsed
299
+ if active is None:
300
+ return {
301
+ "latest_user_source": latest_user_source,
302
+ "active_surface": "local",
303
+ "active_connector": "local",
304
+ "active_chat_type": "local",
305
+ "active_chat_id": "default",
306
+ "active_connector_origin": "none",
307
+ "bound_external_connector_count": len(bound_external),
308
+ }
309
+ return {
310
+ "latest_user_source": latest_user_source,
311
+ "active_surface": "connector",
312
+ "active_connector": str(active.get("connector") or "connector"),
313
+ "active_chat_type": str(active.get("chat_type") or "direct"),
314
+ "active_chat_id": str(active.get("chat_id") or "unknown"),
315
+ "active_connector_origin": origin,
316
+ "bound_external_connector_count": len(bound_external),
317
+ }
318
+
319
+ def _active_external_connector_name(self, *, quest_id: str, snapshot: dict) -> str | None:
320
+ surface_context = self._surface_context(quest_id=quest_id, snapshot=snapshot)
321
+ connector = str(surface_context.get("active_connector") or "").strip().lower()
322
+ if not connector or connector == "local":
323
+ return None
324
+ return connector
325
+
326
+ def _connector_contract_block(self, *, quest_id: str, snapshot: dict) -> str:
327
+ connector = self._active_external_connector_name(quest_id=quest_id, snapshot=snapshot)
328
+ if connector is None:
329
+ return ""
330
+ path = self.repo_root / "src" / "prompts" / "connectors" / f"{connector}.md"
331
+ if not path.exists():
332
+ return ""
333
+ return self._markdown_body(path)
334
+
268
335
  def _active_user_requirements_block(self, quest_root: Path) -> str:
269
336
  path = self.quest_service._active_user_requirements_path(quest_root)
270
337
  if not path.exists():
@@ -563,10 +630,32 @@ class PromptBuilder:
563
630
  return value
564
631
  return "user_gated"
565
632
 
633
+ @staticmethod
634
+ def _launch_mode(snapshot: dict) -> str:
635
+ startup_contract = snapshot.get("startup_contract")
636
+ if isinstance(startup_contract, dict):
637
+ value = str(startup_contract.get("launch_mode") or "").strip().lower()
638
+ if value in {"standard", "custom"}:
639
+ return value
640
+ return "standard"
641
+
642
+ @staticmethod
643
+ def _custom_profile(snapshot: dict) -> str:
644
+ startup_contract = snapshot.get("startup_contract")
645
+ if isinstance(startup_contract, dict):
646
+ value = str(startup_contract.get("custom_profile") or "").strip().lower()
647
+ if value in {"continue_existing_state", "revision_rebuttal", "freeform"}:
648
+ return value
649
+ return "freeform"
650
+
566
651
  def _research_delivery_policy_block(self, snapshot: dict) -> str:
567
652
  need_research_paper = self._need_research_paper(snapshot)
653
+ launch_mode = self._launch_mode(snapshot)
654
+ custom_profile = self._custom_profile(snapshot)
568
655
  lines = [
569
656
  f"- need_research_paper: {need_research_paper}",
657
+ f"- launch_mode: {launch_mode}",
658
+ f"- custom_profile: {custom_profile if launch_mode == 'custom' else 'n/a'}",
570
659
  f"- delivery_mode: {'paper_required' if need_research_paper else 'algorithm_first'}",
571
660
  "- idea_stage_rule: every accepted idea submission should normally create a new branch/worktree and a new user-visible research node.",
572
661
  "- idea_draft_rule: before `artifact.submit_idea(...)`, first finish a concise durable Markdown draft for the chosen route; keep `idea.md` compact and `draft.md` richer.",
@@ -575,12 +664,40 @@ class PromptBuilder:
575
664
  "- post_main_result_rule: after every `artifact.record_main_experiment(...)`, first interpret the measured result and only then choose the next route.",
576
665
  "- foundation_selection_rule: for a genuinely new idea round, default to the current research head but feel free to choose another durable foundation when it is cleaner or stronger; inspect `artifact.list_research_branches(...)` first when the best foundation is not obvious.",
577
666
  ]
667
+ if launch_mode == "custom":
668
+ lines.extend(
669
+ [
670
+ "- custom_launch_rule: do not force the canonical full-research path when the custom startup contract is narrower.",
671
+ "- custom_context_rule: treat `entry_state_summary`, `review_summary`, and `custom_brief` as active runtime context rather than decorative metadata.",
672
+ ]
673
+ )
674
+ if custom_profile == "continue_existing_state":
675
+ lines.extend(
676
+ [
677
+ "- existing_state_entry_rule: if reusable baselines, runs, drafts, or review assets already exist, open `intake-audit` before restarting baseline discovery or new experiments.",
678
+ "- reuse_first_rule: trust-rank and reconcile existing assets before deciding to rerun anything costly.",
679
+ ]
680
+ )
681
+ elif custom_profile == "revision_rebuttal":
682
+ lines.extend(
683
+ [
684
+ "- rebuttal_entry_rule: treat reviewer comments and the current paper state as the active contract; open `rebuttal` before ordinary writing.",
685
+ "- rebuttal_routing_rule: route supplementary reviewer-facing evidence through `analysis-campaign` and manuscript deltas through `write`, but let `rebuttal` orchestrate that mapping.",
686
+ ]
687
+ )
688
+ else:
689
+ lines.extend(
690
+ [
691
+ "- freeform_entry_rule: prefer the custom brief over the default stage order and open only the skills actually needed.",
692
+ ]
693
+ )
578
694
  if need_research_paper:
579
695
  lines.extend(
580
696
  [
581
697
  "- delivery_goal: the quest should normally continue until at least one paper-like deliverable exists.",
582
698
  "- main_result_rule: a strong main experiment is evidence, not the endpoint; usually continue into the necessary analysis, writing, or further strengthening work.",
583
699
  "- writing_rule: when the evidence becomes strong enough, analysis and paper writing remain in scope by default.",
700
+ "- review_gate_rule: before declaring a substantial paper/draft task done, open `review` for an independent skeptical audit; if that audit finds serious gaps, route to `analysis-campaign`, `baseline`, `scout`, or `write` instead of stopping.",
584
701
  "- stop_rule: do not stop with only an improved algorithm or isolated run logs unless the user explicitly narrows scope.",
585
702
  ]
586
703
  )
@@ -602,21 +719,27 @@ class PromptBuilder:
602
719
  bound_conversations = snapshot.get("bound_conversations") or []
603
720
  need_research_paper = self._need_research_paper(snapshot)
604
721
  decision_policy = self._decision_policy(snapshot)
722
+ launch_mode = self._launch_mode(snapshot)
723
+ custom_profile = self._custom_profile(snapshot)
605
724
  lines = [
606
725
  f"- configured_default_locale: {default_locale}",
607
726
  f"- current_turn_language_bias: {'zh' if chinese_turn else 'en'}",
608
727
  f"- bound_conversation_count: {len(bound_conversations)}",
609
728
  f"- decision_policy: {decision_policy}",
729
+ f"- launch_mode: {launch_mode}",
730
+ f"- custom_profile: {custom_profile if launch_mode == 'custom' else 'n/a'}",
610
731
  "- collaboration_mode: long-horizon, continuity-first, artifact-aware",
611
- "- response_pattern: acknowledge current state -> state the next action -> mention the artifact/file/checkpoint that will change",
732
+ "- response_pattern: say what changed -> say what it means -> say what happens next",
612
733
  "- interaction_protocol: first message may be plain conversation; after that, treat artifact.interact threads and mailbox polls as the main continuity spine across TUI, web, and connectors",
613
734
  "- mailbox_protocol: artifact.interact(include_recent_inbound_messages=True) is the queued human-message mailbox; when it returns user text, treat that input as higher priority than background subtasks until it has been acknowledged",
614
735
  "- acknowledgment_protocol: after artifact.interact returns any human message, immediately call artifact.interact(...) again to confirm receipt; if answerable, answer directly, otherwise state the short plan, nearest checkpoint, and that the current background subtask is paused",
615
- "- progress_protocol: emit artifact.interact(kind='progress', reply_mode='threaded', ...) at each real checkpoint and usually every 3-8 tool calls during active work; also poll before another multi-step batch or long bash_exec launch; keep updates high-signal and never filler",
736
+ "- progress_protocol: emit artifact.interact(kind='progress', reply_mode='threaded', ...) only at real human-meaningful checkpoints, after the first meaningful signal from long-running work, and then only occasional keepalives during truly long work, usually about every 20 to 30 minutes",
616
737
  "- long_run_reporting_protocol: for long-running bash_exec monitoring loops, report after each completed sleep/await cycle with real evidence plus the next planned check time and estimated next reply time",
617
738
  "- blocking_protocol: use reply_mode='blocking' only for true unresolved user decisions; ordinary progress updates should stay threaded and non-blocking",
739
+ f"- standby_prefix_rule: when you intentionally leave one blocking standby interaction after task completion, prefix it with {'[等待决策]' if chinese_turn else '[Waiting for decision]'} and wait for a new user reply before continuing",
618
740
  "- stop_notice_protocol: if work must pause or stop, send a user-visible notice that explains why, confirms preserved context, and states that any new message or `/resume` will continue from the same quest",
619
- "- respect_protocol: write user-facing updates as respectful, human, supervisor-style reports; templates are references only and must be adapted to context (do not copy/paste the same template repeatedly)",
741
+ "- respect_protocol: write user-facing updates as natural, respectful, easy-to-follow chat; do not sound like a formal status report or internal tool log",
742
+ "- omission_protocol: for ordinary user-facing updates, omit file paths, artifact ids, branch/worktree ids, session ids, raw commands, raw logs, and internal tool names unless the user asked for them or needs them to act",
620
743
  "- non_research_mode_protocol: if the user message looks like a non-research request, ask for a second confirmation before engaging stage skills or research workflow; after completion, leave one blocking standby interaction instead of repeatedly pinging",
621
744
  "- workspace_discipline: read and modify code inside current_workspace_root; treat quest_root as the canonical repo identity and durable runtime root",
622
745
  "- binary_safety: do not open or rewrite large binary assets unless truly necessary; prefer summaries, metadata, and targeted inspection first",
@@ -21,6 +21,7 @@ QUEST_DIRECTORIES = (
21
21
  "experiments/main",
22
22
  "handoffs",
23
23
  "literature",
24
+ "userfiles",
24
25
  "tmp",
25
26
  "memory/decisions",
26
27
  "memory/episodes",