@researai/deepscientist 1.5.14 → 1.5.15
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 +8 -0
- package/assets/branding/logo-raster.png +0 -0
- package/bin/ds.js +134 -49
- package/docs/en/00_QUICK_START.md +2 -2
- package/docs/en/01_SETTINGS_REFERENCE.md +20 -4
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
- 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/README.md +6 -0
- package/docs/zh/00_QUICK_START.md +2 -2
- package/docs/zh/01_SETTINGS_REFERENCE.md +20 -4
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
- 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/README.md +6 -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/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 +3534 -191
- 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 +79 -64
- package/src/deepscientist/bash_exec/shells.py +87 -0
- package/src/deepscientist/bridges/connectors.py +51 -2
- package/src/deepscientist/config/models.py +6 -3
- package/src/deepscientist/config/service.py +7 -2
- package/src/deepscientist/connector/weixin_support.py +122 -1
- package/src/deepscientist/daemon/api/handlers.py +75 -4
- package/src/deepscientist/daemon/api/router.py +1 -0
- package/src/deepscientist/daemon/app.py +758 -206
- package/src/deepscientist/doctor.py +51 -0
- package/src/deepscientist/file_lock.py +48 -0
- package/src/deepscientist/gitops/diff.py +167 -1
- package/src/deepscientist/mcp/server.py +173 -5
- package/src/deepscientist/process_control.py +161 -0
- package/src/deepscientist/prompts/builder.py +267 -442
- package/src/deepscientist/quest/service.py +2255 -163
- package/src/deepscientist/quest/stage_views.py +171 -0
- package/src/deepscientist/runners/base.py +2 -0
- package/src/deepscientist/runners/codex.py +88 -5
- package/src/deepscientist/runners/runtime_overrides.py +17 -1
- package/src/prompts/contracts/shared_interaction.md +13 -4
- package/src/prompts/system.md +916 -72
- package/src/skills/analysis-campaign/SKILL.md +31 -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 +2 -0
- package/src/skills/decision/SKILL.md +19 -2
- package/src/skills/experiment/SKILL.md +8 -2
- package/src/skills/finalize/SKILL.md +18 -0
- package/src/skills/idea/SKILL.md +78 -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 +1 -1
- package/src/skills/optimize/SKILL.md +1644 -0
- package/src/skills/rebuttal/SKILL.md +2 -1
- package/src/skills/review/SKILL.md +2 -1
- package/src/skills/write/SKILL.md +80 -12
- package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
- package/src/tui/dist/app/AppContainer.js +3 -0
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-DaF9Nge_.js → AiManusChatView-DDjbFnbt.js} +12 -12
- package/src/ui/dist/assets/{AnalysisPlugin-BSVx6dXE.js → AnalysisPlugin-Yb5IdmaU.js} +1 -1
- package/src/ui/dist/assets/CliPlugin-e64sreyu.js +31037 -0
- package/src/ui/dist/assets/{CodeEditorPlugin-DU9G0Tox.js → CodeEditorPlugin-C4D2TIkU.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DoX_fI9l.js → CodeViewerPlugin-BVoNZIvC.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-C4FWIXuU.js → DocViewerPlugin-CLChbllo.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-BgfFMgtf.js → GitDiffViewerPlugin-C4xeFyFQ.js} +20 -20
- package/src/ui/dist/assets/{ImageViewerPlugin-tcPkfY_x.js → ImageViewerPlugin-OiMUAcLi.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-_dKV60Bf.js → LabCopilotPanel-BjD2ThQF.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-Bje0ayoC.js → LabPlugin-DQPg-NrB.js} +2 -2
- package/src/ui/dist/assets/{LatexPlugin-CVsBzAln.js → LatexPlugin-CI05XAV9.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-xjmrqv_8.js → MarkdownViewerPlugin-DpeBLYZf.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-mMM2A8wP.js → MarketplacePlugin-DolE58Q2.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-3kVDSOBo.js → NotebookEditor-7Qm2rSWD.js} +11 -11
- package/src/ui/dist/assets/{NotebookEditor-SoJ8X-MO.js → NotebookEditor-C1kWaxKi.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DElVuHl9.js → PdfLoader-BfOHw8Zw.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-Bq88XT4G.js → PdfMarkdownPlugin-BulDREv1.js} +2 -2
- package/src/ui/dist/assets/{PdfViewerPlugin-CsCXMo9S.js → PdfViewerPlugin-C-daaOaL.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-oUPvy19k.js → SearchPlugin-CjpaiJ3A.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-CRkT9yNy.js → TextViewerPlugin-BxIyqPQC.js} +5 -5
- package/src/ui/dist/assets/{VNCViewer-BgbuvWhR.js → VNCViewer-HAg9mF7M.js} +10 -10
- package/src/ui/dist/assets/{bot-v_RASACv.js → bot-0DYntytV.js} +1 -1
- package/src/ui/dist/assets/{code-5hC9d0VH.js → code-B20Slj_w.js} +1 -1
- package/src/ui/dist/assets/{file-content-D1PxfOrp.js → file-content-DT24KFma.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DG1oT_Hj.js → file-diff-panel-DK13YPql.js} +1 -1
- package/src/ui/dist/assets/{file-socket-BmdFYQlk.js → file-socket-B4T2o4nR.js} +1 -1
- package/src/ui/dist/assets/{image-Dqe2X2tW.js → image-DSeR_sDS.js} +1 -1
- package/src/ui/dist/assets/{index-RDlNXXx1.js → index-BrFje2Uk.js} +2 -2
- package/src/ui/dist/assets/{index-DVsMKK_y.js → index-BwRJaoTl.js} +1 -1
- package/src/ui/dist/assets/{index-Nt9hS4ck.js → index-D_E4281X.js} +5007 -28514
- package/src/ui/dist/assets/{index-Duvz8Ip0.js → index-DnYB3xb1.js} +12 -12
- package/src/ui/dist/assets/{index-BQG-1s2o.css → index-G7AcWcMu.css} +43 -2
- package/src/ui/dist/assets/{monaco-DIXge1CP.js → monaco-LExaAN3Y.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-BBTTQaO-.js → pdf-effect-queue-BJk5okWJ.js} +1 -1
- package/src/ui/dist/assets/{popover-BWlolyxo.js → popover-D3Gg_FoV.js} +1 -1
- package/src/ui/dist/assets/{project-sync-BM5PkFH4.js → project-sync-C_ygLlVU.js} +1 -1
- package/src/ui/dist/assets/{select-D4dAtrA8.js → select-CpAK6uWm.js} +2 -2
- package/src/ui/dist/assets/{sigma-CKbE5jJT.js → sigma-DEccaSgk.js} +1 -1
- package/src/ui/dist/assets/{square-check-big-CZNGMgiB.js → square-check-big-uUfyVsbD.js} +1 -1
- package/src/ui/dist/assets/{trash-DaB37xAz.js → trash-CXvwwSe8.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-C2OmAcWe.js → useCliAccess-Bnop4mgR.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-Dowd1Ij4.js → useFileDiffOverlay-B8eUAX0I.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-BGjAhAUq.js → wrap-text-9vbOBpkW.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-dMZQMXzc.js → zoom-out-BgVMmOW4.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
|
@@ -16,6 +16,7 @@ STANDARD_SKILLS = (
|
|
|
16
16
|
"scout",
|
|
17
17
|
"baseline",
|
|
18
18
|
"idea",
|
|
19
|
+
"optimize",
|
|
19
20
|
"experiment",
|
|
20
21
|
"analysis-campaign",
|
|
21
22
|
"write",
|
|
@@ -43,6 +44,10 @@ STAGE_MEMORY_PLAN = {
|
|
|
43
44
|
"quest": ("papers", "ideas", "decisions", "knowledge"),
|
|
44
45
|
"global": ("papers", "knowledge", "templates"),
|
|
45
46
|
},
|
|
47
|
+
"optimize": {
|
|
48
|
+
"quest": ("episodes", "decisions", "ideas", "knowledge"),
|
|
49
|
+
"global": ("knowledge", "templates"),
|
|
50
|
+
},
|
|
46
51
|
"experiment": {
|
|
47
52
|
"quest": ("ideas", "decisions", "episodes", "knowledge"),
|
|
48
53
|
"global": ("knowledge", "templates"),
|
|
@@ -66,6 +71,38 @@ STAGE_MEMORY_PLAN = {
|
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
|
|
74
|
+
def classify_turn_intent(user_message: str) -> str:
|
|
75
|
+
text = str(user_message or "").strip()
|
|
76
|
+
if not text:
|
|
77
|
+
return "continue_stage"
|
|
78
|
+
normalized = " ".join(text.split()).lower()
|
|
79
|
+
structured_bootstrap_markers = (
|
|
80
|
+
"project bootstrap",
|
|
81
|
+
"primary research request",
|
|
82
|
+
"research goals",
|
|
83
|
+
"baseline context",
|
|
84
|
+
"reference papers",
|
|
85
|
+
"operational constraints",
|
|
86
|
+
"research delivery mode",
|
|
87
|
+
"decision handling mode",
|
|
88
|
+
"launch mode",
|
|
89
|
+
"research contract",
|
|
90
|
+
"mandatory working rules",
|
|
91
|
+
)
|
|
92
|
+
structured_hit_count = sum(1 for marker in structured_bootstrap_markers if marker in normalized)
|
|
93
|
+
if structured_hit_count >= 2:
|
|
94
|
+
return "continue_stage"
|
|
95
|
+
if normalized.startswith("/new ") or normalized.startswith("/new\n"):
|
|
96
|
+
return "continue_stage"
|
|
97
|
+
question_markers = ["?", "?", "现在进展", "全局", "多久", "什么情况", "在哪", "在哪里", "how long", "what", "where"]
|
|
98
|
+
if any(marker in normalized for marker in question_markers):
|
|
99
|
+
return "answer_user_question_first"
|
|
100
|
+
command_markers = ["继续", "发给我", "发送", "运行", "启动", "resume", "send", "run", "launch"]
|
|
101
|
+
if any(marker in normalized for marker in command_markers):
|
|
102
|
+
return "execute_user_command_first"
|
|
103
|
+
return "continue_stage"
|
|
104
|
+
|
|
105
|
+
|
|
69
106
|
class PromptBuilder:
|
|
70
107
|
def __init__(self, repo_root: Path, home: Path) -> None:
|
|
71
108
|
self.repo_root = repo_root
|
|
@@ -83,6 +120,8 @@ class PromptBuilder:
|
|
|
83
120
|
user_message: str,
|
|
84
121
|
model: str,
|
|
85
122
|
turn_reason: str = "user_message",
|
|
123
|
+
turn_intent: str | None = None,
|
|
124
|
+
turn_mode: str | None = None,
|
|
86
125
|
retry_context: dict | None = None,
|
|
87
126
|
) -> str:
|
|
88
127
|
snapshot = self.quest_service.snapshot(quest_id)
|
|
@@ -151,7 +190,12 @@ class PromptBuilder:
|
|
|
151
190
|
[
|
|
152
191
|
"",
|
|
153
192
|
"## Turn Driver",
|
|
154
|
-
self._turn_driver_block(
|
|
193
|
+
self._turn_driver_block(
|
|
194
|
+
turn_reason=turn_reason,
|
|
195
|
+
user_message=user_message,
|
|
196
|
+
turn_intent=turn_intent,
|
|
197
|
+
turn_mode=turn_mode,
|
|
198
|
+
),
|
|
155
199
|
"",
|
|
156
200
|
"## Continuation Guard",
|
|
157
201
|
self._continuation_guard_block(
|
|
@@ -173,12 +217,18 @@ class PromptBuilder:
|
|
|
173
217
|
"## Research Delivery Policy",
|
|
174
218
|
self._research_delivery_policy_block(snapshot),
|
|
175
219
|
"",
|
|
220
|
+
"## Optimization Frontier Snapshot",
|
|
221
|
+
self._optimization_frontier_block(snapshot, quest_root),
|
|
222
|
+
"",
|
|
176
223
|
"## Paper And Evidence Snapshot",
|
|
177
224
|
self._paper_and_evidence_block(snapshot, quest_root),
|
|
178
225
|
"",
|
|
179
226
|
"## Retry Recovery Packet",
|
|
180
227
|
self._retry_recovery_block(retry_context),
|
|
181
228
|
"",
|
|
229
|
+
"## Recovery Resume Packet",
|
|
230
|
+
self._recovery_resume_block(snapshot=snapshot, turn_reason=turn_reason),
|
|
231
|
+
"",
|
|
182
232
|
"## Interaction Style",
|
|
183
233
|
self._interaction_style_block(default_locale=default_locale, user_message=user_message, snapshot=snapshot),
|
|
184
234
|
"",
|
|
@@ -206,7 +256,14 @@ class PromptBuilder:
|
|
|
206
256
|
)
|
|
207
257
|
return "\n\n".join(sections).strip() + "\n"
|
|
208
258
|
|
|
209
|
-
def _turn_driver_block(
|
|
259
|
+
def _turn_driver_block(
|
|
260
|
+
self,
|
|
261
|
+
*,
|
|
262
|
+
turn_reason: str,
|
|
263
|
+
user_message: str,
|
|
264
|
+
turn_intent: str | None = None,
|
|
265
|
+
turn_mode: str | None = None,
|
|
266
|
+
) -> str:
|
|
210
267
|
normalized_reason = str(turn_reason or "user_message").strip() or "user_message"
|
|
211
268
|
lines = [f"- turn_reason: {normalized_reason}"]
|
|
212
269
|
if normalized_reason == "auto_continue":
|
|
@@ -228,9 +285,28 @@ class PromptBuilder:
|
|
|
228
285
|
preview = " ".join(str(user_message or "").split())
|
|
229
286
|
if len(preview) > 220:
|
|
230
287
|
preview = preview[:217].rstrip() + "..."
|
|
288
|
+
resolved_turn_intent = str(turn_intent or self._turn_intent(user_message)).strip() or "continue_stage"
|
|
289
|
+
resolved_turn_mode = str(turn_mode or "stage_execution").strip() or "stage_execution"
|
|
290
|
+
lines.append(f"- turn_intent: {resolved_turn_intent}")
|
|
291
|
+
lines.append(f"- turn_mode: {resolved_turn_mode}")
|
|
292
|
+
if resolved_turn_intent == "answer_user_question_first":
|
|
293
|
+
lines.append(
|
|
294
|
+
"- 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."
|
|
295
|
+
)
|
|
296
|
+
lines.append(
|
|
297
|
+
"- 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."
|
|
298
|
+
)
|
|
299
|
+
elif resolved_turn_intent == "execute_user_command_first":
|
|
300
|
+
lines.append(
|
|
301
|
+
"- command_first_rule: the user primarily gave a concrete instruction. Execute or acknowledge that instruction first before resuming background stage narration."
|
|
302
|
+
)
|
|
231
303
|
lines.append(f"- direct_user_message_preview: {preview or 'none'}")
|
|
232
304
|
return "\n".join(lines)
|
|
233
305
|
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _turn_intent(user_message: str) -> str:
|
|
308
|
+
return classify_turn_intent(user_message)
|
|
309
|
+
|
|
234
310
|
def _active_communication_surface_block(
|
|
235
311
|
self,
|
|
236
312
|
*,
|
|
@@ -245,8 +321,6 @@ class PromptBuilder:
|
|
|
245
321
|
connector = surface_context["active_connector"]
|
|
246
322
|
chat_type = surface_context["active_chat_type"]
|
|
247
323
|
chat_id = surface_context["active_chat_id"]
|
|
248
|
-
qq_config = connectors_config.get("qq") if isinstance(connectors_config.get("qq"), dict) else {}
|
|
249
|
-
|
|
250
324
|
lines = [
|
|
251
325
|
f"- latest_user_source: {source}",
|
|
252
326
|
f"- active_surface: {surface}",
|
|
@@ -264,38 +338,16 @@ class PromptBuilder:
|
|
|
264
338
|
lines.extend(
|
|
265
339
|
[
|
|
266
340
|
"- 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.",
|
|
341
|
+
"- qq_reply_rule: keep outbound replies concise, respectful, text-first, and progress-aware.",
|
|
342
|
+
"- qq_detail_rule: rely on the QQ connector contract for detailed surface formatting instead of expanding it here.",
|
|
284
343
|
]
|
|
285
344
|
)
|
|
286
345
|
elif connector == "weixin":
|
|
287
346
|
lines.extend(
|
|
288
347
|
[
|
|
289
348
|
"- 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.",
|
|
349
|
+
"- weixin_reply_rule: keep outbound replies concise, respectful, text-first, and progress-aware.",
|
|
350
|
+
"- weixin_detail_rule: rely on the Weixin connector contract for detailed transport formatting instead of expanding it here.",
|
|
299
351
|
]
|
|
300
352
|
)
|
|
301
353
|
else:
|
|
@@ -472,7 +524,20 @@ class PromptBuilder:
|
|
|
472
524
|
pending_user_count = int(snapshot.get("pending_user_message_count") or 0)
|
|
473
525
|
if pending_user_count > 0:
|
|
474
526
|
return f"Poll artifact.interact(...) and handle the {pending_user_count} queued user message(s) first."
|
|
527
|
+
continuation_policy = str(snapshot.get("continuation_policy") or "auto").strip().lower() or "auto"
|
|
528
|
+
continuation_anchor = str(snapshot.get("continuation_anchor") or "").strip()
|
|
529
|
+
if continuation_policy == "wait_for_user_or_resume":
|
|
530
|
+
if continuation_anchor:
|
|
531
|
+
return (
|
|
532
|
+
f"The quest is intentionally parked after the latest durable checkpoint. Wait for a new user message or "
|
|
533
|
+
f"`/resume`, then continue from `{continuation_anchor}` instead of auto-continuing the previous stage."
|
|
534
|
+
)
|
|
535
|
+
return "The quest is intentionally parked after the latest durable checkpoint. Wait for a new user message or `/resume`."
|
|
536
|
+
if continuation_policy == "none":
|
|
537
|
+
return "Do not auto-continue this quest. Wait for an explicit new user instruction before doing more work."
|
|
475
538
|
active_anchor = str(snapshot.get("active_anchor") or "decision").strip() or "decision"
|
|
539
|
+
if continuation_anchor:
|
|
540
|
+
active_anchor = continuation_anchor
|
|
476
541
|
active_idea_id = str(snapshot.get("active_idea_id") or "").strip()
|
|
477
542
|
next_slice_id = str(snapshot.get("next_pending_slice_id") or "").strip()
|
|
478
543
|
active_campaign_id = str(snapshot.get("active_analysis_campaign_id") or "").strip()
|
|
@@ -489,6 +554,8 @@ class PromptBuilder:
|
|
|
489
554
|
"Continue idea analysis and route selection until the next durable idea branch is submitted "
|
|
490
555
|
"with `lineage_intent='continue_line'` or `lineage_intent='branch_alternative'`."
|
|
491
556
|
)
|
|
557
|
+
if active_anchor == "optimize":
|
|
558
|
+
return "Continue the optimization loop from the current frontier, candidate pool, durable runs, and branch state."
|
|
492
559
|
if active_anchor == "experiment":
|
|
493
560
|
return "Continue the main experiment workflow from the current workspace, logs, and recorded evidence."
|
|
494
561
|
if active_anchor == "analysis-campaign":
|
|
@@ -634,6 +701,24 @@ class PromptBuilder:
|
|
|
634
701
|
|
|
635
702
|
return "\n".join(lines)
|
|
636
703
|
|
|
704
|
+
@staticmethod
|
|
705
|
+
def _recovery_resume_block(*, snapshot: dict, turn_reason: str) -> str:
|
|
706
|
+
if str(turn_reason or "").strip() != "auto_continue":
|
|
707
|
+
return "- none"
|
|
708
|
+
source = str(snapshot.get("last_resume_source") or "").strip()
|
|
709
|
+
if not source.startswith("auto:daemon-recovery"):
|
|
710
|
+
return "- none"
|
|
711
|
+
lines = [
|
|
712
|
+
f"- resume_source: {source}",
|
|
713
|
+
f"- resumed_at: {snapshot.get('last_resume_at') or 'unknown'}",
|
|
714
|
+
f"- abandoned_run_id: {snapshot.get('last_recovery_abandoned_run_id') or 'none'}",
|
|
715
|
+
f"- recovery_summary: {snapshot.get('last_recovery_summary') or 'none'}",
|
|
716
|
+
"- 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.",
|
|
717
|
+
"- recovery_rule_2: if there is any new user message, handle that before blindly resuming the older subtask.",
|
|
718
|
+
"- 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.",
|
|
719
|
+
]
|
|
720
|
+
return "\n".join(lines)
|
|
721
|
+
|
|
637
722
|
def _prompt_fragment(self, relative_path: str | Path, *, quest_root: Path | None = None) -> str:
|
|
638
723
|
path = self._prompt_path(relative_path, quest_root=quest_root)
|
|
639
724
|
return self._markdown_body(path)
|
|
@@ -693,6 +778,15 @@ class PromptBuilder:
|
|
|
693
778
|
return value
|
|
694
779
|
return "standard"
|
|
695
780
|
|
|
781
|
+
@staticmethod
|
|
782
|
+
def _standard_profile(snapshot: dict) -> str:
|
|
783
|
+
startup_contract = snapshot.get("startup_contract")
|
|
784
|
+
if isinstance(startup_contract, dict):
|
|
785
|
+
value = str(startup_contract.get("standard_profile") or "").strip().lower()
|
|
786
|
+
if value in {"canonical_research_graph", "optimization_task"}:
|
|
787
|
+
return value
|
|
788
|
+
return "canonical_research_graph"
|
|
789
|
+
|
|
696
790
|
@staticmethod
|
|
697
791
|
def _custom_profile(snapshot: dict) -> str:
|
|
698
792
|
startup_contract = snapshot.get("startup_contract")
|
|
@@ -732,6 +826,7 @@ class PromptBuilder:
|
|
|
732
826
|
def _research_delivery_policy_block(self, snapshot: dict) -> str:
|
|
733
827
|
need_research_paper = self._need_research_paper(snapshot)
|
|
734
828
|
launch_mode = self._launch_mode(snapshot)
|
|
829
|
+
standard_profile = self._standard_profile(snapshot)
|
|
735
830
|
custom_profile = self._custom_profile(snapshot)
|
|
736
831
|
baseline_execution_policy = self._baseline_execution_policy(snapshot)
|
|
737
832
|
review_followup_policy = self._review_followup_policy(snapshot)
|
|
@@ -739,6 +834,7 @@ class PromptBuilder:
|
|
|
739
834
|
lines = [
|
|
740
835
|
f"- need_research_paper: {need_research_paper}",
|
|
741
836
|
f"- launch_mode: {launch_mode}",
|
|
837
|
+
f"- standard_profile: {standard_profile if launch_mode == 'standard' else 'n/a'}",
|
|
742
838
|
f"- custom_profile: {custom_profile if launch_mode == 'custom' else 'n/a'}",
|
|
743
839
|
f"- review_followup_policy: {review_followup_policy if custom_profile == 'review_audit' else 'n/a'}",
|
|
744
840
|
f"- baseline_execution_policy: {baseline_execution_policy if launch_mode == 'custom' else 'n/a'}",
|
|
@@ -833,6 +929,15 @@ class PromptBuilder:
|
|
|
833
929
|
"- 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
930
|
]
|
|
835
931
|
)
|
|
932
|
+
elif standard_profile == "optimization_task":
|
|
933
|
+
lines.extend(
|
|
934
|
+
[
|
|
935
|
+
"- 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.",
|
|
936
|
+
"- 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.",
|
|
937
|
+
"- 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.",
|
|
938
|
+
"- standard_optimization_iteration_rule: prefer more justified optimization attempts, branch promotion, or frontier cleanup over paper-facing packaging.",
|
|
939
|
+
]
|
|
940
|
+
)
|
|
836
941
|
if need_research_paper:
|
|
837
942
|
lines.extend(
|
|
838
943
|
[
|
|
@@ -847,6 +952,9 @@ class PromptBuilder:
|
|
|
847
952
|
lines.extend(
|
|
848
953
|
[
|
|
849
954
|
"- delivery_goal: the quest should pursue the strongest justified algorithmic result rather than paper packaging.",
|
|
955
|
+
"- optimization_object_rule: distinguish candidate briefs, durable optimization lines, and implementation-level optimization candidates; do not treat them as one object type.",
|
|
956
|
+
"- 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.",
|
|
957
|
+
"- 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
958
|
"- 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
959
|
"- no_paper_rule: do not default into `artifact.submit_paper_outline(...)`, `artifact.submit_paper_bundle(...)`, or `finalize` while this mode remains active.",
|
|
852
960
|
"- 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,6 +963,69 @@ class PromptBuilder:
|
|
|
855
963
|
)
|
|
856
964
|
return "\n".join(lines)
|
|
857
965
|
|
|
966
|
+
def _optimization_frontier_block(self, snapshot: dict, quest_root: Path) -> str:
|
|
967
|
+
active_anchor = str(snapshot.get("active_anchor") or "").strip().lower()
|
|
968
|
+
if self._need_research_paper(snapshot) and active_anchor != "optimize":
|
|
969
|
+
return "- not primary in the current delivery mode"
|
|
970
|
+
|
|
971
|
+
try:
|
|
972
|
+
from ..artifact import ArtifactService
|
|
973
|
+
|
|
974
|
+
payload = ArtifactService(self.home).get_optimization_frontier(quest_root)
|
|
975
|
+
except Exception:
|
|
976
|
+
payload = {"ok": False}
|
|
977
|
+
|
|
978
|
+
frontier = (
|
|
979
|
+
dict(payload.get("optimization_frontier") or {})
|
|
980
|
+
if isinstance(payload, dict) and isinstance(payload.get("optimization_frontier"), dict)
|
|
981
|
+
else {}
|
|
982
|
+
)
|
|
983
|
+
if not frontier:
|
|
984
|
+
return "- unavailable"
|
|
985
|
+
|
|
986
|
+
best_branch = dict(frontier.get("best_branch") or {}) if isinstance(frontier.get("best_branch"), dict) else {}
|
|
987
|
+
best_run = dict(frontier.get("best_run") or {}) if isinstance(frontier.get("best_run"), dict) else {}
|
|
988
|
+
backlog = dict(frontier.get("candidate_backlog") or {}) if isinstance(frontier.get("candidate_backlog"), dict) else {}
|
|
989
|
+
next_actions = [str(item).strip() for item in (frontier.get("recommended_next_actions") or []) if str(item).strip()]
|
|
990
|
+
stagnant = frontier.get("stagnant_branches") or []
|
|
991
|
+
fusion = frontier.get("fusion_candidates") or []
|
|
992
|
+
local_attempts = [
|
|
993
|
+
dict(item)
|
|
994
|
+
for item in (frontier.get("best_branch_recent_candidates") or [])
|
|
995
|
+
if isinstance(item, dict)
|
|
996
|
+
]
|
|
997
|
+
|
|
998
|
+
lines = [
|
|
999
|
+
f"- frontier_mode: {str(frontier.get('mode') or 'unknown')}",
|
|
1000
|
+
f"- frontier_reason: {str(frontier.get('frontier_reason') or 'none')}",
|
|
1001
|
+
f"- frontier_best_branch: {str(best_branch.get('branch_name') or best_branch.get('branch_no') or 'none')}",
|
|
1002
|
+
f"- frontier_best_run: {str(best_run.get('run_id') or 'none')}",
|
|
1003
|
+
f"- frontier_candidate_briefs: {int(backlog.get('candidate_brief_count') or 0)}",
|
|
1004
|
+
f"- frontier_active_implementation_candidates: {int(backlog.get('active_implementation_candidate_count') or 0)}",
|
|
1005
|
+
f"- frontier_failed_implementation_candidates: {int(backlog.get('failed_implementation_candidate_count') or 0)}",
|
|
1006
|
+
f"- frontier_stagnant_branch_count: {len([item for item in stagnant if isinstance(item, dict)])}",
|
|
1007
|
+
f"- frontier_fusion_candidate_count: {len([item for item in fusion if isinstance(item, dict)])}",
|
|
1008
|
+
"- optimization_frontier_rule: in algorithm-first work, treat this block as the primary route-selection surface before relying on paper-facing state.",
|
|
1009
|
+
]
|
|
1010
|
+
if local_attempts:
|
|
1011
|
+
parts: list[str] = []
|
|
1012
|
+
for item in local_attempts[-3:]:
|
|
1013
|
+
summary_bits = [
|
|
1014
|
+
str(item.get("candidate_id") or "").strip() or "candidate",
|
|
1015
|
+
str(item.get("status") or "").strip() or "unknown",
|
|
1016
|
+
str(item.get("strategy") or "").strip() or None,
|
|
1017
|
+
str(item.get("mechanism_family") or "").strip() or None,
|
|
1018
|
+
str(item.get("failure_kind") or "").strip() or None,
|
|
1019
|
+
]
|
|
1020
|
+
parts.append(" / ".join(bit for bit in summary_bits if bit))
|
|
1021
|
+
lines.append(f"- frontier_same_line_local_attempt_memory: {' | '.join(parts)}")
|
|
1022
|
+
lines.append(
|
|
1023
|
+
"- 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."
|
|
1024
|
+
)
|
|
1025
|
+
if next_actions:
|
|
1026
|
+
lines.append(f"- frontier_next_actions: {' | '.join(next_actions[:3])}")
|
|
1027
|
+
return "\n".join(lines)
|
|
1028
|
+
|
|
858
1029
|
def _interaction_style_block(self, *, default_locale: str, user_message: str, snapshot: dict) -> str:
|
|
859
1030
|
normalized_locale = str(default_locale or "").lower()
|
|
860
1031
|
chinese_turn = normalized_locale.startswith("zh") or bool(re.search(r"[\u4e00-\u9fff]", user_message))
|
|
@@ -862,6 +1033,7 @@ class PromptBuilder:
|
|
|
862
1033
|
need_research_paper = self._need_research_paper(snapshot)
|
|
863
1034
|
decision_policy = self._decision_policy(snapshot)
|
|
864
1035
|
launch_mode = self._launch_mode(snapshot)
|
|
1036
|
+
standard_profile = self._standard_profile(snapshot)
|
|
865
1037
|
custom_profile = self._custom_profile(snapshot)
|
|
866
1038
|
lines = [
|
|
867
1039
|
f"- configured_default_locale: {default_locale}",
|
|
@@ -869,6 +1041,7 @@ class PromptBuilder:
|
|
|
869
1041
|
f"- bound_conversation_count: {len(bound_conversations)}",
|
|
870
1042
|
f"- decision_policy: {decision_policy}",
|
|
871
1043
|
f"- launch_mode: {launch_mode}",
|
|
1044
|
+
f"- standard_profile: {standard_profile if launch_mode == 'standard' else 'n/a'}",
|
|
872
1045
|
f"- custom_profile: {custom_profile if launch_mode == 'custom' else 'n/a'}",
|
|
873
1046
|
"- collaboration_mode: long-horizon, continuity-first, artifact-aware",
|
|
874
1047
|
"- response_pattern: say what changed -> say what it means -> say what happens next",
|
|
@@ -888,17 +1061,19 @@ class PromptBuilder:
|
|
|
888
1061
|
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
1062
|
"- 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
1063
|
"- 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
|
-
"-
|
|
1064
|
+
"- 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.",
|
|
1065
|
+
"- 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.",
|
|
1066
|
+
"- 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.",
|
|
1067
|
+
"- 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
1068
|
"- 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
|
|
1069
|
+
"- 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
1070
|
"- 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
1071
|
"- 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
1072
|
"- 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",
|
|
1073
|
+
"- 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
1074
|
"- 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
1075
|
"- 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
1076
|
"- 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
1077
|
"- workspace_discipline: read and modify code inside current_workspace_root; treat quest_root as the canonical repo identity and durable runtime root",
|
|
903
1078
|
"- binary_safety: do not open or rewrite large binary assets unless truly necessary; prefer summaries, metadata, and targeted inspection first",
|
|
904
1079
|
]
|
|
@@ -913,7 +1088,7 @@ class PromptBuilder:
|
|
|
913
1088
|
else:
|
|
914
1089
|
lines.extend(
|
|
915
1090
|
[
|
|
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.",
|
|
1091
|
+
"- 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
1092
|
"- user_gated_restraint: even in user-gated mode, do not turn ordinary progress or ordinary stage completion into blocking interrupts.",
|
|
918
1093
|
]
|
|
919
1094
|
)
|
|
@@ -925,6 +1100,10 @@ class PromptBuilder:
|
|
|
925
1100
|
lines.append(
|
|
926
1101
|
"- 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
1102
|
)
|
|
1103
|
+
if launch_mode == "standard" and standard_profile == "optimization_task":
|
|
1104
|
+
lines.append(
|
|
1105
|
+
"- 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."
|
|
1106
|
+
)
|
|
928
1107
|
if chinese_turn:
|
|
929
1108
|
lines.extend(
|
|
930
1109
|
[
|
|
@@ -942,141 +1121,40 @@ class PromptBuilder:
|
|
|
942
1121
|
return "\n".join(lines)
|
|
943
1122
|
|
|
944
1123
|
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()
|
|
1124
|
+
return "\n".join(
|
|
1125
|
+
[
|
|
1126
|
+
"- quest_context_rule: quest documents are durable but not pre-expanded here.",
|
|
1127
|
+
"- quest_documents_tool: call artifact.read_quest_documents(names=['brief','plan','status','summary'], mode='excerpt'|'full') when document detail is needed.",
|
|
1128
|
+
"- active_user_requirements_tool: call artifact.read_quest_documents(names=['active_user_requirements'], mode='full') when exact current durable user requirements matter.",
|
|
1129
|
+
]
|
|
1130
|
+
)
|
|
955
1131
|
|
|
956
1132
|
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
1133
|
confirmed_baseline_ref = (
|
|
968
1134
|
dict(snapshot.get("confirmed_baseline_ref") or {})
|
|
969
1135
|
if isinstance(snapshot.get("confirmed_baseline_ref"), dict)
|
|
970
|
-
else
|
|
1136
|
+
else {}
|
|
971
1137
|
)
|
|
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
1138
|
confirmed_metric_contract_json_rel_path = str(
|
|
978
|
-
|
|
1139
|
+
confirmed_baseline_ref.get("metric_contract_json_rel_path") or ""
|
|
979
1140
|
).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
1141
|
lines = [
|
|
987
1142
|
f"- baseline_gate: {snapshot.get('baseline_gate') or 'pending'}",
|
|
988
1143
|
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
1144
|
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
1145
|
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
1146
|
f"- active_analysis_campaign_id: {snapshot.get('active_analysis_campaign_id') or 'none'}",
|
|
1004
|
-
f"-
|
|
1147
|
+
f"- active_paper_line_ref: {snapshot.get('active_paper_line_ref') or 'none'}",
|
|
1148
|
+
f"- current_workspace_branch: {snapshot.get('current_workspace_branch') or 'none'}",
|
|
1149
|
+
f"- current_workspace_root: {snapshot.get('current_workspace_root') or 'none'}",
|
|
1005
1150
|
f"- workspace_mode: {snapshot.get('workspace_mode') or 'quest'}",
|
|
1006
1151
|
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
1152
|
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)}",
|
|
1153
|
+
f"- pending_user_message_count: {snapshot.get('pending_user_message_count') or 0}",
|
|
1154
|
+
f"- continuation_policy: {snapshot.get('continuation_policy') or 'auto'}",
|
|
1155
|
+
f"- continuation_anchor: {snapshot.get('continuation_anchor') or 'none'}",
|
|
1156
|
+
"- quest_state_tool: call artifact.get_quest_state(detail='summary'|'full') for current runtime refs, interactions, recent artifacts, and recent runs.",
|
|
1022
1157
|
]
|
|
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
1158
|
if confirmed_metric_contract_json_rel_path:
|
|
1081
1159
|
lines.extend(
|
|
1082
1160
|
[
|
|
@@ -1084,256 +1162,42 @@ class PromptBuilder:
|
|
|
1084
1162
|
"- 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
1163
|
]
|
|
1086
1164
|
)
|
|
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
1165
|
return "\n".join(lines)
|
|
1183
1166
|
|
|
1184
1167
|
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)
|
|
1168
|
+
paper_contract = (
|
|
1169
|
+
dict(snapshot.get("paper_contract") or {})
|
|
1170
|
+
if isinstance(snapshot.get("paper_contract"), dict)
|
|
1197
1171
|
else {}
|
|
1198
1172
|
)
|
|
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
1173
|
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)}",
|
|
1174
|
+
f"- selected_outline_ref: {str(paper_contract.get('selected_outline_ref') or 'none')}",
|
|
1175
|
+
f"- selected_outline_title: {str(paper_contract.get('title') or 'none')}",
|
|
1243
1176
|
]
|
|
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
|
-
]
|
|
1177
|
+
paper_contract_health = (
|
|
1178
|
+
dict(snapshot.get("paper_contract_health") or {})
|
|
1179
|
+
if isinstance(snapshot.get("paper_contract_health"), dict)
|
|
1180
|
+
else {}
|
|
1264
1181
|
)
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
latex_root_path = str(bundle_manifest.get("latex_root_path") or "").strip()
|
|
1182
|
+
if paper_contract_health:
|
|
1183
|
+
primary_blocker = str(
|
|
1184
|
+
((paper_contract_health.get("blocking_reasons") or [None])[0]) or "none"
|
|
1185
|
+
).strip() or "none"
|
|
1270
1186
|
lines.extend(
|
|
1271
1187
|
[
|
|
1272
|
-
"-
|
|
1273
|
-
f"-
|
|
1274
|
-
f"-
|
|
1275
|
-
f"-
|
|
1276
|
-
f"-
|
|
1277
|
-
|
|
1188
|
+
f"- paper_contract_health: {'ready' if bool(paper_contract_health.get('writing_ready')) else 'blocked'}",
|
|
1189
|
+
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)}",
|
|
1190
|
+
f"- paper_recommended_next_stage: {str(paper_contract_health.get('recommended_next_stage') or 'none')}",
|
|
1191
|
+
f"- paper_recommended_action: {str(paper_contract_health.get('recommended_action') or 'none')}",
|
|
1192
|
+
f"- paper_primary_blocker: {primary_blocker}",
|
|
1193
|
+
"- paper_health_tool: call artifact.get_paper_contract_health(detail='full') before paper-facing write/finalize work when the exact blocking items matter.",
|
|
1194
|
+
"- paper_outline_tool: call artifact.list_paper_outlines(...) when outline inventory or a valid outline_id is needed.",
|
|
1195
|
+
"- paper_campaign_tool: call artifact.get_analysis_campaign(campaign_id='active') when exact supplementary slice status matters.",
|
|
1278
1196
|
]
|
|
1279
1197
|
)
|
|
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
1198
|
lines.append(
|
|
1315
|
-
|
|
1199
|
+
"- 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
1200
|
)
|
|
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
1201
|
return "\n".join(lines)
|
|
1338
1202
|
|
|
1339
1203
|
def _priority_memory_block(
|
|
@@ -1346,46 +1210,15 @@ class PromptBuilder:
|
|
|
1346
1210
|
) -> str:
|
|
1347
1211
|
stage = active_anchor if active_anchor in STAGE_MEMORY_PLAN else skill_id
|
|
1348
1212
|
plan = STAGE_MEMORY_PLAN.get(stage, STAGE_MEMORY_PLAN["decision"])
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
limit=2,
|
|
1359
|
-
)
|
|
1360
|
-
if not cards:
|
|
1361
|
-
continue
|
|
1362
|
-
self._append_priority_memory(
|
|
1363
|
-
selected,
|
|
1364
|
-
seen_paths,
|
|
1365
|
-
card=cards[-1],
|
|
1366
|
-
scope=scope,
|
|
1367
|
-
quest_root=quest_root,
|
|
1368
|
-
reason=f"recent {stage} {kind} memory",
|
|
1369
|
-
)
|
|
1370
|
-
if len(selected) >= 6:
|
|
1371
|
-
return self._format_priority_memory(selected)
|
|
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")
|
|
1377
|
-
self._append_priority_memory(
|
|
1378
|
-
selected,
|
|
1379
|
-
seen_paths,
|
|
1380
|
-
card=card,
|
|
1381
|
-
scope=scope,
|
|
1382
|
-
quest_root=quest_root,
|
|
1383
|
-
reason=f"matches current user message: `{query}`",
|
|
1384
|
-
)
|
|
1385
|
-
if len(selected) >= 8:
|
|
1386
|
-
return self._format_priority_memory(selected)
|
|
1387
|
-
|
|
1388
|
-
return self._format_priority_memory(selected)
|
|
1213
|
+
quest_kinds = ", ".join(plan.get("quest", ())) or "none"
|
|
1214
|
+
global_kinds = ", ".join(plan.get("global", ())) or "none"
|
|
1215
|
+
return "\n".join(
|
|
1216
|
+
[
|
|
1217
|
+
f"- stage_memory_rule: for `{stage}`, prefer quest memory kinds [{quest_kinds}] and global memory kinds [{global_kinds}] when memory lookup is needed.",
|
|
1218
|
+
"- memory_lookup_tool: call memory.list_recent(...) to recover context after pause/restart and memory.search(...) before repeating prior work.",
|
|
1219
|
+
"- memory_injection_rule: memory is intentionally not pre-expanded here; pull only the cards that matter now.",
|
|
1220
|
+
]
|
|
1221
|
+
)
|
|
1389
1222
|
|
|
1390
1223
|
def _append_priority_memory(
|
|
1391
1224
|
self,
|
|
@@ -1447,20 +1280,12 @@ class PromptBuilder:
|
|
|
1447
1280
|
return tokens
|
|
1448
1281
|
|
|
1449
1282
|
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)
|
|
1283
|
+
return "\n".join(
|
|
1284
|
+
[
|
|
1285
|
+
"- conversation_context_rule: recent conversation is not pre-expanded here.",
|
|
1286
|
+
f"- conversation_tool: call artifact.get_conversation_context(limit={limit}, include_attachments=False) when earlier turn continuity matters.",
|
|
1287
|
+
]
|
|
1288
|
+
)
|
|
1464
1289
|
|
|
1465
1290
|
def _markdown_body(self, path: Path) -> str:
|
|
1466
1291
|
text = path.read_text(encoding="utf-8")
|