@researai/deepscientist 1.5.14 → 1.5.16
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 +336 -90
- package/assets/branding/logo-raster.png +0 -0
- package/bin/ds.js +816 -131
- package/docs/en/00_QUICK_START.md +36 -15
- package/docs/en/01_SETTINGS_REFERENCE.md +53 -4
- package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/en/05_TUI_GUIDE.md +6 -0
- package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
- package/docs/en/09_DOCTOR.md +11 -5
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
- package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
- package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
- package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
- package/docs/en/README.md +24 -0
- package/docs/zh/00_QUICK_START.md +36 -15
- package/docs/zh/01_SETTINGS_REFERENCE.md +53 -4
- package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/zh/05_TUI_GUIDE.md +6 -0
- package/docs/zh/09_DOCTOR.md +11 -5
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
- package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
- package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
- package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
- package/docs/zh/README.md +24 -0
- package/install.sh +2 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/acp/envelope.py +6 -0
- package/src/deepscientist/artifact/charts.py +567 -0
- package/src/deepscientist/artifact/guidance.py +50 -10
- package/src/deepscientist/artifact/metrics.py +228 -5
- package/src/deepscientist/artifact/schemas.py +3 -0
- package/src/deepscientist/artifact/service.py +4276 -308
- package/src/deepscientist/bash_exec/models.py +23 -0
- package/src/deepscientist/bash_exec/monitor.py +147 -67
- package/src/deepscientist/bash_exec/runtime.py +218 -156
- package/src/deepscientist/bash_exec/service.py +309 -69
- package/src/deepscientist/bash_exec/shells.py +87 -0
- package/src/deepscientist/bridges/connectors.py +51 -2
- package/src/deepscientist/cli.py +115 -19
- package/src/deepscientist/codex_cli_compat.py +232 -0
- package/src/deepscientist/config/models.py +8 -4
- package/src/deepscientist/config/service.py +38 -11
- package/src/deepscientist/connector/weixin_support.py +122 -1
- package/src/deepscientist/daemon/api/handlers.py +199 -9
- package/src/deepscientist/daemon/api/router.py +5 -0
- package/src/deepscientist/daemon/app.py +1458 -289
- package/src/deepscientist/doctor.py +51 -0
- package/src/deepscientist/file_lock.py +48 -0
- package/src/deepscientist/gitops/__init__.py +10 -1
- package/src/deepscientist/gitops/diff.py +296 -1
- package/src/deepscientist/gitops/service.py +4 -1
- package/src/deepscientist/mcp/server.py +212 -5
- package/src/deepscientist/process_control.py +161 -0
- package/src/deepscientist/prompts/builder.py +501 -453
- package/src/deepscientist/quest/layout.py +15 -2
- package/src/deepscientist/quest/service.py +2539 -195
- package/src/deepscientist/quest/stage_views.py +177 -1
- package/src/deepscientist/runners/base.py +2 -0
- package/src/deepscientist/runners/codex.py +169 -31
- package/src/deepscientist/runners/runtime_overrides.py +17 -1
- package/src/deepscientist/skills/__init__.py +2 -2
- package/src/deepscientist/skills/installer.py +196 -5
- package/src/deepscientist/skills/registry.py +66 -0
- package/src/prompts/connectors/qq.md +18 -8
- package/src/prompts/connectors/weixin.md +16 -6
- package/src/prompts/contracts/shared_interaction.md +24 -4
- package/src/prompts/system.md +921 -72
- package/src/prompts/system_copilot.md +43 -0
- package/src/skills/analysis-campaign/SKILL.md +32 -2
- package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
- package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
- package/src/skills/baseline/SKILL.md +10 -0
- package/src/skills/decision/SKILL.md +27 -2
- package/src/skills/experiment/SKILL.md +16 -2
- package/src/skills/figure-polish/SKILL.md +1 -0
- package/src/skills/finalize/SKILL.md +19 -0
- package/src/skills/idea/SKILL.md +79 -0
- package/src/skills/idea/references/idea-generation-playbook.md +100 -0
- package/src/skills/idea/references/outline-seeding-example.md +60 -0
- package/src/skills/intake-audit/SKILL.md +9 -1
- package/src/skills/mentor/SKILL.md +217 -0
- package/src/skills/mentor/references/correction-rules.md +210 -0
- package/src/skills/mentor/references/knowledge-profile.md +91 -0
- package/src/skills/mentor/references/persona-profile.md +138 -0
- package/src/skills/mentor/references/taste-profile.md +128 -0
- package/src/skills/mentor/references/thought-style-profile.md +138 -0
- package/src/skills/mentor/references/work-profile.md +289 -0
- package/src/skills/mentor/references/workflow-profile.md +240 -0
- package/src/skills/optimize/SKILL.md +1645 -0
- package/src/skills/rebuttal/SKILL.md +3 -1
- package/src/skills/review/SKILL.md +3 -1
- package/src/skills/scout/SKILL.md +8 -0
- package/src/skills/write/SKILL.md +81 -12
- package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
- package/src/tui/dist/app/AppContainer.js +22 -11
- package/src/tui/dist/index.js +4 -1
- package/src/tui/dist/lib/api.js +33 -3
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
- package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
- package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
- package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
- package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
- package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
- package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
- package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
- package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
- package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
- package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
- package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
- package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
- package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
- package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
- package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
- package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
- package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
- package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
- package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
- package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
- package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
- package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
- package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
- package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
- package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
- package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
- package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
- package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
- package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
- package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
- package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
- package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
- package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
- package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
- package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
- package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
- package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
- package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
- package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
- package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
- package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
- package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
- package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
- package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
- package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
- package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
- package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
- package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
- package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
- package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
- package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
- package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
- package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
- package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
- package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
- package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
- package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
- package/src/ui/dist/index.html +5 -2
- package/src/ui/dist/assets/AiManusChatView-DaF9Nge_.js +0 -26597
- package/src/ui/dist/assets/AnalysisPlugin-BSVx6dXE.js +0 -123
- package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
- package/src/ui/dist/assets/CodeEditorPlugin-DU9G0Tox.js +0 -427
- package/src/ui/dist/assets/CodeViewerPlugin-DoX_fI9l.js +0 -905
- package/src/ui/dist/assets/DocViewerPlugin-C4FWIXuU.js +0 -278
- package/src/ui/dist/assets/GitDiffViewerPlugin-BgfFMgtf.js +0 -2661
- package/src/ui/dist/assets/ImageViewerPlugin-tcPkfY_x.js +0 -500
- package/src/ui/dist/assets/LabCopilotPanel-_dKV60Bf.js +0 -4104
- package/src/ui/dist/assets/LabPlugin-Bje0ayoC.js +0 -2677
- package/src/ui/dist/assets/LatexPlugin-CVsBzAln.js +0 -1792
- package/src/ui/dist/assets/MarkdownViewerPlugin-xjmrqv_8.js +0 -308
- package/src/ui/dist/assets/MarketplacePlugin-mMM2A8wP.js +0 -413
- package/src/ui/dist/assets/NotebookEditor-3kVDSOBo.js +0 -4214
- package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
- package/src/ui/dist/assets/NotebookEditor-SoJ8X-MO.js +0 -84873
- package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
- package/src/ui/dist/assets/PdfLoader-DElVuHl9.js +0 -25468
- package/src/ui/dist/assets/PdfMarkdownPlugin-Bq88XT4G.js +0 -409
- package/src/ui/dist/assets/PdfViewerPlugin-CsCXMo9S.js +0 -3095
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
- package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
- package/src/ui/dist/assets/SearchPlugin-oUPvy19k.js +0 -741
- package/src/ui/dist/assets/TextViewerPlugin-CRkT9yNy.js +0 -472
- package/src/ui/dist/assets/VNCViewer-BgbuvWhR.js +0 -18821
- package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
- package/src/ui/dist/assets/bot-v_RASACv.js +0 -21
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
- package/src/ui/dist/assets/code-5hC9d0VH.js +0 -17
- package/src/ui/dist/assets/file-content-D1PxfOrp.js +0 -377
- package/src/ui/dist/assets/file-diff-panel-DG1oT_Hj.js +0 -92
- package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
- package/src/ui/dist/assets/file-socket-BmdFYQlk.js +0 -58
- package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
- package/src/ui/dist/assets/image-Dqe2X2tW.js +0 -18
- package/src/ui/dist/assets/index-BQG-1s2o.css +0 -12553
- package/src/ui/dist/assets/index-DVsMKK_y.js +0 -25
- package/src/ui/dist/assets/index-Duvz8Ip0.js +0 -159
- package/src/ui/dist/assets/index-Nt9hS4ck.js +0 -244829
- package/src/ui/dist/assets/index-RDlNXXx1.js +0 -120
- package/src/ui/dist/assets/monaco-DIXge1CP.js +0 -623
- package/src/ui/dist/assets/pdf-effect-queue-BBTTQaO-.js +0 -47
- package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
- package/src/ui/dist/assets/popover-BWlolyxo.js +0 -476
- package/src/ui/dist/assets/project-sync-BM5PkFH4.js +0 -297
- package/src/ui/dist/assets/select-D4dAtrA8.js +0 -1690
- package/src/ui/dist/assets/sigma-CKbE5jJT.js +0 -22
- package/src/ui/dist/assets/square-check-big-CZNGMgiB.js +0 -17
- package/src/ui/dist/assets/trash-DaB37xAz.js +0 -32
- package/src/ui/dist/assets/useCliAccess-C2OmAcWe.js +0 -957
- package/src/ui/dist/assets/useFileDiffOverlay-Dowd1Ij4.js +0 -53
- package/src/ui/dist/assets/wrap-text-BGjAhAUq.js +0 -35
- package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
- package/src/ui/dist/assets/zoom-out-dMZQMXzc.js +0 -34
|
@@ -11,6 +11,7 @@ from typing import Any
|
|
|
11
11
|
from urllib.error import URLError
|
|
12
12
|
from urllib.request import Request, urlopen
|
|
13
13
|
|
|
14
|
+
from .bash_exec.shells import build_exec_shell_launch, build_terminal_shell_launch
|
|
14
15
|
from .config import ConfigManager
|
|
15
16
|
from .home import ensure_home_layout
|
|
16
17
|
from .runtime_tools import RuntimeToolService
|
|
@@ -321,6 +322,55 @@ def _check_bundles(repo_root: Path) -> dict[str, Any]:
|
|
|
321
322
|
)
|
|
322
323
|
|
|
323
324
|
|
|
325
|
+
def _check_shell_backend() -> dict[str, Any]:
|
|
326
|
+
exec_launch = build_exec_shell_launch("echo ok")
|
|
327
|
+
terminal_launch = build_terminal_shell_launch(Path("doctor-terminal-probe"))
|
|
328
|
+
|
|
329
|
+
def resolve_binary(binary: str) -> str | None:
|
|
330
|
+
candidate = str(binary or "").strip()
|
|
331
|
+
if not candidate:
|
|
332
|
+
return None
|
|
333
|
+
if os.path.isabs(candidate) or os.path.sep in candidate or (os.path.altsep and os.path.altsep in candidate):
|
|
334
|
+
return candidate if Path(candidate).exists() else None
|
|
335
|
+
return which(candidate)
|
|
336
|
+
|
|
337
|
+
details = {
|
|
338
|
+
"exec_shell": exec_launch.shell_name,
|
|
339
|
+
"exec_shell_family": exec_launch.family,
|
|
340
|
+
"exec_argv": exec_launch.argv,
|
|
341
|
+
"terminal_shell": terminal_launch.shell_name,
|
|
342
|
+
"terminal_shell_family": terminal_launch.family,
|
|
343
|
+
"terminal_argv": terminal_launch.argv,
|
|
344
|
+
}
|
|
345
|
+
warnings: list[str] = []
|
|
346
|
+
guidance: list[str] = []
|
|
347
|
+
errors: list[str] = []
|
|
348
|
+
|
|
349
|
+
exec_binary = resolve_binary(exec_launch.argv[0])
|
|
350
|
+
terminal_binary = resolve_binary(terminal_launch.argv[0])
|
|
351
|
+
details["exec_resolved_binary"] = exec_binary
|
|
352
|
+
details["terminal_resolved_binary"] = terminal_binary
|
|
353
|
+
|
|
354
|
+
if sys.platform == "win32":
|
|
355
|
+
warnings.append("Native Windows support is currently experimental; WSL2 remains the most battle-tested path.")
|
|
356
|
+
if not exec_binary:
|
|
357
|
+
errors.append("DeepScientist could not resolve a Windows command shell for bash_exec.")
|
|
358
|
+
guidance.append("Install PowerShell (`pwsh`) or ensure `powershell.exe` is available on PATH.")
|
|
359
|
+
if not terminal_binary:
|
|
360
|
+
errors.append("DeepScientist could not resolve a Windows interactive shell backend.")
|
|
361
|
+
guidance.append("Ensure `powershell.exe` is available on PATH for the interactive terminal surface.")
|
|
362
|
+
return _make_check(
|
|
363
|
+
check_id="shell_backend",
|
|
364
|
+
label="Shell backend",
|
|
365
|
+
ok=len(errors) == 0,
|
|
366
|
+
summary="DeepScientist resolved platform shell backends for command and terminal sessions." if len(errors) == 0 else "DeepScientist could not resolve a required shell backend.",
|
|
367
|
+
warnings=warnings,
|
|
368
|
+
errors=errors,
|
|
369
|
+
guidance=guidance,
|
|
370
|
+
details=details,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
324
374
|
def _check_latex_runtime(home: Path) -> dict[str, Any]:
|
|
325
375
|
runtime = RuntimeToolService(home).status("tinytex")
|
|
326
376
|
pdflatex = runtime.get("binaries", {}).get("pdflatex") or {}
|
|
@@ -436,6 +486,7 @@ def run_doctor(home: Path, *, repo_root: Path) -> dict[str, Any]:
|
|
|
436
486
|
_check_python_runtime(),
|
|
437
487
|
_check_home_writable(home),
|
|
438
488
|
_check_uv(home),
|
|
489
|
+
_check_shell_backend(),
|
|
439
490
|
_check_git(config_manager),
|
|
440
491
|
_check_config_validation(config_manager),
|
|
441
492
|
_check_runner_support(config_manager),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterator, TextIO
|
|
7
|
+
|
|
8
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows
|
|
9
|
+
import msvcrt
|
|
10
|
+
else: # pragma: no cover - exercised on POSIX
|
|
11
|
+
import fcntl
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _ensure_lockable_file(handle: TextIO) -> None:
|
|
15
|
+
handle.seek(0, os.SEEK_END)
|
|
16
|
+
if handle.tell() > 0:
|
|
17
|
+
handle.seek(0)
|
|
18
|
+
return
|
|
19
|
+
handle.write("\0")
|
|
20
|
+
handle.flush()
|
|
21
|
+
handle.seek(0)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _lock_handle(handle: TextIO) -> None:
|
|
25
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows
|
|
26
|
+
_ensure_lockable_file(handle)
|
|
27
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
|
|
28
|
+
return
|
|
29
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _unlock_handle(handle: TextIO) -> None:
|
|
33
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows
|
|
34
|
+
handle.seek(0)
|
|
35
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
36
|
+
return
|
|
37
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@contextmanager
|
|
41
|
+
def advisory_file_lock(path: Path) -> Iterator[TextIO]:
|
|
42
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
with path.open("a+", encoding="utf-8") as handle:
|
|
44
|
+
_lock_handle(handle)
|
|
45
|
+
try:
|
|
46
|
+
yield handle
|
|
47
|
+
finally:
|
|
48
|
+
_unlock_handle(handle)
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
from .diff import
|
|
1
|
+
from .diff import (
|
|
2
|
+
commit_detail,
|
|
3
|
+
compare_refs,
|
|
4
|
+
diff_file_between_refs,
|
|
5
|
+
diff_file_for_commit,
|
|
6
|
+
list_branch_canvas,
|
|
7
|
+
list_commit_canvas,
|
|
8
|
+
log_ref_history,
|
|
9
|
+
)
|
|
2
10
|
from .graph import export_git_graph
|
|
3
11
|
from .service import (
|
|
4
12
|
branch_exists,
|
|
@@ -26,5 +34,6 @@ __all__ = [
|
|
|
26
34
|
"head_commit",
|
|
27
35
|
"init_repo",
|
|
28
36
|
"list_branch_canvas",
|
|
37
|
+
"list_commit_canvas",
|
|
29
38
|
"log_ref_history",
|
|
30
39
|
]
|
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from ..artifact.metrics import extract_latest_metric
|
|
8
|
-
from ..shared import read_json, run_command
|
|
8
|
+
from ..shared import read_json, run_command, slugify
|
|
9
9
|
from .service import branch_exists, current_branch, head_commit
|
|
10
10
|
|
|
11
11
|
|
|
@@ -68,6 +68,12 @@ def list_branch_canvas(repo: Path, *, quest_id: str) -> dict[str, Any]:
|
|
|
68
68
|
"run_id": state.get("run_id"),
|
|
69
69
|
"run_kind": state.get("run_kind"),
|
|
70
70
|
"idea_id": state.get("idea_id"),
|
|
71
|
+
"paper_line_id": state.get("paper_line_id"),
|
|
72
|
+
"paper_line_branch": state.get("paper_line_branch"),
|
|
73
|
+
"selected_outline_ref": state.get("selected_outline_ref"),
|
|
74
|
+
"source_branch": state.get("source_branch"),
|
|
75
|
+
"source_run_id": state.get("source_run_id"),
|
|
76
|
+
"source_idea_id": state.get("source_idea_id"),
|
|
71
77
|
"parent_branch_recorded": state.get("parent_branch"),
|
|
72
78
|
"worktree_root": state.get("worktree_root"),
|
|
73
79
|
"latest_metric": state.get("latest_metric"),
|
|
@@ -107,6 +113,67 @@ def list_branch_canvas(repo: Path, *, quest_id: str) -> dict[str, Any]:
|
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
|
|
116
|
+
def list_commit_canvas(repo: Path, *, quest_id: str, limit: int = 80) -> dict[str, Any]:
|
|
117
|
+
resolved_limit = max(1, min(int(limit), 200))
|
|
118
|
+
head = head_commit(repo)
|
|
119
|
+
current_ref = current_branch(repo)
|
|
120
|
+
commits = _git_commit_canvas_log(repo, limit=resolved_limit)
|
|
121
|
+
nodes: list[dict[str, Any]] = []
|
|
122
|
+
edges: list[dict[str, Any]] = []
|
|
123
|
+
seen_shas = {item["sha"] for item in commits if str(item.get("sha") or "").strip()}
|
|
124
|
+
|
|
125
|
+
for commit in commits:
|
|
126
|
+
sha = str(commit.get("sha") or "").strip()
|
|
127
|
+
if not sha:
|
|
128
|
+
continue
|
|
129
|
+
detail = commit_detail(repo, sha=sha)
|
|
130
|
+
parents = [str(item).strip() for item in (detail.get("parents") or []) if str(item).strip()]
|
|
131
|
+
files = detail.get("files") or []
|
|
132
|
+
changed_paths = [
|
|
133
|
+
str(item.get("path") or "").strip()
|
|
134
|
+
for item in files
|
|
135
|
+
if str(item.get("path") or "").strip()
|
|
136
|
+
]
|
|
137
|
+
node = {
|
|
138
|
+
"sha": sha,
|
|
139
|
+
"short_sha": str(detail.get("short_sha") or commit.get("short_sha") or sha[:7]).strip(),
|
|
140
|
+
"parents": parents,
|
|
141
|
+
"subject": str(detail.get("subject") or commit.get("subject") or "").strip() or sha[:7],
|
|
142
|
+
"body_preview": _body_preview(str(detail.get("body") or "").strip()),
|
|
143
|
+
"authored_at": str(detail.get("authored_at") or commit.get("authored_at") or "").strip() or None,
|
|
144
|
+
"author_name": str(detail.get("author_name") or commit.get("author_name") or "").strip() or None,
|
|
145
|
+
"branch_refs": _normalize_branch_refs(commit.get("decorations")),
|
|
146
|
+
"current": bool(head and sha == head),
|
|
147
|
+
"active_workspace": bool(head and sha == head),
|
|
148
|
+
"changed_paths": changed_paths,
|
|
149
|
+
"file_count": int(detail.get("file_count") or len(files)),
|
|
150
|
+
"added": int(((detail.get("stats") or {}) if isinstance(detail.get("stats"), dict) else {}).get("added") or 0),
|
|
151
|
+
"removed": int(((detail.get("stats") or {}) if isinstance(detail.get("stats"), dict) else {}).get("removed") or 0),
|
|
152
|
+
"compare_base": parents[0] if parents else None,
|
|
153
|
+
"compare_head": sha,
|
|
154
|
+
"selection_type": "git_commit_node",
|
|
155
|
+
}
|
|
156
|
+
nodes.append(node)
|
|
157
|
+
for parent in parents:
|
|
158
|
+
if parent in seen_shas:
|
|
159
|
+
edges.append(
|
|
160
|
+
{
|
|
161
|
+
"from": parent,
|
|
162
|
+
"to": sha,
|
|
163
|
+
"relation": "parent",
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"quest_id": quest_id,
|
|
169
|
+
"workspace_mode": "copilot",
|
|
170
|
+
"head": head,
|
|
171
|
+
"current_ref": current_ref,
|
|
172
|
+
"nodes": nodes,
|
|
173
|
+
"edges": edges,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
110
177
|
def compare_refs(repo: Path, *, base: str, head: str) -> dict[str, Any]:
|
|
111
178
|
_require_ref(repo, base)
|
|
112
179
|
_require_ref(repo, head)
|
|
@@ -356,9 +423,158 @@ def _collect_branch_state(repo: Path) -> dict[str, dict[str, Any]]:
|
|
|
356
423
|
for item in state.get("recent_artifacts", []):
|
|
357
424
|
if isinstance(item, dict):
|
|
358
425
|
item.pop("_sort_key", None)
|
|
426
|
+
for workspace_root in _canvas_workspace_roots(repo):
|
|
427
|
+
state_path = workspace_root / "paper" / "paper_line_state.json"
|
|
428
|
+
if not state_path.exists():
|
|
429
|
+
continue
|
|
430
|
+
payload = read_json(state_path, {})
|
|
431
|
+
if not isinstance(payload, dict) or not payload:
|
|
432
|
+
continue
|
|
433
|
+
paper_branch = str(payload.get("paper_branch") or "").strip() or current_branch(workspace_root)
|
|
434
|
+
if not paper_branch:
|
|
435
|
+
continue
|
|
436
|
+
state = branch_state[paper_branch]
|
|
437
|
+
state.setdefault("branch", paper_branch)
|
|
438
|
+
state["worktree_root"] = str(workspace_root)
|
|
439
|
+
state["paper_line_id"] = str(payload.get("paper_line_id") or "").strip() or state.get("paper_line_id")
|
|
440
|
+
state["paper_line_branch"] = paper_branch
|
|
441
|
+
state["selected_outline_ref"] = str(payload.get("selected_outline_ref") or "").strip() or state.get("selected_outline_ref")
|
|
442
|
+
state["source_branch"] = str(payload.get("source_branch") or "").strip() or state.get("source_branch")
|
|
443
|
+
state["source_run_id"] = str(payload.get("source_run_id") or "").strip() or state.get("source_run_id")
|
|
444
|
+
state["source_idea_id"] = str(payload.get("source_idea_id") or "").strip() or state.get("source_idea_id")
|
|
445
|
+
state["updated_at"] = str(payload.get("updated_at") or state.get("updated_at") or "")
|
|
446
|
+
if not state.get("parent_branch") and state.get("source_branch"):
|
|
447
|
+
state["parent_branch"] = state.get("source_branch")
|
|
448
|
+
for workspace_root in _canvas_workspace_roots(repo):
|
|
449
|
+
paper_root = workspace_root / "paper"
|
|
450
|
+
if not paper_root.exists():
|
|
451
|
+
continue
|
|
452
|
+
state_path = paper_root / "paper_line_state.json"
|
|
453
|
+
if state_path.exists():
|
|
454
|
+
continue
|
|
455
|
+
selected_outline = read_json(paper_root / "selected_outline.json", {})
|
|
456
|
+
bundle_manifest = read_json(paper_root / "paper_bundle_manifest.json", {})
|
|
457
|
+
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
458
|
+
bundle_manifest = bundle_manifest if isinstance(bundle_manifest, dict) else {}
|
|
459
|
+
if not selected_outline and not bundle_manifest:
|
|
460
|
+
continue
|
|
461
|
+
paper_branch = str(bundle_manifest.get("paper_branch") or "").strip() or current_branch(workspace_root)
|
|
462
|
+
if not paper_branch:
|
|
463
|
+
continue
|
|
464
|
+
selected_outline_ref = str(
|
|
465
|
+
selected_outline.get("outline_id") or bundle_manifest.get("selected_outline_ref") or ""
|
|
466
|
+
).strip() or None
|
|
467
|
+
source_run_id = str(bundle_manifest.get("source_run_id") or "").strip() or None
|
|
468
|
+
state = branch_state[paper_branch]
|
|
469
|
+
state.setdefault("branch", paper_branch)
|
|
470
|
+
state["worktree_root"] = str(workspace_root)
|
|
471
|
+
state["paper_line_id"] = state.get("paper_line_id") or slugify(
|
|
472
|
+
"::".join([paper_branch or "paper", selected_outline_ref or "outline", source_run_id or "run"]),
|
|
473
|
+
"paper-line",
|
|
474
|
+
)
|
|
475
|
+
state["paper_line_branch"] = paper_branch
|
|
476
|
+
state["selected_outline_ref"] = selected_outline_ref or state.get("selected_outline_ref")
|
|
477
|
+
state["source_branch"] = str(bundle_manifest.get("source_branch") or "").strip() or state.get("source_branch")
|
|
478
|
+
state["source_run_id"] = source_run_id or state.get("source_run_id")
|
|
479
|
+
state["source_idea_id"] = str(bundle_manifest.get("source_idea_id") or "").strip() or state.get("source_idea_id")
|
|
480
|
+
if not state.get("parent_branch") and state.get("source_branch"):
|
|
481
|
+
state["parent_branch"] = state.get("source_branch")
|
|
482
|
+
campaigns_root = repo / ".ds" / "analysis_campaigns"
|
|
483
|
+
if campaigns_root.exists():
|
|
484
|
+
for path in sorted(campaigns_root.glob("*.json")):
|
|
485
|
+
manifest = read_json(path, {})
|
|
486
|
+
if not isinstance(manifest, dict) or not manifest:
|
|
487
|
+
continue
|
|
488
|
+
campaign_id = str(manifest.get("campaign_id") or path.stem).strip() or path.stem
|
|
489
|
+
paper_line_id = str(manifest.get("paper_line_id") or "").strip() or None
|
|
490
|
+
paper_line_branch = str(manifest.get("paper_line_branch") or "").strip() or None
|
|
491
|
+
analysis_parent_branch = str(manifest.get("parent_branch") or "").strip() or None
|
|
492
|
+
selected_outline_ref = str(manifest.get("selected_outline_ref") or "").strip() or None
|
|
493
|
+
source_idea_id = str(manifest.get("active_idea_id") or "").strip() or None
|
|
494
|
+
if not paper_line_branch:
|
|
495
|
+
paper_line_branch = _infer_paper_line_branch_for_campaign(
|
|
496
|
+
manifest,
|
|
497
|
+
branch_state=branch_state,
|
|
498
|
+
)
|
|
499
|
+
for item in manifest.get("slices") or []:
|
|
500
|
+
if not isinstance(item, dict):
|
|
501
|
+
continue
|
|
502
|
+
branch_name = str(item.get("branch") or "").strip()
|
|
503
|
+
if not branch_name:
|
|
504
|
+
continue
|
|
505
|
+
state = branch_state[branch_name]
|
|
506
|
+
state.setdefault("branch", branch_name)
|
|
507
|
+
state["campaign_id"] = campaign_id
|
|
508
|
+
state["paper_line_id"] = paper_line_id or state.get("paper_line_id")
|
|
509
|
+
state["paper_line_branch"] = paper_line_branch or state.get("paper_line_branch")
|
|
510
|
+
state["analysis_parent_branch"] = analysis_parent_branch or state.get("analysis_parent_branch")
|
|
511
|
+
state["selected_outline_ref"] = selected_outline_ref or state.get("selected_outline_ref")
|
|
512
|
+
state["source_idea_id"] = source_idea_id or state.get("source_idea_id")
|
|
513
|
+
if item.get("worktree_root"):
|
|
514
|
+
state["worktree_root"] = item.get("worktree_root")
|
|
359
515
|
return branch_state
|
|
360
516
|
|
|
361
517
|
|
|
518
|
+
def _canvas_workspace_roots(repo: Path) -> list[Path]:
|
|
519
|
+
roots: list[Path] = [repo]
|
|
520
|
+
research_state = read_json(repo / ".ds" / "research_state.json", {})
|
|
521
|
+
preferred_raw = str((research_state or {}).get("research_head_worktree_root") or "").strip()
|
|
522
|
+
if preferred_raw:
|
|
523
|
+
preferred = Path(preferred_raw)
|
|
524
|
+
if preferred.exists():
|
|
525
|
+
roots.append(preferred)
|
|
526
|
+
worktrees_root = repo / ".ds" / "worktrees"
|
|
527
|
+
if worktrees_root.exists():
|
|
528
|
+
roots.extend(path for path in sorted(worktrees_root.iterdir()) if path.is_dir())
|
|
529
|
+
deduped: list[Path] = []
|
|
530
|
+
seen: set[str] = set()
|
|
531
|
+
for root in roots:
|
|
532
|
+
key = str(root.resolve())
|
|
533
|
+
if key in seen:
|
|
534
|
+
continue
|
|
535
|
+
seen.add(key)
|
|
536
|
+
deduped.append(root)
|
|
537
|
+
return deduped
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _infer_paper_line_branch_for_campaign(
|
|
541
|
+
manifest: dict[str, Any],
|
|
542
|
+
*,
|
|
543
|
+
branch_state: dict[str, dict[str, Any]],
|
|
544
|
+
) -> str | None:
|
|
545
|
+
selected_outline_ref = str(manifest.get("selected_outline_ref") or "").strip() or None
|
|
546
|
+
source_idea_id = str(manifest.get("active_idea_id") or "").strip() or None
|
|
547
|
+
source_run_id = str(manifest.get("parent_run_id") or "").strip() or None
|
|
548
|
+
source_branch = str(manifest.get("parent_branch") or "").strip() or None
|
|
549
|
+
ranked: list[tuple[int, str]] = []
|
|
550
|
+
for branch_name, state in branch_state.items():
|
|
551
|
+
if not branch_name.startswith("paper/"):
|
|
552
|
+
continue
|
|
553
|
+
score = 0
|
|
554
|
+
candidate_outline = str(state.get("selected_outline_ref") or "").strip() or None
|
|
555
|
+
candidate_idea = str(state.get("source_idea_id") or "").strip() or None
|
|
556
|
+
candidate_run = str(state.get("source_run_id") or "").strip() or None
|
|
557
|
+
candidate_branch = str(state.get("source_branch") or "").strip() or None
|
|
558
|
+
if selected_outline_ref:
|
|
559
|
+
if candidate_outline != selected_outline_ref:
|
|
560
|
+
continue
|
|
561
|
+
score += 2
|
|
562
|
+
if source_idea_id and candidate_idea == source_idea_id:
|
|
563
|
+
score += 4
|
|
564
|
+
if source_run_id and candidate_run == source_run_id:
|
|
565
|
+
score += 3
|
|
566
|
+
if source_branch and candidate_branch == source_branch:
|
|
567
|
+
score += 2
|
|
568
|
+
if score > 0:
|
|
569
|
+
ranked.append((score, branch_name))
|
|
570
|
+
if not ranked:
|
|
571
|
+
return None
|
|
572
|
+
ranked.sort(key=lambda item: (item[0], item[1]), reverse=True)
|
|
573
|
+
if len(ranked) > 1 and ranked[0][0] == ranked[1][0]:
|
|
574
|
+
return None
|
|
575
|
+
return ranked[0][1]
|
|
576
|
+
|
|
577
|
+
|
|
362
578
|
def _resolve_run_result_payload(repo: Path, record: dict[str, Any]) -> dict[str, Any]:
|
|
363
579
|
details = dict(record.get("details") or {}) if isinstance(record.get("details"), dict) else {}
|
|
364
580
|
paths = dict(record.get("paths") or {}) if isinstance(record.get("paths"), dict) else {}
|
|
@@ -469,9 +685,17 @@ def _infer_parent_ref(
|
|
|
469
685
|
) -> str | None:
|
|
470
686
|
if ref == default_ref:
|
|
471
687
|
return None
|
|
688
|
+
if classifications[ref]["branch_kind"] == "analysis":
|
|
689
|
+
paper_line_branch = str(state.get("paper_line_branch") or "").strip()
|
|
690
|
+
if paper_line_branch and paper_line_branch in refs and paper_line_branch != ref:
|
|
691
|
+
return paper_line_branch
|
|
472
692
|
parent_branch = str(state.get("parent_branch") or "").strip()
|
|
473
693
|
if parent_branch and parent_branch in refs and parent_branch != ref:
|
|
474
694
|
return parent_branch
|
|
695
|
+
if classifications[ref]["branch_kind"] == "paper":
|
|
696
|
+
source_branch = str(state.get("source_branch") or "").strip()
|
|
697
|
+
if source_branch and source_branch in refs and source_branch != ref:
|
|
698
|
+
return source_branch
|
|
475
699
|
if ref.startswith("idea/"):
|
|
476
700
|
return default_ref
|
|
477
701
|
if state.get("idea_id"):
|
|
@@ -479,6 +703,9 @@ def _infer_parent_ref(
|
|
|
479
703
|
if candidate in refs:
|
|
480
704
|
return candidate
|
|
481
705
|
if classifications[ref]["branch_kind"] == "analysis":
|
|
706
|
+
analysis_parent_branch = str(state.get("analysis_parent_branch") or "").strip()
|
|
707
|
+
if analysis_parent_branch and analysis_parent_branch in refs and analysis_parent_branch != ref:
|
|
708
|
+
return analysis_parent_branch
|
|
482
709
|
major_refs = [
|
|
483
710
|
candidate
|
|
484
711
|
for candidate, meta in classifications.items()
|
|
@@ -609,6 +836,74 @@ def _git_log(repo: Path, *, revspec: str, limit: int = 30) -> list[dict[str, Any
|
|
|
609
836
|
return commits
|
|
610
837
|
|
|
611
838
|
|
|
839
|
+
def _git_commit_canvas_log(repo: Path, *, limit: int = 80) -> list[dict[str, Any]]:
|
|
840
|
+
result = _git_stdout(
|
|
841
|
+
repo,
|
|
842
|
+
[
|
|
843
|
+
"log",
|
|
844
|
+
"--all",
|
|
845
|
+
"--topo-order",
|
|
846
|
+
"--date=iso-strict",
|
|
847
|
+
f"-n{limit}",
|
|
848
|
+
"--decorate=short",
|
|
849
|
+
"--pretty=format:%H%x1f%h%x1f%P%x1f%ad%x1f%an%x1f%s%x1f%b%x1f%D",
|
|
850
|
+
],
|
|
851
|
+
)
|
|
852
|
+
commits: list[dict[str, Any]] = []
|
|
853
|
+
for line in result.splitlines():
|
|
854
|
+
if not line.strip():
|
|
855
|
+
continue
|
|
856
|
+
sha, short_sha, parents_raw, authored_at, author_name, subject, body, decorations = (
|
|
857
|
+
line.split("\x1f") + ["", "", "", "", "", "", "", ""]
|
|
858
|
+
)[:8]
|
|
859
|
+
commits.append(
|
|
860
|
+
{
|
|
861
|
+
"sha": sha.strip(),
|
|
862
|
+
"short_sha": short_sha.strip(),
|
|
863
|
+
"parents": [item for item in parents_raw.strip().split() if item],
|
|
864
|
+
"authored_at": authored_at.strip(),
|
|
865
|
+
"author_name": author_name.strip(),
|
|
866
|
+
"subject": subject.strip(),
|
|
867
|
+
"body": body.strip(),
|
|
868
|
+
"decorations": decorations.strip(),
|
|
869
|
+
}
|
|
870
|
+
)
|
|
871
|
+
return commits
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def _normalize_branch_refs(raw: Any) -> list[str]:
|
|
875
|
+
text = str(raw or "").strip()
|
|
876
|
+
if not text:
|
|
877
|
+
return []
|
|
878
|
+
refs: list[str] = []
|
|
879
|
+
for part in text.split(","):
|
|
880
|
+
cleaned = part.strip()
|
|
881
|
+
if not cleaned:
|
|
882
|
+
continue
|
|
883
|
+
if cleaned.startswith("HEAD -> "):
|
|
884
|
+
cleaned = cleaned[len("HEAD -> ") :].strip()
|
|
885
|
+
if cleaned.startswith("tag: "):
|
|
886
|
+
continue
|
|
887
|
+
if cleaned.startswith("origin/"):
|
|
888
|
+
continue
|
|
889
|
+
if cleaned == "HEAD":
|
|
890
|
+
continue
|
|
891
|
+
refs.append(cleaned)
|
|
892
|
+
return refs
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _body_preview(body: str, *, max_lines: int = 3, max_chars: int = 220) -> str | None:
|
|
896
|
+
if not body:
|
|
897
|
+
return None
|
|
898
|
+
lines = [line.strip() for line in body.splitlines() if line.strip()]
|
|
899
|
+
if not lines:
|
|
900
|
+
return None
|
|
901
|
+
preview = " ".join(lines[:max_lines]).strip()
|
|
902
|
+
if len(preview) > max_chars:
|
|
903
|
+
preview = preview[: max_chars - 1].rstrip() + "…"
|
|
904
|
+
return preview or None
|
|
905
|
+
|
|
906
|
+
|
|
612
907
|
def _normalize_patch_lines(patch: str) -> list[str]:
|
|
613
908
|
lines = [line.rstrip("\n") for line in patch.splitlines()]
|
|
614
909
|
if not lines:
|
|
@@ -100,7 +100,10 @@ def checkpoint_repo(repo: Path, message: str, allow_empty: bool = False) -> dict
|
|
|
100
100
|
"branch": current_branch(repo),
|
|
101
101
|
"head": head_commit(repo),
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
command = ["git", "commit", "-m", message]
|
|
104
|
+
if allow_empty:
|
|
105
|
+
command.insert(2, "--allow-empty")
|
|
106
|
+
result = run_command(command, cwd=repo, check=False)
|
|
104
107
|
return {
|
|
105
108
|
"committed": result.returncode == 0,
|
|
106
109
|
"branch": current_branch(repo),
|