@researai/deepscientist 1.5.15 → 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.
- package/README.md +336 -98
- package/bin/ds.js +691 -91
- package/docs/en/00_QUICK_START.md +36 -15
- package/docs/en/01_SETTINGS_REFERENCE.md +33 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
- package/docs/en/05_TUI_GUIDE.md +6 -0
- package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
- package/docs/en/09_DOCTOR.md +11 -5
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
- package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
- package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
- package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
- package/docs/en/README.md +18 -0
- package/docs/zh/00_QUICK_START.md +36 -15
- package/docs/zh/01_SETTINGS_REFERENCE.md +33 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
- package/docs/zh/05_TUI_GUIDE.md +6 -0
- package/docs/zh/09_DOCTOR.md +11 -5
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
- package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
- package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
- package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
- package/docs/zh/README.md +18 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/acp/envelope.py +6 -0
- package/src/deepscientist/artifact/service.py +647 -22
- package/src/deepscientist/bash_exec/service.py +234 -9
- package/src/deepscientist/cli.py +115 -19
- package/src/deepscientist/codex_cli_compat.py +232 -0
- package/src/deepscientist/config/models.py +2 -1
- package/src/deepscientist/config/service.py +31 -9
- package/src/deepscientist/daemon/api/handlers.py +125 -6
- package/src/deepscientist/daemon/api/router.py +4 -0
- package/src/deepscientist/daemon/app.py +715 -98
- package/src/deepscientist/gitops/__init__.py +10 -1
- package/src/deepscientist/gitops/diff.py +129 -0
- package/src/deepscientist/gitops/service.py +4 -1
- package/src/deepscientist/mcp/server.py +39 -0
- package/src/deepscientist/prompts/builder.py +255 -32
- package/src/deepscientist/quest/layout.py +15 -2
- package/src/deepscientist/quest/service.py +295 -43
- package/src/deepscientist/quest/stage_views.py +6 -1
- package/src/deepscientist/runners/codex.py +86 -31
- package/src/deepscientist/skills/__init__.py +2 -2
- package/src/deepscientist/skills/installer.py +196 -5
- package/src/deepscientist/skills/registry.py +66 -0
- package/src/prompts/connectors/qq.md +18 -8
- package/src/prompts/connectors/weixin.md +16 -6
- package/src/prompts/contracts/shared_interaction.md +12 -1
- package/src/prompts/system.md +10 -5
- package/src/prompts/system_copilot.md +43 -0
- package/src/skills/analysis-campaign/SKILL.md +1 -0
- package/src/skills/baseline/SKILL.md +8 -0
- package/src/skills/decision/SKILL.md +8 -0
- package/src/skills/experiment/SKILL.md +8 -0
- package/src/skills/figure-polish/SKILL.md +1 -0
- package/src/skills/finalize/SKILL.md +1 -0
- package/src/skills/idea/SKILL.md +1 -0
- package/src/skills/intake-audit/SKILL.md +8 -0
- package/src/skills/mentor/SKILL.md +217 -0
- package/src/skills/mentor/references/correction-rules.md +210 -0
- package/src/skills/mentor/references/knowledge-profile.md +91 -0
- package/src/skills/mentor/references/persona-profile.md +138 -0
- package/src/skills/mentor/references/taste-profile.md +128 -0
- package/src/skills/mentor/references/thought-style-profile.md +138 -0
- package/src/skills/mentor/references/work-profile.md +289 -0
- package/src/skills/mentor/references/workflow-profile.md +240 -0
- package/src/skills/optimize/SKILL.md +1 -0
- package/src/skills/rebuttal/SKILL.md +1 -0
- package/src/skills/review/SKILL.md +1 -0
- package/src/skills/scout/SKILL.md +8 -0
- package/src/skills/write/SKILL.md +1 -0
- package/src/tui/dist/app/AppContainer.js +19 -11
- package/src/tui/dist/index.js +4 -1
- package/src/tui/dist/lib/api.js +33 -3
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
- package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
- package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
- package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
- package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
- package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
- package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
- package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
- package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
- package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
- package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
- package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
- package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
- package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
- package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
- package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
- package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
- package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
- package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
- package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
- package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
- package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
- package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
- package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
- package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
- package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
- package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
- package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
- package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
- package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
- package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
- package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
- package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
- package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
- package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
- package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
- package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
- package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
- package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
- package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
- package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
- package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
- package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
- package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
- package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
- package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
- package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
- package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
- package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
- package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
- package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
- package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
- package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
- package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
- package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
- package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
- package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
- package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
- package/src/ui/dist/index.html +5 -2
- package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
- package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
- package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
- package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
- package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
- package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
- package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
- package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
- package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
- package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
- package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
- package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
- package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
- package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
- package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
- package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
- package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
- package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
- package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
- package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
- package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
- package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
- package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
- package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
- package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
- package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
- package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
- package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
- package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
- package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
- package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
- package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
- package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
- package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
- package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
- package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
- package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
- package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
- package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
- package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
- package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
- package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
- package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
- package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
- package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
- package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
- package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
- package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
- package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
- package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
- package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
- package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import signal
|
|
6
|
-
import shutil
|
|
7
6
|
import subprocess
|
|
8
7
|
import sys
|
|
9
8
|
import threading
|
|
@@ -11,12 +10,16 @@ from pathlib import Path
|
|
|
11
10
|
from typing import Any
|
|
12
11
|
|
|
13
12
|
from ..artifact import ArtifactService
|
|
14
|
-
from ..codex_cli_compat import
|
|
13
|
+
from ..codex_cli_compat import (
|
|
14
|
+
materialize_codex_runtime_home,
|
|
15
|
+
normalize_codex_reasoning_effort,
|
|
16
|
+
provider_profile_metadata_from_home,
|
|
17
|
+
)
|
|
15
18
|
from ..config import ConfigManager
|
|
16
19
|
from ..gitops import export_git_graph
|
|
17
20
|
from ..prompts import PromptBuilder
|
|
18
21
|
from ..runtime_logs import JsonlLogger
|
|
19
|
-
from ..shared import append_jsonl, ensure_dir, generate_id,
|
|
22
|
+
from ..shared import append_jsonl, ensure_dir, generate_id, read_yaml, resolve_runner_binary, utc_now, write_json, write_text
|
|
20
23
|
from ..web_search import extract_web_search_payload
|
|
21
24
|
from .base import RunRequest, RunResult
|
|
22
25
|
|
|
@@ -69,6 +72,11 @@ _BUILTIN_MCP_TOOL_APPROVALS: dict[str, tuple[str, ...]] = {
|
|
|
69
72
|
),
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
_PROVIDER_ENV_CONFLICT_KEYS = (
|
|
76
|
+
"OPENAI_API_KEY",
|
|
77
|
+
"OPENAI_BASE_URL",
|
|
78
|
+
)
|
|
79
|
+
|
|
72
80
|
|
|
73
81
|
def _compact_text(value: object, *, limit: int = 1200) -> str:
|
|
74
82
|
if value is None:
|
|
@@ -195,7 +203,9 @@ def _iter_event_texts(event: dict[str, Any]) -> list[str]:
|
|
|
195
203
|
if isinstance(value, str) and value.strip():
|
|
196
204
|
texts.append(value)
|
|
197
205
|
delta = event.get("delta")
|
|
198
|
-
if isinstance(delta,
|
|
206
|
+
if isinstance(delta, str) and delta.strip():
|
|
207
|
+
texts.append(delta)
|
|
208
|
+
elif isinstance(delta, dict):
|
|
199
209
|
for key in ("text", "content"):
|
|
200
210
|
value = delta.get(key)
|
|
201
211
|
if isinstance(value, str) and value.strip():
|
|
@@ -222,6 +232,36 @@ def _web_search_text_payload(item: dict[str, Any]) -> str:
|
|
|
222
232
|
return _compact_text(payload, limit=2400)
|
|
223
233
|
|
|
224
234
|
|
|
235
|
+
def _message_stream_id(event: dict[str, Any], item: dict[str, Any], *, run_id: str, kind: str) -> str:
|
|
236
|
+
for value in (
|
|
237
|
+
event.get("stream_id"),
|
|
238
|
+
item.get("stream_id"),
|
|
239
|
+
event.get("message_id"),
|
|
240
|
+
item.get("message_id"),
|
|
241
|
+
event.get("item_id"),
|
|
242
|
+
item.get("id"),
|
|
243
|
+
event.get("output_item_id"),
|
|
244
|
+
event.get("response_id"),
|
|
245
|
+
):
|
|
246
|
+
if value:
|
|
247
|
+
return str(value)
|
|
248
|
+
normalized_kind = str(kind or "message").strip().lower() or "message"
|
|
249
|
+
return f"{run_id}:{normalized_kind}"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _message_id(event: dict[str, Any], item: dict[str, Any], *, stream_id: str) -> str:
|
|
253
|
+
for value in (
|
|
254
|
+
event.get("message_id"),
|
|
255
|
+
item.get("message_id"),
|
|
256
|
+
event.get("item_id"),
|
|
257
|
+
item.get("id"),
|
|
258
|
+
event.get("output_item_id"),
|
|
259
|
+
):
|
|
260
|
+
if value:
|
|
261
|
+
return str(value)
|
|
262
|
+
return stream_id
|
|
263
|
+
|
|
264
|
+
|
|
225
265
|
def _message_events(
|
|
226
266
|
event: dict[str, Any],
|
|
227
267
|
*,
|
|
@@ -238,6 +278,8 @@ def _message_events(
|
|
|
238
278
|
|
|
239
279
|
if item_type == "agent_message":
|
|
240
280
|
texts = _dedupe_texts(_iter_event_texts(event))
|
|
281
|
+
stream_id = _message_stream_id(event, item, run_id=run_id, kind="assistant")
|
|
282
|
+
message_id = _message_id(event, item, stream_id=stream_id)
|
|
241
283
|
for text in texts:
|
|
242
284
|
quest_events.append(
|
|
243
285
|
{
|
|
@@ -248,6 +290,8 @@ def _message_events(
|
|
|
248
290
|
"source": "codex",
|
|
249
291
|
"skill_id": skill_id,
|
|
250
292
|
"text": text,
|
|
293
|
+
"stream_id": stream_id,
|
|
294
|
+
"message_id": message_id,
|
|
251
295
|
"created_at": created_at,
|
|
252
296
|
}
|
|
253
297
|
)
|
|
@@ -255,6 +299,8 @@ def _message_events(
|
|
|
255
299
|
|
|
256
300
|
if item_type in {"reasoning", "reasoning_summary"} or "reasoning" in event_type:
|
|
257
301
|
texts = _dedupe_texts(_iter_event_texts(event))
|
|
302
|
+
stream_id = _message_stream_id(event, item, run_id=run_id, kind=item_type or "reasoning")
|
|
303
|
+
message_id = _message_id(event, item, stream_id=stream_id)
|
|
258
304
|
for text in texts:
|
|
259
305
|
quest_events.append(
|
|
260
306
|
{
|
|
@@ -265,6 +311,8 @@ def _message_events(
|
|
|
265
311
|
"source": "codex",
|
|
266
312
|
"skill_id": skill_id,
|
|
267
313
|
"text": text,
|
|
314
|
+
"stream_id": stream_id,
|
|
315
|
+
"message_id": message_id,
|
|
268
316
|
"kind": item_type or "reasoning",
|
|
269
317
|
"created_at": created_at,
|
|
270
318
|
}
|
|
@@ -278,6 +326,8 @@ def _message_events(
|
|
|
278
326
|
return [], []
|
|
279
327
|
|
|
280
328
|
texts = _dedupe_texts(_iter_event_texts(event))
|
|
329
|
+
stream_id = _message_stream_id(event, item, run_id=run_id, kind="assistant")
|
|
330
|
+
message_id = _message_id(event, item, stream_id=stream_id)
|
|
281
331
|
for text in texts:
|
|
282
332
|
quest_events.append(
|
|
283
333
|
{
|
|
@@ -288,6 +338,8 @@ def _message_events(
|
|
|
288
338
|
"source": "codex",
|
|
289
339
|
"skill_id": skill_id,
|
|
290
340
|
"text": text,
|
|
341
|
+
"stream_id": stream_id,
|
|
342
|
+
"message_id": message_id,
|
|
291
343
|
"created_at": created_at,
|
|
292
344
|
}
|
|
293
345
|
)
|
|
@@ -729,6 +781,7 @@ class CodexRunner:
|
|
|
729
781
|
continue
|
|
730
782
|
env[env_key] = env_value
|
|
731
783
|
env["CODEX_HOME"] = str(codex_home)
|
|
784
|
+
env = self._sanitize_provider_env(env, runner_config=runner_config)
|
|
732
785
|
env["DEEPSCIENTIST_HOME"] = str(self.home)
|
|
733
786
|
env["DS_HOME"] = str(self.home)
|
|
734
787
|
env["DS_QUEST_ID"] = request.quest_id
|
|
@@ -975,6 +1028,8 @@ class CodexRunner:
|
|
|
975
1028
|
resolved_runner_config = runner_config if isinstance(runner_config, dict) else self._load_runner_config()
|
|
976
1029
|
profile = str(resolved_runner_config.get("profile") or "").strip()
|
|
977
1030
|
normalized_model = str(request.model or "").strip()
|
|
1031
|
+
if profile and normalized_model.lower() not in {"", "inherit", "default", "codex-default"}:
|
|
1032
|
+
normalized_model = "inherit"
|
|
978
1033
|
command = [
|
|
979
1034
|
resolved_binary or self.binary,
|
|
980
1035
|
"--search",
|
|
@@ -1019,36 +1074,16 @@ class CodexRunner:
|
|
|
1019
1074
|
run_id: str,
|
|
1020
1075
|
runner_config: dict[str, Any] | None = None,
|
|
1021
1076
|
) -> Path:
|
|
1022
|
-
target = ensure_dir(workspace_root / ".codex")
|
|
1077
|
+
target = ensure_dir(workspace_root / ".ds" / "codex-home")
|
|
1023
1078
|
resolved_runner_config = runner_config if isinstance(runner_config, dict) else self._load_runner_config()
|
|
1024
1079
|
configured_home = str(resolved_runner_config.get("config_dir") or os.environ.get("CODEX_HOME") or str(Path.home() / ".codex"))
|
|
1025
1080
|
profile = str(resolved_runner_config.get("profile") or "").strip()
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
if source_path.resolve() == target_path.resolve():
|
|
1033
|
-
continue
|
|
1034
|
-
shutil.copy2(source_path, target_path)
|
|
1035
|
-
config_path = target / "config.toml"
|
|
1036
|
-
if profile and config_path.exists():
|
|
1037
|
-
adapted_text, _ = adapt_profile_only_provider_config(read_text(config_path), profile=profile)
|
|
1038
|
-
write_text(config_path, adapted_text)
|
|
1039
|
-
ensure_dir(target / "skills")
|
|
1040
|
-
quest_skills_root = quest_root / ".codex" / "skills"
|
|
1041
|
-
if quest_skills_root.exists():
|
|
1042
|
-
for source_path in sorted(quest_skills_root.rglob("*")):
|
|
1043
|
-
relative = source_path.relative_to(quest_skills_root)
|
|
1044
|
-
target_path = target / "skills" / relative
|
|
1045
|
-
if source_path.is_dir():
|
|
1046
|
-
ensure_dir(target_path)
|
|
1047
|
-
continue
|
|
1048
|
-
if source_path.resolve() == target_path.resolve():
|
|
1049
|
-
continue
|
|
1050
|
-
ensure_dir(target_path.parent)
|
|
1051
|
-
shutil.copy2(source_path, target_path)
|
|
1081
|
+
materialize_codex_runtime_home(
|
|
1082
|
+
source_home=configured_home,
|
|
1083
|
+
target_home=target,
|
|
1084
|
+
profile=profile,
|
|
1085
|
+
quest_codex_root=quest_root / ".codex",
|
|
1086
|
+
)
|
|
1052
1087
|
self._inject_built_in_mcp(
|
|
1053
1088
|
target,
|
|
1054
1089
|
quest_root=quest_root,
|
|
@@ -1157,3 +1192,23 @@ class CodexRunner:
|
|
|
1157
1192
|
except (TypeError, ValueError):
|
|
1158
1193
|
return None
|
|
1159
1194
|
return timeout if timeout > 0 else None
|
|
1195
|
+
|
|
1196
|
+
@staticmethod
|
|
1197
|
+
def _sanitize_provider_env(
|
|
1198
|
+
env: dict[str, str],
|
|
1199
|
+
*,
|
|
1200
|
+
runner_config: dict[str, Any] | None = None,
|
|
1201
|
+
) -> dict[str, str]:
|
|
1202
|
+
resolved_runner_config = runner_config if isinstance(runner_config, dict) else {}
|
|
1203
|
+
profile = str(resolved_runner_config.get("profile") or "").strip()
|
|
1204
|
+
config_home = str(resolved_runner_config.get("config_dir") or env.get("CODEX_HOME") or "").strip()
|
|
1205
|
+
if not profile or not config_home:
|
|
1206
|
+
return env
|
|
1207
|
+
metadata = provider_profile_metadata_from_home(config_home, profile=profile)
|
|
1208
|
+
requires_openai_auth = metadata.get("requires_openai_auth")
|
|
1209
|
+
if requires_openai_auth is not False:
|
|
1210
|
+
return env
|
|
1211
|
+
sanitized = dict(env)
|
|
1212
|
+
for key in _PROVIDER_ENV_CONFLICT_KEYS:
|
|
1213
|
+
sanitized.pop(key, None)
|
|
1214
|
+
return sanitized
|
|
@@ -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
|
-
|
|
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
|
-
|
|
90
|
+
Reference shape:
|
|
81
91
|
|
|
82
92
|
```text
|
|
83
|
-
|
|
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="
|
|
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="##
|
|
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="
|
|
157
|
+
message="都整理好啦 📄 论文初稿已经出炉,我把 PDF 一并发给您,您可以直接查看当前版本。",
|
|
148
158
|
reply_mode="threaded",
|
|
149
159
|
attachments=[
|
|
150
160
|
{
|