@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.
- package/README.md +4 -0
- package/bin/ds.js +220 -5
- package/docs/en/07_MEMORY_AND_MCP.md +40 -3
- package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
- package/docs/zh/07_MEMORY_AND_MCP.md +40 -3
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +1 -0
- package/install.sh +34 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/acp/envelope.py +1 -0
- package/src/deepscientist/artifact/metrics.py +813 -80
- package/src/deepscientist/artifact/schemas.py +1 -0
- package/src/deepscientist/artifact/service.py +1101 -99
- package/src/deepscientist/bash_exec/monitor.py +1 -1
- package/src/deepscientist/bash_exec/service.py +17 -9
- package/src/deepscientist/channels/qq.py +17 -0
- package/src/deepscientist/channels/relay.py +16 -0
- package/src/deepscientist/config/models.py +6 -0
- package/src/deepscientist/config/service.py +70 -2
- package/src/deepscientist/daemon/api/handlers.py +284 -14
- package/src/deepscientist/daemon/api/router.py +1 -0
- package/src/deepscientist/daemon/app.py +291 -20
- package/src/deepscientist/gitops/diff.py +6 -10
- package/src/deepscientist/mcp/server.py +188 -39
- package/src/deepscientist/prompts/builder.py +51 -18
- package/src/deepscientist/quest/service.py +83 -34
- package/src/deepscientist/quest/stage_views.py +74 -29
- package/src/deepscientist/runners/codex.py +1 -1
- package/src/prompts/connectors/qq.md +1 -1
- package/src/prompts/contracts/shared_interaction.md +14 -0
- package/src/prompts/system.md +106 -32
- package/src/skills/analysis-campaign/SKILL.md +10 -14
- package/src/skills/baseline/SKILL.md +51 -38
- package/src/skills/baseline/references/baseline-plan-template.md +2 -0
- package/src/skills/decision/SKILL.md +12 -8
- package/src/skills/experiment/SKILL.md +28 -16
- package/src/skills/experiment/references/main-experiment-plan-template.md +2 -0
- package/src/skills/figure-polish/SKILL.md +1 -0
- package/src/skills/finalize/SKILL.md +3 -8
- package/src/skills/idea/SKILL.md +2 -8
- package/src/skills/intake-audit/SKILL.md +2 -8
- package/src/skills/rebuttal/SKILL.md +2 -8
- package/src/skills/review/SKILL.md +2 -8
- package/src/skills/scout/SKILL.md +2 -8
- package/src/skills/write/SKILL.md +52 -16
- package/src/skills/write/templates/DEEPSCIENTIST_NOTES.md +21 -0
- package/src/skills/write/templates/README.md +408 -0
- package/src/skills/write/templates/UPSTREAM_LICENSE.txt +21 -0
- package/src/skills/write/templates/aaai2026/README.md +534 -0
- package/src/skills/write/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
- package/src/skills/write/templates/aaai2026/aaai2026-unified-template.tex +952 -0
- package/src/skills/write/templates/aaai2026/aaai2026.bib +111 -0
- package/src/skills/write/templates/aaai2026/aaai2026.bst +1493 -0
- package/src/skills/write/templates/aaai2026/aaai2026.sty +315 -0
- package/src/skills/write/templates/acl/README.md +50 -0
- package/src/skills/write/templates/acl/acl.sty +312 -0
- package/src/skills/write/templates/acl/acl_latex.tex +377 -0
- package/src/skills/write/templates/acl/acl_lualatex.tex +101 -0
- package/src/skills/write/templates/acl/acl_natbib.bst +1940 -0
- package/src/skills/write/templates/acl/anthology.bib.txt +26 -0
- package/src/skills/write/templates/acl/custom.bib +70 -0
- package/src/skills/write/templates/acl/formatting.md +326 -0
- package/src/skills/write/templates/asplos2027/main.tex +459 -0
- package/src/skills/write/templates/asplos2027/references.bib +135 -0
- package/src/skills/write/templates/colm2025/README.md +3 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.bib +11 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.bst +1440 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.sty +218 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.tex +305 -0
- package/src/skills/write/templates/colm2025/fancyhdr.sty +485 -0
- package/src/skills/write/templates/colm2025/math_commands.tex +508 -0
- package/src/skills/write/templates/colm2025/natbib.sty +1246 -0
- package/src/skills/write/templates/iclr2026/fancyhdr.sty +485 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.bib +24 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.bst +1440 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.sty +246 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.tex +414 -0
- package/src/skills/write/templates/iclr2026/math_commands.tex +508 -0
- package/src/skills/write/templates/iclr2026/natbib.sty +1246 -0
- package/src/skills/write/templates/icml2026/algorithm.sty +79 -0
- package/src/skills/write/templates/icml2026/algorithmic.sty +201 -0
- package/src/skills/write/templates/icml2026/example_paper.bib +75 -0
- package/src/skills/write/templates/icml2026/example_paper.tex +662 -0
- package/src/skills/write/templates/icml2026/fancyhdr.sty +864 -0
- package/src/skills/write/templates/icml2026/icml2026.bst +1443 -0
- package/src/skills/write/templates/icml2026/icml2026.sty +767 -0
- package/src/skills/write/templates/neurips2025/Makefile +36 -0
- package/src/skills/write/templates/neurips2025/extra_pkgs.tex +53 -0
- package/src/skills/write/templates/neurips2025/main.tex +38 -0
- package/src/skills/write/templates/neurips2025/neurips.sty +382 -0
- package/src/skills/write/templates/nsdi2027/main.tex +426 -0
- package/src/skills/write/templates/nsdi2027/references.bib +151 -0
- package/src/skills/write/templates/nsdi2027/usenix-2020-09.sty +83 -0
- package/src/skills/write/templates/osdi2026/main.tex +429 -0
- package/src/skills/write/templates/osdi2026/references.bib +150 -0
- package/src/skills/write/templates/osdi2026/usenix-2020-09.sty +83 -0
- package/src/skills/write/templates/sosp2026/main.tex +532 -0
- package/src/skills/write/templates/sosp2026/references.bib +148 -0
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-BS3V4ZOk.js → AiManusChatView-m2FNtwbn.js} +110 -14
- package/src/ui/dist/assets/{AnalysisPlugin-DLPXQsmr.js → AnalysisPlugin-BMTF8EGL.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-C-Fr9knQ.js → AutoFigurePlugin-DxPdMUNb.js} +5 -5
- package/src/ui/dist/assets/{CliPlugin-Dd8AHzFg.js → CliPlugin-BEOWgxCI.js} +9 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-Dg-RepTl.js → CodeEditorPlugin-BCXvjqmb.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-D2J_3nyt.js → CodeViewerPlugin-DaJcy3nD.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-ChRLLKNb.js → DocViewerPlugin-ByfeIq4K.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-DgHfcved.js → GitDiffViewerPlugin-Cksf3VZ-.js} +830 -86
- package/src/ui/dist/assets/{ImageViewerPlugin-C89GZMBy.js → ImageViewerPlugin-CFz-OsTS.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-BUfIwUcb.js → LabCopilotPanel-CJ1cJzoX.js} +10 -10
- package/src/ui/dist/assets/{LabPlugin-zvUmQUMq.js → LabPlugin-BF3dVJwa.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-C1SSNuWp.js → LatexPlugin-DDkwZ6Sj.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-D2Mf5tU5.js → MarkdownViewerPlugin-HAuvurcT.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-CF4LgiS2.js → MarketplacePlugin-BtoTYy2C.js} +3 -3
- package/src/ui/dist/assets/{index-Be0NAmh8.js → NotebookEditor-CSJYx7b-.js} +12 -155
- package/src/ui/dist/assets/{NotebookEditor-BM7Bgwlv.js → NotebookEditor-DQgRezm_.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-Bc5qfD-Z.js → PdfLoader-DPa_-fv6.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-sh1-IRcp.js → PdfMarkdownPlugin-BZpXOEjm.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-C_a7CpWG.js → PdfViewerPlugin-BT8a6wGR.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-L4z3HcLf.js → SearchPlugin-D_blveZi.js} +1 -1
- package/src/ui/dist/assets/{Stepper-Dk4aQ3fN.js → Stepper-DH2k75Vo.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-BsNtlKVo.js → TextViewerPlugin-Btx0M3hX.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-BpeDcZ5_.js → VNCViewer-DImJO4rO.js} +9 -9
- package/src/ui/dist/assets/{bibtex-C4QI-bbj.js → bibtex-B-Hqu0Sg.js} +1 -1
- package/src/ui/dist/assets/{code-DuMINRsg.js → code-BUfXGJSl.js} +1 -1
- package/src/ui/dist/assets/{file-content-C3N-432K.js → file-content-VqamwI3X.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-CffQ4ZMg.js → file-diff-panel-C_wOoS7a.js} +1 -1
- package/src/ui/dist/assets/{file-socket-CRH59PCO.js → file-socket-D2bTuMVP.js} +1 -1
- package/src/ui/dist/assets/{file-utils-vYGtW2mI.js → file-utils--zJCPN1i.js} +1 -1
- package/src/ui/dist/assets/{image-DBVGaooo.js → image-BZkGJ4mM.js} +1 -1
- package/src/ui/dist/assets/{index-DjSFDmgB.js → index-CxkvSeKw.js} +2 -2
- package/src/ui/dist/assets/{index-BpjYH9Vg.js → index-D9QIGcmc.js} +1 -1
- package/src/ui/dist/assets/{index-Do9N28uB.css → index-DXZ1daiJ.css} +163 -34
- package/src/ui/dist/assets/index-DdRW6RMJ.js +159 -0
- package/src/ui/dist/assets/{index-B1P6hQRJ.js → index-DjggJovS.js} +3029 -1780
- package/src/ui/dist/assets/{message-square-BsPDBhiY.js → message-square-FUIPIhU2.js} +1 -1
- package/src/ui/dist/assets/{monaco-BTkdPojV.js → monaco-DHMc7kKM.js} +1 -1
- package/src/ui/dist/assets/{popover-cWjCk-vc.js → popover-B85oCgCS.js} +1 -1
- package/src/ui/dist/assets/{project-sync-CXn530xb.js → project-sync-DOMCcPac.js} +1 -1
- package/src/ui/dist/assets/{sigma-04Jr12jg.js → sigma-BO2rQrl3.js} +1 -1
- package/src/ui/dist/assets/{tooltip-BdVDl0G5.js → tooltip-B1OspAkx.js} +1 -1
- package/src/ui/dist/assets/{trash-CB_GlQyC.js → trash-BsVEH_dV.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-BL932NwS.js → useCliAccess-b8L6JuZm.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-B2WK7Tvq.js → useFileDiffOverlay-BY7uA9hV.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-YC68g12z.js → wrap-text-BwyVuUIK.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-C0RJvFiJ.js → zoom-out-RDpLugQP.js} +1 -1
- package/src/ui/dist/index.html +5 -2
- /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,
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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":
|
|
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.
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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(
|
|
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
|
-
"
|
|
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
|
-
|
|
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("
|
|
1274
|
-
self._file_entry("
|
|
1275
|
-
self._file_entry("
|
|
1276
|
-
self._file_entry("
|
|
1277
|
-
self._file_entry("
|
|
1278
|
-
self._file_entry("
|
|
1279
|
-
self._file_entry("
|
|
1280
|
-
self._file_entry("
|
|
1281
|
-
self._file_entry("
|
|
1282
|
-
self._file_entry("
|
|
1283
|
-
self._file_entry("
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 (
|
|
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":
|
|
1381
|
-
"draft_path":
|
|
1425
|
+
"writing_plan_path": writing_plan_rel,
|
|
1426
|
+
"draft_path": draft_rel,
|
|
1382
1427
|
"references_count": references_count,
|
|
1383
|
-
"claim_evidence_map_path":
|
|
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
|
|
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,
|
|
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.
|