@researai/deepscientist 1.5.7 → 1.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +186 -21
  2. package/README.md +8 -4
  3. package/bin/ds.js +224 -9
  4. package/docs/en/00_QUICK_START.md +2 -2
  5. package/docs/en/07_MEMORY_AND_MCP.md +40 -3
  6. package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
  7. package/docs/zh/00_QUICK_START.md +2 -2
  8. package/docs/zh/07_MEMORY_AND_MCP.md +40 -3
  9. package/docs/zh/99_ACKNOWLEDGEMENTS.md +1 -0
  10. package/install.sh +34 -0
  11. package/package.json +2 -2
  12. package/pyproject.toml +2 -2
  13. package/src/deepscientist/__init__.py +1 -1
  14. package/src/deepscientist/acp/envelope.py +1 -0
  15. package/src/deepscientist/artifact/metrics.py +814 -83
  16. package/src/deepscientist/artifact/schemas.py +1 -0
  17. package/src/deepscientist/artifact/service.py +2001 -229
  18. package/src/deepscientist/bash_exec/monitor.py +1 -1
  19. package/src/deepscientist/bash_exec/service.py +17 -9
  20. package/src/deepscientist/channels/qq.py +17 -0
  21. package/src/deepscientist/channels/relay.py +16 -0
  22. package/src/deepscientist/config/models.py +6 -0
  23. package/src/deepscientist/config/service.py +70 -2
  24. package/src/deepscientist/daemon/api/handlers.py +414 -14
  25. package/src/deepscientist/daemon/api/router.py +4 -0
  26. package/src/deepscientist/daemon/app.py +292 -21
  27. package/src/deepscientist/gitops/diff.py +6 -10
  28. package/src/deepscientist/mcp/server.py +191 -40
  29. package/src/deepscientist/prompts/builder.py +65 -19
  30. package/src/deepscientist/quest/node_traces.py +129 -2
  31. package/src/deepscientist/quest/service.py +140 -34
  32. package/src/deepscientist/quest/stage_views.py +175 -33
  33. package/src/deepscientist/registries/baseline.py +56 -4
  34. package/src/deepscientist/runners/codex.py +1 -1
  35. package/src/prompts/connectors/qq.md +1 -1
  36. package/src/prompts/contracts/shared_interaction.md +14 -0
  37. package/src/prompts/system.md +113 -32
  38. package/src/skills/analysis-campaign/SKILL.md +10 -14
  39. package/src/skills/baseline/SKILL.md +51 -38
  40. package/src/skills/baseline/references/baseline-plan-template.md +2 -0
  41. package/src/skills/decision/SKILL.md +12 -8
  42. package/src/skills/experiment/SKILL.md +28 -16
  43. package/src/skills/experiment/references/main-experiment-plan-template.md +2 -0
  44. package/src/skills/figure-polish/SKILL.md +1 -0
  45. package/src/skills/finalize/SKILL.md +3 -8
  46. package/src/skills/idea/SKILL.md +18 -8
  47. package/src/skills/idea/references/literature-survey-template.md +24 -0
  48. package/src/skills/idea/references/related-work-playbook.md +4 -0
  49. package/src/skills/idea/references/selection-gate.md +9 -0
  50. package/src/skills/intake-audit/SKILL.md +2 -8
  51. package/src/skills/rebuttal/SKILL.md +2 -8
  52. package/src/skills/review/SKILL.md +2 -8
  53. package/src/skills/scout/SKILL.md +2 -8
  54. package/src/skills/write/SKILL.md +53 -17
  55. package/src/skills/write/templates/DEEPSCIENTIST_NOTES.md +21 -0
  56. package/src/skills/write/templates/README.md +408 -0
  57. package/src/skills/write/templates/UPSTREAM_LICENSE.txt +21 -0
  58. package/src/skills/write/templates/aaai2026/README.md +534 -0
  59. package/src/skills/write/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
  60. package/src/skills/write/templates/aaai2026/aaai2026-unified-template.tex +952 -0
  61. package/src/skills/write/templates/aaai2026/aaai2026.bib +111 -0
  62. package/src/skills/write/templates/aaai2026/aaai2026.bst +1493 -0
  63. package/src/skills/write/templates/aaai2026/aaai2026.sty +315 -0
  64. package/src/skills/write/templates/acl/README.md +50 -0
  65. package/src/skills/write/templates/acl/acl.sty +312 -0
  66. package/src/skills/write/templates/acl/acl_latex.tex +377 -0
  67. package/src/skills/write/templates/acl/acl_lualatex.tex +101 -0
  68. package/src/skills/write/templates/acl/acl_natbib.bst +1940 -0
  69. package/src/skills/write/templates/acl/anthology.bib.txt +26 -0
  70. package/src/skills/write/templates/acl/custom.bib +70 -0
  71. package/src/skills/write/templates/acl/formatting.md +326 -0
  72. package/src/skills/write/templates/asplos2027/main.tex +459 -0
  73. package/src/skills/write/templates/asplos2027/references.bib +135 -0
  74. package/src/skills/write/templates/colm2025/README.md +3 -0
  75. package/src/skills/write/templates/colm2025/colm2025_conference.bib +11 -0
  76. package/src/skills/write/templates/colm2025/colm2025_conference.bst +1440 -0
  77. package/src/skills/write/templates/colm2025/colm2025_conference.sty +218 -0
  78. package/src/skills/write/templates/colm2025/colm2025_conference.tex +305 -0
  79. package/src/skills/write/templates/colm2025/fancyhdr.sty +485 -0
  80. package/src/skills/write/templates/colm2025/math_commands.tex +508 -0
  81. package/src/skills/write/templates/colm2025/natbib.sty +1246 -0
  82. package/src/skills/write/templates/iclr2026/fancyhdr.sty +485 -0
  83. package/src/skills/write/templates/iclr2026/iclr2026_conference.bib +24 -0
  84. package/src/skills/write/templates/iclr2026/iclr2026_conference.bst +1440 -0
  85. package/src/skills/write/templates/iclr2026/iclr2026_conference.sty +246 -0
  86. package/src/skills/write/templates/iclr2026/iclr2026_conference.tex +414 -0
  87. package/src/skills/write/templates/iclr2026/math_commands.tex +508 -0
  88. package/src/skills/write/templates/iclr2026/natbib.sty +1246 -0
  89. package/src/skills/write/templates/icml2026/algorithm.sty +79 -0
  90. package/src/skills/write/templates/icml2026/algorithmic.sty +201 -0
  91. package/src/skills/write/templates/icml2026/example_paper.bib +75 -0
  92. package/src/skills/write/templates/icml2026/example_paper.tex +662 -0
  93. package/src/skills/write/templates/icml2026/fancyhdr.sty +864 -0
  94. package/src/skills/write/templates/icml2026/icml2026.bst +1443 -0
  95. package/src/skills/write/templates/icml2026/icml2026.sty +767 -0
  96. package/src/skills/write/templates/neurips2025/Makefile +36 -0
  97. package/src/skills/write/templates/neurips2025/extra_pkgs.tex +53 -0
  98. package/src/skills/write/templates/neurips2025/main.tex +38 -0
  99. package/src/skills/write/templates/neurips2025/neurips.sty +382 -0
  100. package/src/skills/write/templates/nsdi2027/main.tex +426 -0
  101. package/src/skills/write/templates/nsdi2027/references.bib +151 -0
  102. package/src/skills/write/templates/nsdi2027/usenix-2020-09.sty +83 -0
  103. package/src/skills/write/templates/osdi2026/main.tex +429 -0
  104. package/src/skills/write/templates/osdi2026/references.bib +150 -0
  105. package/src/skills/write/templates/osdi2026/usenix-2020-09.sty +83 -0
  106. package/src/skills/write/templates/sosp2026/main.tex +532 -0
  107. package/src/skills/write/templates/sosp2026/references.bib +148 -0
  108. package/src/tui/package.json +1 -1
  109. package/src/ui/dist/assets/{AiManusChatView-BS3V4ZOk.js → AiManusChatView-BKZ103sn.js} +110 -14
  110. package/src/ui/dist/assets/{AnalysisPlugin-DLPXQsmr.js → AnalysisPlugin-mTTzGAlK.js} +1 -1
  111. package/src/ui/dist/assets/{AutoFigurePlugin-C-Fr9knQ.js → AutoFigurePlugin-C_wWw4AP.js} +5 -5
  112. package/src/ui/dist/assets/{CliPlugin-Dd8AHzFg.js → CliPlugin-BH58n3GY.js} +9 -9
  113. package/src/ui/dist/assets/{CodeEditorPlugin-Dg-RepTl.js → CodeEditorPlugin-BKGRUH7e.js} +8 -8
  114. package/src/ui/dist/assets/{CodeViewerPlugin-D2J_3nyt.js → CodeViewerPlugin-BMADwFWJ.js} +5 -5
  115. package/src/ui/dist/assets/{DocViewerPlugin-ChRLLKNb.js → DocViewerPlugin-ZOnTIHLN.js} +3 -3
  116. package/src/ui/dist/assets/{GitDiffViewerPlugin-DgHfcved.js → GitDiffViewerPlugin-CQ7h1Djm.js} +830 -86
  117. package/src/ui/dist/assets/{ImageViewerPlugin-C89GZMBy.js → ImageViewerPlugin-GVS5MsnC.js} +5 -5
  118. package/src/ui/dist/assets/{LabCopilotPanel-BUfIwUcb.js → LabCopilotPanel-BZNv1JML.js} +10 -10
  119. package/src/ui/dist/assets/{LabPlugin-zvUmQUMq.js → LabPlugin-TWcJsdQA.js} +1 -1
  120. package/src/ui/dist/assets/{LatexPlugin-C1SSNuWp.js → LatexPlugin-DIjHiR2x.js} +7 -7
  121. package/src/ui/dist/assets/{MarkdownViewerPlugin-D2Mf5tU5.js → MarkdownViewerPlugin-D3ooGAH0.js} +4 -4
  122. package/src/ui/dist/assets/{MarketplacePlugin-CF4LgiS2.js → MarketplacePlugin-DfVfE9hN.js} +3 -3
  123. package/src/ui/dist/assets/{NotebookEditor-BM7Bgwlv.js → NotebookEditor-DDl0_Mc0.js} +1 -1
  124. package/src/ui/dist/assets/{index-Be0NAmh8.js → NotebookEditor-s8JhzuX1.js} +12 -155
  125. package/src/ui/dist/assets/{PdfLoader-Bc5qfD-Z.js → PdfLoader-C2Sf6SJM.js} +1 -1
  126. package/src/ui/dist/assets/{PdfMarkdownPlugin-sh1-IRcp.js → PdfMarkdownPlugin-CXFLoIsa.js} +3 -3
  127. package/src/ui/dist/assets/{PdfViewerPlugin-C_a7CpWG.js → PdfViewerPlugin-BYTmz2fK.js} +10 -10
  128. package/src/ui/dist/assets/{SearchPlugin-L4z3HcLf.js → SearchPlugin-CjWBI1O9.js} +1 -1
  129. package/src/ui/dist/assets/{Stepper-Dk4aQ3fN.js → Stepper-B0Dd8CxK.js} +1 -1
  130. package/src/ui/dist/assets/{TextViewerPlugin-BsNtlKVo.js → TextViewerPlugin-DdOBU3-S.js} +4 -4
  131. package/src/ui/dist/assets/{VNCViewer-BpeDcZ5_.js → VNCViewer-B8HGgLwQ.js} +9 -9
  132. package/src/ui/dist/assets/{bibtex-C4QI-bbj.js → bibtex-CKaefIN2.js} +1 -1
  133. package/src/ui/dist/assets/{code-DuMINRsg.js → code-BWAY76JP.js} +1 -1
  134. package/src/ui/dist/assets/{file-content-C3N-432K.js → file-content-C1NwU5oQ.js} +1 -1
  135. package/src/ui/dist/assets/{file-diff-panel-CffQ4ZMg.js → file-diff-panel-CywslwB9.js} +1 -1
  136. package/src/ui/dist/assets/{file-socket-CRH59PCO.js → file-socket-B4kzuOBQ.js} +1 -1
  137. package/src/ui/dist/assets/{file-utils-vYGtW2mI.js → file-utils-H2fjA46S.js} +1 -1
  138. package/src/ui/dist/assets/{image-DBVGaooo.js → image-D-NZM-6P.js} +1 -1
  139. package/src/ui/dist/assets/{index-B1P6hQRJ.js → index-7Chr1g9c.js} +3734 -1862
  140. package/src/ui/dist/assets/{index-DjSFDmgB.js → index-BdM1Gqfr.js} +2 -2
  141. package/src/ui/dist/assets/{index-BpjYH9Vg.js → index-CDxNdQdz.js} +1 -1
  142. package/src/ui/dist/assets/{index-Do9N28uB.css → index-DGIYDuTv.css} +163 -34
  143. package/src/ui/dist/assets/index-DHZJ_0TI.js +159 -0
  144. package/src/ui/dist/assets/{message-square-BsPDBhiY.js → message-square-BzjLiXir.js} +1 -1
  145. package/src/ui/dist/assets/{monaco-BTkdPojV.js → monaco-Cb2uKKe6.js} +1 -1
  146. package/src/ui/dist/assets/{popover-cWjCk-vc.js → popover-Bg72DGgT.js} +1 -1
  147. package/src/ui/dist/assets/{project-sync-CXn530xb.js → project-sync-Ce_0BglY.js} +1 -1
  148. package/src/ui/dist/assets/{sigma-04Jr12jg.js → sigma-DPaACDrh.js} +1 -1
  149. package/src/ui/dist/assets/{tooltip-BdVDl0G5.js → tooltip-C_mA6R0w.js} +1 -1
  150. package/src/ui/dist/assets/{trash-CB_GlQyC.js → trash-BvTgE5__.js} +1 -1
  151. package/src/ui/dist/assets/{useCliAccess-BL932NwS.js → useCliAccess-CgPeMOwP.js} +1 -1
  152. package/src/ui/dist/assets/{useFileDiffOverlay-B2WK7Tvq.js → useFileDiffOverlay-xPhz7P5B.js} +1 -1
  153. package/src/ui/dist/assets/{wrap-text-YC68g12z.js → wrap-text-C3Un3YQr.js} +1 -1
  154. package/src/ui/dist/assets/{zoom-out-C0RJvFiJ.js → zoom-out-BgxLa0Ri.js} +1 -1
  155. package/src/ui/dist/index.html +5 -2
  156. /package/src/ui/dist/assets/{index-CccQYZjX.css → NotebookEditor-CccQYZjX.css} +0 -0
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- from pathlib import Path
4
+ from pathlib import Path, PurePosixPath
5
5
  from typing import Any
6
6
 
7
7
  from ..memory.frontmatter import load_markdown_document
@@ -117,7 +117,7 @@ class QuestStageViewBuilder:
117
117
  self.snapshot = dict(snapshot or {})
118
118
  self.selection = dict(selection or {})
119
119
  self.trace = dict(trace or {})
120
- self.workspace_root = Path(str(self.snapshot.get("active_workspace_root") or quest_root))
120
+ self.workspace_root = self._resolve_workspace_root()
121
121
  self.branch_name = (
122
122
  str(
123
123
  self.selection.get("branch_name")
@@ -135,8 +135,27 @@ class QuestStageViewBuilder:
135
135
  self.stage_status = str(self.selection.get("status") or self.trace.get("status") or "").strip() or None
136
136
  self.artifacts = sorted(list(self.quest_service._collect_artifacts(quest_root)), key=_artifact_sort_key)
137
137
 
138
+ def _resolve_workspace_root(self) -> Path:
139
+ for raw in (
140
+ self.selection.get("worktree_rel_path"),
141
+ self.trace.get("worktree_rel_path"),
142
+ self.selection.get("worktree_root"),
143
+ self.trace.get("worktree_root"),
144
+ self.snapshot.get("active_workspace_root"),
145
+ ):
146
+ text = str(raw or "").strip()
147
+ if not text:
148
+ continue
149
+ candidate = Path(text)
150
+ if not candidate.is_absolute():
151
+ candidate = (self.quest_root / text).resolve()
152
+ if candidate.exists():
153
+ return candidate
154
+ return self.quest_root
155
+
138
156
  def build(self) -> dict[str, Any]:
139
- if str(self.selection.get("selection_type") or "").strip() == "branch_node":
157
+ selection_type = str(self.selection.get("selection_type") or "").strip()
158
+ if selection_type == "branch_node" and self.stage_key not in {"experiment", "analysis", "paper"}:
140
159
  return self._build_branch()
141
160
  if self.stage_key == "baseline":
142
161
  return self._build_baseline()
@@ -208,7 +227,7 @@ class QuestStageViewBuilder:
208
227
  "idea": self._idea_scope_paths(),
209
228
  "experiment": self._experiment_scope_paths(None),
210
229
  "analysis": self._analysis_scope_paths(None),
211
- "paper": ["paper"],
230
+ "paper": self._paper_scope_paths(),
212
231
  }
213
232
  return defaults.get(self.stage_key, [])
214
233
 
@@ -314,6 +333,52 @@ class QuestStageViewBuilder:
314
333
  text = str(body or "").strip()
315
334
  return text or None
316
335
 
336
+ def _recent_trace_actions(self, *, limit: int = 6) -> list[dict[str, Any]]:
337
+ raw_actions = self.trace.get("actions") if isinstance(self.trace, dict) else []
338
+ if not isinstance(raw_actions, list) or not raw_actions:
339
+ return []
340
+ normalized: list[dict[str, Any]] = []
341
+ for item in raw_actions[-limit:]:
342
+ if not isinstance(item, dict):
343
+ continue
344
+ summary = _compact(item.get("summary") or item.get("output") or item.get("args"), limit=1000)
345
+ normalized.append(
346
+ {
347
+ "action_id": item.get("action_id"),
348
+ "title": item.get("title") or item.get("tool_name") or item.get("raw_event_type") or item.get("kind"),
349
+ "summary": summary,
350
+ "status": item.get("status"),
351
+ "created_at": item.get("created_at"),
352
+ "tool_name": item.get("tool_name"),
353
+ "artifact_kind": item.get("artifact_kind"),
354
+ }
355
+ )
356
+ return normalized
357
+
358
+ def _trace_summary(self) -> str | None:
359
+ if not isinstance(self.trace, dict):
360
+ return None
361
+ return _compact(
362
+ self.trace.get("summary")
363
+ or self.selection.get("summary")
364
+ or self.trace.get("title"),
365
+ limit=600,
366
+ )
367
+
368
+ def _trace_markdown(self, *, limit: int = 5) -> str | None:
369
+ items = self._recent_trace_actions(limit=limit)
370
+ if not items:
371
+ return None
372
+ lines: list[str] = []
373
+ for item in items:
374
+ title = str(item.get("title") or "Trace").strip() or "Trace"
375
+ summary = str(item.get("summary") or "").strip()
376
+ if summary:
377
+ lines.append(f"- **{title}**: {summary}")
378
+ else:
379
+ lines.append(f"- **{title}**")
380
+ return "\n".join(lines) if lines else None
381
+
317
382
  def _file_entry(
318
383
  self,
319
384
  raw_path: object,
@@ -356,9 +421,29 @@ class QuestStageViewBuilder:
356
421
  }
357
422
 
358
423
  def _paper_bundle_manifest(self) -> dict[str, Any]:
359
- payload = read_json(self.quest_root / "paper" / "paper_bundle_manifest.json", {})
424
+ payload = read_json(self._paper_root() / "paper_bundle_manifest.json", {})
360
425
  return payload if isinstance(payload, dict) else {}
361
426
 
427
+ def _paper_root(self) -> Path:
428
+ candidates = [self.workspace_root / "paper", self.quest_root / "paper"]
429
+ for candidate in candidates:
430
+ if candidate.exists():
431
+ return candidate
432
+ return candidates[0]
433
+
434
+ def _paper_scope_paths(self) -> list[str]:
435
+ try:
436
+ return [self._paper_root().relative_to(self.quest_root).as_posix()]
437
+ except ValueError:
438
+ return ["paper"]
439
+
440
+ def _open_source_root(self) -> Path:
441
+ candidates = [self.workspace_root / "release" / "open_source", self.quest_root / "release" / "open_source"]
442
+ for candidate in candidates:
443
+ if candidate.exists():
444
+ return candidate
445
+ return candidates[0]
446
+
362
447
  def _paper_relative_path(self, raw_path: object) -> str | None:
363
448
  resolved = self._path_in_quest(raw_path)
364
449
  if resolved is None:
@@ -370,9 +455,10 @@ class QuestStageViewBuilder:
370
455
  preferred = self._paper_relative_path(bundle_manifest.get("latex_root_path"))
371
456
  if preferred:
372
457
  return preferred
373
- for candidate in ("paper/latex", "paper/tex"):
374
- if (self.quest_root / candidate).exists():
375
- return candidate
458
+ paper_root = self._paper_root()
459
+ for candidate in (paper_root / "latex", paper_root / "tex"):
460
+ if candidate.exists():
461
+ return candidate.relative_to(self.quest_root).as_posix()
376
462
  return None
377
463
 
378
464
  def _paper_main_tex(self, latex_root_rel: str | None) -> str | None:
@@ -401,7 +487,7 @@ class QuestStageViewBuilder:
401
487
  guessed = str(PurePosixPath(main_tex_rel).with_suffix(".pdf"))
402
488
  if (self.quest_root / guessed).exists():
403
489
  candidates.append(guessed)
404
- for path in sorted((self.quest_root / "paper").glob("*.pdf")):
490
+ for path in sorted(self._paper_root().glob("*.pdf")):
405
491
  candidates.append(path.relative_to(self.quest_root).as_posix())
406
492
  deduped: list[str] = []
407
493
  seen: set[str] = set()
@@ -689,6 +775,8 @@ class QuestStageViewBuilder:
689
775
  or self.snapshot.get("active_idea_draft_path")
690
776
  or str(Path(worktree_root) / "memory" / "ideas" / idea_id / "draft.md")
691
777
  )
778
+ idea_markdown = self._markdown_body_for_path(idea_md_path)
779
+ idea_md_rel_path = self._relative_path_or_raw(idea_md_path)
692
780
  draft_md_rel_path = self._relative_path_or_raw(draft_md_path)
693
781
  draft_markdown = self._markdown_body_for_path(draft_md_path)
694
782
  lineage_intent = str(payload.get("lineage_intent") or details.get("lineage_intent") or "").strip() or None
@@ -761,9 +849,12 @@ class QuestStageViewBuilder:
761
849
  "risks": details.get("risks") or [],
762
850
  "evidence_paths": details.get("evidence_paths") or [],
763
851
  "lineage_intent": lineage_intent,
852
+ "idea_path": idea_md_rel_path,
853
+ "idea_markdown": idea_markdown,
764
854
  "draft_path": draft_md_rel_path,
765
855
  "draft_markdown": draft_markdown,
766
856
  "literature_files": literature_files,
857
+ "decision_reason": payload.get("reason"),
767
858
  }
768
859
  },
769
860
  lineage_intent=lineage_intent,
@@ -830,6 +921,7 @@ class QuestStageViewBuilder:
830
921
  if isinstance(latest_experiment_payload.get("paths"), dict)
831
922
  else {}
832
923
  )
924
+ latest_run_markdown = self._markdown_body_for_path(latest_experiment_paths.get("run_md"))
833
925
  latest_result_payload = (
834
926
  read_json(Path(str(latest_experiment_paths.get("result_json"))), {})
835
927
  if str(latest_experiment_paths.get("result_json") or "").strip()
@@ -865,6 +957,7 @@ class QuestStageViewBuilder:
865
957
  if str(analysis_manifest.get("campaign_id") or "").strip()
866
958
  else None
867
959
  )
960
+ analysis_summary_markdown = self._markdown_body_for_path(analysis_summary_path)
868
961
 
869
962
  branch_items = [
870
963
  item
@@ -948,7 +1041,7 @@ class QuestStageViewBuilder:
948
1041
  description="Merged analysis summary for this branch.",
949
1042
  ),
950
1043
  self._file_entry(
951
- "paper/selected_outline.json" if (self.quest_root / "paper" / "selected_outline.json").exists() else None,
1044
+ (self._paper_root() / "selected_outline.json") if (self._paper_root() / "selected_outline.json").exists() else None,
952
1045
  label="Selected Outline",
953
1046
  description="Current selected paper outline.",
954
1047
  ),
@@ -969,6 +1062,11 @@ class QuestStageViewBuilder:
969
1062
  "next_target": next_target,
970
1063
  "idea_draft_path": idea_draft_rel_path,
971
1064
  "idea_draft_markdown": idea_draft_markdown,
1065
+ "idea_hypothesis": latest_idea_details.get("hypothesis"),
1066
+ "idea_mechanism": latest_idea_details.get("mechanism"),
1067
+ "idea_expected_gain": latest_idea_details.get("expected_gain"),
1068
+ "idea_risks": latest_idea_details.get("risks") or [],
1069
+ "decision_reason": latest_idea_payload.get("reason"),
972
1070
  "latest_main_experiment": {
973
1071
  "run_id": latest_run_id,
974
1072
  "summary": latest_experiment_payload.get("summary"),
@@ -978,10 +1076,14 @@ class QuestStageViewBuilder:
978
1076
  "progress_eval": latest_progress_eval,
979
1077
  "evaluation_summary": latest_evaluation_summary,
980
1078
  "run_md_path": latest_experiment_paths.get("run_md"),
1079
+ "run_markdown": latest_run_markdown,
981
1080
  "result_json_path": latest_experiment_paths.get("result_json"),
1081
+ "result_payload": latest_result_payload,
982
1082
  }
983
1083
  if latest_run_id
984
1084
  else None,
1085
+ "analysis_summary_path": self._relative_path_or_raw(analysis_summary_path),
1086
+ "analysis_summary_markdown": analysis_summary_markdown,
985
1087
  }
986
1088
  },
987
1089
  lineage_intent=lineage_intent,
@@ -1018,8 +1120,17 @@ class QuestStageViewBuilder:
1018
1120
  baseline_ref = payload.get("baseline_ref") or result_payload.get("baseline_ref") or {}
1019
1121
  evaluation_summary = _evaluation_summary(payload.get("evaluation_summary") or result_payload.get("evaluation_summary"))
1020
1122
  run_id = str(payload.get("run_id") or "pending").strip() or "pending"
1123
+ run_markdown = self._markdown_body_for_path(paths.get("run_md"))
1124
+ trace_summary = self._trace_summary()
1125
+ trace_markdown = self._trace_markdown()
1021
1126
  note = (
1022
- str(payload.get("summary") or result_payload.get("conclusion") or (progress_eval or {}).get("reason") or "").strip()
1127
+ str(
1128
+ payload.get("summary")
1129
+ or result_payload.get("conclusion")
1130
+ or (progress_eval or {}).get("reason")
1131
+ or trace_summary
1132
+ or ""
1133
+ ).strip()
1023
1134
  or "No durable main experiment result has been recorded yet."
1024
1135
  )
1025
1136
  title = f"Experiment · {run_id}"
@@ -1080,7 +1191,13 @@ class QuestStageViewBuilder:
1080
1191
  "metrics_summary": metrics_summary,
1081
1192
  "progress_eval": progress_eval,
1082
1193
  "evaluation_summary": evaluation_summary,
1194
+ "run_path": self._relative_path_or_raw(paths.get("run_md")),
1195
+ "run_markdown": run_markdown,
1196
+ "result_json_path": self._relative_path_or_raw(paths.get("result_json")),
1083
1197
  "result_payload": result_payload,
1198
+ "trace_summary": trace_summary,
1199
+ "trace_markdown": trace_markdown,
1200
+ "trace_actions": self._recent_trace_actions(),
1084
1201
  }
1085
1202
  },
1086
1203
  )
@@ -1106,7 +1223,9 @@ class QuestStageViewBuilder:
1106
1223
  flow_type = str(payload.get("flow_type") or "").strip()
1107
1224
  if flow_type not in {"analysis_campaign", "analysis_slice"}:
1108
1225
  continue
1109
- if campaign_id and str(payload.get("campaign_id") or "").strip() != campaign_id:
1226
+ if campaign_id:
1227
+ if str(payload.get("campaign_id") or "").strip() == campaign_id:
1228
+ items.append(item)
1110
1229
  continue
1111
1230
  if self._branch_matches(payload, allow_parent=True, include_unscoped=False):
1112
1231
  items.append(item)
@@ -1189,13 +1308,23 @@ class QuestStageViewBuilder:
1189
1308
  "plan_path": item.get("plan_path"),
1190
1309
  "result_path": item.get("result_path"),
1191
1310
  "mirror_path": item.get("mirror_path"),
1311
+ "plan_markdown": self._markdown_body_for_path(item.get("plan_path")),
1312
+ "result_markdown": self._markdown_body_for_path(item.get("result_path")),
1313
+ "mirror_markdown": self._markdown_body_for_path(item.get("mirror_path")),
1192
1314
  }
1193
1315
  )
1194
1316
  title = f"Analysis · {campaign_id}"
1317
+ trace_summary = self._trace_summary()
1195
1318
  note = (
1196
- str(latest_payload.get("summary") or manifest.get("goal") or "").strip()
1319
+ str(latest_payload.get("summary") or manifest.get("goal") or trace_summary or "").strip()
1197
1320
  or "No durable analysis campaign has been created yet."
1198
1321
  )
1322
+ summary_path = (
1323
+ self.quest_root / "experiments" / "analysis-results" / campaign_id / "SUMMARY.md"
1324
+ if campaign_id != "pending"
1325
+ else None
1326
+ )
1327
+ summary_markdown = self._markdown_body_for_path(summary_path) if summary_path else None
1199
1328
  key_files = self._dedupe_files(
1200
1329
  [
1201
1330
  self._file_entry(manifest.get("_manifest_path"), label="Campaign Manifest", description="Structured analysis campaign manifest."),
@@ -1254,7 +1383,14 @@ class QuestStageViewBuilder:
1254
1383
  "summary": latest_payload.get("summary"),
1255
1384
  "manifest_path": manifest.get("_manifest_path"),
1256
1385
  "charter_path": manifest.get("charter_path"),
1386
+ "charter_markdown": self._markdown_body_for_path(manifest.get("charter_path")),
1257
1387
  "todo_manifest_path": manifest.get("todo_manifest_path"),
1388
+ "todo_manifest_markdown": self._markdown_body_for_path(manifest.get("todo_manifest_path")),
1389
+ "summary_path": self._relative_path_or_raw(summary_path) if summary_path else None,
1390
+ "summary_markdown": summary_markdown,
1391
+ "trace_summary": trace_summary,
1392
+ "trace_markdown": self._trace_markdown(),
1393
+ "trace_actions": self._recent_trace_actions(),
1258
1394
  }
1259
1395
  },
1260
1396
  )
@@ -1264,23 +1400,25 @@ class QuestStageViewBuilder:
1264
1400
  latex_root_rel = self._paper_latex_root(bundle_manifest)
1265
1401
  main_tex_rel = self._paper_main_tex(latex_root_rel)
1266
1402
  pdf_candidates = self._paper_pdf_candidates(bundle_manifest, main_tex_rel=main_tex_rel)
1267
- candidates = sorted((self.quest_root / "paper" / "outlines" / "candidates").glob("*.json"))
1403
+ paper_root = self._paper_root()
1404
+ open_source_root = self._open_source_root()
1405
+ candidates = sorted((paper_root / "outlines" / "candidates").glob("*.json"))
1268
1406
  files: list[dict[str, Any] | None] = [
1269
1407
  *[
1270
1408
  self._file_entry(path, label=f"Outline Candidate · {path.stem}", description="Paper outline candidate JSON.")
1271
1409
  for path in candidates
1272
1410
  ],
1273
- self._file_entry("paper/selected_outline.json", label="Selected Outline", description="Chosen paper outline."),
1274
- self._file_entry("paper/outline_selection.md", label="Outline Selection Note", description="Outline selection rationale."),
1275
- self._file_entry("paper/draft.md", label="Draft Markdown", description="Current paper draft."),
1276
- self._file_entry("paper/writing_plan.md", label="Writing Plan", description="Paper writing plan."),
1277
- self._file_entry("paper/references.bib", label="References", description="Bibliography file."),
1278
- self._file_entry("paper/claim_evidence_map.json", label="Claim-Evidence Map", description="Claim to evidence mapping."),
1279
- self._file_entry("paper/baseline_inventory.json", label="Baseline Inventory", description="Canonical and supplementary baseline inventory for writing."),
1280
- self._file_entry("paper/build/compile_report.json", label="Compile Report", description="Paper build/compile report."),
1281
- self._file_entry("paper/paper_bundle_manifest.json", label="Bundle Manifest", description="Final paper bundle manifest."),
1282
- self._file_entry("release/open_source/manifest.json", label="Open Source Manifest", description="Open-source cleanup and release preparation manifest."),
1283
- self._file_entry("release/open_source/cleanup_plan.md", label="Open Source Cleanup Plan", description="Checklist for cleaning the paper branch into a public release."),
1411
+ self._file_entry(paper_root / "selected_outline.json", label="Selected Outline", description="Chosen paper outline."),
1412
+ self._file_entry(paper_root / "outline_selection.md", label="Outline Selection Note", description="Outline selection rationale."),
1413
+ self._file_entry(paper_root / "draft.md", label="Draft Markdown", description="Current paper draft."),
1414
+ self._file_entry(paper_root / "writing_plan.md", label="Writing Plan", description="Paper writing plan."),
1415
+ self._file_entry(paper_root / "references.bib", label="References", description="Bibliography file."),
1416
+ self._file_entry(paper_root / "claim_evidence_map.json", label="Claim-Evidence Map", description="Claim to evidence mapping."),
1417
+ self._file_entry(paper_root / "baseline_inventory.json", label="Baseline Inventory", description="Canonical and supplementary baseline inventory for writing."),
1418
+ self._file_entry(paper_root / "build" / "compile_report.json", label="Compile Report", description="Paper build/compile report."),
1419
+ self._file_entry(paper_root / "paper_bundle_manifest.json", label="Bundle Manifest", description="Final paper bundle manifest."),
1420
+ self._file_entry(open_source_root / "manifest.json", label="Open Source Manifest", description="Open-source cleanup and release preparation manifest."),
1421
+ self._file_entry(open_source_root / "cleanup_plan.md", label="Open Source Cleanup Plan", description="Checklist for cleaning the paper branch into a public release."),
1284
1422
  self._file_entry(latex_root_rel, label="LaTeX Sources", description="LaTeX source folder.", expected_kind="directory"),
1285
1423
  self._file_entry(main_tex_rel, label="Main TeX", description="Primary TeX source file."),
1286
1424
  ]
@@ -1290,7 +1428,7 @@ class QuestStageViewBuilder:
1290
1428
 
1291
1429
  def _paper_candidates(self) -> list[dict[str, Any]]:
1292
1430
  candidates: list[dict[str, Any]] = []
1293
- for path in sorted((self.quest_root / "paper" / "outlines" / "candidates").glob("*.json")):
1431
+ for path in sorted((self._paper_root() / "outlines" / "candidates").glob("*.json")):
1294
1432
  payload = read_json(path, {})
1295
1433
  if not isinstance(payload, dict):
1296
1434
  payload = {}
@@ -1319,7 +1457,8 @@ class QuestStageViewBuilder:
1319
1457
  )
1320
1458
  and self._branch_matches(self._payload(item), allow_parent=True)
1321
1459
  ]
1322
- selected_outline_path = self.quest_root / "paper" / "selected_outline.json"
1460
+ paper_root = self._paper_root()
1461
+ selected_outline_path = paper_root / "selected_outline.json"
1323
1462
  selected_outline = read_json(selected_outline_path, {}) if selected_outline_path.exists() else {}
1324
1463
  if not isinstance(selected_outline, dict):
1325
1464
  selected_outline = {}
@@ -1329,15 +1468,18 @@ class QuestStageViewBuilder:
1329
1468
  else {}
1330
1469
  )
1331
1470
  candidates = self._paper_candidates()
1332
- compile_report = read_json(self.quest_root / "paper" / "build" / "compile_report.json", {})
1471
+ compile_report = read_json(paper_root / "build" / "compile_report.json", {})
1333
1472
  if not isinstance(compile_report, dict):
1334
1473
  compile_report = {}
1335
1474
  bundle_manifest = self._paper_bundle_manifest()
1336
1475
  latex_root_rel = self._paper_latex_root(bundle_manifest)
1337
1476
  main_tex_rel = self._paper_main_tex(latex_root_rel)
1338
- references_bib = read_text(self.quest_root / "paper" / "references.bib", "")
1477
+ references_bib = read_text(paper_root / "references.bib", "")
1339
1478
  references_count = sum(1 for line in references_bib.splitlines() if line.lstrip().startswith("@"))
1340
1479
  pdf_paths = self._paper_pdf_candidates(bundle_manifest, main_tex_rel=main_tex_rel)
1480
+ draft_rel = self._paper_relative_path(paper_root / "draft.md") or "paper/draft.md"
1481
+ writing_plan_rel = self._paper_relative_path(paper_root / "writing_plan.md") or "paper/writing_plan.md"
1482
+ claim_map_rel = self._paper_relative_path(paper_root / "claim_evidence_map.json") or "paper/claim_evidence_map.json"
1341
1483
  selected_title = str(
1342
1484
  detailed.get("title") or selected_outline.get("title") or bundle_manifest.get("title") or "Drafting"
1343
1485
  ).strip() or "Drafting"
@@ -1356,7 +1498,7 @@ class QuestStageViewBuilder:
1356
1498
  overview=[
1357
1499
  _field("Selected Outline", selected_title if selected_outline else "Not selected"),
1358
1500
  _field("Candidate Count", len(candidates)),
1359
- _field("Draft Status", "present" if (self.quest_root / "paper" / "draft.md").exists() else "missing"),
1501
+ _field("Draft Status", "present" if (paper_root / "draft.md").exists() else "missing"),
1360
1502
  _field("Bundle Status", "present" if bundle_manifest else "missing"),
1361
1503
  ],
1362
1504
  key_facts=[
@@ -1377,10 +1519,10 @@ class QuestStageViewBuilder:
1377
1519
  "outline_candidates": candidates,
1378
1520
  "selected_outline": selected_outline,
1379
1521
  "drafting": {
1380
- "writing_plan_path": "paper/writing_plan.md",
1381
- "draft_path": "paper/draft.md",
1522
+ "writing_plan_path": writing_plan_rel,
1523
+ "draft_path": draft_rel,
1382
1524
  "references_count": references_count,
1383
- "claim_evidence_map_path": "paper/claim_evidence_map.json",
1525
+ "claim_evidence_map_path": claim_map_rel,
1384
1526
  },
1385
1527
  "build": {
1386
1528
  "compile_report": compile_report,
@@ -22,26 +22,49 @@ class BaselineRegistry:
22
22
  self.reconcile_confirmed_quests()
23
23
  entry_files = sorted(self.entries_root.glob("*.yaml"))
24
24
  if entry_files:
25
- return sorted((self._load_entry_file(path) for path in entry_files), key=self._entry_sort_key)
25
+ return sorted(
26
+ (
27
+ entry
28
+ for path in entry_files
29
+ for entry in [self._load_entry_file(path)]
30
+ if not self._is_deleted_entry(entry)
31
+ ),
32
+ key=self._entry_sort_key,
33
+ )
26
34
 
27
35
  latest_by_id: dict[str, dict] = {}
28
36
  for item in self._history_entries():
29
37
  baseline_id = str(item.get("baseline_id") or item.get("entry_id") or "").strip()
30
38
  if baseline_id:
31
39
  latest_by_id[baseline_id] = item
32
- return sorted(latest_by_id.values(), key=self._entry_sort_key)
40
+ return sorted(
41
+ (item for item in latest_by_id.values() if not self._is_deleted_entry(item)),
42
+ key=self._entry_sort_key,
43
+ )
33
44
 
34
- def get(self, baseline_id: str) -> dict | None:
45
+ def get(self, baseline_id: str, *, include_deleted: bool = False) -> dict | None:
35
46
  normalized_id = self._normalize_identifier(baseline_id, field_name="Baseline id")
36
47
  path = self._entry_path(normalized_id)
37
48
  if path.exists():
38
- return self._load_entry_file(path)
49
+ entry = self._load_entry_file(path)
50
+ if self._is_deleted_entry(entry) and not include_deleted:
51
+ return None
52
+ return entry
39
53
  latest_match = None
40
54
  for item in self._history_entries():
41
55
  if item.get("baseline_id") == normalized_id or item.get("entry_id") == normalized_id:
42
56
  latest_match = item
57
+ if self._is_deleted_entry(latest_match) and not include_deleted:
58
+ return None
43
59
  return latest_match
44
60
 
61
+ def is_deleted(self, baseline_id: str) -> bool:
62
+ try:
63
+ entry = self.get(baseline_id, include_deleted=True)
64
+ except ValueError:
65
+ return False
66
+ return self._is_deleted_entry(entry)
67
+
45
68
  def publish(self, entry: dict) -> dict:
46
69
  timestamp = utc_now()
47
70
  baseline_id = self._normalize_identifier(
@@ -201,6 +224,8 @@ class BaselineRegistry:
201
224
  }
202
225
 
203
226
  existing = self._existing_entry(baseline_id)
227
+ if self._is_deleted_entry(existing):
228
+ continue
204
229
  if self._entry_needs_publish(existing, entry):
205
230
  synchronized.append(self.publish(entry))
206
231
  elif existing:
@@ -244,6 +269,27 @@ class BaselineRegistry:
244
269
  write_yaml(attachment_root / "attachment.yaml", attachment)
245
270
  return attachment
246
271
 
272
+ def delete(self, baseline_id: str) -> dict:
273
+ normalized_id = self._normalize_identifier(baseline_id, field_name="Baseline id")
274
+ existing = self.get(normalized_id, include_deleted=True) or {}
275
+ timestamp = utc_now()
276
+ deleted_entry = {
277
+ **existing,
278
+ "registry_kind": "baseline",
279
+ "schema_version": 1,
280
+ "entry_id": normalized_id,
281
+ "baseline_id": normalized_id,
282
+ "status": "deleted",
283
+ "updated_at": timestamp,
284
+ "deleted_at": timestamp,
285
+ "summary": str(existing.get("summary") or "").strip(),
286
+ }
287
+ if not deleted_entry.get("created_at"):
288
+ deleted_entry["created_at"] = timestamp
289
+ write_yaml(self._entry_path(normalized_id), deleted_entry)
290
+ append_jsonl(self.index_path, deleted_entry)
291
+ return deleted_entry
292
+
247
293
  def _history_entries(self) -> list[dict]:
248
294
  return read_jsonl(self.index_path)
249
295
 
@@ -292,6 +338,12 @@ class BaselineRegistry:
292
338
  entry = self._load_entry_file(path)
293
339
  return entry if isinstance(entry, dict) and entry else None
294
340
 
341
+ @staticmethod
342
+ def _is_deleted_entry(entry: dict[str, Any] | None) -> bool:
343
+ if not isinstance(entry, dict):
344
+ return False
345
+ return str(entry.get("status") or "").strip().lower() == "deleted"
346
+
295
347
  @staticmethod
296
348
  def _entry_needs_publish(existing: dict[str, Any] | None, candidate: dict[str, Any]) -> bool:
297
349
  if not existing:
@@ -771,7 +771,7 @@ class CodexRunner:
771
771
  ]
772
772
  if request.approval_policy:
773
773
  command.extend(["-c", f'approval_policy="{request.approval_policy}"'])
774
- reasoning_effort = request.reasoning_effort if request.reasoning_effort is not None else "xhigh"
774
+ reasoning_effort = request.reasoning_effort
775
775
  if reasoning_effort:
776
776
  command.extend(["-c", f'model_reasoning_effort="{reasoning_effort}"'])
777
777
  tool_timeout_sec = self._positive_timeout_seconds(resolved_runner_config.get("mcp_tool_timeout_sec"))
@@ -10,7 +10,7 @@
10
10
  - qq_summary_first_rule: start with the conclusion the user cares about, then what it means, then the next action
11
11
  - qq_progress_shape_rule: make the current task, the main difficulty or latest real progress, and the next concrete measure explicit whenever possible
12
12
  - qq_eta_rule: for baseline reproduction, main experiments, analysis experiments, and other important long-running research phases, include a rough ETA for the next meaningful result or the next update; if uncertain, say that and still give the next check-in window
13
- - qq_tool_call_keepalive_rule: for ordinary active work, if roughly 10 to 30 tool calls pass without a user-visible checkpoint, send one concise QQ progress update before continuing
13
+ - qq_tool_call_keepalive_rule: for ordinary active work, prefer one concise QQ progress update after roughly 10 tool calls when there is already a human-meaningful delta, and do not let work drift beyond roughly 20 tool calls or about 15 minutes without a user-visible checkpoint
14
14
  - qq_internal_detail_rule: omit worker names, heartbeat timestamps, retry counters, pending/running/completed counts, file names, and monitor-window narration unless the user asked for them or the detail changes the recommended action
15
15
  - qq_translation_rule: convert internal execution and file-management work into user value, such as saying the baseline record is now organized for easier later comparison instead of listing touched files
16
16
  - qq_preflight_rule: before sending a QQ progress update, rewrite it if it still sounds like a monitoring log, execution diary, or file inventory
@@ -0,0 +1,14 @@
1
+ # Shared Interaction Contract
2
+
3
+ This shared contract is injected once per turn and applies across the stage and companion skills that use `artifact.interact(...)` as the main user-visible continuity channel.
4
+
5
+ ## Shared interaction rules
6
+
7
+ - Treat `artifact.interact(...)` as the main long-lived communication thread across TUI, web, and bound connectors.
8
+ - If `artifact.interact(...)` returns queued user requirements, treat them as the highest-priority user instruction bundle before continuing the current stage or companion-skill task.
9
+ - Immediately follow any non-empty mailbox poll with another `artifact.interact(...)` update that confirms receipt; if the request is directly answerable, answer there, otherwise say the current subtask is paused, give a short plan plus nearest report-back point, and handle that request first.
10
+ - Emit `artifact.interact(kind='progress', reply_mode='threaded', ...)` when there is real user-visible progress: a meaningful checkpoint, route-shaping update, or a concise keepalive once active work has crossed roughly 10 tool calls with a human-meaningful delta. Do not let ordinary active work drift beyond roughly 20 tool calls or about 15 minutes without a user-visible update.
11
+ - Keep progress updates chat-like and easy to understand: say what changed, what it means, and what happens next.
12
+ - Default to plain-language summaries. Do not mention file paths, artifact ids, branch/worktree ids, session ids, raw commands, or raw logs unless the user asks or needs them to act.
13
+ - Use `reply_mode='blocking'` only for real user decisions that cannot be resolved from local evidence.
14
+ - For any blocking decision request, provide 1 to 3 concrete options, put the recommended option first, explain each option's actual content plus pros and cons, and wait up to 1 day when feasible. If the blocker is a missing external credential or secret that only the user can provide, keep the quest waiting, ask the user to supply it or choose an alternative, and do not self-resolve; if resumed without that credential and no other work is possible, a long low-frequency wait such as `bash_exec(command='sleep 3600', mode='await', timeout_seconds=3700)` is acceptable. Otherwise choose the best option yourself and notify the user of the chosen option if the timeout expires.