@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
|
@@ -448,7 +448,7 @@ def run_monitor(session_dir: Path) -> int:
|
|
|
448
448
|
_terminate_process(process, process_group_id)
|
|
449
449
|
stop_requested = True
|
|
450
450
|
|
|
451
|
-
if
|
|
451
|
+
if output_fd is not None and process.poll() is None:
|
|
452
452
|
cursor_payload = read_json(input_cursor_path, {}) or {}
|
|
453
453
|
offset = int(cursor_payload.get("offset") or 0)
|
|
454
454
|
input_entries = read_jsonl(input_path)
|
|
@@ -555,6 +555,7 @@ class BashExecService:
|
|
|
555
555
|
before_seq: int | None = None,
|
|
556
556
|
after_seq: int | None = None,
|
|
557
557
|
order: str = "asc",
|
|
558
|
+
prefer_visible: bool = False,
|
|
558
559
|
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
559
560
|
if not self.meta_path(quest_root, bash_id).exists():
|
|
560
561
|
raise FileNotFoundError(f"Unknown bash session `{bash_id}`.")
|
|
@@ -579,9 +580,16 @@ class BashExecService:
|
|
|
579
580
|
entries = [entry for entry in entries if int(entry.get("seq") or 0) > normalized_after]
|
|
580
581
|
if normalized_before is not None:
|
|
581
582
|
entries = [entry for entry in entries if int(entry.get("seq") or 0) < normalized_before]
|
|
583
|
+
selection_pool = entries
|
|
584
|
+
if prefer_visible:
|
|
585
|
+
visible_entries = [
|
|
586
|
+
entry for entry in entries if str(entry.get("stream") or "") not in {"system", "prompt"}
|
|
587
|
+
]
|
|
588
|
+
if visible_entries:
|
|
589
|
+
selection_pool = visible_entries
|
|
582
590
|
normalized_limit = max(1, limit)
|
|
583
|
-
truncated = len(
|
|
584
|
-
selected =
|
|
591
|
+
truncated = len(selection_pool) > normalized_limit
|
|
592
|
+
selected = selection_pool[-normalized_limit:]
|
|
585
593
|
if order == "desc":
|
|
586
594
|
selected = list(reversed(selected))
|
|
587
595
|
tail_start_seq = int(selected[0].get("seq") or 0) if selected else None
|
|
@@ -1025,15 +1033,14 @@ class BashExecService:
|
|
|
1025
1033
|
if not normalized_data:
|
|
1026
1034
|
raise ValueError("terminal_input_required")
|
|
1027
1035
|
session = self.reconcile_session(quest_root, bash_id)
|
|
1028
|
-
if _normalize_string(session.get("kind")).lower() != "terminal":
|
|
1029
|
-
raise ValueError("not_terminal_session")
|
|
1030
1036
|
status = _normalize_string(session.get("status")).lower()
|
|
1031
1037
|
if status in TERMINAL_STATUSES:
|
|
1032
1038
|
raise ValueError("terminal_session_inactive")
|
|
1033
1039
|
runtime = self._terminal_runtime_manager.get_runtime(quest_root, bash_id)
|
|
1034
|
-
if runtime is None:
|
|
1040
|
+
if runtime is None and _normalize_string(session.get("kind")).lower() == "terminal":
|
|
1035
1041
|
raise ValueError("terminal_runtime_inactive")
|
|
1036
|
-
runtime
|
|
1042
|
+
if runtime is not None:
|
|
1043
|
+
runtime.write_input(normalized_data)
|
|
1037
1044
|
|
|
1038
1045
|
entry = {
|
|
1039
1046
|
"input_id": generate_id("tin"),
|
|
@@ -1081,9 +1088,10 @@ class BashExecService:
|
|
|
1081
1088
|
return True
|
|
1082
1089
|
|
|
1083
1090
|
def issue_terminal_attach_token(self, quest_root: Path, bash_id: str, *, ttl_seconds: int = 60) -> dict[str, Any]:
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1091
|
+
session = self.reconcile_session(quest_root, bash_id)
|
|
1092
|
+
status = _normalize_string(session.get("status")).lower()
|
|
1093
|
+
if status in TERMINAL_STATUSES:
|
|
1094
|
+
raise ValueError("terminal_session_inactive")
|
|
1087
1095
|
token = self._terminal_runtime_manager.issue_attach_token(
|
|
1088
1096
|
quest_root,
|
|
1089
1097
|
bash_id,
|
|
@@ -293,6 +293,20 @@ class QQRelayChannel(BaseChannel):
|
|
|
293
293
|
def matches_profile(item: dict[str, Any], profile_id: str) -> bool:
|
|
294
294
|
item_profile_id = str(item.get("profile_id") or "").strip()
|
|
295
295
|
return item_profile_id == profile_id or (not item_profile_id and len(profiles) == 1)
|
|
296
|
+
|
|
297
|
+
def count_profile_records(path: Path, profile_id: str) -> int:
|
|
298
|
+
total = 0
|
|
299
|
+
for raw in read_jsonl(path):
|
|
300
|
+
if not isinstance(raw, dict):
|
|
301
|
+
continue
|
|
302
|
+
record_profile_id = str(raw.get("profile_id") or "").strip()
|
|
303
|
+
if not record_profile_id:
|
|
304
|
+
parsed = parse_conversation_id(str(raw.get("conversation_id") or "").strip())
|
|
305
|
+
record_profile_id = str((parsed or {}).get("profile_id") or "").strip()
|
|
306
|
+
if record_profile_id == profile_id or (not record_profile_id and len(profiles) == 1):
|
|
307
|
+
total += 1
|
|
308
|
+
return total
|
|
309
|
+
|
|
296
310
|
profile_snapshots = []
|
|
297
311
|
for profile in profiles:
|
|
298
312
|
profile_id = str(profile.get("profile_id") or "").strip()
|
|
@@ -342,6 +356,9 @@ class QQRelayChannel(BaseChannel):
|
|
|
342
356
|
"discovered_targets": profile_targets,
|
|
343
357
|
"recent_conversations": profile_recent_conversations,
|
|
344
358
|
"bindings": profile_bindings,
|
|
359
|
+
"inbox_count": count_profile_records(self.inbox_path, profile_id),
|
|
360
|
+
"outbox_count": count_profile_records(self.outbox_path, profile_id),
|
|
361
|
+
"ignored_count": count_profile_records(self.ignored_path, profile_id),
|
|
345
362
|
"target_count": len(profile_targets),
|
|
346
363
|
"binding_count": len(profile_bindings),
|
|
347
364
|
"last_error": gateway_state.get("last_error") if isinstance(gateway_state, dict) else None,
|
|
@@ -282,6 +282,19 @@ class GenericRelayChannel(BaseChannel):
|
|
|
282
282
|
item_profile_id = str(item.get("profile_id") or "").strip()
|
|
283
283
|
return item_profile_id == profile_id or (not item_profile_id and len(profiles) == 1)
|
|
284
284
|
|
|
285
|
+
def count_profile_records(path: Path, profile_id: str) -> int:
|
|
286
|
+
total = 0
|
|
287
|
+
for raw in read_jsonl(path):
|
|
288
|
+
if not isinstance(raw, dict):
|
|
289
|
+
continue
|
|
290
|
+
record_profile_id = str(raw.get("profile_id") or "").strip()
|
|
291
|
+
if not record_profile_id:
|
|
292
|
+
parsed = parse_conversation_id(str(raw.get("conversation_id") or "").strip())
|
|
293
|
+
record_profile_id = str((parsed or {}).get("profile_id") or "").strip()
|
|
294
|
+
if record_profile_id == profile_id or (not record_profile_id and len(profiles) == 1):
|
|
295
|
+
total += 1
|
|
296
|
+
return total
|
|
297
|
+
|
|
285
298
|
profile_snapshots = []
|
|
286
299
|
for profile in profiles:
|
|
287
300
|
profile_id = str(profile.get("profile_id") or "").strip()
|
|
@@ -322,6 +335,9 @@ class GenericRelayChannel(BaseChannel):
|
|
|
322
335
|
"discovered_targets": profile_targets,
|
|
323
336
|
"recent_conversations": profile_recent_conversations,
|
|
324
337
|
"bindings": profile_bindings,
|
|
338
|
+
"inbox_count": count_profile_records(self.inbox_path, profile_id),
|
|
339
|
+
"outbox_count": count_profile_records(self.outbox_path, profile_id),
|
|
340
|
+
"ignored_count": count_profile_records(self.ignored_path, profile_id),
|
|
325
341
|
"target_count": len(profile_targets),
|
|
326
342
|
"binding_count": len(profile_bindings),
|
|
327
343
|
"last_error": profile_runtime_state.get("last_error") if isinstance(profile_runtime_state, dict) else None,
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
CONFIG_NAMES = ("config", "runners", "connectors", "plugins", "mcp_servers")
|
|
7
7
|
REQUIRED_CONFIG_NAMES = ("config", "runners", "connectors")
|
|
8
8
|
OPTIONAL_CONFIG_NAMES = ("plugins", "mcp_servers")
|
|
9
|
+
SYSTEM_CONNECTOR_NAMES = ("qq", "telegram", "discord", "slack", "feishu", "whatsapp", "lingzhu")
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@dataclass(frozen=True)
|
|
@@ -20,6 +21,10 @@ def config_filename(name: str) -> str:
|
|
|
20
21
|
return f"{name}.yaml"
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def default_system_enabled_connectors() -> dict[str, bool]:
|
|
25
|
+
return {name: name == "qq" for name in SYSTEM_CONNECTOR_NAMES}
|
|
26
|
+
|
|
27
|
+
|
|
23
28
|
def default_config(home: Path) -> dict:
|
|
24
29
|
return {
|
|
25
30
|
"home": str(home),
|
|
@@ -65,6 +70,7 @@ def default_config(home: Path) -> dict:
|
|
|
65
70
|
"auto_ack": True,
|
|
66
71
|
"milestone_push": True,
|
|
67
72
|
"direct_chat_enabled": True,
|
|
73
|
+
"system_enabled": default_system_enabled_connectors(),
|
|
68
74
|
},
|
|
69
75
|
"cloud": {
|
|
70
76
|
"enabled": False,
|
|
@@ -37,6 +37,7 @@ from .models import (
|
|
|
37
37
|
OPTIONAL_CONFIG_NAMES,
|
|
38
38
|
REQUIRED_CONFIG_NAMES,
|
|
39
39
|
ConfigFileInfo,
|
|
40
|
+
SYSTEM_CONNECTOR_NAMES,
|
|
40
41
|
config_filename,
|
|
41
42
|
default_payload,
|
|
42
43
|
)
|
|
@@ -95,6 +96,33 @@ class ConfigManager:
|
|
|
95
96
|
def load_runners_config(self) -> dict:
|
|
96
97
|
return apply_runners_runtime_overrides(self.load_named_normalized("runners"))
|
|
97
98
|
|
|
99
|
+
def load_runtime_config(self) -> dict:
|
|
100
|
+
return self.load_named_normalized("config")
|
|
101
|
+
|
|
102
|
+
def system_connector_gates(self) -> dict[str, bool]:
|
|
103
|
+
config = self.load_runtime_config()
|
|
104
|
+
connectors = config.get("connectors") if isinstance(config.get("connectors"), dict) else {}
|
|
105
|
+
system_enabled = connectors.get("system_enabled") if isinstance(connectors.get("system_enabled"), dict) else {}
|
|
106
|
+
return {
|
|
107
|
+
name: self._coerce_bool(system_enabled.get(name), default=name == "qq")
|
|
108
|
+
for name in SYSTEM_CONNECTOR_NAMES
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
def system_enabled_connector_names(self) -> list[str]:
|
|
112
|
+
gates = self.system_connector_gates()
|
|
113
|
+
return [name for name in SYSTEM_CONNECTOR_NAMES if gates.get(name, False)]
|
|
114
|
+
|
|
115
|
+
def is_connector_system_enabled(self, name: str) -> bool:
|
|
116
|
+
normalized = str(name or "").strip().lower()
|
|
117
|
+
if not normalized:
|
|
118
|
+
return False
|
|
119
|
+
if normalized == "local":
|
|
120
|
+
return True
|
|
121
|
+
gates = self.system_connector_gates()
|
|
122
|
+
if normalized in gates:
|
|
123
|
+
return gates[normalized]
|
|
124
|
+
return True
|
|
125
|
+
|
|
98
126
|
def load_named_text(self, name: str, create_optional: bool = False) -> str:
|
|
99
127
|
path = self.path_for(name)
|
|
100
128
|
if create_optional and name in OPTIONAL_CONFIG_NAMES and not path.exists():
|
|
@@ -1047,6 +1075,12 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1047
1075
|
checked_at = utc_now()
|
|
1048
1076
|
binary = str(config.get("binary") or "codex").strip() or "codex"
|
|
1049
1077
|
resolved_binary = resolve_runner_binary(binary, runner_name="codex")
|
|
1078
|
+
raw_reasoning_effort = config.get("model_reasoning_effort")
|
|
1079
|
+
reasoning_effort = (
|
|
1080
|
+
str(raw_reasoning_effort).strip()
|
|
1081
|
+
if raw_reasoning_effort is not None and str(raw_reasoning_effort).strip()
|
|
1082
|
+
else ("xhigh" if raw_reasoning_effort is None else None)
|
|
1083
|
+
)
|
|
1050
1084
|
details: dict[str, object] = {
|
|
1051
1085
|
"binary": binary,
|
|
1052
1086
|
"resolved_binary": resolved_binary,
|
|
@@ -1054,7 +1088,7 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1054
1088
|
"model": str(config.get("model") or "gpt-5.4"),
|
|
1055
1089
|
"approval_policy": str(config.get("approval_policy") or "on-request"),
|
|
1056
1090
|
"sandbox_mode": str(config.get("sandbox_mode") or "workspace-write"),
|
|
1057
|
-
"reasoning_effort":
|
|
1091
|
+
"reasoning_effort": reasoning_effort,
|
|
1058
1092
|
"checked_at": checked_at,
|
|
1059
1093
|
}
|
|
1060
1094
|
if not resolved_binary:
|
|
@@ -1087,7 +1121,6 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1087
1121
|
approval_policy = str(config.get("approval_policy") or "on-request").strip()
|
|
1088
1122
|
if approval_policy:
|
|
1089
1123
|
command.extend(["-c", f'approval_policy="{approval_policy}"'])
|
|
1090
|
-
reasoning_effort = str(config.get("model_reasoning_effort") or "xhigh").strip()
|
|
1091
1124
|
if reasoning_effort:
|
|
1092
1125
|
command.extend(["-c", f'model_reasoning_effort="{reasoning_effort}"'])
|
|
1093
1126
|
sandbox_mode = str(config.get("sandbox_mode") or "workspace-write").strip()
|
|
@@ -1378,6 +1411,8 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1378
1411
|
normalized = self._deep_merge(defaults, payload)
|
|
1379
1412
|
bootstrap = normalized.get("bootstrap") if isinstance(normalized.get("bootstrap"), dict) else {}
|
|
1380
1413
|
raw_bootstrap = payload.get("bootstrap") if isinstance(payload.get("bootstrap"), dict) else {}
|
|
1414
|
+
connectors = normalized.get("connectors") if isinstance(normalized.get("connectors"), dict) else {}
|
|
1415
|
+
raw_connectors = payload.get("connectors") if isinstance(payload.get("connectors"), dict) else {}
|
|
1381
1416
|
default_locale = str(defaults.get("default_locale") or "").strip()
|
|
1382
1417
|
current_locale = str(normalized.get("default_locale") or "").strip()
|
|
1383
1418
|
locale_source = str(raw_bootstrap.get("locale_source") or "").strip().lower()
|
|
@@ -1401,6 +1436,25 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1401
1436
|
bootstrap["locale_initialized_at"] = bootstrap.get("locale_initialized_at")
|
|
1402
1437
|
bootstrap["locale_initialized_browser_locale"] = bootstrap.get("locale_initialized_browser_locale")
|
|
1403
1438
|
normalized["bootstrap"] = bootstrap
|
|
1439
|
+
raw_system_enabled = raw_connectors.get("system_enabled") if isinstance(raw_connectors.get("system_enabled"), dict) else {}
|
|
1440
|
+
default_system_enabled = (
|
|
1441
|
+
defaults.get("connectors", {}).get("system_enabled")
|
|
1442
|
+
if isinstance(defaults.get("connectors"), dict)
|
|
1443
|
+
else {}
|
|
1444
|
+
)
|
|
1445
|
+
current_system_enabled = (
|
|
1446
|
+
connectors.get("system_enabled")
|
|
1447
|
+
if isinstance(connectors.get("system_enabled"), dict)
|
|
1448
|
+
else {}
|
|
1449
|
+
)
|
|
1450
|
+
connectors["system_enabled"] = {
|
|
1451
|
+
name: self._coerce_bool(
|
|
1452
|
+
raw_system_enabled.get(name, current_system_enabled.get(name)),
|
|
1453
|
+
default=bool(default_system_enabled.get(name, False)),
|
|
1454
|
+
)
|
|
1455
|
+
for name in SYSTEM_CONNECTOR_NAMES
|
|
1456
|
+
}
|
|
1457
|
+
normalized["connectors"] = connectors
|
|
1404
1458
|
return normalized
|
|
1405
1459
|
|
|
1406
1460
|
@staticmethod
|
|
@@ -1413,6 +1467,20 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1413
1467
|
return False
|
|
1414
1468
|
return abs(initial - 1.0) < 1e-9 and abs(multiplier - 2.0) < 1e-9 and abs(max_backoff - 8.0) < 1e-9
|
|
1415
1469
|
|
|
1470
|
+
@staticmethod
|
|
1471
|
+
def _coerce_bool(value: object, *, default: bool = False) -> bool:
|
|
1472
|
+
if value is None:
|
|
1473
|
+
return default
|
|
1474
|
+
if isinstance(value, bool):
|
|
1475
|
+
return value
|
|
1476
|
+
if isinstance(value, str):
|
|
1477
|
+
normalized = value.strip().lower()
|
|
1478
|
+
if normalized in {"1", "true", "yes", "on", "y"}:
|
|
1479
|
+
return True
|
|
1480
|
+
if normalized in {"0", "false", "no", "off", "n", ""}:
|
|
1481
|
+
return False
|
|
1482
|
+
return bool(value)
|
|
1483
|
+
|
|
1416
1484
|
def _normalize_plugins_payload(self, payload: dict) -> dict:
|
|
1417
1485
|
normalized = deepcopy(payload)
|
|
1418
1486
|
if "load_paths" not in normalized and isinstance(normalized.get("search_paths"), list):
|
|
@@ -14,7 +14,7 @@ from ... import __version__ as DEEPSCIENTIST_VERSION
|
|
|
14
14
|
from ...gitops import commit_detail, compare_refs, diff_file_between_refs, diff_file_for_commit, export_git_graph, list_branch_canvas, log_ref_history
|
|
15
15
|
from ...memory import MemoryService
|
|
16
16
|
from ...quest import QuestService
|
|
17
|
-
from ...shared import generate_id, read_text, resolve_within, sha256_text, utc_now
|
|
17
|
+
from ...shared import generate_id, read_json, read_text, resolve_within, run_command, sha256_text, utc_now
|
|
18
18
|
from ...runners import RunRequest
|
|
19
19
|
|
|
20
20
|
|
|
@@ -171,9 +171,9 @@ npm --prefix src/ui run build</pre>
|
|
|
171
171
|
|
|
172
172
|
def cli_health(self) -> dict:
|
|
173
173
|
online_channels = [
|
|
174
|
-
|
|
175
|
-
for
|
|
176
|
-
if
|
|
174
|
+
snapshot
|
|
175
|
+
for snapshot in self.app.list_connector_statuses()
|
|
176
|
+
if snapshot.get("enabled") is not False
|
|
177
177
|
]
|
|
178
178
|
return {
|
|
179
179
|
"status": "ok",
|
|
@@ -248,12 +248,15 @@ npm --prefix src/ui run build</pre>
|
|
|
248
248
|
else []
|
|
249
249
|
)
|
|
250
250
|
force_connector_rebind_raw = body.get("force_connector_rebind")
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
251
|
+
if force_connector_rebind_raw is None:
|
|
252
|
+
force_connector_rebind = True
|
|
253
|
+
else:
|
|
254
|
+
force_connector_rebind = bool(force_connector_rebind_raw) and str(force_connector_rebind_raw).strip().lower() not in {
|
|
255
|
+
"0",
|
|
256
|
+
"false",
|
|
257
|
+
"no",
|
|
258
|
+
"off",
|
|
259
|
+
}
|
|
257
260
|
requested_baseline_ref = body.get("requested_baseline_ref")
|
|
258
261
|
startup_contract = body.get("startup_contract")
|
|
259
262
|
auto_start = body.get("auto_start") is True
|
|
@@ -645,8 +648,6 @@ npm --prefix src/ui run build</pre>
|
|
|
645
648
|
session = self.app.bash_exec_service.get_session(quest_root, session_id)
|
|
646
649
|
except FileNotFoundError:
|
|
647
650
|
return 404, {"ok": False, "message": f"Unknown terminal session `{session_id}`."}
|
|
648
|
-
if str(session.get("kind") or "").lower() != "terminal":
|
|
649
|
-
return 400, {"ok": False, "message": "not_terminal_session"}
|
|
650
651
|
if str(session.get("status") or "").lower() in {"completed", "failed", "terminated"}:
|
|
651
652
|
return 409, {"ok": False, "message": "terminal_session_inactive", "session": session}
|
|
652
653
|
try:
|
|
@@ -747,6 +748,12 @@ npm --prefix src/ui run build</pre>
|
|
|
747
748
|
def git_branches(self, quest_id: str) -> dict:
|
|
748
749
|
quest_root = self._fresh_quest_service()._quest_root(quest_id)
|
|
749
750
|
payload = list_branch_canvas(quest_root, quest_id=quest_id)
|
|
751
|
+
research_state = self.app.quest_service.read_research_state(quest_root)
|
|
752
|
+
active_workspace_branch = str(research_state.get("current_workspace_branch") or "").strip() or None
|
|
753
|
+
research_head_branch = str(research_state.get("research_head_branch") or "").strip() or None
|
|
754
|
+
payload["active_workspace_ref"] = active_workspace_branch
|
|
755
|
+
payload["research_head_ref"] = research_head_branch
|
|
756
|
+
payload["workspace_mode"] = str(research_state.get("workspace_mode") or "quest").strip() or "quest"
|
|
750
757
|
try:
|
|
751
758
|
branch_summary = self.app.artifact_service.list_research_branches(quest_root)
|
|
752
759
|
except Exception:
|
|
@@ -761,6 +768,8 @@ npm --prefix src/ui run build</pre>
|
|
|
761
768
|
if not ref:
|
|
762
769
|
continue
|
|
763
770
|
summary = branch_summary_by_name.get(ref)
|
|
771
|
+
node["active_workspace"] = ref == active_workspace_branch
|
|
772
|
+
node["research_head"] = ref == research_head_branch
|
|
764
773
|
if not isinstance(summary, dict):
|
|
765
774
|
continue
|
|
766
775
|
node["branch_no"] = summary.get("branch_no")
|
|
@@ -818,6 +827,126 @@ npm --prefix src/ui run build</pre>
|
|
|
818
827
|
quest_root = self._fresh_quest_service()._quest_root(quest_id)
|
|
819
828
|
return diff_file_between_refs(quest_root, base=base, head=head, path=file_path)
|
|
820
829
|
|
|
830
|
+
def file_change_diff(self, quest_id: str, path: str) -> dict:
|
|
831
|
+
query = self.parse_query(path)
|
|
832
|
+
run_id = ((query.get("run_id") or [""])[0] or "").strip()
|
|
833
|
+
event_id = ((query.get("event_id") or [""])[0] or "").strip() or None
|
|
834
|
+
raw_path = ((query.get("path") or [""])[0] or "").strip()
|
|
835
|
+
if not run_id or not raw_path:
|
|
836
|
+
return self._file_change_diff_unavailable(
|
|
837
|
+
raw_path=raw_path,
|
|
838
|
+
run_id=run_id or None,
|
|
839
|
+
event_id=event_id,
|
|
840
|
+
message="`run_id` and `path` are required.",
|
|
841
|
+
ok=False,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
quest_root = self._fresh_quest_service()._quest_root(quest_id)
|
|
845
|
+
run_artifact = read_json(quest_root / ".ds" / "runs" / run_id / "artifact.json", default={})
|
|
846
|
+
if not isinstance(run_artifact, dict):
|
|
847
|
+
return self._file_change_diff_unavailable(
|
|
848
|
+
raw_path=raw_path,
|
|
849
|
+
run_id=run_id,
|
|
850
|
+
event_id=event_id,
|
|
851
|
+
message="Historical patch unavailable. Run artifact metadata is missing.",
|
|
852
|
+
ok=False,
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
record = run_artifact.get("record") if isinstance(run_artifact.get("record"), dict) else {}
|
|
856
|
+
checkpoint = run_artifact.get("checkpoint") if isinstance(run_artifact.get("checkpoint"), dict) else {}
|
|
857
|
+
base = str(record.get("head_commit") or "").strip()
|
|
858
|
+
head = str(checkpoint.get("head") or "").strip()
|
|
859
|
+
branch = str(record.get("branch") or "").strip() or None
|
|
860
|
+
workspace_root = self._file_change_workspace_root(run_artifact, record)
|
|
861
|
+
relative_path = None
|
|
862
|
+
display_path = None
|
|
863
|
+
|
|
864
|
+
if not base or not head:
|
|
865
|
+
relative_path = self._relative_file_change_path(None, raw_path, workspace_root)
|
|
866
|
+
display_path = self._display_file_change_path(quest_root, raw_path, relative_path=relative_path)
|
|
867
|
+
return self._file_change_diff_unavailable(
|
|
868
|
+
raw_path=raw_path,
|
|
869
|
+
run_id=run_id,
|
|
870
|
+
event_id=event_id,
|
|
871
|
+
base=base,
|
|
872
|
+
head=head,
|
|
873
|
+
branch=branch,
|
|
874
|
+
relative_path=relative_path,
|
|
875
|
+
display_path=display_path,
|
|
876
|
+
message="Historical patch unavailable. Run artifact metadata is missing the recorded commit range.",
|
|
877
|
+
ok=True,
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
repo_root = self._resolve_git_repo_root_for_file_change(raw_path, workspace_root)
|
|
881
|
+
relative_path = self._relative_file_change_path(repo_root, raw_path, workspace_root)
|
|
882
|
+
display_path = self._display_file_change_path(quest_root, raw_path, relative_path=relative_path)
|
|
883
|
+
|
|
884
|
+
if repo_root is None or not relative_path:
|
|
885
|
+
return self._file_change_diff_unavailable(
|
|
886
|
+
raw_path=raw_path,
|
|
887
|
+
run_id=run_id,
|
|
888
|
+
event_id=event_id,
|
|
889
|
+
base=base,
|
|
890
|
+
head=head,
|
|
891
|
+
branch=branch,
|
|
892
|
+
relative_path=relative_path,
|
|
893
|
+
display_path=display_path,
|
|
894
|
+
message="Historical patch unavailable. DeepScientist could not map this file to a git worktree.",
|
|
895
|
+
ok=True,
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
try:
|
|
899
|
+
diff = diff_file_between_refs(repo_root, base=base, head=head, path=relative_path)
|
|
900
|
+
except Exception:
|
|
901
|
+
return self._file_change_diff_unavailable(
|
|
902
|
+
raw_path=raw_path,
|
|
903
|
+
run_id=run_id,
|
|
904
|
+
event_id=event_id,
|
|
905
|
+
base=base,
|
|
906
|
+
head=head,
|
|
907
|
+
branch=branch,
|
|
908
|
+
relative_path=relative_path,
|
|
909
|
+
display_path=display_path,
|
|
910
|
+
message=(
|
|
911
|
+
"Historical patch unavailable. The saved run checkpoint does not match the git repository "
|
|
912
|
+
"that currently owns this file."
|
|
913
|
+
),
|
|
914
|
+
ok=True,
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
if (
|
|
918
|
+
not diff.get("binary")
|
|
919
|
+
and not diff.get("lines")
|
|
920
|
+
and int(diff.get("added") or 0) == 0
|
|
921
|
+
and int(diff.get("removed") or 0) == 0
|
|
922
|
+
):
|
|
923
|
+
return self._file_change_diff_unavailable(
|
|
924
|
+
raw_path=raw_path,
|
|
925
|
+
run_id=run_id,
|
|
926
|
+
event_id=event_id,
|
|
927
|
+
base=base,
|
|
928
|
+
head=head,
|
|
929
|
+
branch=branch,
|
|
930
|
+
relative_path=relative_path,
|
|
931
|
+
display_path=display_path,
|
|
932
|
+
message=(
|
|
933
|
+
"Historical patch unavailable. This event only preserved file-level metadata or the edit "
|
|
934
|
+
"did not land in the run's final checkpoint."
|
|
935
|
+
),
|
|
936
|
+
ok=True,
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
return {
|
|
940
|
+
**diff,
|
|
941
|
+
"available": True,
|
|
942
|
+
"source": "run_range",
|
|
943
|
+
"display_path": display_path or relative_path,
|
|
944
|
+
"run_id": run_id,
|
|
945
|
+
"event_id": event_id,
|
|
946
|
+
"branch": branch,
|
|
947
|
+
"message": None,
|
|
948
|
+
}
|
|
949
|
+
|
|
821
950
|
def git_commit_file(self, quest_id: str, path: str) -> dict:
|
|
822
951
|
query = self.parse_query(path)
|
|
823
952
|
sha = ((query.get("sha") or [""])[0] or "").strip()
|
|
@@ -1207,6 +1336,16 @@ npm --prefix src/ui run build</pre>
|
|
|
1207
1336
|
"ok": False,
|
|
1208
1337
|
"message": str(exc),
|
|
1209
1338
|
}
|
|
1339
|
+
raw_reasoning_effort = (
|
|
1340
|
+
body.get("model_reasoning_effort")
|
|
1341
|
+
if "model_reasoning_effort" in body
|
|
1342
|
+
else runner_cfg.get("model_reasoning_effort")
|
|
1343
|
+
)
|
|
1344
|
+
reasoning_effort = (
|
|
1345
|
+
str(raw_reasoning_effort).strip()
|
|
1346
|
+
if raw_reasoning_effort is not None and str(raw_reasoning_effort).strip()
|
|
1347
|
+
else ("xhigh" if raw_reasoning_effort is None else None)
|
|
1348
|
+
)
|
|
1210
1349
|
request = RunRequest(
|
|
1211
1350
|
quest_id=quest_id,
|
|
1212
1351
|
quest_root=quest_root,
|
|
@@ -1218,8 +1357,7 @@ npm --prefix src/ui run build</pre>
|
|
|
1218
1357
|
approval_policy=runner_cfg.get("approval_policy", "on-request"),
|
|
1219
1358
|
sandbox_mode=runner_cfg.get("sandbox_mode", "workspace-write"),
|
|
1220
1359
|
turn_reason=body.get("turn_reason") or "user_message",
|
|
1221
|
-
reasoning_effort=
|
|
1222
|
-
or runner_cfg.get("model_reasoning_effort", "xhigh"),
|
|
1360
|
+
reasoning_effort=reasoning_effort,
|
|
1223
1361
|
)
|
|
1224
1362
|
result = runner.run(request)
|
|
1225
1363
|
if result.output_text:
|
|
@@ -1355,6 +1493,8 @@ npm --prefix src/ui run build</pre>
|
|
|
1355
1493
|
result = self.app.config_manager.save_named_payload(name, body["structured"])
|
|
1356
1494
|
else:
|
|
1357
1495
|
result = self.app.config_manager.save_named_text(name, body.get("content", ""))
|
|
1496
|
+
if result.get("ok") and name == "config":
|
|
1497
|
+
result["runtime_reload"] = self.app.reload_runtime_config()
|
|
1358
1498
|
if result.get("ok") and name == "connectors":
|
|
1359
1499
|
result["runtime_reload"] = self.app.reload_connectors_config()
|
|
1360
1500
|
if result.get("ok") and name == "runners":
|
|
@@ -1411,6 +1551,136 @@ npm --prefix src/ui run build</pre>
|
|
|
1411
1551
|
return {}
|
|
1412
1552
|
return json.loads(raw.decode("utf-8"))
|
|
1413
1553
|
|
|
1554
|
+
@staticmethod
|
|
1555
|
+
def _file_change_workspace_root(run_artifact: dict, record: dict) -> Path | None:
|
|
1556
|
+
workspace_root = str(run_artifact.get("workspace_root") or record.get("workspace_root") or "").strip()
|
|
1557
|
+
if not workspace_root:
|
|
1558
|
+
return None
|
|
1559
|
+
return Path(workspace_root).expanduser()
|
|
1560
|
+
|
|
1561
|
+
@staticmethod
|
|
1562
|
+
def _file_change_path_candidates(raw_path: str, workspace_root: Path | None) -> list[Path]:
|
|
1563
|
+
text = str(raw_path or "").strip()
|
|
1564
|
+
if not text:
|
|
1565
|
+
return []
|
|
1566
|
+
raw_candidate = Path(text).expanduser()
|
|
1567
|
+
candidates: list[Path] = []
|
|
1568
|
+
if raw_candidate.is_absolute():
|
|
1569
|
+
candidates.append(raw_candidate)
|
|
1570
|
+
elif workspace_root is not None:
|
|
1571
|
+
candidates.append(workspace_root / raw_candidate)
|
|
1572
|
+
else:
|
|
1573
|
+
candidates.append(raw_candidate)
|
|
1574
|
+
if workspace_root is not None:
|
|
1575
|
+
candidates.append(workspace_root)
|
|
1576
|
+
|
|
1577
|
+
resolved: list[Path] = []
|
|
1578
|
+
seen: set[str] = set()
|
|
1579
|
+
for candidate in candidates:
|
|
1580
|
+
for variant in (candidate, candidate.resolve(strict=False)):
|
|
1581
|
+
key = str(variant)
|
|
1582
|
+
if key in seen:
|
|
1583
|
+
continue
|
|
1584
|
+
seen.add(key)
|
|
1585
|
+
resolved.append(variant)
|
|
1586
|
+
return resolved
|
|
1587
|
+
|
|
1588
|
+
@staticmethod
|
|
1589
|
+
def _git_probe_root(candidate: Path) -> Path:
|
|
1590
|
+
if candidate.exists():
|
|
1591
|
+
return candidate if candidate.is_dir() else candidate.parent
|
|
1592
|
+
for parent in candidate.parents:
|
|
1593
|
+
if parent.exists():
|
|
1594
|
+
return parent
|
|
1595
|
+
return candidate.parent
|
|
1596
|
+
|
|
1597
|
+
def _resolve_git_repo_root_for_file_change(self, raw_path: str, workspace_root: Path | None) -> Path | None:
|
|
1598
|
+
for candidate in self._file_change_path_candidates(raw_path, workspace_root):
|
|
1599
|
+
probe_root = self._git_probe_root(candidate)
|
|
1600
|
+
if not str(probe_root).strip():
|
|
1601
|
+
continue
|
|
1602
|
+
result = run_command(["git", "rev-parse", "--show-toplevel"], cwd=probe_root, check=False)
|
|
1603
|
+
if result.returncode != 0:
|
|
1604
|
+
continue
|
|
1605
|
+
top_level = result.stdout.strip()
|
|
1606
|
+
if top_level:
|
|
1607
|
+
return Path(top_level).resolve()
|
|
1608
|
+
return None
|
|
1609
|
+
|
|
1610
|
+
def _relative_file_change_path(self, repo_root: Path | None, raw_path: str, workspace_root: Path | None) -> str | None:
|
|
1611
|
+
if repo_root is None:
|
|
1612
|
+
if Path(str(raw_path or "").strip()).is_absolute():
|
|
1613
|
+
return None
|
|
1614
|
+
normalized = str(raw_path or "").strip().replace("\\", "/").lstrip("/")
|
|
1615
|
+
return normalized or None
|
|
1616
|
+
|
|
1617
|
+
repo_root_resolved = repo_root.resolve()
|
|
1618
|
+
for candidate in self._file_change_path_candidates(raw_path, workspace_root):
|
|
1619
|
+
try:
|
|
1620
|
+
return candidate.relative_to(repo_root_resolved).as_posix()
|
|
1621
|
+
except ValueError:
|
|
1622
|
+
continue
|
|
1623
|
+
if Path(str(raw_path or "").strip()).is_absolute():
|
|
1624
|
+
return None
|
|
1625
|
+
normalized = str(raw_path or "").strip().replace("\\", "/").lstrip("/")
|
|
1626
|
+
return normalized or None
|
|
1627
|
+
|
|
1628
|
+
@staticmethod
|
|
1629
|
+
def _display_file_change_path(quest_root: Path, raw_path: str, *, relative_path: str | None = None) -> str:
|
|
1630
|
+
quest_root_resolved = quest_root.resolve()
|
|
1631
|
+
for candidate in [Path(str(raw_path or "").strip()).expanduser()]:
|
|
1632
|
+
for variant in (candidate, candidate.resolve(strict=False)):
|
|
1633
|
+
try:
|
|
1634
|
+
relative = variant.relative_to(quest_root_resolved)
|
|
1635
|
+
except ValueError:
|
|
1636
|
+
continue
|
|
1637
|
+
parts = relative.parts
|
|
1638
|
+
if len(parts) >= 3 and parts[0] == ".ds" and parts[1] == "worktrees":
|
|
1639
|
+
branch_root = parts[2]
|
|
1640
|
+
remainder = Path(*parts[3:]).as_posix() if len(parts) > 3 else ""
|
|
1641
|
+
return f"{branch_root}/{remainder}" if remainder else branch_root
|
|
1642
|
+
return relative.as_posix()
|
|
1643
|
+
if relative_path:
|
|
1644
|
+
return relative_path
|
|
1645
|
+
text = str(raw_path or "").strip()
|
|
1646
|
+
return Path(text).name or text
|
|
1647
|
+
|
|
1648
|
+
@staticmethod
|
|
1649
|
+
def _file_change_diff_unavailable(
|
|
1650
|
+
*,
|
|
1651
|
+
raw_path: str,
|
|
1652
|
+
run_id: str | None,
|
|
1653
|
+
event_id: str | None,
|
|
1654
|
+
message: str,
|
|
1655
|
+
ok: bool,
|
|
1656
|
+
base: str = "",
|
|
1657
|
+
head: str = "",
|
|
1658
|
+
branch: str | None = None,
|
|
1659
|
+
relative_path: str | None = None,
|
|
1660
|
+
display_path: str | None = None,
|
|
1661
|
+
) -> dict:
|
|
1662
|
+
normalized_path = relative_path or str(raw_path or "").strip()
|
|
1663
|
+
return {
|
|
1664
|
+
"ok": ok,
|
|
1665
|
+
"available": False,
|
|
1666
|
+
"source": "unavailable",
|
|
1667
|
+
"run_id": run_id,
|
|
1668
|
+
"event_id": event_id,
|
|
1669
|
+
"branch": branch,
|
|
1670
|
+
"display_path": display_path or normalized_path,
|
|
1671
|
+
"base": base,
|
|
1672
|
+
"head": head,
|
|
1673
|
+
"path": normalized_path,
|
|
1674
|
+
"old_path": None,
|
|
1675
|
+
"status": "modified",
|
|
1676
|
+
"binary": False,
|
|
1677
|
+
"added": 0,
|
|
1678
|
+
"removed": 0,
|
|
1679
|
+
"lines": [],
|
|
1680
|
+
"truncated": False,
|
|
1681
|
+
"message": message,
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1414
1684
|
@staticmethod
|
|
1415
1685
|
def _markdown_title(path: Path) -> str:
|
|
1416
1686
|
content = read_text(path)
|
|
@@ -60,6 +60,7 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
|
|
|
60
60
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/commit$"), "git_commit"),
|
|
61
61
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/diff-file$"), "git_diff_file"),
|
|
62
62
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/commit-file$"), "git_commit_file"),
|
|
63
|
+
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/operations/file-change-diff$"), "file_change_diff"),
|
|
63
64
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/runs$"), "runs"),
|
|
64
65
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/memory$"), "quest_memory"),
|
|
65
66
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/documents$"), "documents"),
|