@researai/deepscientist 1.5.7 → 1.5.8

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 (148) hide show
  1. package/README.md +4 -0
  2. package/bin/ds.js +220 -5
  3. package/docs/en/07_MEMORY_AND_MCP.md +40 -3
  4. package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
  5. package/docs/zh/07_MEMORY_AND_MCP.md +40 -3
  6. package/docs/zh/99_ACKNOWLEDGEMENTS.md +1 -0
  7. package/install.sh +34 -0
  8. package/package.json +1 -1
  9. package/pyproject.toml +1 -1
  10. package/src/deepscientist/__init__.py +1 -1
  11. package/src/deepscientist/acp/envelope.py +1 -0
  12. package/src/deepscientist/artifact/metrics.py +813 -80
  13. package/src/deepscientist/artifact/schemas.py +1 -0
  14. package/src/deepscientist/artifact/service.py +1101 -99
  15. package/src/deepscientist/bash_exec/monitor.py +1 -1
  16. package/src/deepscientist/bash_exec/service.py +17 -9
  17. package/src/deepscientist/channels/qq.py +17 -0
  18. package/src/deepscientist/channels/relay.py +16 -0
  19. package/src/deepscientist/config/models.py +6 -0
  20. package/src/deepscientist/config/service.py +70 -2
  21. package/src/deepscientist/daemon/api/handlers.py +284 -14
  22. package/src/deepscientist/daemon/api/router.py +1 -0
  23. package/src/deepscientist/daemon/app.py +291 -20
  24. package/src/deepscientist/gitops/diff.py +6 -10
  25. package/src/deepscientist/mcp/server.py +188 -39
  26. package/src/deepscientist/prompts/builder.py +51 -18
  27. package/src/deepscientist/quest/service.py +83 -34
  28. package/src/deepscientist/quest/stage_views.py +74 -29
  29. package/src/deepscientist/runners/codex.py +1 -1
  30. package/src/prompts/connectors/qq.md +1 -1
  31. package/src/prompts/contracts/shared_interaction.md +14 -0
  32. package/src/prompts/system.md +106 -32
  33. package/src/skills/analysis-campaign/SKILL.md +10 -14
  34. package/src/skills/baseline/SKILL.md +51 -38
  35. package/src/skills/baseline/references/baseline-plan-template.md +2 -0
  36. package/src/skills/decision/SKILL.md +12 -8
  37. package/src/skills/experiment/SKILL.md +28 -16
  38. package/src/skills/experiment/references/main-experiment-plan-template.md +2 -0
  39. package/src/skills/figure-polish/SKILL.md +1 -0
  40. package/src/skills/finalize/SKILL.md +3 -8
  41. package/src/skills/idea/SKILL.md +2 -8
  42. package/src/skills/intake-audit/SKILL.md +2 -8
  43. package/src/skills/rebuttal/SKILL.md +2 -8
  44. package/src/skills/review/SKILL.md +2 -8
  45. package/src/skills/scout/SKILL.md +2 -8
  46. package/src/skills/write/SKILL.md +52 -16
  47. package/src/skills/write/templates/DEEPSCIENTIST_NOTES.md +21 -0
  48. package/src/skills/write/templates/README.md +408 -0
  49. package/src/skills/write/templates/UPSTREAM_LICENSE.txt +21 -0
  50. package/src/skills/write/templates/aaai2026/README.md +534 -0
  51. package/src/skills/write/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
  52. package/src/skills/write/templates/aaai2026/aaai2026-unified-template.tex +952 -0
  53. package/src/skills/write/templates/aaai2026/aaai2026.bib +111 -0
  54. package/src/skills/write/templates/aaai2026/aaai2026.bst +1493 -0
  55. package/src/skills/write/templates/aaai2026/aaai2026.sty +315 -0
  56. package/src/skills/write/templates/acl/README.md +50 -0
  57. package/src/skills/write/templates/acl/acl.sty +312 -0
  58. package/src/skills/write/templates/acl/acl_latex.tex +377 -0
  59. package/src/skills/write/templates/acl/acl_lualatex.tex +101 -0
  60. package/src/skills/write/templates/acl/acl_natbib.bst +1940 -0
  61. package/src/skills/write/templates/acl/anthology.bib.txt +26 -0
  62. package/src/skills/write/templates/acl/custom.bib +70 -0
  63. package/src/skills/write/templates/acl/formatting.md +326 -0
  64. package/src/skills/write/templates/asplos2027/main.tex +459 -0
  65. package/src/skills/write/templates/asplos2027/references.bib +135 -0
  66. package/src/skills/write/templates/colm2025/README.md +3 -0
  67. package/src/skills/write/templates/colm2025/colm2025_conference.bib +11 -0
  68. package/src/skills/write/templates/colm2025/colm2025_conference.bst +1440 -0
  69. package/src/skills/write/templates/colm2025/colm2025_conference.sty +218 -0
  70. package/src/skills/write/templates/colm2025/colm2025_conference.tex +305 -0
  71. package/src/skills/write/templates/colm2025/fancyhdr.sty +485 -0
  72. package/src/skills/write/templates/colm2025/math_commands.tex +508 -0
  73. package/src/skills/write/templates/colm2025/natbib.sty +1246 -0
  74. package/src/skills/write/templates/iclr2026/fancyhdr.sty +485 -0
  75. package/src/skills/write/templates/iclr2026/iclr2026_conference.bib +24 -0
  76. package/src/skills/write/templates/iclr2026/iclr2026_conference.bst +1440 -0
  77. package/src/skills/write/templates/iclr2026/iclr2026_conference.sty +246 -0
  78. package/src/skills/write/templates/iclr2026/iclr2026_conference.tex +414 -0
  79. package/src/skills/write/templates/iclr2026/math_commands.tex +508 -0
  80. package/src/skills/write/templates/iclr2026/natbib.sty +1246 -0
  81. package/src/skills/write/templates/icml2026/algorithm.sty +79 -0
  82. package/src/skills/write/templates/icml2026/algorithmic.sty +201 -0
  83. package/src/skills/write/templates/icml2026/example_paper.bib +75 -0
  84. package/src/skills/write/templates/icml2026/example_paper.tex +662 -0
  85. package/src/skills/write/templates/icml2026/fancyhdr.sty +864 -0
  86. package/src/skills/write/templates/icml2026/icml2026.bst +1443 -0
  87. package/src/skills/write/templates/icml2026/icml2026.sty +767 -0
  88. package/src/skills/write/templates/neurips2025/Makefile +36 -0
  89. package/src/skills/write/templates/neurips2025/extra_pkgs.tex +53 -0
  90. package/src/skills/write/templates/neurips2025/main.tex +38 -0
  91. package/src/skills/write/templates/neurips2025/neurips.sty +382 -0
  92. package/src/skills/write/templates/nsdi2027/main.tex +426 -0
  93. package/src/skills/write/templates/nsdi2027/references.bib +151 -0
  94. package/src/skills/write/templates/nsdi2027/usenix-2020-09.sty +83 -0
  95. package/src/skills/write/templates/osdi2026/main.tex +429 -0
  96. package/src/skills/write/templates/osdi2026/references.bib +150 -0
  97. package/src/skills/write/templates/osdi2026/usenix-2020-09.sty +83 -0
  98. package/src/skills/write/templates/sosp2026/main.tex +532 -0
  99. package/src/skills/write/templates/sosp2026/references.bib +148 -0
  100. package/src/tui/package.json +1 -1
  101. package/src/ui/dist/assets/{AiManusChatView-BS3V4ZOk.js → AiManusChatView-m2FNtwbn.js} +110 -14
  102. package/src/ui/dist/assets/{AnalysisPlugin-DLPXQsmr.js → AnalysisPlugin-BMTF8EGL.js} +1 -1
  103. package/src/ui/dist/assets/{AutoFigurePlugin-C-Fr9knQ.js → AutoFigurePlugin-DxPdMUNb.js} +5 -5
  104. package/src/ui/dist/assets/{CliPlugin-Dd8AHzFg.js → CliPlugin-BEOWgxCI.js} +9 -9
  105. package/src/ui/dist/assets/{CodeEditorPlugin-Dg-RepTl.js → CodeEditorPlugin-BCXvjqmb.js} +8 -8
  106. package/src/ui/dist/assets/{CodeViewerPlugin-D2J_3nyt.js → CodeViewerPlugin-DaJcy3nD.js} +5 -5
  107. package/src/ui/dist/assets/{DocViewerPlugin-ChRLLKNb.js → DocViewerPlugin-ByfeIq4K.js} +3 -3
  108. package/src/ui/dist/assets/{GitDiffViewerPlugin-DgHfcved.js → GitDiffViewerPlugin-Cksf3VZ-.js} +830 -86
  109. package/src/ui/dist/assets/{ImageViewerPlugin-C89GZMBy.js → ImageViewerPlugin-CFz-OsTS.js} +5 -5
  110. package/src/ui/dist/assets/{LabCopilotPanel-BUfIwUcb.js → LabCopilotPanel-CJ1cJzoX.js} +10 -10
  111. package/src/ui/dist/assets/{LabPlugin-zvUmQUMq.js → LabPlugin-BF3dVJwa.js} +1 -1
  112. package/src/ui/dist/assets/{LatexPlugin-C1SSNuWp.js → LatexPlugin-DDkwZ6Sj.js} +7 -7
  113. package/src/ui/dist/assets/{MarkdownViewerPlugin-D2Mf5tU5.js → MarkdownViewerPlugin-HAuvurcT.js} +4 -4
  114. package/src/ui/dist/assets/{MarketplacePlugin-CF4LgiS2.js → MarketplacePlugin-BtoTYy2C.js} +3 -3
  115. package/src/ui/dist/assets/{index-Be0NAmh8.js → NotebookEditor-CSJYx7b-.js} +12 -155
  116. package/src/ui/dist/assets/{NotebookEditor-BM7Bgwlv.js → NotebookEditor-DQgRezm_.js} +1 -1
  117. package/src/ui/dist/assets/{PdfLoader-Bc5qfD-Z.js → PdfLoader-DPa_-fv6.js} +1 -1
  118. package/src/ui/dist/assets/{PdfMarkdownPlugin-sh1-IRcp.js → PdfMarkdownPlugin-BZpXOEjm.js} +3 -3
  119. package/src/ui/dist/assets/{PdfViewerPlugin-C_a7CpWG.js → PdfViewerPlugin-BT8a6wGR.js} +10 -10
  120. package/src/ui/dist/assets/{SearchPlugin-L4z3HcLf.js → SearchPlugin-D_blveZi.js} +1 -1
  121. package/src/ui/dist/assets/{Stepper-Dk4aQ3fN.js → Stepper-DH2k75Vo.js} +1 -1
  122. package/src/ui/dist/assets/{TextViewerPlugin-BsNtlKVo.js → TextViewerPlugin-Btx0M3hX.js} +4 -4
  123. package/src/ui/dist/assets/{VNCViewer-BpeDcZ5_.js → VNCViewer-DImJO4rO.js} +9 -9
  124. package/src/ui/dist/assets/{bibtex-C4QI-bbj.js → bibtex-B-Hqu0Sg.js} +1 -1
  125. package/src/ui/dist/assets/{code-DuMINRsg.js → code-BUfXGJSl.js} +1 -1
  126. package/src/ui/dist/assets/{file-content-C3N-432K.js → file-content-VqamwI3X.js} +1 -1
  127. package/src/ui/dist/assets/{file-diff-panel-CffQ4ZMg.js → file-diff-panel-C_wOoS7a.js} +1 -1
  128. package/src/ui/dist/assets/{file-socket-CRH59PCO.js → file-socket-D2bTuMVP.js} +1 -1
  129. package/src/ui/dist/assets/{file-utils-vYGtW2mI.js → file-utils--zJCPN1i.js} +1 -1
  130. package/src/ui/dist/assets/{image-DBVGaooo.js → image-BZkGJ4mM.js} +1 -1
  131. package/src/ui/dist/assets/{index-DjSFDmgB.js → index-CxkvSeKw.js} +2 -2
  132. package/src/ui/dist/assets/{index-BpjYH9Vg.js → index-D9QIGcmc.js} +1 -1
  133. package/src/ui/dist/assets/{index-Do9N28uB.css → index-DXZ1daiJ.css} +163 -34
  134. package/src/ui/dist/assets/index-DdRW6RMJ.js +159 -0
  135. package/src/ui/dist/assets/{index-B1P6hQRJ.js → index-DjggJovS.js} +3029 -1780
  136. package/src/ui/dist/assets/{message-square-BsPDBhiY.js → message-square-FUIPIhU2.js} +1 -1
  137. package/src/ui/dist/assets/{monaco-BTkdPojV.js → monaco-DHMc7kKM.js} +1 -1
  138. package/src/ui/dist/assets/{popover-cWjCk-vc.js → popover-B85oCgCS.js} +1 -1
  139. package/src/ui/dist/assets/{project-sync-CXn530xb.js → project-sync-DOMCcPac.js} +1 -1
  140. package/src/ui/dist/assets/{sigma-04Jr12jg.js → sigma-BO2rQrl3.js} +1 -1
  141. package/src/ui/dist/assets/{tooltip-BdVDl0G5.js → tooltip-B1OspAkx.js} +1 -1
  142. package/src/ui/dist/assets/{trash-CB_GlQyC.js → trash-BsVEH_dV.js} +1 -1
  143. package/src/ui/dist/assets/{useCliAccess-BL932NwS.js → useCliAccess-b8L6JuZm.js} +1 -1
  144. package/src/ui/dist/assets/{useFileDiffOverlay-B2WK7Tvq.js → useFileDiffOverlay-BY7uA9hV.js} +1 -1
  145. package/src/ui/dist/assets/{wrap-text-YC68g12z.js → wrap-text-BwyVuUIK.js} +1 -1
  146. package/src/ui/dist/assets/{zoom-out-C0RJvFiJ.js → zoom-out-RDpLugQP.js} +1 -1
  147. package/src/ui/dist/index.html +5 -2
  148. /package/src/ui/dist/assets/{index-CccQYZjX.css → NotebookEditor-CccQYZjX.css} +0 -0
@@ -17,7 +17,7 @@ try:
17
17
  except ImportError: # pragma: no cover
18
18
  fcntl = None
19
19
 
20
- from ..artifact.metrics import build_metrics_timeline, normalize_metrics_summary
20
+ from ..artifact.metrics import build_metrics_timeline, extract_latest_metric
21
21
  from ..config import ConfigManager
22
22
  from ..connector_runtime import conversation_identity_key, normalize_conversation_id
23
23
  from ..gitops import current_branch, export_git_graph, head_commit, init_repo
@@ -129,6 +129,9 @@ class QuestService:
129
129
  "active_analysis_campaign_id": None,
130
130
  "analysis_parent_branch": None,
131
131
  "analysis_parent_worktree_root": None,
132
+ "paper_parent_branch": None,
133
+ "paper_parent_worktree_root": None,
134
+ "paper_parent_run_id": None,
132
135
  "next_pending_slice_id": None,
133
136
  "workspace_mode": "quest",
134
137
  "last_flow_type": None,
@@ -151,6 +154,9 @@ class QuestService:
151
154
  parent_root = str(merged.get("analysis_parent_worktree_root") or "").strip()
152
155
  if parent_root and not Path(parent_root).exists():
153
156
  merged["analysis_parent_worktree_root"] = None
157
+ paper_parent_root = str(merged.get("paper_parent_worktree_root") or "").strip()
158
+ if paper_parent_root and not Path(paper_parent_root).exists():
159
+ merged["paper_parent_worktree_root"] = None
154
160
  return merged
155
161
 
156
162
  def write_research_state(self, quest_root: Path, payload: dict[str, Any]) -> dict[str, Any]:
@@ -206,9 +212,37 @@ class QuestService:
206
212
  def _artifact_roots(self, quest_root: Path) -> list[Path]:
207
213
  return [root for root in self.workspace_roots(quest_root) if (root / "artifacts").exists()]
208
214
 
215
+ @staticmethod
216
+ def _artifact_item_identity(path: Path, payload: dict[str, Any], *, kind: str) -> str:
217
+ normalized_kind = str(kind or payload.get("kind") or path.parent.name or "artifact").strip() or "artifact"
218
+ artifact_id = str(payload.get("artifact_id") or payload.get("id") or "").strip()
219
+ if artifact_id:
220
+ return f"{normalized_kind}:artifact:{artifact_id}"
221
+ branch_name = str(payload.get("branch") or "").strip()
222
+ run_id = str(payload.get("run_id") or "").strip()
223
+ if normalized_kind == "runs" and run_id and branch_name:
224
+ return f"{normalized_kind}:branch_run:{branch_name}:{run_id}"
225
+ if normalized_kind == "runs" and run_id:
226
+ return f"{normalized_kind}:run:{run_id}"
227
+ idea_id = str(payload.get("idea_id") or "").strip()
228
+ if normalized_kind == "ideas" and idea_id and branch_name:
229
+ return f"{normalized_kind}:branch_idea:{branch_name}:{idea_id}"
230
+ if normalized_kind == "ideas" and idea_id:
231
+ return f"{normalized_kind}:idea:{idea_id}"
232
+ return f"path:{path.resolve()}"
233
+
234
+ @staticmethod
235
+ def _artifact_item_rank(payload: dict[str, Any], *, path: Path, mtime_ns: int) -> tuple[str, str, int, int, str]:
236
+ return (
237
+ str(payload.get("updated_at") or ""),
238
+ str(payload.get("created_at") or ""),
239
+ len(payload),
240
+ mtime_ns,
241
+ str(path),
242
+ )
243
+
209
244
  def _collect_artifacts(self, quest_root: Path) -> list[dict[str, Any]]:
210
- artifacts: list[dict[str, Any]] = []
211
- seen_paths: set[str] = set()
245
+ artifacts_by_identity: dict[str, dict[str, Any]] = {}
212
246
  for root in self._artifact_roots(quest_root):
213
247
  artifacts_root = root / "artifacts"
214
248
  if not artifacts_root.exists():
@@ -217,19 +251,37 @@ class QuestService:
217
251
  if not folder.is_dir():
218
252
  continue
219
253
  for path in sorted(folder.glob("*.json")):
220
- resolved_key = str(path.resolve())
221
- if resolved_key in seen_paths:
222
- continue
223
- seen_paths.add(resolved_key)
224
254
  item = self._read_cached_json(path, {})
225
- artifacts.append(
226
- {
227
- "kind": folder.name,
228
- "path": str(path),
229
- "payload": item,
230
- "workspace_root": str(root),
231
- }
232
- )
255
+ payload = item if isinstance(item, dict) else {}
256
+ try:
257
+ mtime_ns = path.stat().st_mtime_ns
258
+ except OSError:
259
+ mtime_ns = 0
260
+ artifact = {
261
+ "kind": folder.name,
262
+ "path": str(path),
263
+ "payload": item,
264
+ "workspace_root": str(root),
265
+ }
266
+ identity = self._artifact_item_identity(path, payload, kind=folder.name)
267
+ existing = artifacts_by_identity.get(identity)
268
+ existing_payload = existing.get("payload") if isinstance((existing or {}).get("payload"), dict) else {}
269
+ existing_path = Path(str((existing or {}).get("path") or path))
270
+ try:
271
+ existing_mtime_ns = existing_path.stat().st_mtime_ns if existing else 0
272
+ except OSError:
273
+ existing_mtime_ns = 0
274
+ if existing is None or self._artifact_item_rank(
275
+ payload,
276
+ path=path,
277
+ mtime_ns=mtime_ns,
278
+ ) >= self._artifact_item_rank(
279
+ existing_payload,
280
+ path=existing_path,
281
+ mtime_ns=existing_mtime_ns,
282
+ ):
283
+ artifacts_by_identity[identity] = artifact
284
+ artifacts = list(artifacts_by_identity.values())
233
285
  artifacts.sort(
234
286
  key=lambda item: str(
235
287
  ((item.get("payload") or {}).get("updated_at"))
@@ -267,22 +319,7 @@ class QuestService:
267
319
 
268
320
  @staticmethod
269
321
  def _latest_metric_from_payload(payload: dict[str, Any]) -> dict[str, Any] | None:
270
- metrics_summary = normalize_metrics_summary(payload.get("metrics_summary"))
271
- if not metrics_summary:
272
- return None
273
- progress_eval = payload.get("progress_eval") if isinstance(payload.get("progress_eval"), dict) else {}
274
- comparisons = payload.get("baseline_comparisons") if isinstance(payload.get("baseline_comparisons"), dict) else {}
275
- primary_metric_id = (
276
- str(progress_eval.get("primary_metric_id") or comparisons.get("primary_metric_id") or "").strip()
277
- or next(iter(metrics_summary.keys()))
278
- )
279
- result = {
280
- "key": primary_metric_id,
281
- "value": metrics_summary.get(primary_metric_id),
282
- }
283
- if progress_eval.get("delta_vs_baseline") is not None:
284
- result["delta_vs_baseline"] = progress_eval.get("delta_vs_baseline")
285
- return result
322
+ return extract_latest_metric(payload)
286
323
 
287
324
  @staticmethod
288
325
  def _parse_numeric_quest_id(value: str | None) -> int | None:
@@ -980,6 +1017,9 @@ class QuestService:
980
1017
  "active_analysis_campaign_id": research_state.get("active_analysis_campaign_id"),
981
1018
  "analysis_parent_branch": research_state.get("analysis_parent_branch"),
982
1019
  "analysis_parent_worktree_root": research_state.get("analysis_parent_worktree_root"),
1020
+ "paper_parent_branch": research_state.get("paper_parent_branch"),
1021
+ "paper_parent_worktree_root": research_state.get("paper_parent_worktree_root"),
1022
+ "paper_parent_run_id": research_state.get("paper_parent_run_id"),
983
1023
  "next_pending_slice_id": research_state.get("next_pending_slice_id"),
984
1024
  "workspace_mode": research_state.get("workspace_mode") or "quest",
985
1025
  "active_baseline_id": active_baseline_id,
@@ -1981,7 +2021,11 @@ class QuestService:
1981
2021
  },
1982
2022
  }
1983
2023
 
1984
- resolution_root = quest_root if document_id.startswith("questpath::") else workspace_root
2024
+ resolution_root = (
2025
+ quest_root
2026
+ if document_id.startswith(("questpath::", "memory::"))
2027
+ else workspace_root
2028
+ )
1985
2029
  path, writable, scope, source_kind = self._resolve_document(resolution_root, document_id)
1986
2030
  renderer_hint, mime_type = self._renderer_hint_for(path)
1987
2031
  is_text = self._is_text_document(path, mime_type, renderer_hint)
@@ -2162,11 +2206,16 @@ class QuestService:
2162
2206
  asset_name = f"{safe_stem}-{generate_id('asset').split('-', 1)[1]}{asset_suffix}"
2163
2207
  asset_relative_dir = self._markdown_asset_directory(base_relative)
2164
2208
  asset_relative = (asset_relative_dir / asset_name).as_posix()
2165
- asset_root = quest_root if document_id.startswith("questpath::") else workspace_root
2209
+ asset_root = (
2210
+ quest_root
2211
+ if document_id.startswith(("questpath::", "memory::"))
2212
+ else workspace_root
2213
+ )
2166
2214
  asset_path = resolve_within(asset_root, asset_relative)
2167
2215
  ensure_dir(asset_path.parent)
2168
2216
  asset_path.write_bytes(content)
2169
- asset_document_id = f"{'questpath' if document_id.startswith('questpath::') else 'path'}::{asset_relative}"
2217
+ asset_document_scope = "questpath" if document_id.startswith(("questpath::", "memory::")) else "path"
2218
+ asset_document_id = f"{asset_document_scope}::{asset_relative}"
2170
2219
  relative_markdown_path = self._relative_path_from_base(base_relative, asset_relative)
2171
2220
  return {
2172
2221
  "ok": True,
@@ -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,6 +135,24 @@ 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
157
  if str(self.selection.get("selection_type") or "").strip() == "branch_node":
140
158
  return self._build_branch()
@@ -208,7 +226,7 @@ class QuestStageViewBuilder:
208
226
  "idea": self._idea_scope_paths(),
209
227
  "experiment": self._experiment_scope_paths(None),
210
228
  "analysis": self._analysis_scope_paths(None),
211
- "paper": ["paper"],
229
+ "paper": self._paper_scope_paths(),
212
230
  }
213
231
  return defaults.get(self.stage_key, [])
214
232
 
@@ -356,9 +374,29 @@ class QuestStageViewBuilder:
356
374
  }
357
375
 
358
376
  def _paper_bundle_manifest(self) -> dict[str, Any]:
359
- payload = read_json(self.quest_root / "paper" / "paper_bundle_manifest.json", {})
377
+ payload = read_json(self._paper_root() / "paper_bundle_manifest.json", {})
360
378
  return payload if isinstance(payload, dict) else {}
361
379
 
380
+ def _paper_root(self) -> Path:
381
+ candidates = [self.workspace_root / "paper", self.quest_root / "paper"]
382
+ for candidate in candidates:
383
+ if candidate.exists():
384
+ return candidate
385
+ return candidates[0]
386
+
387
+ def _paper_scope_paths(self) -> list[str]:
388
+ try:
389
+ return [self._paper_root().relative_to(self.quest_root).as_posix()]
390
+ except ValueError:
391
+ return ["paper"]
392
+
393
+ def _open_source_root(self) -> Path:
394
+ candidates = [self.workspace_root / "release" / "open_source", self.quest_root / "release" / "open_source"]
395
+ for candidate in candidates:
396
+ if candidate.exists():
397
+ return candidate
398
+ return candidates[0]
399
+
362
400
  def _paper_relative_path(self, raw_path: object) -> str | None:
363
401
  resolved = self._path_in_quest(raw_path)
364
402
  if resolved is None:
@@ -370,9 +408,10 @@ class QuestStageViewBuilder:
370
408
  preferred = self._paper_relative_path(bundle_manifest.get("latex_root_path"))
371
409
  if preferred:
372
410
  return preferred
373
- for candidate in ("paper/latex", "paper/tex"):
374
- if (self.quest_root / candidate).exists():
375
- return candidate
411
+ paper_root = self._paper_root()
412
+ for candidate in (paper_root / "latex", paper_root / "tex"):
413
+ if candidate.exists():
414
+ return candidate.relative_to(self.quest_root).as_posix()
376
415
  return None
377
416
 
378
417
  def _paper_main_tex(self, latex_root_rel: str | None) -> str | None:
@@ -401,7 +440,7 @@ class QuestStageViewBuilder:
401
440
  guessed = str(PurePosixPath(main_tex_rel).with_suffix(".pdf"))
402
441
  if (self.quest_root / guessed).exists():
403
442
  candidates.append(guessed)
404
- for path in sorted((self.quest_root / "paper").glob("*.pdf")):
443
+ for path in sorted(self._paper_root().glob("*.pdf")):
405
444
  candidates.append(path.relative_to(self.quest_root).as_posix())
406
445
  deduped: list[str] = []
407
446
  seen: set[str] = set()
@@ -948,7 +987,7 @@ class QuestStageViewBuilder:
948
987
  description="Merged analysis summary for this branch.",
949
988
  ),
950
989
  self._file_entry(
951
- "paper/selected_outline.json" if (self.quest_root / "paper" / "selected_outline.json").exists() else None,
990
+ (self._paper_root() / "selected_outline.json") if (self._paper_root() / "selected_outline.json").exists() else None,
952
991
  label="Selected Outline",
953
992
  description="Current selected paper outline.",
954
993
  ),
@@ -1264,23 +1303,25 @@ class QuestStageViewBuilder:
1264
1303
  latex_root_rel = self._paper_latex_root(bundle_manifest)
1265
1304
  main_tex_rel = self._paper_main_tex(latex_root_rel)
1266
1305
  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"))
1306
+ paper_root = self._paper_root()
1307
+ open_source_root = self._open_source_root()
1308
+ candidates = sorted((paper_root / "outlines" / "candidates").glob("*.json"))
1268
1309
  files: list[dict[str, Any] | None] = [
1269
1310
  *[
1270
1311
  self._file_entry(path, label=f"Outline Candidate · {path.stem}", description="Paper outline candidate JSON.")
1271
1312
  for path in candidates
1272
1313
  ],
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."),
1314
+ self._file_entry(paper_root / "selected_outline.json", label="Selected Outline", description="Chosen paper outline."),
1315
+ self._file_entry(paper_root / "outline_selection.md", label="Outline Selection Note", description="Outline selection rationale."),
1316
+ self._file_entry(paper_root / "draft.md", label="Draft Markdown", description="Current paper draft."),
1317
+ self._file_entry(paper_root / "writing_plan.md", label="Writing Plan", description="Paper writing plan."),
1318
+ self._file_entry(paper_root / "references.bib", label="References", description="Bibliography file."),
1319
+ self._file_entry(paper_root / "claim_evidence_map.json", label="Claim-Evidence Map", description="Claim to evidence mapping."),
1320
+ self._file_entry(paper_root / "baseline_inventory.json", label="Baseline Inventory", description="Canonical and supplementary baseline inventory for writing."),
1321
+ self._file_entry(paper_root / "build" / "compile_report.json", label="Compile Report", description="Paper build/compile report."),
1322
+ self._file_entry(paper_root / "paper_bundle_manifest.json", label="Bundle Manifest", description="Final paper bundle manifest."),
1323
+ self._file_entry(open_source_root / "manifest.json", label="Open Source Manifest", description="Open-source cleanup and release preparation manifest."),
1324
+ 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
1325
  self._file_entry(latex_root_rel, label="LaTeX Sources", description="LaTeX source folder.", expected_kind="directory"),
1285
1326
  self._file_entry(main_tex_rel, label="Main TeX", description="Primary TeX source file."),
1286
1327
  ]
@@ -1290,7 +1331,7 @@ class QuestStageViewBuilder:
1290
1331
 
1291
1332
  def _paper_candidates(self) -> list[dict[str, Any]]:
1292
1333
  candidates: list[dict[str, Any]] = []
1293
- for path in sorted((self.quest_root / "paper" / "outlines" / "candidates").glob("*.json")):
1334
+ for path in sorted((self._paper_root() / "outlines" / "candidates").glob("*.json")):
1294
1335
  payload = read_json(path, {})
1295
1336
  if not isinstance(payload, dict):
1296
1337
  payload = {}
@@ -1319,7 +1360,8 @@ class QuestStageViewBuilder:
1319
1360
  )
1320
1361
  and self._branch_matches(self._payload(item), allow_parent=True)
1321
1362
  ]
1322
- selected_outline_path = self.quest_root / "paper" / "selected_outline.json"
1363
+ paper_root = self._paper_root()
1364
+ selected_outline_path = paper_root / "selected_outline.json"
1323
1365
  selected_outline = read_json(selected_outline_path, {}) if selected_outline_path.exists() else {}
1324
1366
  if not isinstance(selected_outline, dict):
1325
1367
  selected_outline = {}
@@ -1329,15 +1371,18 @@ class QuestStageViewBuilder:
1329
1371
  else {}
1330
1372
  )
1331
1373
  candidates = self._paper_candidates()
1332
- compile_report = read_json(self.quest_root / "paper" / "build" / "compile_report.json", {})
1374
+ compile_report = read_json(paper_root / "build" / "compile_report.json", {})
1333
1375
  if not isinstance(compile_report, dict):
1334
1376
  compile_report = {}
1335
1377
  bundle_manifest = self._paper_bundle_manifest()
1336
1378
  latex_root_rel = self._paper_latex_root(bundle_manifest)
1337
1379
  main_tex_rel = self._paper_main_tex(latex_root_rel)
1338
- references_bib = read_text(self.quest_root / "paper" / "references.bib", "")
1380
+ references_bib = read_text(paper_root / "references.bib", "")
1339
1381
  references_count = sum(1 for line in references_bib.splitlines() if line.lstrip().startswith("@"))
1340
1382
  pdf_paths = self._paper_pdf_candidates(bundle_manifest, main_tex_rel=main_tex_rel)
1383
+ draft_rel = self._paper_relative_path(paper_root / "draft.md") or "paper/draft.md"
1384
+ writing_plan_rel = self._paper_relative_path(paper_root / "writing_plan.md") or "paper/writing_plan.md"
1385
+ claim_map_rel = self._paper_relative_path(paper_root / "claim_evidence_map.json") or "paper/claim_evidence_map.json"
1341
1386
  selected_title = str(
1342
1387
  detailed.get("title") or selected_outline.get("title") or bundle_manifest.get("title") or "Drafting"
1343
1388
  ).strip() or "Drafting"
@@ -1356,7 +1401,7 @@ class QuestStageViewBuilder:
1356
1401
  overview=[
1357
1402
  _field("Selected Outline", selected_title if selected_outline else "Not selected"),
1358
1403
  _field("Candidate Count", len(candidates)),
1359
- _field("Draft Status", "present" if (self.quest_root / "paper" / "draft.md").exists() else "missing"),
1404
+ _field("Draft Status", "present" if (paper_root / "draft.md").exists() else "missing"),
1360
1405
  _field("Bundle Status", "present" if bundle_manifest else "missing"),
1361
1406
  ],
1362
1407
  key_facts=[
@@ -1377,10 +1422,10 @@ class QuestStageViewBuilder:
1377
1422
  "outline_candidates": candidates,
1378
1423
  "selected_outline": selected_outline,
1379
1424
  "drafting": {
1380
- "writing_plan_path": "paper/writing_plan.md",
1381
- "draft_path": "paper/draft.md",
1425
+ "writing_plan_path": writing_plan_rel,
1426
+ "draft_path": draft_rel,
1382
1427
  "references_count": references_count,
1383
- "claim_evidence_map_path": "paper/claim_evidence_map.json",
1428
+ "claim_evidence_map_path": claim_map_rel,
1384
1429
  },
1385
1430
  "build": {
1386
1431
  "compile_report": compile_report,
@@ -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.