@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.
- package/README.md +336 -90
- package/assets/branding/logo-raster.png +0 -0
- package/bin/ds.js +816 -131
- package/docs/en/00_QUICK_START.md +36 -15
- package/docs/en/01_SETTINGS_REFERENCE.md +53 -4
- package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -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/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
- package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/en/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 +24 -0
- package/docs/zh/00_QUICK_START.md +36 -15
- package/docs/zh/01_SETTINGS_REFERENCE.md +53 -4
- package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/zh/05_TUI_GUIDE.md +6 -0
- package/docs/zh/09_DOCTOR.md +11 -5
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
- package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/zh/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 +24 -0
- package/install.sh +2 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/acp/envelope.py +6 -0
- package/src/deepscientist/artifact/charts.py +567 -0
- package/src/deepscientist/artifact/guidance.py +50 -10
- package/src/deepscientist/artifact/metrics.py +228 -5
- package/src/deepscientist/artifact/schemas.py +3 -0
- package/src/deepscientist/artifact/service.py +4276 -308
- package/src/deepscientist/bash_exec/models.py +23 -0
- package/src/deepscientist/bash_exec/monitor.py +147 -67
- package/src/deepscientist/bash_exec/runtime.py +218 -156
- package/src/deepscientist/bash_exec/service.py +309 -69
- package/src/deepscientist/bash_exec/shells.py +87 -0
- package/src/deepscientist/bridges/connectors.py +51 -2
- package/src/deepscientist/cli.py +115 -19
- package/src/deepscientist/codex_cli_compat.py +232 -0
- package/src/deepscientist/config/models.py +8 -4
- package/src/deepscientist/config/service.py +38 -11
- package/src/deepscientist/connector/weixin_support.py +122 -1
- package/src/deepscientist/daemon/api/handlers.py +199 -9
- package/src/deepscientist/daemon/api/router.py +5 -0
- package/src/deepscientist/daemon/app.py +1458 -289
- package/src/deepscientist/doctor.py +51 -0
- package/src/deepscientist/file_lock.py +48 -0
- package/src/deepscientist/gitops/__init__.py +10 -1
- package/src/deepscientist/gitops/diff.py +296 -1
- package/src/deepscientist/gitops/service.py +4 -1
- package/src/deepscientist/mcp/server.py +212 -5
- package/src/deepscientist/process_control.py +161 -0
- package/src/deepscientist/prompts/builder.py +501 -453
- package/src/deepscientist/quest/layout.py +15 -2
- package/src/deepscientist/quest/service.py +2539 -195
- package/src/deepscientist/quest/stage_views.py +177 -1
- package/src/deepscientist/runners/base.py +2 -0
- package/src/deepscientist/runners/codex.py +169 -31
- package/src/deepscientist/runners/runtime_overrides.py +17 -1
- 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 +24 -4
- package/src/prompts/system.md +921 -72
- package/src/prompts/system_copilot.md +43 -0
- package/src/skills/analysis-campaign/SKILL.md +32 -2
- package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
- package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
- package/src/skills/baseline/SKILL.md +10 -0
- package/src/skills/decision/SKILL.md +27 -2
- package/src/skills/experiment/SKILL.md +16 -2
- package/src/skills/figure-polish/SKILL.md +1 -0
- package/src/skills/finalize/SKILL.md +19 -0
- package/src/skills/idea/SKILL.md +79 -0
- package/src/skills/idea/references/idea-generation-playbook.md +100 -0
- package/src/skills/idea/references/outline-seeding-example.md +60 -0
- package/src/skills/intake-audit/SKILL.md +9 -1
- 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 +1645 -0
- package/src/skills/rebuttal/SKILL.md +3 -1
- package/src/skills/review/SKILL.md +3 -1
- package/src/skills/scout/SKILL.md +8 -0
- package/src/skills/write/SKILL.md +81 -12
- package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
- package/src/tui/dist/app/AppContainer.js +22 -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-DaF9Nge_.js +0 -26597
- package/src/ui/dist/assets/AnalysisPlugin-BSVx6dXE.js +0 -123
- package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
- package/src/ui/dist/assets/CodeEditorPlugin-DU9G0Tox.js +0 -427
- package/src/ui/dist/assets/CodeViewerPlugin-DoX_fI9l.js +0 -905
- package/src/ui/dist/assets/DocViewerPlugin-C4FWIXuU.js +0 -278
- package/src/ui/dist/assets/GitDiffViewerPlugin-BgfFMgtf.js +0 -2661
- package/src/ui/dist/assets/ImageViewerPlugin-tcPkfY_x.js +0 -500
- package/src/ui/dist/assets/LabCopilotPanel-_dKV60Bf.js +0 -4104
- package/src/ui/dist/assets/LabPlugin-Bje0ayoC.js +0 -2677
- package/src/ui/dist/assets/LatexPlugin-CVsBzAln.js +0 -1792
- package/src/ui/dist/assets/MarkdownViewerPlugin-xjmrqv_8.js +0 -308
- package/src/ui/dist/assets/MarketplacePlugin-mMM2A8wP.js +0 -413
- package/src/ui/dist/assets/NotebookEditor-3kVDSOBo.js +0 -4214
- package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
- package/src/ui/dist/assets/NotebookEditor-SoJ8X-MO.js +0 -84873
- package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
- package/src/ui/dist/assets/PdfLoader-DElVuHl9.js +0 -25468
- package/src/ui/dist/assets/PdfMarkdownPlugin-Bq88XT4G.js +0 -409
- package/src/ui/dist/assets/PdfViewerPlugin-CsCXMo9S.js +0 -3095
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
- package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
- package/src/ui/dist/assets/SearchPlugin-oUPvy19k.js +0 -741
- package/src/ui/dist/assets/TextViewerPlugin-CRkT9yNy.js +0 -472
- package/src/ui/dist/assets/VNCViewer-BgbuvWhR.js +0 -18821
- package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
- package/src/ui/dist/assets/bot-v_RASACv.js +0 -21
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
- package/src/ui/dist/assets/code-5hC9d0VH.js +0 -17
- package/src/ui/dist/assets/file-content-D1PxfOrp.js +0 -377
- package/src/ui/dist/assets/file-diff-panel-DG1oT_Hj.js +0 -92
- package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
- package/src/ui/dist/assets/file-socket-BmdFYQlk.js +0 -58
- package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
- package/src/ui/dist/assets/image-Dqe2X2tW.js +0 -18
- package/src/ui/dist/assets/index-BQG-1s2o.css +0 -12553
- package/src/ui/dist/assets/index-DVsMKK_y.js +0 -25
- package/src/ui/dist/assets/index-Duvz8Ip0.js +0 -159
- package/src/ui/dist/assets/index-Nt9hS4ck.js +0 -244829
- package/src/ui/dist/assets/index-RDlNXXx1.js +0 -120
- package/src/ui/dist/assets/monaco-DIXge1CP.js +0 -623
- package/src/ui/dist/assets/pdf-effect-queue-BBTTQaO-.js +0 -47
- package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
- package/src/ui/dist/assets/popover-BWlolyxo.js +0 -476
- package/src/ui/dist/assets/project-sync-BM5PkFH4.js +0 -297
- package/src/ui/dist/assets/select-D4dAtrA8.js +0 -1690
- package/src/ui/dist/assets/sigma-CKbE5jJT.js +0 -22
- package/src/ui/dist/assets/square-check-big-CZNGMgiB.js +0 -17
- package/src/ui/dist/assets/trash-DaB37xAz.js +0 -32
- package/src/ui/dist/assets/useCliAccess-C2OmAcWe.js +0 -957
- package/src/ui/dist/assets/useFileDiffOverlay-Dowd1Ij4.js +0 -53
- package/src/ui/dist/assets/wrap-text-BGjAhAUq.js +0 -35
- package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
- package/src/ui/dist/assets/zoom-out-dMZQMXzc.js +0 -34
|
@@ -6,29 +6,21 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
from ..connector_runtime import normalize_conversation_id, parse_conversation_id
|
|
8
8
|
from ..config import ConfigManager
|
|
9
|
+
from ..home import repo_root
|
|
9
10
|
from ..memory import MemoryService
|
|
10
11
|
from ..memory.frontmatter import load_markdown_document
|
|
11
12
|
from ..quest import QuestService
|
|
12
13
|
from ..registries import BaselineRegistry
|
|
13
14
|
from ..shared import read_json, read_text, read_yaml
|
|
15
|
+
from ..skills import SkillInstaller, companion_skill_ids, stage_skill_ids
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"finalize",
|
|
23
|
-
"decision",
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
COMPANION_SKILLS = (
|
|
27
|
-
"figure-polish",
|
|
28
|
-
"intake-audit",
|
|
29
|
-
"review",
|
|
30
|
-
"rebuttal",
|
|
31
|
-
)
|
|
17
|
+
# Backward-compatible snapshots for modules or tests that still import these names directly.
|
|
18
|
+
# Runtime routing should call `current_standard_skills(...)` / `current_companion_skills(...)`.
|
|
19
|
+
STANDARD_SKILLS = stage_skill_ids(repo_root())
|
|
20
|
+
|
|
21
|
+
_AUTO_CONTINUE_MONITOR_INTERVAL_SECONDS = 240
|
|
22
|
+
|
|
23
|
+
COMPANION_SKILLS = companion_skill_ids(repo_root())
|
|
32
24
|
|
|
33
25
|
STAGE_MEMORY_PLAN = {
|
|
34
26
|
"scout": {
|
|
@@ -43,6 +35,10 @@ STAGE_MEMORY_PLAN = {
|
|
|
43
35
|
"quest": ("papers", "ideas", "decisions", "knowledge"),
|
|
44
36
|
"global": ("papers", "knowledge", "templates"),
|
|
45
37
|
},
|
|
38
|
+
"optimize": {
|
|
39
|
+
"quest": ("episodes", "decisions", "ideas", "knowledge"),
|
|
40
|
+
"global": ("knowledge", "templates"),
|
|
41
|
+
},
|
|
46
42
|
"experiment": {
|
|
47
43
|
"quest": ("ideas", "decisions", "episodes", "knowledge"),
|
|
48
44
|
"global": ("knowledge", "templates"),
|
|
@@ -66,14 +62,56 @@ STAGE_MEMORY_PLAN = {
|
|
|
66
62
|
}
|
|
67
63
|
|
|
68
64
|
|
|
65
|
+
def current_standard_skills(repo_root_path: Path | None = None) -> tuple[str, ...]:
|
|
66
|
+
return stage_skill_ids(repo_root_path or repo_root())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def current_companion_skills(repo_root_path: Path | None = None) -> tuple[str, ...]:
|
|
70
|
+
return companion_skill_ids(repo_root_path or repo_root())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def classify_turn_intent(user_message: str) -> str:
|
|
74
|
+
text = str(user_message or "").strip()
|
|
75
|
+
if not text:
|
|
76
|
+
return "continue_stage"
|
|
77
|
+
normalized = " ".join(text.split()).lower()
|
|
78
|
+
structured_bootstrap_markers = (
|
|
79
|
+
"project bootstrap",
|
|
80
|
+
"primary research request",
|
|
81
|
+
"research goals",
|
|
82
|
+
"baseline context",
|
|
83
|
+
"reference papers",
|
|
84
|
+
"operational constraints",
|
|
85
|
+
"research delivery mode",
|
|
86
|
+
"decision handling mode",
|
|
87
|
+
"launch mode",
|
|
88
|
+
"research contract",
|
|
89
|
+
"mandatory working rules",
|
|
90
|
+
)
|
|
91
|
+
structured_hit_count = sum(1 for marker in structured_bootstrap_markers if marker in normalized)
|
|
92
|
+
if structured_hit_count >= 2:
|
|
93
|
+
return "continue_stage"
|
|
94
|
+
if normalized.startswith("/new ") or normalized.startswith("/new\n"):
|
|
95
|
+
return "continue_stage"
|
|
96
|
+
question_markers = ["?", "?", "现在进展", "全局", "多久", "什么情况", "在哪", "在哪里", "how long", "what", "where"]
|
|
97
|
+
if any(marker in normalized for marker in question_markers):
|
|
98
|
+
return "answer_user_question_first"
|
|
99
|
+
command_markers = ["继续", "发给我", "发送", "运行", "启动", "resume", "send", "run", "launch"]
|
|
100
|
+
if any(marker in normalized for marker in command_markers):
|
|
101
|
+
return "execute_user_command_first"
|
|
102
|
+
return "continue_stage"
|
|
103
|
+
|
|
104
|
+
|
|
69
105
|
class PromptBuilder:
|
|
70
|
-
def __init__(self, repo_root: Path, home: Path) -> None:
|
|
106
|
+
def __init__(self, repo_root: Path, home: Path, *, prompt_version_selection: str | None = None) -> None:
|
|
71
107
|
self.repo_root = repo_root
|
|
72
108
|
self.home = home
|
|
73
109
|
self.quest_service = QuestService(home)
|
|
74
110
|
self.memory_service = MemoryService(home)
|
|
75
111
|
self.baseline_registry = BaselineRegistry(home)
|
|
76
112
|
self.config_manager = ConfigManager(home)
|
|
113
|
+
self.skill_installer = SkillInstaller(repo_root, home)
|
|
114
|
+
self.prompt_version_selection = str(prompt_version_selection or "").strip() or None
|
|
77
115
|
|
|
78
116
|
def build(
|
|
79
117
|
self,
|
|
@@ -83,15 +121,22 @@ class PromptBuilder:
|
|
|
83
121
|
user_message: str,
|
|
84
122
|
model: str,
|
|
85
123
|
turn_reason: str = "user_message",
|
|
124
|
+
turn_intent: str | None = None,
|
|
125
|
+
turn_mode: str | None = None,
|
|
86
126
|
retry_context: dict | None = None,
|
|
87
127
|
) -> str:
|
|
88
128
|
snapshot = self.quest_service.snapshot(quest_id)
|
|
89
129
|
runtime_config = self.config_manager.load_named("config")
|
|
90
130
|
connectors_config = self.config_manager.load_named_normalized("connectors")
|
|
91
131
|
quest_root = Path(snapshot["quest_root"])
|
|
132
|
+
self.skill_installer.sync_quest_prompts(quest_root)
|
|
92
133
|
active_anchor = str(snapshot.get("active_anchor") or skill_id)
|
|
93
134
|
default_locale = str(runtime_config.get("default_locale") or "en-US")
|
|
94
|
-
|
|
135
|
+
workspace_mode = self._workspace_mode(snapshot)
|
|
136
|
+
system_block = self._prompt_fragment(
|
|
137
|
+
"system_copilot.md" if workspace_mode == "copilot" else "system.md",
|
|
138
|
+
quest_root=quest_root,
|
|
139
|
+
)
|
|
95
140
|
shared_interaction_block = self._prompt_fragment(
|
|
96
141
|
Path("contracts") / "shared_interaction.md",
|
|
97
142
|
quest_root=quest_root,
|
|
@@ -120,7 +165,7 @@ class PromptBuilder:
|
|
|
120
165
|
f"conversation_id: quest:{quest_id}",
|
|
121
166
|
f"default_locale: {default_locale}",
|
|
122
167
|
"built_in_mcp_namespaces: memory, artifact, bash_exec",
|
|
123
|
-
"mcp_namespace_note: any shell-like command execution must use bash_exec
|
|
168
|
+
"mcp_namespace_note: **any shell-like command execution must use `bash_exec(...)`, including curl/python/bash/node/git/npm/uv and similar CLI tools; do not use native `shell_command` / `command_execution`.**",
|
|
124
169
|
"",
|
|
125
170
|
"Canonical stage skills root:",
|
|
126
171
|
str((self.repo_root / "src" / "skills").resolve()),
|
|
@@ -151,7 +196,12 @@ class PromptBuilder:
|
|
|
151
196
|
[
|
|
152
197
|
"",
|
|
153
198
|
"## Turn Driver",
|
|
154
|
-
self._turn_driver_block(
|
|
199
|
+
self._turn_driver_block(
|
|
200
|
+
turn_reason=turn_reason,
|
|
201
|
+
user_message=user_message,
|
|
202
|
+
turn_intent=turn_intent,
|
|
203
|
+
turn_mode=turn_mode,
|
|
204
|
+
),
|
|
155
205
|
"",
|
|
156
206
|
"## Continuation Guard",
|
|
157
207
|
self._continuation_guard_block(
|
|
@@ -173,12 +223,26 @@ class PromptBuilder:
|
|
|
173
223
|
"## Research Delivery Policy",
|
|
174
224
|
self._research_delivery_policy_block(snapshot),
|
|
175
225
|
"",
|
|
226
|
+
"## Optimization Frontier Snapshot",
|
|
227
|
+
self._optimization_frontier_block(snapshot, quest_root),
|
|
228
|
+
"",
|
|
176
229
|
"## Paper And Evidence Snapshot",
|
|
177
230
|
self._paper_and_evidence_block(snapshot, quest_root),
|
|
178
231
|
"",
|
|
179
232
|
"## Retry Recovery Packet",
|
|
180
233
|
self._retry_recovery_block(retry_context),
|
|
181
234
|
"",
|
|
235
|
+
"## Recovery Resume Packet",
|
|
236
|
+
self._recovery_resume_block(snapshot=snapshot, turn_reason=turn_reason),
|
|
237
|
+
"",
|
|
238
|
+
"## Resume Context Spine",
|
|
239
|
+
self._resume_context_spine_block(
|
|
240
|
+
quest_id=quest_id,
|
|
241
|
+
quest_root=quest_root,
|
|
242
|
+
snapshot=snapshot,
|
|
243
|
+
turn_reason=turn_reason,
|
|
244
|
+
),
|
|
245
|
+
"",
|
|
182
246
|
"## Interaction Style",
|
|
183
247
|
self._interaction_style_block(default_locale=default_locale, user_message=user_message, snapshot=snapshot),
|
|
184
248
|
"",
|
|
@@ -206,7 +270,14 @@ class PromptBuilder:
|
|
|
206
270
|
)
|
|
207
271
|
return "\n\n".join(sections).strip() + "\n"
|
|
208
272
|
|
|
209
|
-
def _turn_driver_block(
|
|
273
|
+
def _turn_driver_block(
|
|
274
|
+
self,
|
|
275
|
+
*,
|
|
276
|
+
turn_reason: str,
|
|
277
|
+
user_message: str,
|
|
278
|
+
turn_intent: str | None = None,
|
|
279
|
+
turn_mode: str | None = None,
|
|
280
|
+
) -> str:
|
|
210
281
|
normalized_reason = str(turn_reason or "user_message").strip() or "user_message"
|
|
211
282
|
lines = [f"- turn_reason: {normalized_reason}"]
|
|
212
283
|
if normalized_reason == "auto_continue":
|
|
@@ -228,9 +299,28 @@ class PromptBuilder:
|
|
|
228
299
|
preview = " ".join(str(user_message or "").split())
|
|
229
300
|
if len(preview) > 220:
|
|
230
301
|
preview = preview[:217].rstrip() + "..."
|
|
302
|
+
resolved_turn_intent = str(turn_intent or self._turn_intent(user_message)).strip() or "continue_stage"
|
|
303
|
+
resolved_turn_mode = str(turn_mode or "stage_execution").strip() or "stage_execution"
|
|
304
|
+
lines.append(f"- turn_intent: {resolved_turn_intent}")
|
|
305
|
+
lines.append(f"- turn_mode: {resolved_turn_mode}")
|
|
306
|
+
if resolved_turn_intent == "answer_user_question_first":
|
|
307
|
+
lines.append(
|
|
308
|
+
"- answer_first_rule: the user primarily asked a direct question. Answer it in plain language before resuming any background stage work or generating new route artifacts."
|
|
309
|
+
)
|
|
310
|
+
lines.append(
|
|
311
|
+
"- direct_answer_tool_rule: if the question is about overall progress, paper readiness, current best result, or next step, call artifact.get_global_status(detail='brief'|'full', locale='zh'|'en') before answering from memory or local stage context."
|
|
312
|
+
)
|
|
313
|
+
elif resolved_turn_intent == "execute_user_command_first":
|
|
314
|
+
lines.append(
|
|
315
|
+
"- command_first_rule: the user primarily gave a concrete instruction. Execute or acknowledge that instruction first before resuming background stage narration."
|
|
316
|
+
)
|
|
231
317
|
lines.append(f"- direct_user_message_preview: {preview or 'none'}")
|
|
232
318
|
return "\n".join(lines)
|
|
233
319
|
|
|
320
|
+
@staticmethod
|
|
321
|
+
def _turn_intent(user_message: str) -> str:
|
|
322
|
+
return classify_turn_intent(user_message)
|
|
323
|
+
|
|
234
324
|
def _active_communication_surface_block(
|
|
235
325
|
self,
|
|
236
326
|
*,
|
|
@@ -245,8 +335,6 @@ class PromptBuilder:
|
|
|
245
335
|
connector = surface_context["active_connector"]
|
|
246
336
|
chat_type = surface_context["active_chat_type"]
|
|
247
337
|
chat_id = surface_context["active_chat_id"]
|
|
248
|
-
qq_config = connectors_config.get("qq") if isinstance(connectors_config.get("qq"), dict) else {}
|
|
249
|
-
|
|
250
338
|
lines = [
|
|
251
339
|
f"- latest_user_source: {source}",
|
|
252
340
|
f"- active_surface: {surface}",
|
|
@@ -264,38 +352,16 @@ class PromptBuilder:
|
|
|
264
352
|
lines.extend(
|
|
265
353
|
[
|
|
266
354
|
"- qq_surface_rule: QQ is a milestone-report surface, not a full artifact browser.",
|
|
267
|
-
"-
|
|
268
|
-
"- qq_detail_rule:
|
|
269
|
-
"- qq_length_rule: for ordinary QQ progress replies, normally use only 2 to 4 short sentences, or 3 very short bullets at most.",
|
|
270
|
-
"- qq_summary_first_rule: start with the user-facing conclusion, then the immediate meaning, then the next action; do not make the user reverse-engineer the status from telemetry.",
|
|
271
|
-
"- qq_internal_signal_rule: omit worker names, heartbeat timestamps, retry counters, pending/running/completed counts, file names, and monitor-window narration unless that detail is necessary for a user decision or to explain a real risk.",
|
|
272
|
-
"- qq_translation_rule: translate internal actions into user value, for example say that you organized the baseline record for easier comparison later instead of listing the files you touched.",
|
|
273
|
-
"- 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, next step, or next update; if the runtime is uncertain, say that directly and still give the next check-in window.",
|
|
274
|
-
f"- qq_auto_send_main_experiment_png: {bool(qq_config.get('auto_send_main_experiment_png', True))}",
|
|
275
|
-
f"- qq_auto_send_analysis_summary_png: {bool(qq_config.get('auto_send_analysis_summary_png', True))}",
|
|
276
|
-
f"- qq_auto_send_slice_png: {bool(qq_config.get('auto_send_slice_png', False))}",
|
|
277
|
-
f"- qq_auto_send_paper_pdf: {bool(qq_config.get('auto_send_paper_pdf', True))}",
|
|
278
|
-
f"- qq_enable_markdown_send: {bool(qq_config.get('enable_markdown_send', False))}",
|
|
279
|
-
f"- qq_enable_file_upload_experimental: {bool(qq_config.get('enable_file_upload_experimental', False))}",
|
|
280
|
-
"- 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.",
|
|
281
|
-
"- 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.",
|
|
282
|
-
"- 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.",
|
|
283
|
-
"- 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.",
|
|
355
|
+
"- qq_reply_rule: keep outbound replies concise, respectful, text-first, and progress-aware.",
|
|
356
|
+
"- qq_detail_rule: rely on the QQ connector contract for detailed surface formatting instead of expanding it here.",
|
|
284
357
|
]
|
|
285
358
|
)
|
|
286
359
|
elif connector == "weixin":
|
|
287
360
|
lines.extend(
|
|
288
361
|
[
|
|
289
362
|
"- weixin_surface_rule: Weixin is a concise operator surface, not a full artifact browser.",
|
|
290
|
-
"-
|
|
291
|
-
"-
|
|
292
|
-
"- weixin_summary_first_rule: start with the user-facing conclusion, then the immediate meaning, then the next action.",
|
|
293
|
-
"- weixin_progress_shape_rule: make the current task, the main difficulty or latest real progress, and the next concrete next step explicit whenever possible.",
|
|
294
|
-
"- weixin_eta_rule: for important long-running phases, include a rough ETA or next check-in window when it is helpful and defensible.",
|
|
295
|
-
"- weixin_internal_detail_rule: do not proactively dump file inventories, path lists, retry counters, or monitor-log style telemetry unless the user asked for them or they explain a real risk.",
|
|
296
|
-
"- weixin_context_token_rule: reply continuity is managed by the runtime through `context_token`; do not invent your own reply token scheme.",
|
|
297
|
-
"- weixin_media_rule: when you want native Weixin image, video, or file delivery, request it through artifact.interact(..., attachments=[...]) with `connector_delivery={'weixin': {'media_kind': ...}}` instead of inventing connector-specific inline tag syntax.",
|
|
298
|
-
"- weixin_inbound_media_rule: inbound Weixin image, video, and file messages can arrive as quest-local attachments under `userfiles/weixin/...`; read those files when the user sent media.",
|
|
363
|
+
"- weixin_reply_rule: keep outbound replies concise, respectful, text-first, and progress-aware.",
|
|
364
|
+
"- weixin_detail_rule: rely on the Weixin connector contract for detailed transport formatting instead of expanding it here.",
|
|
299
365
|
]
|
|
300
366
|
)
|
|
301
367
|
else:
|
|
@@ -434,8 +500,14 @@ class PromptBuilder:
|
|
|
434
500
|
]
|
|
435
501
|
)
|
|
436
502
|
if str(turn_reason or "").strip() == "auto_continue":
|
|
437
|
-
lines.
|
|
438
|
-
|
|
503
|
+
lines.extend(
|
|
504
|
+
[
|
|
505
|
+
"- auto_continue_rule: this turn has no new user message; continue from the active requirements, durable artifacts, current quest state, and resume context spine instead of replaying the previous user message",
|
|
506
|
+
f"- auto_continue_interval_rule: when a real long-running external task is already active, background-progress auto-continue becomes a low-frequency monitoring pass, about every {_AUTO_CONTINUE_MONITOR_INTERVAL_SECONDS} seconds rather than sub-minute polling",
|
|
507
|
+
"- auto_continue_fast_prepare_rule: in autonomous mode before a real external long-running task exists, auto-continue may advance quickly, around 0.2 seconds between turns, so the agent can keep preparing or launching the real work without idling",
|
|
508
|
+
"- autonomous_prepare_rule: in autonomous mode, if no real long-running external task is active yet, use the next turns to keep preparing, launching, or durably deciding the next real unit of work instead of parking idly",
|
|
509
|
+
"- copilot_park_rule: in copilot mode, once the current requested unit is complete, it is normal to park and wait for the next user message or `/resume` instead of continuing autonomously",
|
|
510
|
+
]
|
|
439
511
|
)
|
|
440
512
|
else:
|
|
441
513
|
lines.append(
|
|
@@ -472,7 +544,20 @@ class PromptBuilder:
|
|
|
472
544
|
pending_user_count = int(snapshot.get("pending_user_message_count") or 0)
|
|
473
545
|
if pending_user_count > 0:
|
|
474
546
|
return f"Poll artifact.interact(...) and handle the {pending_user_count} queued user message(s) first."
|
|
547
|
+
continuation_policy = str(snapshot.get("continuation_policy") or "auto").strip().lower() or "auto"
|
|
548
|
+
continuation_anchor = str(snapshot.get("continuation_anchor") or "").strip()
|
|
549
|
+
if continuation_policy == "wait_for_user_or_resume":
|
|
550
|
+
if continuation_anchor:
|
|
551
|
+
return (
|
|
552
|
+
f"The quest is intentionally parked after the latest durable checkpoint. Wait for a new user message or "
|
|
553
|
+
f"`/resume`, then continue from `{continuation_anchor}` instead of auto-continuing the previous stage."
|
|
554
|
+
)
|
|
555
|
+
return "The quest is intentionally parked after the latest durable checkpoint. Wait for a new user message or `/resume`."
|
|
556
|
+
if continuation_policy == "none":
|
|
557
|
+
return "Do not auto-continue this quest. Wait for an explicit new user instruction before doing more work."
|
|
475
558
|
active_anchor = str(snapshot.get("active_anchor") or "decision").strip() or "decision"
|
|
559
|
+
if continuation_anchor:
|
|
560
|
+
active_anchor = continuation_anchor
|
|
476
561
|
active_idea_id = str(snapshot.get("active_idea_id") or "").strip()
|
|
477
562
|
next_slice_id = str(snapshot.get("next_pending_slice_id") or "").strip()
|
|
478
563
|
active_campaign_id = str(snapshot.get("active_analysis_campaign_id") or "").strip()
|
|
@@ -489,6 +574,8 @@ class PromptBuilder:
|
|
|
489
574
|
"Continue idea analysis and route selection until the next durable idea branch is submitted "
|
|
490
575
|
"with `lineage_intent='continue_line'` or `lineage_intent='branch_alternative'`."
|
|
491
576
|
)
|
|
577
|
+
if active_anchor == "optimize":
|
|
578
|
+
return "Continue the optimization loop from the current frontier, candidate pool, durable runs, and branch state."
|
|
492
579
|
if active_anchor == "experiment":
|
|
493
580
|
return "Continue the main experiment workflow from the current workspace, logs, and recorded evidence."
|
|
494
581
|
if active_anchor == "analysis-campaign":
|
|
@@ -547,6 +634,76 @@ class PromptBuilder:
|
|
|
547
634
|
lines.append(f"- remaining_attachment_count: {len(attachments) - 6}")
|
|
548
635
|
return "\n".join(lines)
|
|
549
636
|
|
|
637
|
+
def _resume_context_spine_block(self, *, quest_id: str, quest_root: Path, snapshot: dict, turn_reason: str) -> str:
|
|
638
|
+
if str(turn_reason or "").strip() != "auto_continue":
|
|
639
|
+
return "- none"
|
|
640
|
+
lines = [
|
|
641
|
+
"- resume_spine_rule: on auto_continue turns, first continue from the latest durable user requirement, the latest assistant checkpoint, the latest run summary, and recent memory cues instead of reconstructing intent from scratch",
|
|
642
|
+
]
|
|
643
|
+
bash_running_count = int(((snapshot.get("counts") or {}).get("bash_running_count")) or 0)
|
|
644
|
+
latest_bash_session = (
|
|
645
|
+
dict((snapshot.get("summary") or {}).get("latest_bash_session") or {})
|
|
646
|
+
if isinstance((snapshot.get("summary") or {}).get("latest_bash_session"), dict)
|
|
647
|
+
else {}
|
|
648
|
+
)
|
|
649
|
+
lines.append(f"- active_bash_exec_run_count: {bash_running_count}")
|
|
650
|
+
if latest_bash_session:
|
|
651
|
+
command_preview = " ".join(str(latest_bash_session.get("command") or "").split())
|
|
652
|
+
if len(command_preview) > 180:
|
|
653
|
+
command_preview = command_preview[:177].rstrip() + "..."
|
|
654
|
+
lines.append(
|
|
655
|
+
f"- latest_bash_exec_session: bash_id={str(latest_bash_session.get('bash_id') or 'none')} | "
|
|
656
|
+
f"status={str(latest_bash_session.get('status') or 'unknown')} | "
|
|
657
|
+
f"command={command_preview or 'none'}"
|
|
658
|
+
)
|
|
659
|
+
latest_user = self._latest_user_message(quest_id)
|
|
660
|
+
if latest_user is not None:
|
|
661
|
+
preview = " ".join(str(latest_user.get("content") or "").split())
|
|
662
|
+
if len(preview) > 320:
|
|
663
|
+
preview = preview[:317].rstrip() + "..."
|
|
664
|
+
lines.append(
|
|
665
|
+
f"- latest_user_message: {str(latest_user.get('created_at') or 'unknown')} | "
|
|
666
|
+
f"source={str(latest_user.get('source') or 'unknown')} | "
|
|
667
|
+
f"reply_to={str(latest_user.get('reply_to_interaction_id') or 'none')} | "
|
|
668
|
+
f"preview={preview or 'none'}"
|
|
669
|
+
)
|
|
670
|
+
latest_assistant = self._latest_assistant_message(quest_id)
|
|
671
|
+
if latest_assistant is not None:
|
|
672
|
+
preview = " ".join(str(latest_assistant.get("content") or "").split())
|
|
673
|
+
if len(preview) > 360:
|
|
674
|
+
preview = preview[:357].rstrip() + "..."
|
|
675
|
+
lines.append(
|
|
676
|
+
f"- latest_assistant_checkpoint: {str(latest_assistant.get('created_at') or 'unknown')} | "
|
|
677
|
+
f"skill={str(latest_assistant.get('skill_id') or 'none')} | "
|
|
678
|
+
f"run_id={str(latest_assistant.get('run_id') or 'none')} | "
|
|
679
|
+
f"preview={preview or 'none'}"
|
|
680
|
+
)
|
|
681
|
+
latest_run = self._latest_run_result(quest_root)
|
|
682
|
+
if latest_run is not None:
|
|
683
|
+
preview = " ".join(str(latest_run.get("preview") or "").split())
|
|
684
|
+
if len(preview) > 360:
|
|
685
|
+
preview = preview[:357].rstrip() + "..."
|
|
686
|
+
lines.append(
|
|
687
|
+
f"- latest_run_result: {str(latest_run.get('completed_at') or 'unknown')} | "
|
|
688
|
+
f"run_id={str(latest_run.get('run_id') or 'none')} | "
|
|
689
|
+
f"exit_code={latest_run.get('exit_code') if latest_run.get('exit_code') is not None else 'none'} | "
|
|
690
|
+
f"preview={preview or 'none'}"
|
|
691
|
+
)
|
|
692
|
+
recent_memory = self.memory_service.list_recent(scope="quest", quest_root=quest_root, limit=3)
|
|
693
|
+
if recent_memory:
|
|
694
|
+
lines.append("- recent_memory_cues:")
|
|
695
|
+
for item in recent_memory:
|
|
696
|
+
title = str(item.get("title") or "memory").strip() or "memory"
|
|
697
|
+
card_type = str(item.get("type") or "memory").strip() or "memory"
|
|
698
|
+
excerpt = " ".join(str(item.get("excerpt") or "").split())
|
|
699
|
+
if len(excerpt) > 200:
|
|
700
|
+
excerpt = excerpt[:197].rstrip() + "..."
|
|
701
|
+
lines.append(f" - [{card_type}] {title}: {excerpt or 'no excerpt'}")
|
|
702
|
+
else:
|
|
703
|
+
lines.append("- recent_memory_cues: none")
|
|
704
|
+
lines.append("- resume_spine_conflict_rule: if these spine items conflict with newer durable files or artifacts, trust the newer durable state and update the summary rather than replaying the older plan verbatim")
|
|
705
|
+
return "\n".join(lines)
|
|
706
|
+
|
|
550
707
|
def _retry_recovery_block(self, retry_context: dict | None) -> str:
|
|
551
708
|
if not isinstance(retry_context, dict) or not retry_context:
|
|
552
709
|
return "- none"
|
|
@@ -634,6 +791,24 @@ class PromptBuilder:
|
|
|
634
791
|
|
|
635
792
|
return "\n".join(lines)
|
|
636
793
|
|
|
794
|
+
@staticmethod
|
|
795
|
+
def _recovery_resume_block(*, snapshot: dict, turn_reason: str) -> str:
|
|
796
|
+
if str(turn_reason or "").strip() != "auto_continue":
|
|
797
|
+
return "- none"
|
|
798
|
+
source = str(snapshot.get("last_resume_source") or "").strip()
|
|
799
|
+
if not source.startswith("auto:daemon-recovery"):
|
|
800
|
+
return "- none"
|
|
801
|
+
lines = [
|
|
802
|
+
f"- resume_source: {source}",
|
|
803
|
+
f"- resumed_at: {snapshot.get('last_resume_at') or 'unknown'}",
|
|
804
|
+
f"- abandoned_run_id: {snapshot.get('last_recovery_abandoned_run_id') or 'none'}",
|
|
805
|
+
f"- recovery_summary: {snapshot.get('last_recovery_summary') or 'none'}",
|
|
806
|
+
"- recovery_rule: this turn exists because the daemon/runtime previously died or stale running state was reconciled; first re-establish the current truth before continuing any old stage loop.",
|
|
807
|
+
"- recovery_rule_2: if there is any new user message, handle that before blindly resuming the older subtask.",
|
|
808
|
+
"- recovery_rule_3: do not assume the previous branch-local route is still the right immediate action until branch/workspace, run state, and user intent are checked together.",
|
|
809
|
+
]
|
|
810
|
+
return "\n".join(lines)
|
|
811
|
+
|
|
637
812
|
def _prompt_fragment(self, relative_path: str | Path, *, quest_root: Path | None = None) -> str:
|
|
638
813
|
path = self._prompt_path(relative_path, quest_root=quest_root)
|
|
639
814
|
return self._markdown_body(path)
|
|
@@ -641,6 +816,19 @@ class PromptBuilder:
|
|
|
641
816
|
def _prompt_path(self, relative_path: str | Path, *, quest_root: Path | None = None) -> Path:
|
|
642
817
|
normalized = Path(relative_path)
|
|
643
818
|
if quest_root is not None:
|
|
819
|
+
selected_version = str(self.prompt_version_selection or "").strip()
|
|
820
|
+
if selected_version and selected_version not in {"latest", "current", "active"}:
|
|
821
|
+
selected_root = self.skill_installer.resolve_prompt_version_root(quest_root, selected_version)
|
|
822
|
+
if selected_root is None:
|
|
823
|
+
raise FileNotFoundError(
|
|
824
|
+
f"Prompt version `{selected_version}` is unavailable for quest `{quest_root.name}`."
|
|
825
|
+
)
|
|
826
|
+
selected_path = selected_root / normalized
|
|
827
|
+
if not selected_path.exists():
|
|
828
|
+
raise FileNotFoundError(
|
|
829
|
+
f"Prompt version `{selected_version}` does not include `{normalized.as_posix()}` for quest `{quest_root.name}`."
|
|
830
|
+
)
|
|
831
|
+
return selected_path
|
|
644
832
|
quest_path = quest_root / ".codex" / "prompts" / normalized
|
|
645
833
|
if quest_path.exists():
|
|
646
834
|
return quest_path
|
|
@@ -652,16 +840,45 @@ class PromptBuilder:
|
|
|
652
840
|
return item
|
|
653
841
|
return None
|
|
654
842
|
|
|
843
|
+
def _latest_assistant_message(self, quest_id: str) -> dict | None:
|
|
844
|
+
for item in reversed(self.quest_service.history(quest_id, limit=120)):
|
|
845
|
+
if str(item.get("role") or "") == "assistant":
|
|
846
|
+
return item
|
|
847
|
+
return None
|
|
848
|
+
|
|
849
|
+
@staticmethod
|
|
850
|
+
def _latest_run_result(quest_root: Path) -> dict[str, object] | None:
|
|
851
|
+
runs_root = quest_root / ".ds" / "runs"
|
|
852
|
+
if not runs_root.exists():
|
|
853
|
+
return None
|
|
854
|
+
candidates = [path for path in runs_root.glob("*/result.json") if path.is_file()]
|
|
855
|
+
if not candidates:
|
|
856
|
+
return None
|
|
857
|
+
latest = max(candidates, key=lambda path: path.stat().st_mtime)
|
|
858
|
+
payload = read_json(latest, {})
|
|
859
|
+
if not isinstance(payload, dict):
|
|
860
|
+
return None
|
|
861
|
+
preview = (
|
|
862
|
+
str(payload.get("output_text") or "").strip()
|
|
863
|
+
or str(payload.get("stderr_text") or "").strip()
|
|
864
|
+
)
|
|
865
|
+
return {
|
|
866
|
+
"run_id": latest.parent.name,
|
|
867
|
+
"completed_at": str(payload.get("completed_at") or "").strip() or None,
|
|
868
|
+
"exit_code": payload.get("exit_code"),
|
|
869
|
+
"preview": preview,
|
|
870
|
+
}
|
|
871
|
+
|
|
655
872
|
def _skill_paths_block(self) -> str:
|
|
656
873
|
lines = []
|
|
657
|
-
for skill_id in
|
|
874
|
+
for skill_id in current_standard_skills(self.repo_root):
|
|
658
875
|
primary = (self.repo_root / "src" / "skills" / skill_id / "SKILL.md").resolve()
|
|
659
876
|
lines.append(f"- {skill_id}: primary={primary}")
|
|
660
877
|
return "\n".join(lines)
|
|
661
878
|
|
|
662
879
|
def _companion_skill_paths_block(self) -> str:
|
|
663
880
|
lines = []
|
|
664
|
-
for skill_id in
|
|
881
|
+
for skill_id in current_companion_skills(self.repo_root):
|
|
665
882
|
primary = (self.repo_root / "src" / "skills" / skill_id / "SKILL.md").resolve()
|
|
666
883
|
lines.append(f"- {skill_id}: primary={primary}")
|
|
667
884
|
return "\n".join(lines)
|
|
@@ -675,6 +892,18 @@ class PromptBuilder:
|
|
|
675
892
|
return value
|
|
676
893
|
return True
|
|
677
894
|
|
|
895
|
+
@staticmethod
|
|
896
|
+
def _workspace_mode(snapshot: dict) -> str:
|
|
897
|
+
value = str(snapshot.get("workspace_mode") or "").strip().lower()
|
|
898
|
+
if value in {"copilot", "autonomous"}:
|
|
899
|
+
return value
|
|
900
|
+
startup_contract = snapshot.get("startup_contract")
|
|
901
|
+
if isinstance(startup_contract, dict):
|
|
902
|
+
value = str(startup_contract.get("workspace_mode") or "").strip().lower()
|
|
903
|
+
if value in {"copilot", "autonomous"}:
|
|
904
|
+
return value
|
|
905
|
+
return "autonomous"
|
|
906
|
+
|
|
678
907
|
@staticmethod
|
|
679
908
|
def _decision_policy(snapshot: dict) -> str:
|
|
680
909
|
startup_contract = snapshot.get("startup_contract")
|
|
@@ -693,6 +922,15 @@ class PromptBuilder:
|
|
|
693
922
|
return value
|
|
694
923
|
return "standard"
|
|
695
924
|
|
|
925
|
+
@staticmethod
|
|
926
|
+
def _standard_profile(snapshot: dict) -> str:
|
|
927
|
+
startup_contract = snapshot.get("startup_contract")
|
|
928
|
+
if isinstance(startup_contract, dict):
|
|
929
|
+
value = str(startup_contract.get("standard_profile") or "").strip().lower()
|
|
930
|
+
if value in {"canonical_research_graph", "optimization_task"}:
|
|
931
|
+
return value
|
|
932
|
+
return "canonical_research_graph"
|
|
933
|
+
|
|
696
934
|
@staticmethod
|
|
697
935
|
def _custom_profile(snapshot: dict) -> str:
|
|
698
936
|
startup_contract = snapshot.get("startup_contract")
|
|
@@ -730,8 +968,21 @@ class PromptBuilder:
|
|
|
730
968
|
return "none"
|
|
731
969
|
|
|
732
970
|
def _research_delivery_policy_block(self, snapshot: dict) -> str:
|
|
971
|
+
if self._workspace_mode(snapshot) == "copilot":
|
|
972
|
+
return "\n".join(
|
|
973
|
+
[
|
|
974
|
+
"- workspace_mode: copilot",
|
|
975
|
+
"- delivery_goal: complete the user-requested unit of work instead of forcing the full research graph by default.",
|
|
976
|
+
"- task_scope_rule: arbitrary research tasks such as reading, coding, debugging, experiment design, run inspection, analysis, writing, and planning can all be handled directly in this mode.",
|
|
977
|
+
"- autonomy_boundary: only expand into longer autonomous continuation when the user explicitly asks for end-to-end or unattended progress.",
|
|
978
|
+
"- routing_rule: open only the skills actually needed for the current request.",
|
|
979
|
+
"- durability_rule: keep important plan, evidence, decisions, and outputs durable in quest files or artifacts so later turns can resume cleanly.",
|
|
980
|
+
"- completion_rule: after the requested unit is complete, summarize what changed and stop instead of auto-continuing.",
|
|
981
|
+
]
|
|
982
|
+
)
|
|
733
983
|
need_research_paper = self._need_research_paper(snapshot)
|
|
734
984
|
launch_mode = self._launch_mode(snapshot)
|
|
985
|
+
standard_profile = self._standard_profile(snapshot)
|
|
735
986
|
custom_profile = self._custom_profile(snapshot)
|
|
736
987
|
baseline_execution_policy = self._baseline_execution_policy(snapshot)
|
|
737
988
|
review_followup_policy = self._review_followup_policy(snapshot)
|
|
@@ -739,6 +990,7 @@ class PromptBuilder:
|
|
|
739
990
|
lines = [
|
|
740
991
|
f"- need_research_paper: {need_research_paper}",
|
|
741
992
|
f"- launch_mode: {launch_mode}",
|
|
993
|
+
f"- standard_profile: {standard_profile if launch_mode == 'standard' else 'n/a'}",
|
|
742
994
|
f"- custom_profile: {custom_profile if launch_mode == 'custom' else 'n/a'}",
|
|
743
995
|
f"- review_followup_policy: {review_followup_policy if custom_profile == 'review_audit' else 'n/a'}",
|
|
744
996
|
f"- baseline_execution_policy: {baseline_execution_policy if launch_mode == 'custom' else 'n/a'}",
|
|
@@ -833,6 +1085,15 @@ class PromptBuilder:
|
|
|
833
1085
|
"- manuscript_edit_rule: when manuscript revision is needed, provide section-level copy-ready replacement text and explicit deltas even if no LaTeX source is available.",
|
|
834
1086
|
]
|
|
835
1087
|
)
|
|
1088
|
+
elif standard_profile == "optimization_task":
|
|
1089
|
+
lines.extend(
|
|
1090
|
+
[
|
|
1091
|
+
"- standard_optimization_entry_rule: this standard entry is explicitly optimization-only; treat repeated implementation attempts and measured main-experiment results as the primary progress loop.",
|
|
1092
|
+
"- standard_optimization_no_analysis_default: do not route into `analysis-campaign` by default; only run extra analysis when it directly validates a suspected win, disambiguates a frontier decision, or exposes a concrete failure mode that changes the next optimization move.",
|
|
1093
|
+
"- standard_optimization_no_writing_default: do not route into `write`, `review`, or `finalize` while this optimization task profile remains active unless the user explicitly broadens scope.",
|
|
1094
|
+
"- standard_optimization_iteration_rule: prefer more justified optimization attempts, branch promotion, or frontier cleanup over paper-facing packaging.",
|
|
1095
|
+
]
|
|
1096
|
+
)
|
|
836
1097
|
if need_research_paper:
|
|
837
1098
|
lines.extend(
|
|
838
1099
|
[
|
|
@@ -847,6 +1108,9 @@ class PromptBuilder:
|
|
|
847
1108
|
lines.extend(
|
|
848
1109
|
[
|
|
849
1110
|
"- delivery_goal: the quest should pursue the strongest justified algorithmic result rather than paper packaging.",
|
|
1111
|
+
"- optimization_object_rule: distinguish candidate briefs, durable optimization lines, and implementation-level optimization candidates; do not treat them as one object type.",
|
|
1112
|
+
"- optimization_frontier_rule: before major route selection in algorithm-first work, read `artifact.get_optimization_frontier(...)` and treat the current frontier as the primary optimize-state summary.",
|
|
1113
|
+
"- optimization_promotion_rule: `submission_mode='candidate'` is branchless pre-promotion state, while `submission_mode='line'` is a committed durable line with a branch/worktree.",
|
|
850
1114
|
"- main_result_rule: use each measured main-experiment result to decide whether to create a `continue_line` child branch, create a `branch_alternative` sibling-like branch, run more analysis, or stop.",
|
|
851
1115
|
"- no_paper_rule: do not default into `artifact.submit_paper_outline(...)`, `artifact.submit_paper_bundle(...)`, or `finalize` while this mode remains active.",
|
|
852
1116
|
"- autonomy_rule: choose the next optimization foundation from durable evidence such as baseline state, the current research head, and recent main-experiment results; do not routinely ask the user to choose that.",
|
|
@@ -855,13 +1119,101 @@ class PromptBuilder:
|
|
|
855
1119
|
)
|
|
856
1120
|
return "\n".join(lines)
|
|
857
1121
|
|
|
1122
|
+
def _optimization_frontier_block(self, snapshot: dict, quest_root: Path) -> str:
|
|
1123
|
+
active_anchor = str(snapshot.get("active_anchor") or "").strip().lower()
|
|
1124
|
+
if self._need_research_paper(snapshot) and active_anchor != "optimize":
|
|
1125
|
+
return "- not primary in the current delivery mode"
|
|
1126
|
+
|
|
1127
|
+
try:
|
|
1128
|
+
from ..artifact import ArtifactService
|
|
1129
|
+
|
|
1130
|
+
payload = ArtifactService(self.home).get_optimization_frontier(quest_root)
|
|
1131
|
+
except Exception:
|
|
1132
|
+
payload = {"ok": False}
|
|
1133
|
+
|
|
1134
|
+
frontier = (
|
|
1135
|
+
dict(payload.get("optimization_frontier") or {})
|
|
1136
|
+
if isinstance(payload, dict) and isinstance(payload.get("optimization_frontier"), dict)
|
|
1137
|
+
else {}
|
|
1138
|
+
)
|
|
1139
|
+
if not frontier:
|
|
1140
|
+
return "- unavailable"
|
|
1141
|
+
|
|
1142
|
+
best_branch = dict(frontier.get("best_branch") or {}) if isinstance(frontier.get("best_branch"), dict) else {}
|
|
1143
|
+
best_run = dict(frontier.get("best_run") or {}) if isinstance(frontier.get("best_run"), dict) else {}
|
|
1144
|
+
backlog = dict(frontier.get("candidate_backlog") or {}) if isinstance(frontier.get("candidate_backlog"), dict) else {}
|
|
1145
|
+
next_actions = [str(item).strip() for item in (frontier.get("recommended_next_actions") or []) if str(item).strip()]
|
|
1146
|
+
stagnant = frontier.get("stagnant_branches") or []
|
|
1147
|
+
fusion = frontier.get("fusion_candidates") or []
|
|
1148
|
+
local_attempts = [
|
|
1149
|
+
dict(item)
|
|
1150
|
+
for item in (frontier.get("best_branch_recent_candidates") or [])
|
|
1151
|
+
if isinstance(item, dict)
|
|
1152
|
+
]
|
|
1153
|
+
|
|
1154
|
+
lines = [
|
|
1155
|
+
f"- frontier_mode: {str(frontier.get('mode') or 'unknown')}",
|
|
1156
|
+
f"- frontier_reason: {str(frontier.get('frontier_reason') or 'none')}",
|
|
1157
|
+
f"- frontier_best_branch: {str(best_branch.get('branch_name') or best_branch.get('branch_no') or 'none')}",
|
|
1158
|
+
f"- frontier_best_run: {str(best_run.get('run_id') or 'none')}",
|
|
1159
|
+
f"- frontier_candidate_briefs: {int(backlog.get('candidate_brief_count') or 0)}",
|
|
1160
|
+
f"- frontier_active_implementation_candidates: {int(backlog.get('active_implementation_candidate_count') or 0)}",
|
|
1161
|
+
f"- frontier_failed_implementation_candidates: {int(backlog.get('failed_implementation_candidate_count') or 0)}",
|
|
1162
|
+
f"- frontier_stagnant_branch_count: {len([item for item in stagnant if isinstance(item, dict)])}",
|
|
1163
|
+
f"- frontier_fusion_candidate_count: {len([item for item in fusion if isinstance(item, dict)])}",
|
|
1164
|
+
"- optimization_frontier_rule: in algorithm-first work, treat this block as the primary route-selection surface before relying on paper-facing state.",
|
|
1165
|
+
]
|
|
1166
|
+
if local_attempts:
|
|
1167
|
+
parts: list[str] = []
|
|
1168
|
+
for item in local_attempts[-3:]:
|
|
1169
|
+
summary_bits = [
|
|
1170
|
+
str(item.get("candidate_id") or "").strip() or "candidate",
|
|
1171
|
+
str(item.get("status") or "").strip() or "unknown",
|
|
1172
|
+
str(item.get("strategy") or "").strip() or None,
|
|
1173
|
+
str(item.get("mechanism_family") or "").strip() or None,
|
|
1174
|
+
str(item.get("failure_kind") or "").strip() or None,
|
|
1175
|
+
]
|
|
1176
|
+
parts.append(" / ".join(bit for bit in summary_bits if bit))
|
|
1177
|
+
lines.append(f"- frontier_same_line_local_attempt_memory: {' | '.join(parts)}")
|
|
1178
|
+
lines.append(
|
|
1179
|
+
"- optimization_local_memory_rule: before seed, loop, or debug work on the leading line, inspect this same-line local attempt memory so you do not repeat a near-duplicate change blindly."
|
|
1180
|
+
)
|
|
1181
|
+
if next_actions:
|
|
1182
|
+
lines.append(f"- frontier_next_actions: {' | '.join(next_actions[:3])}")
|
|
1183
|
+
return "\n".join(lines)
|
|
1184
|
+
|
|
858
1185
|
def _interaction_style_block(self, *, default_locale: str, user_message: str, snapshot: dict) -> str:
|
|
859
1186
|
normalized_locale = str(default_locale or "").lower()
|
|
860
1187
|
chinese_turn = normalized_locale.startswith("zh") or bool(re.search(r"[\u4e00-\u9fff]", user_message))
|
|
1188
|
+
if self._workspace_mode(snapshot) == "copilot":
|
|
1189
|
+
lines = [
|
|
1190
|
+
f"- configured_default_locale: {default_locale}",
|
|
1191
|
+
f"- current_turn_language_bias: {'zh' if chinese_turn else 'en'}",
|
|
1192
|
+
"- collaboration_mode: user-directed copilot",
|
|
1193
|
+
"- freeform_task_rule: if the user asks for a concrete research task, solve that task directly before introducing stage-routing language.",
|
|
1194
|
+
"- requested_skill_hint_rule: in copilot mode, treat `requested_skill` as a lightweight routing hint, not as an instruction to default into `decision` for ordinary direct tasks.",
|
|
1195
|
+
"- response_pattern: say what changed -> say what it means -> say what happens next",
|
|
1196
|
+
"- mailbox_protocol: artifact.interact(include_recent_inbound_messages=True) remains the queued human-message mailbox and should be checked whenever human continuity matters.",
|
|
1197
|
+
"- planning_rule: before non-trivial execution, make the immediate plan explicit and keep the first step small.",
|
|
1198
|
+
"- tool_rule: use memory for durable recall, artifact for quest state and git-aware research operations, and bash_exec for terminal execution.",
|
|
1199
|
+
"- copilot_sop_rule: classify the request first, choose the narrowest correct tool path, execute the smallest useful unit, persist the important result, then answer plainly.",
|
|
1200
|
+
"- shell_tool_mandate: **for any shell, CLI, Python, bash, node, git, npm, uv, or environment command execution, use `bash_exec(...)`; do not use native `shell_command` or Codex `command_execution`.**",
|
|
1201
|
+
"- git_tool_mandate: for git work inside the current quest repository or worktree, prefer `artifact.git(...)` before raw shell git commands.",
|
|
1202
|
+
"- git_test_rule: if the user wants a generic git smoke test rather than a quest-repo mutation, use `bash_exec(...)` in an isolated scratch repository.",
|
|
1203
|
+
"- decision_entry_rule: use `decision` only for real route, scope, cost, branch, or scientific-direction judgments; do not default to it for ordinary repo, code, environment, or execution tasks.",
|
|
1204
|
+
"- stop_rule: once the current requested unit is done, send a concise update and wait for the next message or `/resume`.",
|
|
1205
|
+
"- escalation_rule: if a route change materially affects cost, scope, or scientific direction, ask before proceeding.",
|
|
1206
|
+
]
|
|
1207
|
+
if chinese_turn:
|
|
1208
|
+
lines.append("- tone_hint: 使用自然、礼貌、专业的中文,先解释结论,再说明下一步。")
|
|
1209
|
+
else:
|
|
1210
|
+
lines.append("- tone_hint: use concise, natural, professional English and lead with the conclusion.")
|
|
1211
|
+
return "\n".join(lines)
|
|
861
1212
|
bound_conversations = snapshot.get("bound_conversations") or []
|
|
862
1213
|
need_research_paper = self._need_research_paper(snapshot)
|
|
863
1214
|
decision_policy = self._decision_policy(snapshot)
|
|
864
1215
|
launch_mode = self._launch_mode(snapshot)
|
|
1216
|
+
standard_profile = self._standard_profile(snapshot)
|
|
865
1217
|
custom_profile = self._custom_profile(snapshot)
|
|
866
1218
|
lines = [
|
|
867
1219
|
f"- configured_default_locale: {default_locale}",
|
|
@@ -869,11 +1221,13 @@ class PromptBuilder:
|
|
|
869
1221
|
f"- bound_conversation_count: {len(bound_conversations)}",
|
|
870
1222
|
f"- decision_policy: {decision_policy}",
|
|
871
1223
|
f"- launch_mode: {launch_mode}",
|
|
1224
|
+
f"- standard_profile: {standard_profile if launch_mode == 'standard' else 'n/a'}",
|
|
872
1225
|
f"- custom_profile: {custom_profile if launch_mode == 'custom' else 'n/a'}",
|
|
873
1226
|
"- collaboration_mode: long-horizon, continuity-first, artifact-aware",
|
|
874
1227
|
"- response_pattern: say what changed -> say what it means -> say what happens next",
|
|
875
1228
|
"- 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",
|
|
876
1229
|
"- shared_interaction_contract_precedence: use the shared interaction contract as the default user-facing cadence; the rules below add runtime-specific execution behavior instead of restating the same chat cadence",
|
|
1230
|
+
"- shell_tool_mandate: **native `shell_command` / `command_execution` is forbidden; all shell-like execution must use `bash_exec(...)`.**",
|
|
877
1231
|
"- 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",
|
|
878
1232
|
"- acknowledgment_protocol: after artifact.interact returns any human message, immediately send one substantive artifact.interact(...) follow-up; if the active connector runtime already emitted a transport-level receipt acknowledgement, do not send a redundant receipt-only message; if answerable, answer directly, otherwise state the short plan, nearest checkpoint, and that the current background subtask is paused",
|
|
879
1233
|
"- subtask_boundary_protocol: 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",
|
|
@@ -882,23 +1236,29 @@ class PromptBuilder:
|
|
|
882
1236
|
"- long_run_reporting_protocol: inspect real logs/status after each meaningful await cycle and at least once every 30 minutes at worst, but only send a user-visible update when there is a human-meaningful delta, blocker, recovery, route change, or the visibility bound would otherwise be exceeded",
|
|
883
1237
|
"- intervention_threshold_protocol: do not kill or restart a run merely because a short watch window passed without final completion; intervene only on explicit failure, clear invalidity, process exit, or no meaningful delta across a sufficiently long observation window",
|
|
884
1238
|
"- timeout_protocol: before using bash_exec(mode='await', ...), estimate whether the command can finish within the selected wait window; if runtime is uncertain or likely longer, use bash_exec(mode='detach', ...) and monitor instead of guessing a fake deadline",
|
|
1239
|
+
f"- auto_continue_monitoring_protocol: if the runtime schedules background-progress auto_continue turns while a real external task is already active, treat them as low-frequency monitoring passes roughly every {_AUTO_CONTINUE_MONITOR_INTERVAL_SECONDS} seconds rather than as a fast polling loop",
|
|
1240
|
+
"- auto_continue_prepare_protocol: in autonomous mode before a real long-running external task exists, rapid auto-continue passes around 0.2 seconds apart are acceptable only for active preparation, launch, or durable route closure work; they are not a substitute for starting the real task",
|
|
1241
|
+
"- long_run_ownership_protocol: real long-running execution should stay alive in detached bash_exec sessions or the runtime process it launched; do not rely on repeated model turns to simulate continuous execution",
|
|
1242
|
+
"- auto_continue_resume_protocol: on auto_continue turns, read the resume context spine first and continue from the latest durable user requirement, latest assistant checkpoint, latest run summary, recent memory cues, and current bash_exec state before changing route",
|
|
885
1243
|
"- blocking_protocol: use reply_mode='blocking' only for true unresolved user decisions; ordinary progress updates should stay threaded and non-blocking",
|
|
886
1244
|
"- credential_blocking_protocol: if continuation requires user-supplied external credentials or secrets such as an API key, GitHub key/token, or Hugging Face key/token, emit one structured blocking decision request that asks the user to provide the credential or choose an alternative route; do not invent placeholders or silently skip the blocked step",
|
|
887
1245
|
"- credential_wait_protocol: if that credential request remains unanswered, keep the quest waiting rather than self-resolving; if you are resumed without new credentials and no other work is possible, a long low-frequency park such as `bash_exec(command='sleep 3600', mode='await', timeout_seconds=3700)` is acceptable to avoid busy-looping",
|
|
888
1246
|
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",
|
|
889
1247
|
"- 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",
|
|
890
1248
|
"- 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",
|
|
891
|
-
"-
|
|
1249
|
+
"- novice_context_protocol: assume the user may not know the repo layout, branch model, artifact schema, or tool names; explain progress in task language first.",
|
|
1250
|
+
"- structure_protocol: when explaining 2 to 3 options, tradeoffs, or next steps, prefer a short numbered structure so the user can scan the decision surface quickly.",
|
|
1251
|
+
"- example_and_numbers_protocol: when it materially improves understanding, include one short example or 1 to 3 key numbers or comparisons instead of relying only on vague adjectives such as better, slower, or more stable.",
|
|
1252
|
+
"- omission_protocol: for ordinary user-facing updates, omit file paths, file names, 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",
|
|
892
1253
|
"- compaction_protocol: ordinary artifact.interact progress updates should usually fit in 2 to 4 short sentences and should not read like a monitoring transcript or execution diary",
|
|
893
|
-
"- watchdog_payload_protocol: if a tool result includes `watchdog_notes`, `progress_watchdog_note`, `visibility_watchdog_note`, or `state_change_watchdog_note`, treat that as an action item and
|
|
1254
|
+
"- watchdog_payload_protocol: if a tool result includes `watchdog_notes`, `progress_watchdog_note`, `visibility_watchdog_note`, or `state_change_watchdog_note`, treat that as an action item to inspect state and decide whether a fresh user-visible update is actually needed; do not emit duplicate progress by reflex",
|
|
894
1255
|
"- human_progress_shape_protocol: ordinary progress updates should usually make three things explicit in human language: the current task, the main difficulty or latest real progress, and the concrete next measure you will take",
|
|
895
1256
|
"- stage_contract_protocol: stage-specific plan/checklist rules, milestone rules, literature rules, and writing rules belong in the requested skill; do not expect this runtime block to restate them",
|
|
896
1257
|
"- teammate_voice_protocol: write like a calm capable teammate using natural first-person phrasing when helpful, for example 'I'm working on ...', 'The main issue right now is ...', 'Next I'll ...'; do not sound like a dashboard or incident log",
|
|
897
|
-
"- translation_protocol: convert internal actions into user-facing meaning; describe what was finished and why it matters instead of naming every touched file, counter, timestamp, or subprocess",
|
|
1258
|
+
"- translation_protocol: convert internal actions into user-facing meaning; describe what was finished and why it matters instead of naming every touched file, path, branch, counter, timestamp, or subprocess",
|
|
898
1259
|
"- detail_gate_protocol: include exact counters, worker labels, timestamps, retry counts, or file names only when the user explicitly asked for them, when they change the recommended action, or when they are the only honest way to explain a real blocker",
|
|
899
1260
|
"- monitoring_summary_protocol: for long-running monitoring loops, summarize the frontier state in plain language such as still progressing, temporarily stalled, recovered, or needs intervention; do not narrate each watch window",
|
|
900
1261
|
"- preflight_rewrite_protocol: before sending artifact.interact, quickly self-check whether the draft reads like a monitoring log, file inventory, or internal diary; if it mentions watch windows, heartbeats, retry counters, raw counts, timestamps, or multiple file names without being necessary for user action, rewrite it into conclusion -> meaning -> next step first",
|
|
901
|
-
"- 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",
|
|
902
1262
|
"- workspace_discipline: read and modify code inside current_workspace_root; treat quest_root as the canonical repo identity and durable runtime root",
|
|
903
1263
|
"- binary_safety: do not open or rewrite large binary assets unless truly necessary; prefer summaries, metadata, and targeted inspection first",
|
|
904
1264
|
]
|
|
@@ -913,7 +1273,7 @@ class PromptBuilder:
|
|
|
913
1273
|
else:
|
|
914
1274
|
lines.extend(
|
|
915
1275
|
[
|
|
916
|
-
"- user_gated_decision_protocol: when continuation truly depends on user preference, approval, or scope choice, use one structured blocking decision request with 1 to 3 concrete options.",
|
|
1276
|
+
"- user_gated_decision_protocol: when continuation truly depends on user preference, approval, or scope choice, use one structured blocking decision request with 1 to 3 concrete options; for each option say what it means, how strongly you recommend it, and what impact it would have on speed, quality, cost, or risk.",
|
|
917
1277
|
"- user_gated_restraint: even in user-gated mode, do not turn ordinary progress or ordinary stage completion into blocking interrupts.",
|
|
918
1278
|
]
|
|
919
1279
|
)
|
|
@@ -925,6 +1285,10 @@ class PromptBuilder:
|
|
|
925
1285
|
lines.append(
|
|
926
1286
|
"- completion_protocol: when `startup_contract.need_research_paper` is false, the quest goal is the strongest justified algorithmic result; keep iterating from measured main-experiment results and do not self-route into paper work by default"
|
|
927
1287
|
)
|
|
1288
|
+
if launch_mode == "standard" and standard_profile == "optimization_task":
|
|
1289
|
+
lines.append(
|
|
1290
|
+
"- standard_optimization_completion_protocol: in this entry profile, do not treat missing paper artifacts or missing analysis-campaign artifacts as unfinished work by themselves; keep pushing the optimization frontier until the result plateaus, a blocker appears, or the user changes scope."
|
|
1291
|
+
)
|
|
928
1292
|
if chinese_turn:
|
|
929
1293
|
lines.extend(
|
|
930
1294
|
[
|
|
@@ -942,141 +1306,40 @@ class PromptBuilder:
|
|
|
942
1306
|
return "\n".join(lines)
|
|
943
1307
|
|
|
944
1308
|
def _quest_context_block(self, quest_root: Path) -> str:
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
)
|
|
952
|
-
text = read_text(quest_root / filename).strip() or "(empty)"
|
|
953
|
-
parts.extend([f"{title} ({filename}):", text, ""])
|
|
954
|
-
return "\n".join(parts).strip()
|
|
1309
|
+
return "\n".join(
|
|
1310
|
+
[
|
|
1311
|
+
"- quest_context_rule: quest documents are durable but not pre-expanded here.",
|
|
1312
|
+
"- quest_documents_tool: call artifact.read_quest_documents(names=['brief','plan','status','summary'], mode='excerpt'|'full') when document detail is needed.",
|
|
1313
|
+
"- active_user_requirements_tool: call artifact.read_quest_documents(names=['active_user_requirements'], mode='full') when exact current durable user requirements matter.",
|
|
1314
|
+
]
|
|
1315
|
+
)
|
|
955
1316
|
|
|
956
1317
|
def _durable_state_block(self, snapshot: dict, quest_root: Path) -> str:
|
|
957
|
-
requested_baseline_ref = (
|
|
958
|
-
dict(snapshot.get("requested_baseline_ref") or {})
|
|
959
|
-
if isinstance(snapshot.get("requested_baseline_ref"), dict)
|
|
960
|
-
else None
|
|
961
|
-
)
|
|
962
|
-
startup_contract = (
|
|
963
|
-
dict(snapshot.get("startup_contract") or {})
|
|
964
|
-
if isinstance(snapshot.get("startup_contract"), dict)
|
|
965
|
-
else None
|
|
966
|
-
)
|
|
967
1318
|
confirmed_baseline_ref = (
|
|
968
1319
|
dict(snapshot.get("confirmed_baseline_ref") or {})
|
|
969
1320
|
if isinstance(snapshot.get("confirmed_baseline_ref"), dict)
|
|
970
|
-
else
|
|
1321
|
+
else {}
|
|
971
1322
|
)
|
|
972
|
-
requested_baseline_id = str((requested_baseline_ref or {}).get("baseline_id") or "").strip()
|
|
973
|
-
confirmed_baseline_id = str((confirmed_baseline_ref or {}).get("baseline_id") or "").strip()
|
|
974
|
-
confirmed_baseline_rel_path = str(
|
|
975
|
-
(confirmed_baseline_ref or {}).get("baseline_root_rel_path") or ""
|
|
976
|
-
).strip()
|
|
977
1323
|
confirmed_metric_contract_json_rel_path = str(
|
|
978
|
-
|
|
1324
|
+
confirmed_baseline_ref.get("metric_contract_json_rel_path") or ""
|
|
979
1325
|
).strip()
|
|
980
|
-
prebound_baseline_ready = bool(
|
|
981
|
-
requested_baseline_id
|
|
982
|
-
and confirmed_baseline_id
|
|
983
|
-
and requested_baseline_id == confirmed_baseline_id
|
|
984
|
-
and str(snapshot.get("baseline_gate") or "").strip().lower() == "confirmed"
|
|
985
|
-
)
|
|
986
1326
|
lines = [
|
|
987
1327
|
f"- baseline_gate: {snapshot.get('baseline_gate') or 'pending'}",
|
|
988
1328
|
f"- active_baseline_id: {snapshot.get('active_baseline_id') or 'none'}",
|
|
989
|
-
f"- active_baseline_variant_id: {snapshot.get('active_baseline_variant_id') or 'none'}",
|
|
990
|
-
f"- requested_baseline_ref: {json.dumps(requested_baseline_ref, ensure_ascii=False, sort_keys=True) if requested_baseline_ref else 'none'}",
|
|
991
|
-
f"- startup_contract: {json.dumps(startup_contract, ensure_ascii=False, sort_keys=True) if startup_contract else 'none'}",
|
|
992
|
-
f"- startup_decision_policy: {self._decision_policy(snapshot)}",
|
|
993
|
-
f"- confirmed_baseline_ref: {json.dumps(confirmed_baseline_ref, ensure_ascii=False, sort_keys=True) if confirmed_baseline_ref else 'none'}",
|
|
994
|
-
f"- confirmed_baseline_import_root: {confirmed_baseline_rel_path or 'none'}",
|
|
995
|
-
f"- prebound_baseline_ready: {prebound_baseline_ready}",
|
|
996
1329
|
f"- active_run_id: {snapshot.get('active_run_id') or 'none'}",
|
|
997
|
-
f"- research_head_branch: {snapshot.get('research_head_branch') or 'none'}",
|
|
998
|
-
f"- research_head_worktree_root: {snapshot.get('research_head_worktree_root') or 'none'}",
|
|
999
|
-
f"- current_workspace_branch: {snapshot.get('current_workspace_branch') or 'none'}",
|
|
1000
|
-
f"- current_workspace_root: {snapshot.get('current_workspace_root') or 'none'}",
|
|
1001
1330
|
f"- active_idea_id: {snapshot.get('active_idea_id') or 'none'}",
|
|
1002
|
-
f"- active_idea_md_path: {snapshot.get('active_idea_md_path') or 'none'}",
|
|
1003
1331
|
f"- active_analysis_campaign_id: {snapshot.get('active_analysis_campaign_id') or 'none'}",
|
|
1004
|
-
f"-
|
|
1332
|
+
f"- active_paper_line_ref: {snapshot.get('active_paper_line_ref') or 'none'}",
|
|
1333
|
+
f"- current_workspace_branch: {snapshot.get('current_workspace_branch') or 'none'}",
|
|
1334
|
+
f"- current_workspace_root: {snapshot.get('current_workspace_root') or 'none'}",
|
|
1005
1335
|
f"- workspace_mode: {snapshot.get('workspace_mode') or 'quest'}",
|
|
1006
1336
|
f"- runtime_status: {snapshot.get('runtime_status') or snapshot.get('status') or 'unknown'}",
|
|
1007
|
-
f"- stop_reason: {snapshot.get('stop_reason') or 'none'}",
|
|
1008
|
-
f"- pending_decisions: {', '.join(snapshot.get('pending_decisions') or []) or 'none'}",
|
|
1009
|
-
f"- pending_user_message_count: {snapshot.get('pending_user_message_count') or 0}",
|
|
1010
|
-
f"- active_interaction_count: {len(snapshot.get('active_interactions') or [])}",
|
|
1011
1337
|
f"- waiting_interaction_id: {snapshot.get('waiting_interaction_id') or 'none'}",
|
|
1012
|
-
f"-
|
|
1013
|
-
f"-
|
|
1014
|
-
f"-
|
|
1015
|
-
|
|
1016
|
-
f"- tool_calls_since_last_artifact_interact: {snapshot.get('tool_calls_since_last_artifact_interact') or 0}",
|
|
1017
|
-
f"- last_tool_activity_at: {snapshot.get('last_tool_activity_at') or 'none'}",
|
|
1018
|
-
f"- last_tool_activity_name: {snapshot.get('last_tool_activity_name') or 'none'}",
|
|
1019
|
-
f"- last_delivered_batch_id: {snapshot.get('last_delivered_batch_id') or 'none'}",
|
|
1020
|
-
f"- bound_conversations: {', '.join(snapshot.get('bound_conversations') or []) or 'none'}",
|
|
1021
|
-
f"- cloud_linked: {snapshot.get('cloud', {}).get('linked', False)}",
|
|
1338
|
+
f"- pending_user_message_count: {snapshot.get('pending_user_message_count') or 0}",
|
|
1339
|
+
f"- continuation_policy: {snapshot.get('continuation_policy') or 'auto'}",
|
|
1340
|
+
f"- continuation_anchor: {snapshot.get('continuation_anchor') or 'none'}",
|
|
1341
|
+
"- quest_state_tool: call artifact.get_quest_state(detail='summary'|'full') for current runtime refs, interactions, recent artifacts, and recent runs.",
|
|
1022
1342
|
]
|
|
1023
|
-
if prebound_baseline_ready and confirmed_baseline_rel_path:
|
|
1024
|
-
lines.extend(
|
|
1025
|
-
[
|
|
1026
|
-
"- prebound_baseline_execution_policy: runtime already attached and confirmed the requested baseline before this turn.",
|
|
1027
|
-
f"- prebound_baseline_runtime_path: {confirmed_baseline_rel_path}",
|
|
1028
|
-
"- prebound_baseline_agent_rule: do not redo baseline discovery or reproduction unless you find a concrete incompatibility, corruption, or missing evidence problem.",
|
|
1029
|
-
]
|
|
1030
|
-
)
|
|
1031
|
-
active_workspace_root = Path(str(snapshot.get("current_workspace_root") or quest_root))
|
|
1032
|
-
attachment_root = active_workspace_root / "baselines" / "imported"
|
|
1033
|
-
if attachment_root.exists():
|
|
1034
|
-
attachments = [read_yaml(path, {}) for path in sorted(attachment_root.glob("*/attachment.yaml"))]
|
|
1035
|
-
attachments = [
|
|
1036
|
-
item
|
|
1037
|
-
for item in attachments
|
|
1038
|
-
if isinstance(item, dict)
|
|
1039
|
-
and item
|
|
1040
|
-
and (
|
|
1041
|
-
not str(item.get("source_baseline_id") or "").strip()
|
|
1042
|
-
or not self.baseline_registry.is_deleted(str(item.get("source_baseline_id") or "").strip())
|
|
1043
|
-
)
|
|
1044
|
-
]
|
|
1045
|
-
if attachments:
|
|
1046
|
-
attachment = max(
|
|
1047
|
-
attachments,
|
|
1048
|
-
key=lambda item: (
|
|
1049
|
-
str(item.get("attached_at") or ""),
|
|
1050
|
-
str(item.get("source_baseline_id") or ""),
|
|
1051
|
-
),
|
|
1052
|
-
)
|
|
1053
|
-
entry = attachment.get("entry") if isinstance(attachment.get("entry"), dict) else {}
|
|
1054
|
-
confirmation = attachment.get("confirmation") if isinstance(attachment.get("confirmation"), dict) else {}
|
|
1055
|
-
if not confirmed_metric_contract_json_rel_path:
|
|
1056
|
-
confirmed_metric_contract_json_rel_path = str(
|
|
1057
|
-
confirmation.get("metric_contract_json_rel_path") or ""
|
|
1058
|
-
).strip()
|
|
1059
|
-
contract = entry.get("metric_contract") if isinstance(entry.get("metric_contract"), dict) else {}
|
|
1060
|
-
primary_metric_id = str(contract.get("primary_metric_id") or "").strip() or "none"
|
|
1061
|
-
metric_ids = [
|
|
1062
|
-
str(item.get("metric_id") or "").strip()
|
|
1063
|
-
for item in contract.get("metrics", [])
|
|
1064
|
-
if isinstance(item, dict) and str(item.get("metric_id") or "").strip()
|
|
1065
|
-
]
|
|
1066
|
-
lines.extend(
|
|
1067
|
-
[
|
|
1068
|
-
f"- active_baseline_primary_metric_id: {primary_metric_id}",
|
|
1069
|
-
f"- active_baseline_metric_ids: {', '.join(metric_ids) or 'none'}",
|
|
1070
|
-
]
|
|
1071
|
-
)
|
|
1072
|
-
if (
|
|
1073
|
-
not confirmed_metric_contract_json_rel_path
|
|
1074
|
-
and confirmed_baseline_rel_path
|
|
1075
|
-
and (quest_root / confirmed_baseline_rel_path / "json" / "metric_contract.json").exists()
|
|
1076
|
-
):
|
|
1077
|
-
confirmed_metric_contract_json_rel_path = str(
|
|
1078
|
-
Path(confirmed_baseline_rel_path, "json", "metric_contract.json").as_posix()
|
|
1079
|
-
)
|
|
1080
1343
|
if confirmed_metric_contract_json_rel_path:
|
|
1081
1344
|
lines.extend(
|
|
1082
1345
|
[
|
|
@@ -1084,256 +1347,42 @@ class PromptBuilder:
|
|
|
1084
1347
|
"- active_baseline_metric_contract_rule: before planning or running `experiment` or `analysis-campaign`, read this JSON file and treat it as the canonical baseline comparison contract unless a newer confirmed baseline explicitly replaces it.",
|
|
1085
1348
|
]
|
|
1086
1349
|
)
|
|
1087
|
-
analysis_baseline_inventory = read_json(quest_root / "artifacts" / "baselines" / "analysis_inventory.json", {})
|
|
1088
|
-
analysis_baseline_inventory = analysis_baseline_inventory if isinstance(analysis_baseline_inventory, dict) else {}
|
|
1089
|
-
analysis_inventory_entries = (
|
|
1090
|
-
analysis_baseline_inventory.get("entries") if isinstance(analysis_baseline_inventory.get("entries"), list) else []
|
|
1091
|
-
)
|
|
1092
|
-
registered_count = sum(
|
|
1093
|
-
1
|
|
1094
|
-
for item in analysis_inventory_entries
|
|
1095
|
-
if isinstance(item, dict) and str(item.get("status") or "").strip().lower() == "registered"
|
|
1096
|
-
)
|
|
1097
|
-
if analysis_inventory_entries:
|
|
1098
|
-
lines.extend(
|
|
1099
|
-
[
|
|
1100
|
-
f"- supplementary_baseline_inventory_status: artifacts/baselines/analysis_inventory.json [exists]",
|
|
1101
|
-
f"- supplementary_baseline_count: {len(analysis_inventory_entries)}",
|
|
1102
|
-
f"- supplementary_baseline_registered_count: {registered_count}",
|
|
1103
|
-
]
|
|
1104
|
-
)
|
|
1105
|
-
else:
|
|
1106
|
-
lines.append("- supplementary_baseline_inventory_status: artifacts/baselines/analysis_inventory.json [missing]")
|
|
1107
|
-
lines.extend(["", "Active interactions:"])
|
|
1108
|
-
active_interactions = snapshot.get("active_interactions") or []
|
|
1109
|
-
if active_interactions:
|
|
1110
|
-
for item in active_interactions[-3:]:
|
|
1111
|
-
interaction_id = item.get("interaction_id") or item.get("artifact_id") or "interaction"
|
|
1112
|
-
status = item.get("status") or "unknown"
|
|
1113
|
-
message = str(item.get("message") or "").strip().replace("\n", " ")
|
|
1114
|
-
if len(message) > 180:
|
|
1115
|
-
message = message[:177].rstrip() + "..."
|
|
1116
|
-
lines.append(f"- {interaction_id} [{status}] {message or '(no message)'}")
|
|
1117
|
-
else:
|
|
1118
|
-
lines.append("- none")
|
|
1119
|
-
if int(snapshot.get("pending_user_message_count") or 0) > 0:
|
|
1120
|
-
lines.extend(
|
|
1121
|
-
[
|
|
1122
|
-
"",
|
|
1123
|
-
"Queued user-message notice:",
|
|
1124
|
-
"- There are queued user messages waiting to be picked up via artifact.interact(include_recent_inbound_messages=True).",
|
|
1125
|
-
"- Before continuing a resumed or follow-up turn, retrieve that mailbox payload first.",
|
|
1126
|
-
"- After the mailbox returns user text, immediately send a follow-up artifact.interact acknowledgement or direct answer before resuming background work.",
|
|
1127
|
-
]
|
|
1128
|
-
)
|
|
1129
|
-
|
|
1130
|
-
lines.extend(
|
|
1131
|
-
[
|
|
1132
|
-
"",
|
|
1133
|
-
"Recent artifacts:",
|
|
1134
|
-
]
|
|
1135
|
-
)
|
|
1136
|
-
recent_artifacts = snapshot.get("recent_artifacts") or []
|
|
1137
|
-
if recent_artifacts:
|
|
1138
|
-
for item in recent_artifacts[-5:]:
|
|
1139
|
-
payload = item.get("payload") or {}
|
|
1140
|
-
label = payload.get("artifact_id") or Path(item.get("path", "")).stem or "artifact"
|
|
1141
|
-
summary = payload.get("summary") or payload.get("reason") or "No summary provided."
|
|
1142
|
-
lines.append(f"- {item.get('kind')}: {label} -> {summary}")
|
|
1143
|
-
else:
|
|
1144
|
-
lines.append("- none")
|
|
1145
|
-
|
|
1146
|
-
lines.extend(["", "Recent runs:"])
|
|
1147
|
-
recent_runs = snapshot.get("recent_runs") or []
|
|
1148
|
-
if recent_runs:
|
|
1149
|
-
for item in recent_runs[-5:]:
|
|
1150
|
-
run_id = item.get("run_id") or "unknown-run"
|
|
1151
|
-
summary = item.get("summary") or "No summary provided."
|
|
1152
|
-
lines.append(f"- {run_id}: {summary}")
|
|
1153
|
-
else:
|
|
1154
|
-
lines.append("- none")
|
|
1155
|
-
|
|
1156
|
-
lines.extend(["", "Recent quest memory cards:"])
|
|
1157
|
-
quest_cards = self.memory_service.list_recent(scope="quest", quest_root=quest_root, limit=5)
|
|
1158
|
-
if quest_cards:
|
|
1159
|
-
for card in quest_cards:
|
|
1160
|
-
lines.append(f"- {card.get('type')}: {card.get('title')} ({card.get('path')})")
|
|
1161
|
-
else:
|
|
1162
|
-
lines.append("- none")
|
|
1163
|
-
|
|
1164
|
-
lines.extend(["", "Recent global memory cards:"])
|
|
1165
|
-
global_cards = self.memory_service.list_recent(scope="global", limit=3)
|
|
1166
|
-
if global_cards:
|
|
1167
|
-
for card in global_cards:
|
|
1168
|
-
lines.append(f"- {card.get('type')}: {card.get('title')} ({card.get('path')})")
|
|
1169
|
-
else:
|
|
1170
|
-
lines.append("- none")
|
|
1171
|
-
|
|
1172
|
-
lines.extend(["", "Reusable baselines:"])
|
|
1173
|
-
baseline_entries = self.baseline_registry.list_entries()[-5:]
|
|
1174
|
-
if baseline_entries:
|
|
1175
|
-
for entry in baseline_entries:
|
|
1176
|
-
baseline_id = entry.get("baseline_id") or entry.get("entry_id") or "unknown-baseline"
|
|
1177
|
-
summary = entry.get("summary") or entry.get("task") or "No summary provided."
|
|
1178
|
-
status = str(entry.get("status") or "unknown").strip() or "unknown"
|
|
1179
|
-
lines.append(f"- {baseline_id} [{status}]: {summary}")
|
|
1180
|
-
else:
|
|
1181
|
-
lines.append("- none")
|
|
1182
1350
|
return "\n".join(lines)
|
|
1183
1351
|
|
|
1184
1352
|
def _paper_and_evidence_block(self, snapshot: dict, quest_root: Path) -> str:
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
paper_root = quest_root / "paper"
|
|
1189
|
-
open_source_root = workspace_root / "release" / "open_source"
|
|
1190
|
-
if not open_source_root.exists():
|
|
1191
|
-
open_source_root = quest_root / "release" / "open_source"
|
|
1192
|
-
selected_outline = read_json(paper_root / "selected_outline.json", {})
|
|
1193
|
-
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
1194
|
-
detailed_outline = (
|
|
1195
|
-
dict(selected_outline.get("detailed_outline") or {})
|
|
1196
|
-
if isinstance(selected_outline.get("detailed_outline"), dict)
|
|
1353
|
+
paper_contract = (
|
|
1354
|
+
dict(snapshot.get("paper_contract") or {})
|
|
1355
|
+
if isinstance(snapshot.get("paper_contract"), dict)
|
|
1197
1356
|
else {}
|
|
1198
1357
|
)
|
|
1199
|
-
bundle_manifest = read_json(paper_root / "paper_bundle_manifest.json", {})
|
|
1200
|
-
bundle_manifest = bundle_manifest if isinstance(bundle_manifest, dict) else {}
|
|
1201
|
-
paper_baseline_inventory = read_json(paper_root / "baseline_inventory.json", {})
|
|
1202
|
-
paper_baseline_inventory = paper_baseline_inventory if isinstance(paper_baseline_inventory, dict) else {}
|
|
1203
|
-
claim_evidence_map = read_json(paper_root / "claim_evidence_map.json", {})
|
|
1204
|
-
claim_evidence_map = claim_evidence_map if isinstance(claim_evidence_map, dict) else {}
|
|
1205
|
-
compile_report = read_json(paper_root / "build" / "compile_report.json", {})
|
|
1206
|
-
compile_report = compile_report if isinstance(compile_report, dict) else {}
|
|
1207
|
-
open_source_manifest = read_json(open_source_root / "manifest.json", {})
|
|
1208
|
-
open_source_manifest = open_source_manifest if isinstance(open_source_manifest, dict) else {}
|
|
1209
|
-
default_paper_prefix = (
|
|
1210
|
-
paper_root.relative_to(quest_root).as_posix()
|
|
1211
|
-
if paper_root.is_relative_to(quest_root)
|
|
1212
|
-
else "paper"
|
|
1213
|
-
)
|
|
1214
|
-
default_release_prefix = (
|
|
1215
|
-
open_source_root.relative_to(quest_root).as_posix()
|
|
1216
|
-
if open_source_root.is_relative_to(quest_root)
|
|
1217
|
-
else "release/open_source"
|
|
1218
|
-
)
|
|
1219
|
-
|
|
1220
|
-
selected_outline_ref = str(
|
|
1221
|
-
selected_outline.get("outline_id") or bundle_manifest.get("selected_outline_ref") or ""
|
|
1222
|
-
).strip()
|
|
1223
|
-
selected_outline_title = str(
|
|
1224
|
-
detailed_outline.get("title") or selected_outline.get("title") or bundle_manifest.get("title") or ""
|
|
1225
|
-
).strip()
|
|
1226
|
-
research_questions_raw = detailed_outline.get("research_questions")
|
|
1227
|
-
research_questions: list[str] = []
|
|
1228
|
-
if isinstance(research_questions_raw, list):
|
|
1229
|
-
for item in research_questions_raw:
|
|
1230
|
-
if isinstance(item, dict):
|
|
1231
|
-
question = str(item.get("question_text") or item.get("title") or item.get("id") or "").strip()
|
|
1232
|
-
else:
|
|
1233
|
-
question = str(item or "").strip()
|
|
1234
|
-
if question:
|
|
1235
|
-
research_questions.append(question)
|
|
1236
|
-
|
|
1237
1358
|
lines = [
|
|
1238
|
-
f"- selected_outline_ref: {selected_outline_ref or 'none'}",
|
|
1239
|
-
f"- selected_outline_title: {
|
|
1240
|
-
f"- selected_outline_story_present: {bool(selected_outline.get('story'))}",
|
|
1241
|
-
f"- selected_outline_ten_questions_present: {bool(selected_outline.get('ten_questions'))}",
|
|
1242
|
-
f"- active_research_question_count: {len(research_questions)}",
|
|
1359
|
+
f"- selected_outline_ref: {str(paper_contract.get('selected_outline_ref') or 'none')}",
|
|
1360
|
+
f"- selected_outline_title: {str(paper_contract.get('title') or 'none')}",
|
|
1243
1361
|
]
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
def _path_status(path_str: str | None, *, fallback: str) -> str:
|
|
1249
|
-
resolved = str(path_str or fallback).strip() or fallback
|
|
1250
|
-
exists = (quest_root / resolved).exists()
|
|
1251
|
-
return f"{resolved} [{'exists' if exists else 'missing'}]"
|
|
1252
|
-
|
|
1253
|
-
lines.extend(
|
|
1254
|
-
[
|
|
1255
|
-
f"- writing_plan_status: {_path_status(bundle_manifest.get('writing_plan_path'), fallback=f'{default_paper_prefix}/writing_plan.md')}",
|
|
1256
|
-
f"- draft_status: {_path_status(bundle_manifest.get('draft_path'), fallback=f'{default_paper_prefix}/draft.md')}",
|
|
1257
|
-
f"- references_status: {_path_status(bundle_manifest.get('references_path'), fallback=f'{default_paper_prefix}/references.bib')}",
|
|
1258
|
-
f"- claim_evidence_map_status: {_path_status(bundle_manifest.get('claim_evidence_map_path'), fallback=f'{default_paper_prefix}/claim_evidence_map.json')}",
|
|
1259
|
-
f"- baseline_inventory_status: {_path_status(bundle_manifest.get('baseline_inventory_path'), fallback=f'{default_paper_prefix}/baseline_inventory.json')}",
|
|
1260
|
-
f"- review_status: {f'{default_paper_prefix}/review/review.md [exists]' if (paper_root / 'review' / 'review.md').exists() else f'{default_paper_prefix}/review/review.md [missing]'}",
|
|
1261
|
-
f"- proofing_report_status: {f'{default_paper_prefix}/proofing/proofing_report.md [exists]' if (paper_root / 'proofing' / 'proofing_report.md').exists() else f'{default_paper_prefix}/proofing/proofing_report.md [missing]'}",
|
|
1262
|
-
f"- page_images_manifest_status: {f'{default_paper_prefix}/proofing/page_images_manifest.json [exists]' if (paper_root / 'proofing' / 'page_images_manifest.json').exists() else f'{default_paper_prefix}/proofing/page_images_manifest.json [missing]'}",
|
|
1263
|
-
]
|
|
1362
|
+
paper_contract_health = (
|
|
1363
|
+
dict(snapshot.get("paper_contract_health") or {})
|
|
1364
|
+
if isinstance(snapshot.get("paper_contract_health"), dict)
|
|
1365
|
+
else {}
|
|
1264
1366
|
)
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
latex_root_path = str(bundle_manifest.get("latex_root_path") or "").strip()
|
|
1367
|
+
if paper_contract_health:
|
|
1368
|
+
primary_blocker = str(
|
|
1369
|
+
((paper_contract_health.get("blocking_reasons") or [None])[0]) or "none"
|
|
1370
|
+
).strip() or "none"
|
|
1270
1371
|
lines.extend(
|
|
1271
1372
|
[
|
|
1272
|
-
"-
|
|
1273
|
-
f"-
|
|
1274
|
-
f"-
|
|
1275
|
-
f"-
|
|
1276
|
-
f"-
|
|
1277
|
-
|
|
1373
|
+
f"- paper_contract_health: {'ready' if bool(paper_contract_health.get('writing_ready')) else 'blocked'}",
|
|
1374
|
+
f"- paper_health_counts: unresolved_required={int(paper_contract_health.get('unresolved_required_count') or 0)}, unmapped_completed={int(paper_contract_health.get('unmapped_completed_count') or 0)}, blocking_pending={int(paper_contract_health.get('blocking_open_supplementary_count') or 0)}",
|
|
1375
|
+
f"- paper_recommended_next_stage: {str(paper_contract_health.get('recommended_next_stage') or 'none')}",
|
|
1376
|
+
f"- paper_recommended_action: {str(paper_contract_health.get('recommended_action') or 'none')}",
|
|
1377
|
+
f"- paper_primary_blocker: {primary_blocker}",
|
|
1378
|
+
"- paper_health_tool: call artifact.get_paper_contract_health(detail='full') before paper-facing write/finalize work when the exact blocking items matter.",
|
|
1379
|
+
"- paper_outline_tool: call artifact.list_paper_outlines(...) when outline inventory or a valid outline_id is needed.",
|
|
1380
|
+
"- paper_campaign_tool: call artifact.get_analysis_campaign(campaign_id='active') when exact supplementary slice status matters.",
|
|
1278
1381
|
]
|
|
1279
1382
|
)
|
|
1280
|
-
else:
|
|
1281
|
-
lines.append("- paper_bundle_manifest_present: False")
|
|
1282
|
-
|
|
1283
|
-
claims = claim_evidence_map.get("claims") if isinstance(claim_evidence_map.get("claims"), list) else []
|
|
1284
|
-
counts = {"supported": 0, "partial": 0, "unsupported": 0, "deferred": 0}
|
|
1285
|
-
unresolved: list[str] = []
|
|
1286
|
-
for item in claims:
|
|
1287
|
-
if not isinstance(item, dict):
|
|
1288
|
-
continue
|
|
1289
|
-
status = str(item.get("support_status") or "").strip().lower()
|
|
1290
|
-
if status in counts:
|
|
1291
|
-
counts[status] += 1
|
|
1292
|
-
if status in {"partial", "unsupported", "deferred"}:
|
|
1293
|
-
claim_id = str(item.get("claim_id") or item.get("claim_text") or "claim").strip()
|
|
1294
|
-
unresolved.append(f"{claim_id} [{status}]")
|
|
1295
|
-
lines.append(
|
|
1296
|
-
"- claim_status_counts: "
|
|
1297
|
-
+ ", ".join(f"{key}={value}" for key, value in counts.items())
|
|
1298
|
-
)
|
|
1299
|
-
if unresolved:
|
|
1300
|
-
lines.append(f"- downgrade_watchlist: {'; '.join(unresolved[:5])}")
|
|
1301
|
-
else:
|
|
1302
|
-
lines.append("- downgrade_watchlist: none")
|
|
1303
|
-
|
|
1304
|
-
if compile_report:
|
|
1305
|
-
lines.append(f"- compile_report_ok: {compile_report.get('ok') if 'ok' in compile_report else 'unknown'}")
|
|
1306
|
-
supplementary_baselines = (
|
|
1307
|
-
paper_baseline_inventory.get("supplementary_baselines")
|
|
1308
|
-
if isinstance(paper_baseline_inventory.get("supplementary_baselines"), list)
|
|
1309
|
-
else []
|
|
1310
|
-
)
|
|
1311
|
-
if paper_baseline_inventory:
|
|
1312
|
-
lines.append(f"- paper_supplementary_baseline_count: {len(supplementary_baselines)}")
|
|
1313
|
-
if open_source_manifest:
|
|
1314
1383
|
lines.append(
|
|
1315
|
-
|
|
1384
|
+
"- paper_contract_rule: if the paper state is blocked, do not stabilize draft prose as if the paper were settled; follow the recommended paper action first."
|
|
1316
1385
|
)
|
|
1317
|
-
|
|
1318
|
-
lines.extend(["", "Recent supporting runs:"])
|
|
1319
|
-
recent_runs = snapshot.get("recent_runs") or []
|
|
1320
|
-
supporting_runs = [
|
|
1321
|
-
item
|
|
1322
|
-
for item in recent_runs
|
|
1323
|
-
if isinstance(item, dict) and str(item.get("run_id") or "").strip()
|
|
1324
|
-
]
|
|
1325
|
-
if supporting_runs:
|
|
1326
|
-
for item in supporting_runs[-3:]:
|
|
1327
|
-
run_id = str(item.get("run_id") or "run").strip()
|
|
1328
|
-
summary = str(item.get("summary") or "").strip() or "No summary provided."
|
|
1329
|
-
lines.append(f"- {run_id}: {summary}")
|
|
1330
|
-
else:
|
|
1331
|
-
lines.append("- none")
|
|
1332
|
-
|
|
1333
|
-
lines.append("")
|
|
1334
|
-
lines.append(
|
|
1335
|
-
"- paper_state_rule: when drafting, reviewing, bundling, or finalizing, treat the selected outline, claim-evidence map, bundle manifest, proofing outputs, and downgrade watchlist as the active writing truth surface."
|
|
1336
|
-
)
|
|
1337
1386
|
return "\n".join(lines)
|
|
1338
1387
|
|
|
1339
1388
|
def _priority_memory_block(
|
|
@@ -1346,46 +1395,53 @@ class PromptBuilder:
|
|
|
1346
1395
|
) -> str:
|
|
1347
1396
|
stage = active_anchor if active_anchor in STAGE_MEMORY_PLAN else skill_id
|
|
1348
1397
|
plan = STAGE_MEMORY_PLAN.get(stage, STAGE_MEMORY_PLAN["decision"])
|
|
1398
|
+
quest_kinds = ", ".join(plan.get("quest", ())) or "none"
|
|
1399
|
+
global_kinds = ", ".join(plan.get("global", ())) or "none"
|
|
1400
|
+
lines = [
|
|
1401
|
+
f"- stage_memory_rule: for `{stage}`, prefer quest memory kinds [{quest_kinds}] and global memory kinds [{global_kinds}] when memory lookup is needed.",
|
|
1402
|
+
"- memory_lookup_tool: call memory.list_recent(...) to recover context after pause/restart and memory.search(...) before repeating prior work.",
|
|
1403
|
+
"- memory_injection_rule: keep the injected memory compact, but do not drop all continuity on auto_continue turns; reuse a few recent durable cues directly when they materially anchor the next action.",
|
|
1404
|
+
]
|
|
1349
1405
|
selected: list[dict] = []
|
|
1350
1406
|
seen_paths: set[str] = set()
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
for kind in plan.get(scope, ()):
|
|
1354
|
-
cards = self.memory_service.list_recent(
|
|
1355
|
-
scope=scope,
|
|
1356
|
-
quest_root=quest_root if scope == "quest" else None,
|
|
1357
|
-
kind=kind,
|
|
1358
|
-
limit=2,
|
|
1359
|
-
)
|
|
1360
|
-
if not cards:
|
|
1361
|
-
continue
|
|
1407
|
+
for kind in plan.get("quest", ())[:2]:
|
|
1408
|
+
for card in self.memory_service.list_recent(scope="quest", quest_root=quest_root, limit=2, kind=kind)[:1]:
|
|
1362
1409
|
self._append_priority_memory(
|
|
1363
1410
|
selected,
|
|
1364
1411
|
seen_paths,
|
|
1365
|
-
card=
|
|
1366
|
-
scope=
|
|
1412
|
+
card=card,
|
|
1413
|
+
scope="quest",
|
|
1367
1414
|
quest_root=quest_root,
|
|
1368
|
-
reason=f"recent
|
|
1415
|
+
reason=f"recent quest memory for stage `{stage}`",
|
|
1369
1416
|
)
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
for query in self._memory_queries(user_message):
|
|
1374
|
-
matches = self.memory_service.search(query, scope="both", quest_root=quest_root, limit=6)
|
|
1375
|
-
for card in matches:
|
|
1376
|
-
scope = str(card.get("scope") or "quest")
|
|
1417
|
+
for kind in plan.get("global", ())[:2]:
|
|
1418
|
+
for card in self.memory_service.list_recent(scope="global", limit=2, kind=kind)[:1]:
|
|
1377
1419
|
self._append_priority_memory(
|
|
1378
1420
|
selected,
|
|
1379
1421
|
seen_paths,
|
|
1380
1422
|
card=card,
|
|
1381
|
-
scope=
|
|
1423
|
+
scope="global",
|
|
1382
1424
|
quest_root=quest_root,
|
|
1383
|
-
reason=f"
|
|
1425
|
+
reason=f"recent global memory for stage `{stage}`",
|
|
1384
1426
|
)
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1427
|
+
for query in self._memory_queries(user_message)[:2]:
|
|
1428
|
+
for scope in ("quest", "global"):
|
|
1429
|
+
for card in self.memory_service.search(
|
|
1430
|
+
query,
|
|
1431
|
+
scope=scope if scope == "global" else "quest",
|
|
1432
|
+
quest_root=quest_root if scope == "quest" else None,
|
|
1433
|
+
limit=1,
|
|
1434
|
+
):
|
|
1435
|
+
self._append_priority_memory(
|
|
1436
|
+
selected,
|
|
1437
|
+
seen_paths,
|
|
1438
|
+
card=card,
|
|
1439
|
+
scope=scope,
|
|
1440
|
+
quest_root=quest_root,
|
|
1441
|
+
reason=f"matched current-turn query `{query}`",
|
|
1442
|
+
)
|
|
1443
|
+
lines.extend(["- selected_memory:", self._format_priority_memory(selected)])
|
|
1444
|
+
return "\n".join(lines)
|
|
1389
1445
|
|
|
1390
1446
|
def _append_priority_memory(
|
|
1391
1447
|
self,
|
|
@@ -1447,20 +1503,12 @@ class PromptBuilder:
|
|
|
1447
1503
|
return tokens
|
|
1448
1504
|
|
|
1449
1505
|
def _conversation_block(self, quest_id: str, limit: int = 12) -> str:
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
source = str(item.get("source") or "unknown")
|
|
1457
|
-
content = str(item.get("content") or "").strip().replace("\n", " ")
|
|
1458
|
-
if len(content) > 400:
|
|
1459
|
-
content = content[:397].rstrip() + "..."
|
|
1460
|
-
reply_to = str(item.get("reply_to_interaction_id") or "").strip()
|
|
1461
|
-
suffix = f" -> reply_to:{reply_to}" if reply_to else ""
|
|
1462
|
-
lines.append(f"- [{role}|{source}]{suffix} {content}")
|
|
1463
|
-
return "\n".join(lines)
|
|
1506
|
+
return "\n".join(
|
|
1507
|
+
[
|
|
1508
|
+
"- conversation_context_rule: recent conversation is not pre-expanded here.",
|
|
1509
|
+
f"- conversation_tool: call artifact.get_conversation_context(limit={limit}, include_attachments=False) when earlier turn continuity matters.",
|
|
1510
|
+
]
|
|
1511
|
+
)
|
|
1464
1512
|
|
|
1465
1513
|
def _markdown_body(self, path: Path) -> str:
|
|
1466
1514
|
text = path.read_text(encoding="utf-8")
|