@researai/deepscientist 1.5.15 → 1.5.17
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 +385 -104
- package/bin/ds.js +1241 -110
- package/docs/en/00_QUICK_START.md +100 -19
- package/docs/en/01_SETTINGS_REFERENCE.md +34 -1
- package/docs/en/02_START_RESEARCH_GUIDE.md +7 -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 +25 -8
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +37 -11
- 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/21_LOCAL_MODEL_BACKENDS_GUIDE.md +283 -0
- package/docs/en/91_DEVELOPMENT.md +237 -0
- package/docs/en/README.md +24 -2
- package/docs/zh/00_QUICK_START.md +89 -19
- package/docs/zh/01_SETTINGS_REFERENCE.md +34 -1
- package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
- package/docs/zh/05_TUI_GUIDE.md +6 -0
- package/docs/zh/09_DOCTOR.md +26 -9
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +37 -11
- 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/21_LOCAL_MODEL_BACKENDS_GUIDE.md +281 -0
- package/docs/zh/README.md +24 -2
- package/install.sh +46 -4
- package/package.json +2 -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/service.py +647 -22
- package/src/deepscientist/bash_exec/service.py +234 -9
- package/src/deepscientist/bridges/connectors.py +8 -2
- package/src/deepscientist/cli.py +115 -19
- package/src/deepscientist/codex_cli_compat.py +367 -22
- package/src/deepscientist/config/models.py +2 -1
- package/src/deepscientist/config/service.py +183 -13
- package/src/deepscientist/daemon/api/handlers.py +255 -31
- package/src/deepscientist/daemon/api/router.py +9 -0
- package/src/deepscientist/daemon/app.py +1146 -105
- package/src/deepscientist/diagnostics/__init__.py +6 -0
- package/src/deepscientist/diagnostics/runner_failures.py +130 -0
- package/src/deepscientist/doctor.py +207 -3
- package/src/deepscientist/gitops/__init__.py +10 -1
- package/src/deepscientist/gitops/diff.py +129 -0
- package/src/deepscientist/gitops/service.py +4 -1
- package/src/deepscientist/mcp/server.py +39 -0
- package/src/deepscientist/prompts/builder.py +275 -34
- package/src/deepscientist/quest/layout.py +15 -2
- package/src/deepscientist/quest/service.py +707 -55
- package/src/deepscientist/quest/stage_views.py +6 -1
- package/src/deepscientist/runners/codex.py +143 -43
- package/src/deepscientist/shared.py +19 -0
- 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 +14 -2
- package/src/prompts/system.md +23 -5
- package/src/prompts/system_copilot.md +56 -0
- package/src/skills/analysis-campaign/SKILL.md +1 -0
- package/src/skills/baseline/SKILL.md +8 -0
- package/src/skills/decision/SKILL.md +8 -0
- package/src/skills/experiment/SKILL.md +8 -0
- package/src/skills/figure-polish/SKILL.md +1 -0
- package/src/skills/finalize/SKILL.md +1 -0
- package/src/skills/idea/SKILL.md +1 -0
- package/src/skills/intake-audit/SKILL.md +8 -0
- 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 +1 -0
- package/src/skills/rebuttal/SKILL.md +1 -0
- package/src/skills/review/SKILL.md +1 -0
- package/src/skills/scout/SKILL.md +8 -0
- package/src/skills/write/SKILL.md +1 -0
- package/src/tui/dist/app/AppContainer.js +19 -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-Bv-Z8YpU.js +204 -0
- package/src/ui/dist/assets/AnalysisPlugin-BCKAfjba.js +1 -0
- package/src/ui/dist/assets/CliPlugin-BCKcpc35.js +109 -0
- package/src/ui/dist/assets/CodeEditorPlugin-DbOfSJ8K.js +2 -0
- package/src/ui/dist/assets/CodeViewerPlugin-CbaFRrUU.js +270 -0
- package/src/ui/dist/assets/DocViewerPlugin-DAjLVeQD.js +7 -0
- package/src/ui/dist/assets/GitCommitViewerPlugin-CIUqbUDO.js +1 -0
- package/src/ui/dist/assets/GitDiffViewerPlugin-CQACjoAA.js +6 -0
- package/src/ui/dist/assets/GitSnapshotViewer-0r4nLPke.js +30 -0
- package/src/ui/dist/assets/ImageViewerPlugin-nBOmI2v_.js +26 -0
- package/src/ui/dist/assets/LabCopilotPanel-BHxOxF4z.js +14 -0
- package/src/ui/dist/assets/LabPlugin-BKoZGs95.js +22 -0
- package/src/ui/dist/assets/LatexPlugin-ZwtV8pIp.js +25 -0
- package/src/ui/dist/assets/MarkdownViewerPlugin-DKqVfKyW.js +128 -0
- package/src/ui/dist/assets/MarketplacePlugin-BwxStZ9D.js +13 -0
- package/src/ui/dist/assets/NotebookEditor-BEQhaQbt.js +81 -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-DB9N_T9q.js +361 -0
- package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
- package/src/ui/dist/assets/PdfLoader-eWBONbQP.js +16 -0
- package/src/ui/dist/assets/PdfMarkdownPlugin-D22YOZL3.js +1 -0
- package/src/ui/dist/assets/PdfViewerPlugin-c-RK9DLM.js +17 -0
- package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
- package/src/ui/dist/assets/SearchPlugin-CxF9ytAx.js +16 -0
- package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
- package/src/ui/dist/assets/TextViewerPlugin-C5xqeeUH.js +54 -0
- package/src/ui/dist/assets/VNCViewer-BoLGLnHz.js +11 -0
- package/src/ui/dist/assets/bot-DREQOxzP.js +6 -0
- package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
- package/src/ui/dist/assets/chevron-up-C9Qpx4DE.js +6 -0
- package/src/ui/dist/assets/code-WlFHE7z_.js +6 -0
- package/src/ui/dist/assets/file-content-BZMz3RYp.js +1 -0
- package/src/ui/dist/assets/file-diff-panel-CQhw0jS2.js +1 -0
- package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
- package/src/ui/dist/assets/file-socket-CfQPKQKj.js +1 -0
- package/src/ui/dist/assets/git-commit-horizontal-DxZ8DCZh.js +6 -0
- package/src/ui/dist/assets/image-Bgl4VIyx.js +6 -0
- package/src/ui/dist/assets/index-BpV6lusQ.css +33 -0
- package/src/ui/dist/assets/index-CBNVuWcP.js +2496 -0
- package/src/ui/dist/assets/index-CwNu1aH4.js +11 -0
- package/src/ui/dist/assets/index-DrUnlf6K.js +1 -0
- package/src/ui/dist/assets/index-NW-h8VzN.js +1 -0
- package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
- package/src/ui/dist/assets/pdf-effect-queue-J8OnM0jE.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-CLc0pPP8.js +1 -0
- package/src/ui/dist/assets/project-sync-C9IdzdZW.js +1 -0
- package/src/ui/dist/assets/select-Cs2PmzwL.js +11 -0
- package/src/ui/dist/assets/sigma-ClKcHAXm.js +6 -0
- package/src/ui/dist/assets/trash-DwpbFr3w.js +11 -0
- package/src/ui/dist/assets/useCliAccess-NQ8m0Let.js +1 -0
- package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
- package/src/ui/dist/assets/wrap-text-BC-Hltpd.js +11 -0
- package/src/ui/dist/assets/zoom-out-E_gaeAxL.js +11 -0
- package/src/ui/dist/index.html +5 -2
- package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
- package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
- package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
- package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
- package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
- package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
- package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
- package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
- package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
- package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
- package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
- package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
- package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
- package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
- package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
- package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
- package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
- package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
- package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
- package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
- package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
- package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
- package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
- package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
- package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
- package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
- package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
- package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
- package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
- package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
- package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
- package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
- package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
- package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
- package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
- package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
- package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
- package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
- package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
- package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
- package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
- package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
- package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
- package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
- package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
- package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
- package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
- package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
- package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
- package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
- package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
- package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class FailureDiagnosis:
|
|
8
|
+
code: str
|
|
9
|
+
problem: str
|
|
10
|
+
why: str
|
|
11
|
+
guidance: tuple[str, ...]
|
|
12
|
+
retriable: bool
|
|
13
|
+
matched_text: str | None = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_MODEL_UNAVAILABLE_MARKERS = (
|
|
17
|
+
"unknown model",
|
|
18
|
+
"invalid model",
|
|
19
|
+
"model not found",
|
|
20
|
+
"unsupported model",
|
|
21
|
+
"model is not available",
|
|
22
|
+
"not authorized to use model",
|
|
23
|
+
"you do not have access",
|
|
24
|
+
"access to model",
|
|
25
|
+
"model access",
|
|
26
|
+
"unrecognized model",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_haystack(*values: object) -> str:
|
|
31
|
+
return "\n".join(str(value or "") for value in values if str(value or "").strip())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _contains(text: str, marker: str) -> bool:
|
|
35
|
+
return marker in text.lower()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def diagnose_runner_failure(
|
|
39
|
+
*,
|
|
40
|
+
runner_name: str,
|
|
41
|
+
summary: str = "",
|
|
42
|
+
stderr_text: str = "",
|
|
43
|
+
output_text: str = "",
|
|
44
|
+
) -> FailureDiagnosis | None:
|
|
45
|
+
haystack = _build_haystack(summary, stderr_text, output_text)
|
|
46
|
+
lower = haystack.lower()
|
|
47
|
+
normalized_runner = str(runner_name or "").strip().lower()
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
"tool call result does not follow tool call (2013)" in lower
|
|
51
|
+
or "tool result's tool id" in lower
|
|
52
|
+
):
|
|
53
|
+
return FailureDiagnosis(
|
|
54
|
+
code="minimax_tool_result_sequence_error",
|
|
55
|
+
problem="MiniMax rejected the tool result sequence.",
|
|
56
|
+
why=(
|
|
57
|
+
"The tool result did not immediately follow the corresponding tool call, "
|
|
58
|
+
"or the tool result referenced a tool call id that was no longer valid."
|
|
59
|
+
),
|
|
60
|
+
guidance=(
|
|
61
|
+
"Keep each tool result immediately after its matching tool call.",
|
|
62
|
+
"Do not insert an extra assistant message between a tool call and its tool result.",
|
|
63
|
+
"For MiniMax chat-wire sessions, serialize tool use one call at a time.",
|
|
64
|
+
),
|
|
65
|
+
retriable=False,
|
|
66
|
+
matched_text="2013",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
"invalid function arguments json string" in lower
|
|
71
|
+
or "failed to parse tool call arguments" in lower
|
|
72
|
+
or "trailing characters at line 1 column" in lower
|
|
73
|
+
):
|
|
74
|
+
return FailureDiagnosis(
|
|
75
|
+
code="chat_wire_tool_argument_parse_error",
|
|
76
|
+
problem="The runner emitted malformed tool-call arguments.",
|
|
77
|
+
why=(
|
|
78
|
+
"The tool-call arguments were not a single valid JSON object. "
|
|
79
|
+
"This usually happens when multiple tool calls are batched into one response "
|
|
80
|
+
"or when the arguments string contains trailing characters."
|
|
81
|
+
),
|
|
82
|
+
guidance=(
|
|
83
|
+
"Serialize tool calls one at a time instead of batching multiple MCP calls together.",
|
|
84
|
+
"Make sure each tool call emits exactly one complete JSON object for its arguments.",
|
|
85
|
+
"If this is a MiniMax chat-wire path, stay on the serialized single-tool compatibility route.",
|
|
86
|
+
),
|
|
87
|
+
retriable=False,
|
|
88
|
+
matched_text="tool-call arguments",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if "missing environment variable" in lower:
|
|
92
|
+
return FailureDiagnosis(
|
|
93
|
+
code="provider_env_missing",
|
|
94
|
+
problem="A required provider environment variable is missing.",
|
|
95
|
+
why="The configured model provider expects an API key or env var that was not present in the runner environment.",
|
|
96
|
+
guidance=(
|
|
97
|
+
"Set the required key in `~/DeepScientist/config/runners.yaml` under `runners.codex.env`.",
|
|
98
|
+
"If you launch from a shell, export the provider key in that same shell before starting `ds`.",
|
|
99
|
+
),
|
|
100
|
+
retriable=False,
|
|
101
|
+
matched_text="missing environment variable",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if any(marker in lower for marker in _MODEL_UNAVAILABLE_MARKERS):
|
|
105
|
+
return FailureDiagnosis(
|
|
106
|
+
code="runner_model_unavailable",
|
|
107
|
+
problem="The configured runner model is not available.",
|
|
108
|
+
why="The selected provider or Codex account could not access the requested model id.",
|
|
109
|
+
guidance=(
|
|
110
|
+
"Set `model: inherit` for provider-backed Codex profiles unless the provider explicitly supports the model id.",
|
|
111
|
+
"If you need a fixed model, verify that the same model works in plain `codex exec` before retrying DeepScientist.",
|
|
112
|
+
),
|
|
113
|
+
retriable=False,
|
|
114
|
+
matched_text="model unavailable",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if normalized_runner == "codex" and "invalid params" in lower and "bad_request_error" in lower:
|
|
118
|
+
return FailureDiagnosis(
|
|
119
|
+
code="provider_invalid_params",
|
|
120
|
+
problem="The provider rejected the request parameters.",
|
|
121
|
+
why="The upstream provider returned a deterministic request-shape error instead of a transient transport failure.",
|
|
122
|
+
guidance=(
|
|
123
|
+
"Inspect the immediately preceding tool call / tool result sequence for protocol ordering or JSON-shape mistakes.",
|
|
124
|
+
"Do not keep retrying the same request until the request payload or provider config is corrected.",
|
|
125
|
+
),
|
|
126
|
+
retriable=False,
|
|
127
|
+
matched_text="invalid params",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return None
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from datetime import UTC, datetime, timedelta
|
|
3
4
|
import os
|
|
4
5
|
import socket
|
|
5
6
|
import subprocess
|
|
@@ -13,9 +14,13 @@ from urllib.request import Request, urlopen
|
|
|
13
14
|
|
|
14
15
|
from .bash_exec.shells import build_exec_shell_launch, build_terminal_shell_launch
|
|
15
16
|
from .config import ConfigManager
|
|
17
|
+
from .diagnostics import diagnose_runner_failure
|
|
16
18
|
from .home import ensure_home_layout
|
|
17
19
|
from .runtime_tools import RuntimeToolService
|
|
18
|
-
from .shared import resolve_runner_binary, utc_now
|
|
20
|
+
from .shared import read_json, read_jsonl_tail, resolve_runner_binary, utc_now
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_RUNTIME_FAILURE_LOOKBACK = timedelta(hours=24)
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
def _browser_ui_url(host: str, port: int) -> str:
|
|
@@ -42,6 +47,10 @@ def _make_check(
|
|
|
42
47
|
errors: list[str] | None = None,
|
|
43
48
|
guidance: list[str] | None = None,
|
|
44
49
|
details: dict[str, Any] | None = None,
|
|
50
|
+
problem: str | None = None,
|
|
51
|
+
why: str | None = None,
|
|
52
|
+
fix: list[str] | None = None,
|
|
53
|
+
evidence: list[str] | None = None,
|
|
45
54
|
) -> dict[str, Any]:
|
|
46
55
|
normalized_warnings = list(warnings or [])
|
|
47
56
|
normalized_errors = list(errors or [])
|
|
@@ -55,6 +64,10 @@ def _make_check(
|
|
|
55
64
|
"errors": normalized_errors,
|
|
56
65
|
"guidance": list(guidance or []),
|
|
57
66
|
"details": dict(details or {}),
|
|
67
|
+
"problem": str(problem or "").strip() or None,
|
|
68
|
+
"why": str(why or "").strip() or None,
|
|
69
|
+
"fix": [str(line) for line in (fix or []) if str(line).strip()],
|
|
70
|
+
"evidence": [str(line) for line in (evidence or []) if str(line).strip()],
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
|
|
@@ -273,6 +286,13 @@ def _check_codex(config_manager: ConfigManager) -> dict[str, Any]:
|
|
|
273
286
|
probe_warnings = [str(value) for value in probe.get("warnings") or []]
|
|
274
287
|
probe_guidance = [str(value) for value in probe.get("guidance") or []]
|
|
275
288
|
summary = str(probe.get("summary") or "Codex startup probe completed.")
|
|
289
|
+
probe_details = probe.get("details") if isinstance(probe.get("details"), dict) else {}
|
|
290
|
+
diagnosis = diagnose_runner_failure(
|
|
291
|
+
runner_name="codex",
|
|
292
|
+
summary="\n".join([summary, *probe_errors]),
|
|
293
|
+
stderr_text=str(probe_details.get("stderr_excerpt") or ""),
|
|
294
|
+
output_text=str(probe_details.get("stdout_excerpt") or ""),
|
|
295
|
+
)
|
|
276
296
|
if probe.get("ok"):
|
|
277
297
|
return _make_check(
|
|
278
298
|
check_id="codex",
|
|
@@ -284,17 +304,188 @@ def _check_codex(config_manager: ConfigManager) -> dict[str, Any]:
|
|
|
284
304
|
)
|
|
285
305
|
if not probe_guidance:
|
|
286
306
|
probe_guidance = [
|
|
287
|
-
"Run `codex
|
|
307
|
+
"Run `codex login` (or just `codex`) manually once and complete login, then retry `ds doctor`.",
|
|
288
308
|
]
|
|
289
309
|
return _make_check(
|
|
290
310
|
check_id="codex",
|
|
291
311
|
label="Codex CLI",
|
|
292
312
|
ok=False,
|
|
293
|
-
summary=summary,
|
|
313
|
+
summary=diagnosis.problem if diagnosis is not None else summary,
|
|
294
314
|
warnings=probe_warnings,
|
|
295
315
|
errors=probe_errors or ["Codex startup probe did not succeed."],
|
|
296
316
|
guidance=probe_guidance,
|
|
297
317
|
details={"resolved_binary": resolved_binary},
|
|
318
|
+
problem=diagnosis.problem if diagnosis is not None else None,
|
|
319
|
+
why=diagnosis.why if diagnosis is not None else None,
|
|
320
|
+
fix=list(diagnosis.guidance) if diagnosis is not None else None,
|
|
321
|
+
evidence=(
|
|
322
|
+
[f"matched: {diagnosis.matched_text}"] if diagnosis is not None and diagnosis.matched_text else None
|
|
323
|
+
),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _parse_timestamp(value: object) -> datetime | None:
|
|
328
|
+
normalized = str(value or "").strip()
|
|
329
|
+
if not normalized:
|
|
330
|
+
return None
|
|
331
|
+
candidate = normalized.replace("Z", "+00:00")
|
|
332
|
+
try:
|
|
333
|
+
parsed = datetime.fromisoformat(candidate)
|
|
334
|
+
except ValueError:
|
|
335
|
+
return None
|
|
336
|
+
if parsed.tzinfo is None:
|
|
337
|
+
parsed = parsed.replace(tzinfo=UTC)
|
|
338
|
+
return parsed.astimezone(UTC)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _read_runtime_failure_record(home: Path) -> dict[str, Any] | None:
|
|
342
|
+
quests_root = home / "quests"
|
|
343
|
+
if not quests_root.exists():
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
latest: dict[str, Any] | None = None
|
|
347
|
+
latest_at: datetime | None = None
|
|
348
|
+
cutoff = datetime.now(UTC) - _RUNTIME_FAILURE_LOOKBACK
|
|
349
|
+
interesting_types = {
|
|
350
|
+
"runner.turn_error",
|
|
351
|
+
"runner.turn_retry_exhausted",
|
|
352
|
+
"quest.runtime_auto_resume_suppressed",
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for quest_root in sorted(quests_root.glob("*/")):
|
|
356
|
+
events = read_jsonl_tail(quest_root / ".ds" / "events.jsonl", 300)
|
|
357
|
+
for event in reversed(events):
|
|
358
|
+
event_type = str(event.get("type") or "").strip()
|
|
359
|
+
if event_type not in interesting_types:
|
|
360
|
+
continue
|
|
361
|
+
created_at = _parse_timestamp(event.get("created_at"))
|
|
362
|
+
if created_at is None or created_at < cutoff:
|
|
363
|
+
continue
|
|
364
|
+
run_id = str(event.get("run_id") or "").strip() or None
|
|
365
|
+
stderr_text = ""
|
|
366
|
+
output_text = ""
|
|
367
|
+
if run_id:
|
|
368
|
+
run_root = quest_root / ".ds" / "runs" / run_id
|
|
369
|
+
result_payload = read_json(run_root / "result.json", {})
|
|
370
|
+
if isinstance(result_payload, dict):
|
|
371
|
+
stderr_text = str(result_payload.get("stderr_text") or "").strip()
|
|
372
|
+
output_text = str(result_payload.get("output_text") or "").strip()
|
|
373
|
+
stderr_path = run_root / "stderr.txt"
|
|
374
|
+
if not stderr_text and stderr_path.exists():
|
|
375
|
+
try:
|
|
376
|
+
stderr_text = stderr_path.read_text(encoding="utf-8")
|
|
377
|
+
except OSError:
|
|
378
|
+
stderr_text = ""
|
|
379
|
+
candidate = {
|
|
380
|
+
"quest_id": quest_root.name,
|
|
381
|
+
"run_id": run_id,
|
|
382
|
+
"event_type": event_type,
|
|
383
|
+
"summary": str(event.get("summary") or "").strip(),
|
|
384
|
+
"created_at": created_at.isoformat(),
|
|
385
|
+
"stderr_text": stderr_text,
|
|
386
|
+
"output_text": output_text,
|
|
387
|
+
"recent_attempts": event.get("recent_attempts"),
|
|
388
|
+
}
|
|
389
|
+
if latest_at is None or created_at > latest_at:
|
|
390
|
+
latest = candidate
|
|
391
|
+
latest_at = created_at
|
|
392
|
+
break
|
|
393
|
+
return latest
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _check_recent_runtime_failures(home: Path) -> dict[str, Any]:
|
|
397
|
+
record = _read_runtime_failure_record(home)
|
|
398
|
+
if record is None:
|
|
399
|
+
return _make_check(
|
|
400
|
+
check_id="recent_runtime_failures",
|
|
401
|
+
label="Recent runtime failures",
|
|
402
|
+
ok=True,
|
|
403
|
+
summary="No recent quest runtime failures were found.",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
event_type = str(record.get("event_type") or "").strip()
|
|
407
|
+
quest_id = str(record.get("quest_id") or "").strip() or None
|
|
408
|
+
run_id = str(record.get("run_id") or "").strip() or None
|
|
409
|
+
summary = str(record.get("summary") or "").strip()
|
|
410
|
+
details = {
|
|
411
|
+
"quest_id": quest_id,
|
|
412
|
+
"run_id": run_id,
|
|
413
|
+
"event_type": event_type,
|
|
414
|
+
"observed_at": record.get("created_at"),
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if event_type == "quest.runtime_auto_resume_suppressed":
|
|
418
|
+
recent_attempts = int(record.get("recent_attempts") or 0)
|
|
419
|
+
return _make_check(
|
|
420
|
+
check_id="recent_runtime_failures",
|
|
421
|
+
label="Recent runtime failures",
|
|
422
|
+
ok=True,
|
|
423
|
+
summary="DeepScientist recently suppressed auto-resume to avoid a crash loop.",
|
|
424
|
+
warnings=["Automatic continuation was paused after repeated recovery attempts in a short window."],
|
|
425
|
+
guidance=[
|
|
426
|
+
"Inspect the most recent failing runner path before using `/resume` again.",
|
|
427
|
+
"If the failure was a provider-side 400/protocol error, fix that request path first instead of retrying immediately.",
|
|
428
|
+
],
|
|
429
|
+
details=details,
|
|
430
|
+
problem="Automatic crash recovery was suppressed.",
|
|
431
|
+
why="The same quest hit repeated recovery attempts in a short window, so DeepScientist parked it instead of looping forever.",
|
|
432
|
+
fix=[
|
|
433
|
+
"Open the latest failing quest logs and identify the deterministic runner/provider error.",
|
|
434
|
+
"Resume manually only after the underlying runner or provider issue is corrected.",
|
|
435
|
+
],
|
|
436
|
+
evidence=[
|
|
437
|
+
*( [f"quest: {quest_id}"] if quest_id else [] ),
|
|
438
|
+
f"recent recovery attempts: {recent_attempts}",
|
|
439
|
+
],
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
diagnosis = diagnose_runner_failure(
|
|
443
|
+
runner_name="codex",
|
|
444
|
+
summary=summary,
|
|
445
|
+
stderr_text=str(record.get("stderr_text") or ""),
|
|
446
|
+
output_text=str(record.get("output_text") or ""),
|
|
447
|
+
)
|
|
448
|
+
if diagnosis is None:
|
|
449
|
+
return _make_check(
|
|
450
|
+
check_id="recent_runtime_failures",
|
|
451
|
+
label="Recent runtime failures",
|
|
452
|
+
ok=True,
|
|
453
|
+
summary="A recent quest runtime failure was found.",
|
|
454
|
+
warnings=[summary or "The latest quest run failed, but doctor could not classify it precisely yet."],
|
|
455
|
+
guidance=[
|
|
456
|
+
"Open the latest run stderr and events journal for the failing quest.",
|
|
457
|
+
"If the same failure repeats, capture the run_id and provider response text before retrying again.",
|
|
458
|
+
],
|
|
459
|
+
details=details,
|
|
460
|
+
problem="A recent quest run failed.",
|
|
461
|
+
why="Doctor found a recent runtime failure event but could not match it to a known deterministic error pattern.",
|
|
462
|
+
fix=[
|
|
463
|
+
"Inspect the failing run's stderr and provider response text.",
|
|
464
|
+
"If the error is deterministic, avoid burning the retry budget until the request shape or config is fixed.",
|
|
465
|
+
],
|
|
466
|
+
evidence=[
|
|
467
|
+
*( [f"quest: {quest_id}"] if quest_id else [] ),
|
|
468
|
+
*( [f"run: {run_id}"] if run_id else [] ),
|
|
469
|
+
*( [f"summary: {summary}"] if summary else [] ),
|
|
470
|
+
],
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
return _make_check(
|
|
474
|
+
check_id="recent_runtime_failures",
|
|
475
|
+
label="Recent runtime failures",
|
|
476
|
+
ok=True,
|
|
477
|
+
summary=diagnosis.problem,
|
|
478
|
+
warnings=[summary] if summary and summary != diagnosis.problem else [],
|
|
479
|
+
guidance=list(diagnosis.guidance),
|
|
480
|
+
details=details,
|
|
481
|
+
problem=diagnosis.problem,
|
|
482
|
+
why=diagnosis.why,
|
|
483
|
+
fix=list(diagnosis.guidance),
|
|
484
|
+
evidence=[
|
|
485
|
+
*( [f"quest: {quest_id}"] if quest_id else [] ),
|
|
486
|
+
*( [f"run: {run_id}"] if run_id else [] ),
|
|
487
|
+
*( [f"matched: {diagnosis.matched_text}"] if diagnosis.matched_text else [] ),
|
|
488
|
+
],
|
|
298
489
|
)
|
|
299
490
|
|
|
300
491
|
|
|
@@ -491,6 +682,7 @@ def run_doctor(home: Path, *, repo_root: Path) -> dict[str, Any]:
|
|
|
491
682
|
_check_config_validation(config_manager),
|
|
492
683
|
_check_runner_support(config_manager),
|
|
493
684
|
_check_codex(config_manager),
|
|
685
|
+
_check_recent_runtime_failures(home),
|
|
494
686
|
_check_latex_runtime(home),
|
|
495
687
|
_check_bundles(repo_root),
|
|
496
688
|
_check_ui_port(home, config_manager),
|
|
@@ -519,6 +711,18 @@ def render_doctor_report(report: dict[str, Any]) -> str:
|
|
|
519
711
|
status = str(item.get("status") or "ok").upper()
|
|
520
712
|
icon = {"OK": "[ok]", "WARN": "[warn]", "ERROR": "[fail]"}.get(status, "[info]")
|
|
521
713
|
lines.append(f"{icon} {item.get('label')}: {item.get('summary')}")
|
|
714
|
+
problem = str(item.get("problem") or "").strip()
|
|
715
|
+
why = str(item.get("why") or "").strip()
|
|
716
|
+
fix_lines = [str(line) for line in item.get("fix") or [] if str(line).strip()]
|
|
717
|
+
evidence_lines = [str(line) for line in item.get("evidence") or [] if str(line).strip()]
|
|
718
|
+
if problem:
|
|
719
|
+
lines.append(f" problem: {problem}")
|
|
720
|
+
if why:
|
|
721
|
+
lines.append(f" why: {why}")
|
|
722
|
+
for line in fix_lines:
|
|
723
|
+
lines.append(f" fix: {line}")
|
|
724
|
+
for line in evidence_lines:
|
|
725
|
+
lines.append(f" evidence: {line}")
|
|
522
726
|
for warning in item.get("warnings") or []:
|
|
523
727
|
lines.append(f" warning: {warning}")
|
|
524
728
|
for error in item.get("errors") or []:
|
|
@@ -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
|
]
|
|
@@ -113,6 +113,67 @@ def list_branch_canvas(repo: Path, *, quest_id: str) -> dict[str, Any]:
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
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
|
+
|
|
116
177
|
def compare_refs(repo: Path, *, base: str, head: str) -> dict[str, Any]:
|
|
117
178
|
_require_ref(repo, base)
|
|
118
179
|
_require_ref(repo, head)
|
|
@@ -775,6 +836,74 @@ def _git_log(repo: Path, *, revspec: str, limit: int = 30) -> list[dict[str, Any
|
|
|
775
836
|
return commits
|
|
776
837
|
|
|
777
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
|
+
|
|
778
907
|
def _normalize_patch_lines(patch: str) -> list[str]:
|
|
779
908
|
lines = [line.rstrip("\n") for line in patch.splitlines()]
|
|
780
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),
|
|
@@ -526,6 +526,45 @@ def build_artifact_server(context: McpContext) -> FastMCP:
|
|
|
526
526
|
allow_empty=allow_empty,
|
|
527
527
|
)
|
|
528
528
|
|
|
529
|
+
@server.tool(
|
|
530
|
+
name="git",
|
|
531
|
+
description=(
|
|
532
|
+
"Run git-oriented workspace operations for Copilot mode and general quest maintenance. "
|
|
533
|
+
"Use action=status|commit|branch|checkout|log|show|diff|graph."
|
|
534
|
+
),
|
|
535
|
+
)
|
|
536
|
+
def git(
|
|
537
|
+
action: str,
|
|
538
|
+
message: str | None = None,
|
|
539
|
+
ref: str | None = None,
|
|
540
|
+
base: str | None = None,
|
|
541
|
+
head: str | None = None,
|
|
542
|
+
sha: str | None = None,
|
|
543
|
+
path: str | None = None,
|
|
544
|
+
branch: str | None = None,
|
|
545
|
+
create_from: str | None = None,
|
|
546
|
+
limit: int = 30,
|
|
547
|
+
allow_empty: bool = False,
|
|
548
|
+
checkout_new_branch: bool = False,
|
|
549
|
+
comment: str | dict[str, Any] | None = None,
|
|
550
|
+
) -> dict[str, Any]:
|
|
551
|
+
return service.git_action(
|
|
552
|
+
context.require_quest_root(),
|
|
553
|
+
action=action,
|
|
554
|
+
workspace_root=context.worktree_root,
|
|
555
|
+
message=message,
|
|
556
|
+
ref=ref,
|
|
557
|
+
base=base,
|
|
558
|
+
head=head,
|
|
559
|
+
sha=sha,
|
|
560
|
+
path=path,
|
|
561
|
+
branch=branch,
|
|
562
|
+
create_from=create_from,
|
|
563
|
+
limit=limit,
|
|
564
|
+
allow_empty=allow_empty,
|
|
565
|
+
checkout_new_branch=checkout_new_branch,
|
|
566
|
+
)
|
|
567
|
+
|
|
529
568
|
@server.tool(name="prepare_branch", description="Prepare an idea or run branch and optional worktree.")
|
|
530
569
|
def prepare_branch(
|
|
531
570
|
run_id: str | None = None,
|