@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
|
@@ -18,12 +18,18 @@ from ..gitops import (
|
|
|
18
18
|
branch_exists,
|
|
19
19
|
canonical_worktree_root,
|
|
20
20
|
checkpoint_repo,
|
|
21
|
+
commit_detail,
|
|
22
|
+
compare_refs,
|
|
21
23
|
create_worktree,
|
|
22
24
|
current_branch,
|
|
25
|
+
diff_file_between_refs,
|
|
26
|
+
diff_file_for_commit,
|
|
23
27
|
ensure_branch,
|
|
24
28
|
export_git_graph,
|
|
25
29
|
head_commit,
|
|
30
|
+
log_ref_history,
|
|
26
31
|
)
|
|
32
|
+
from ..home import repo_root
|
|
27
33
|
from ..registries import BaselineRegistry
|
|
28
34
|
from ..shared import (
|
|
29
35
|
append_jsonl,
|
|
@@ -972,6 +978,43 @@ class ArtifactService:
|
|
|
972
978
|
return "idea"
|
|
973
979
|
return "quest"
|
|
974
980
|
|
|
981
|
+
@staticmethod
|
|
982
|
+
def _collaboration_workspace_mode(state: dict[str, Any]) -> str | None:
|
|
983
|
+
normalized = str(state.get("workspace_mode") or "").strip().lower()
|
|
984
|
+
if normalized in {"copilot", "autonomous"}:
|
|
985
|
+
return normalized
|
|
986
|
+
return None
|
|
987
|
+
|
|
988
|
+
def _resolve_workspace_modes(
|
|
989
|
+
self,
|
|
990
|
+
state: dict[str, Any],
|
|
991
|
+
*,
|
|
992
|
+
branch_name: str | None,
|
|
993
|
+
has_idea: bool = False,
|
|
994
|
+
) -> tuple[str, str]:
|
|
995
|
+
branch_mode = self._workspace_mode_for_branch(branch_name, has_idea=has_idea)
|
|
996
|
+
collaboration_mode = self._collaboration_workspace_mode(state)
|
|
997
|
+
return collaboration_mode or branch_mode, branch_mode
|
|
998
|
+
|
|
999
|
+
@staticmethod
|
|
1000
|
+
def _active_workspace_branch_mode(state: dict[str, Any], *, branch_name: str | None) -> str:
|
|
1001
|
+
normalized = str(state.get("workspace_branch_mode") or "").strip().lower()
|
|
1002
|
+
if normalized:
|
|
1003
|
+
return normalized
|
|
1004
|
+
legacy = str(state.get("workspace_mode") or "").strip().lower()
|
|
1005
|
+
if legacy in {"idea", "run", "analysis", "paper", "quest"}:
|
|
1006
|
+
return legacy
|
|
1007
|
+
branch_kind = str(branch_name or "").strip().lower()
|
|
1008
|
+
if branch_kind.startswith("paper/") or branch_kind == "paper":
|
|
1009
|
+
return "paper"
|
|
1010
|
+
if branch_kind.startswith("analysis/") or branch_kind == "analysis":
|
|
1011
|
+
return "analysis"
|
|
1012
|
+
if branch_kind.startswith("run/") or branch_kind == "run":
|
|
1013
|
+
return "run"
|
|
1014
|
+
if branch_kind.startswith("idea/") or branch_kind == "idea":
|
|
1015
|
+
return "idea"
|
|
1016
|
+
return "quest"
|
|
1017
|
+
|
|
975
1018
|
def _prepare_branch_worktree_root(
|
|
976
1019
|
self,
|
|
977
1020
|
quest_root: Path,
|
|
@@ -4674,8 +4717,8 @@ class ArtifactService:
|
|
|
4674
4717
|
current_branch_raw = str(state.get("current_workspace_branch") or "").strip()
|
|
4675
4718
|
research_head_branch_raw = str(state.get("research_head_branch") or "").strip()
|
|
4676
4719
|
paper_parent_branch_raw = str(state.get("paper_parent_branch") or "").strip()
|
|
4677
|
-
|
|
4678
|
-
prefer_paper_parent =
|
|
4720
|
+
branch_mode = self._active_workspace_branch_mode(state, branch_name=current_branch_raw)
|
|
4721
|
+
prefer_paper_parent = branch_mode == "paper" or self._branch_kind_from_name(current_branch_raw) == "paper"
|
|
4679
4722
|
parent_worktree_root: Path | None = None
|
|
4680
4723
|
root_candidates = (
|
|
4681
4724
|
(paper_parent_root_raw, head_root_raw, current_root_raw)
|
|
@@ -6027,7 +6070,7 @@ class ArtifactService:
|
|
|
6027
6070
|
try:
|
|
6028
6071
|
self.quest_service.schedule_projection_refresh(
|
|
6029
6072
|
quest_root,
|
|
6030
|
-
kinds=("details", "canvas"),
|
|
6073
|
+
kinds=("details", "canvas", "git_canvas"),
|
|
6031
6074
|
throttle_seconds=0.0,
|
|
6032
6075
|
)
|
|
6033
6076
|
except Exception:
|
|
@@ -6150,6 +6193,7 @@ class ArtifactService:
|
|
|
6150
6193
|
def checkpoint(self, quest_root: Path, message: str, *, allow_empty: bool = False) -> dict:
|
|
6151
6194
|
result = checkpoint_repo(quest_root, message, allow_empty=allow_empty)
|
|
6152
6195
|
self._touch_quest_updated_at(quest_root)
|
|
6196
|
+
self._refresh_git_surfaces(quest_root)
|
|
6153
6197
|
return {
|
|
6154
6198
|
"ok": True,
|
|
6155
6199
|
"message": message,
|
|
@@ -6157,6 +6201,459 @@ class ArtifactService:
|
|
|
6157
6201
|
**result,
|
|
6158
6202
|
}
|
|
6159
6203
|
|
|
6204
|
+
def _refresh_git_surfaces(self, quest_root: Path) -> dict[str, Any]:
|
|
6205
|
+
projection_refresh = {
|
|
6206
|
+
"details": True,
|
|
6207
|
+
"canvas": True,
|
|
6208
|
+
"git_canvas": True,
|
|
6209
|
+
"graph": True,
|
|
6210
|
+
}
|
|
6211
|
+
try:
|
|
6212
|
+
self.quest_service.schedule_projection_refresh(
|
|
6213
|
+
quest_root,
|
|
6214
|
+
kinds=("details", "canvas", "git_canvas"),
|
|
6215
|
+
throttle_seconds=0.0,
|
|
6216
|
+
)
|
|
6217
|
+
except Exception:
|
|
6218
|
+
pass
|
|
6219
|
+
try:
|
|
6220
|
+
export_git_graph(quest_root, ensure_dir(quest_root / "artifacts" / "graphs"))
|
|
6221
|
+
except Exception:
|
|
6222
|
+
projection_refresh["graph"] = False
|
|
6223
|
+
return projection_refresh
|
|
6224
|
+
|
|
6225
|
+
def _append_git_event(
|
|
6226
|
+
self,
|
|
6227
|
+
quest_root: Path,
|
|
6228
|
+
*,
|
|
6229
|
+
action: str,
|
|
6230
|
+
repo: Path,
|
|
6231
|
+
result: dict[str, Any],
|
|
6232
|
+
) -> None:
|
|
6233
|
+
append_jsonl(
|
|
6234
|
+
quest_root / ".ds" / "events.jsonl",
|
|
6235
|
+
{
|
|
6236
|
+
"type": "artifact.git",
|
|
6237
|
+
"quest_id": quest_root.name,
|
|
6238
|
+
"action": action,
|
|
6239
|
+
"repo": str(repo),
|
|
6240
|
+
"result": result,
|
|
6241
|
+
"recorded_at": utc_now(),
|
|
6242
|
+
},
|
|
6243
|
+
)
|
|
6244
|
+
|
|
6245
|
+
def _record_git_operation_artifact(
|
|
6246
|
+
self,
|
|
6247
|
+
quest_root: Path,
|
|
6248
|
+
*,
|
|
6249
|
+
repo: Path,
|
|
6250
|
+
action: str,
|
|
6251
|
+
payload: dict[str, Any],
|
|
6252
|
+
) -> dict[str, Any] | None:
|
|
6253
|
+
state = self.quest_service.read_research_state(quest_root)
|
|
6254
|
+
if self._collaboration_workspace_mode(state) != "copilot":
|
|
6255
|
+
return None
|
|
6256
|
+
|
|
6257
|
+
normalized_action = str(action or "").strip().lower()
|
|
6258
|
+
before_branch = str(payload.get("before_branch") or "").strip() or None
|
|
6259
|
+
payload_branch = str(payload.get("branch") or "").strip() or None
|
|
6260
|
+
after_branch = (
|
|
6261
|
+
str(payload.get("after_branch") or "").strip()
|
|
6262
|
+
or payload_branch
|
|
6263
|
+
or before_branch
|
|
6264
|
+
)
|
|
6265
|
+
target_ref = str(payload.get("target_ref") or payload.get("target") or "").strip() or after_branch
|
|
6266
|
+
before_head = str(payload.get("before_head") or "").strip() or None
|
|
6267
|
+
after_head = str(payload.get("after_head") or payload.get("head") or payload.get("sha") or "").strip() or None
|
|
6268
|
+
commit_subject = str(payload.get("subject") or "").strip() or None
|
|
6269
|
+
changed_files = [
|
|
6270
|
+
str(item.get("path") or "").strip()
|
|
6271
|
+
for item in (payload.get("files") or [])
|
|
6272
|
+
if isinstance(item, dict) and str(item.get("path") or "").strip()
|
|
6273
|
+
]
|
|
6274
|
+
|
|
6275
|
+
should_record = False
|
|
6276
|
+
summary = ""
|
|
6277
|
+
reason = ""
|
|
6278
|
+
status = "completed"
|
|
6279
|
+
record_branch = after_branch
|
|
6280
|
+
parent_branch: str | None = None
|
|
6281
|
+
|
|
6282
|
+
if normalized_action == "commit":
|
|
6283
|
+
if bool(payload.get("committed")) and after_branch:
|
|
6284
|
+
should_record = True
|
|
6285
|
+
record_branch = after_branch
|
|
6286
|
+
summary = (
|
|
6287
|
+
f"Committed on `{after_branch}`: {commit_subject}"
|
|
6288
|
+
if commit_subject
|
|
6289
|
+
else f"Committed changes on `{after_branch}`."
|
|
6290
|
+
)
|
|
6291
|
+
reason = "A durable Git commit changed the active branch state."
|
|
6292
|
+
elif normalized_action == "branch":
|
|
6293
|
+
created = bool(payload.get("created"))
|
|
6294
|
+
switched = bool(after_branch and before_branch and after_branch != before_branch)
|
|
6295
|
+
create_from = str(payload.get("create_from") or "").strip() or before_branch
|
|
6296
|
+
if created or switched:
|
|
6297
|
+
should_record = True
|
|
6298
|
+
record_branch = target_ref or after_branch or payload_branch or before_branch
|
|
6299
|
+
if created and switched:
|
|
6300
|
+
summary = f"Created and switched to `{target_ref}`."
|
|
6301
|
+
elif created:
|
|
6302
|
+
summary = f"Created branch `{target_ref}`."
|
|
6303
|
+
else:
|
|
6304
|
+
summary = f"Switched to existing branch `{after_branch}`."
|
|
6305
|
+
reason = "A durable Git branch operation changed the available branch graph."
|
|
6306
|
+
status = "completed" if created else "existing"
|
|
6307
|
+
if created:
|
|
6308
|
+
parent_branch = create_from
|
|
6309
|
+
elif normalized_action == "checkout":
|
|
6310
|
+
switched = bool(after_branch and before_branch and after_branch != before_branch)
|
|
6311
|
+
moved_head = bool(after_head and before_head and after_head != before_head)
|
|
6312
|
+
if bool(payload.get("ok")) and (switched or moved_head):
|
|
6313
|
+
should_record = True
|
|
6314
|
+
record_branch = after_branch or target_ref
|
|
6315
|
+
summary = (
|
|
6316
|
+
f"Checked out `{target_ref}`."
|
|
6317
|
+
if target_ref
|
|
6318
|
+
else f"Switched workspace branch from `{before_branch or 'unknown'}` to `{after_branch or 'unknown'}`."
|
|
6319
|
+
)
|
|
6320
|
+
reason = "A Git checkout changed the active branch or HEAD for the workspace."
|
|
6321
|
+
|
|
6322
|
+
if not should_record or not record_branch:
|
|
6323
|
+
return None
|
|
6324
|
+
|
|
6325
|
+
worktree_root: str | None = None
|
|
6326
|
+
worktree_rel_path: str | None = None
|
|
6327
|
+
if after_branch and record_branch == after_branch:
|
|
6328
|
+
worktree_root = str(repo)
|
|
6329
|
+
worktree_rel_path = self._workspace_relative(quest_root, repo)
|
|
6330
|
+
|
|
6331
|
+
return self.record(
|
|
6332
|
+
quest_root,
|
|
6333
|
+
{
|
|
6334
|
+
"kind": "report",
|
|
6335
|
+
"status": status,
|
|
6336
|
+
"report_type": "git_operation",
|
|
6337
|
+
"suppress_if_semantically_equivalent": False,
|
|
6338
|
+
"report_id": generate_id("report"),
|
|
6339
|
+
"summary": summary,
|
|
6340
|
+
"reason": reason,
|
|
6341
|
+
"branch": record_branch,
|
|
6342
|
+
"parent_branch": parent_branch,
|
|
6343
|
+
"head_commit": after_head,
|
|
6344
|
+
"worktree_root": worktree_root,
|
|
6345
|
+
"worktree_rel_path": worktree_rel_path,
|
|
6346
|
+
"flow_type": "git_operation",
|
|
6347
|
+
"protocol_step": normalized_action,
|
|
6348
|
+
"paths": {
|
|
6349
|
+
"workspace_root": str(repo),
|
|
6350
|
+
},
|
|
6351
|
+
"details": {
|
|
6352
|
+
"git_action": normalized_action,
|
|
6353
|
+
"target_ref": target_ref,
|
|
6354
|
+
"create_from": str(payload.get("create_from") or "").strip() or None,
|
|
6355
|
+
"before_branch": before_branch,
|
|
6356
|
+
"after_branch": after_branch,
|
|
6357
|
+
"record_branch": record_branch,
|
|
6358
|
+
"before_head": before_head,
|
|
6359
|
+
"after_head": after_head,
|
|
6360
|
+
"commit_subject": commit_subject,
|
|
6361
|
+
"changed_files": changed_files,
|
|
6362
|
+
},
|
|
6363
|
+
"source": {"kind": "system", "role": "artifact"},
|
|
6364
|
+
},
|
|
6365
|
+
checkpoint=False,
|
|
6366
|
+
workspace_root=repo,
|
|
6367
|
+
)
|
|
6368
|
+
|
|
6369
|
+
def _git_status_payload(self, repo: Path) -> dict[str, Any]:
|
|
6370
|
+
result = run_command(["git", "status", "--porcelain", "-b"], cwd=repo, check=False)
|
|
6371
|
+
lines = [line.rstrip() for line in str(result.stdout or "").splitlines()]
|
|
6372
|
+
branch_line = lines[0] if lines else ""
|
|
6373
|
+
changes: list[dict[str, Any]] = []
|
|
6374
|
+
staged_count = 0
|
|
6375
|
+
unstaged_count = 0
|
|
6376
|
+
untracked_count = 0
|
|
6377
|
+
for raw in lines[1:]:
|
|
6378
|
+
if not raw:
|
|
6379
|
+
continue
|
|
6380
|
+
status = raw[:2]
|
|
6381
|
+
path = raw[3:].strip() if len(raw) > 3 else raw.strip()
|
|
6382
|
+
staged = status[0] not in {" ", "?"}
|
|
6383
|
+
unstaged = status[1] not in {" "}
|
|
6384
|
+
untracked = status == "??"
|
|
6385
|
+
if staged:
|
|
6386
|
+
staged_count += 1
|
|
6387
|
+
if unstaged:
|
|
6388
|
+
unstaged_count += 1
|
|
6389
|
+
if untracked:
|
|
6390
|
+
untracked_count += 1
|
|
6391
|
+
changes.append(
|
|
6392
|
+
{
|
|
6393
|
+
"path": path,
|
|
6394
|
+
"status": status,
|
|
6395
|
+
"staged": staged,
|
|
6396
|
+
"unstaged": unstaged,
|
|
6397
|
+
"untracked": untracked,
|
|
6398
|
+
}
|
|
6399
|
+
)
|
|
6400
|
+
return {
|
|
6401
|
+
"ok": result.returncode == 0,
|
|
6402
|
+
"repo": str(repo),
|
|
6403
|
+
"branch": current_branch(repo),
|
|
6404
|
+
"head": head_commit(repo),
|
|
6405
|
+
"branch_status": branch_line[3:].strip() if branch_line.startswith("## ") else None,
|
|
6406
|
+
"has_changes": bool(changes),
|
|
6407
|
+
"staged_count": staged_count,
|
|
6408
|
+
"unstaged_count": unstaged_count,
|
|
6409
|
+
"untracked_count": untracked_count,
|
|
6410
|
+
"changes": changes,
|
|
6411
|
+
}
|
|
6412
|
+
|
|
6413
|
+
def git_action(
|
|
6414
|
+
self,
|
|
6415
|
+
quest_root: Path,
|
|
6416
|
+
*,
|
|
6417
|
+
action: str,
|
|
6418
|
+
workspace_root: Path | None = None,
|
|
6419
|
+
message: str | None = None,
|
|
6420
|
+
ref: str | None = None,
|
|
6421
|
+
base: str | None = None,
|
|
6422
|
+
head: str | None = None,
|
|
6423
|
+
sha: str | None = None,
|
|
6424
|
+
path: str | None = None,
|
|
6425
|
+
branch: str | None = None,
|
|
6426
|
+
create_from: str | None = None,
|
|
6427
|
+
limit: int = 30,
|
|
6428
|
+
allow_empty: bool = False,
|
|
6429
|
+
checkout_new_branch: bool = False,
|
|
6430
|
+
) -> dict[str, Any]:
|
|
6431
|
+
resolved_action = str(action or "").strip().lower()
|
|
6432
|
+
repo = self._workspace_root_for(quest_root, workspace_root=workspace_root)
|
|
6433
|
+
before_branch = current_branch(repo)
|
|
6434
|
+
before_head = head_commit(repo)
|
|
6435
|
+
projection_refresh = {
|
|
6436
|
+
"details": False,
|
|
6437
|
+
"canvas": False,
|
|
6438
|
+
"git_canvas": False,
|
|
6439
|
+
"graph": False,
|
|
6440
|
+
}
|
|
6441
|
+
|
|
6442
|
+
if resolved_action == "status":
|
|
6443
|
+
result = self._git_status_payload(repo)
|
|
6444
|
+
return {
|
|
6445
|
+
"ok": True,
|
|
6446
|
+
"action": resolved_action,
|
|
6447
|
+
"quest_id": quest_root.name,
|
|
6448
|
+
"current_ref": current_branch(repo),
|
|
6449
|
+
"head": head_commit(repo),
|
|
6450
|
+
"projection_refresh": projection_refresh,
|
|
6451
|
+
"result": result,
|
|
6452
|
+
}
|
|
6453
|
+
|
|
6454
|
+
if resolved_action == "commit":
|
|
6455
|
+
commit_message = str(message or "").strip() or "Update workspace"
|
|
6456
|
+
result = checkpoint_repo(repo, commit_message, allow_empty=allow_empty)
|
|
6457
|
+
self._touch_quest_updated_at(quest_root)
|
|
6458
|
+
projection_refresh = self._refresh_git_surfaces(quest_root)
|
|
6459
|
+
if result.get("committed"):
|
|
6460
|
+
head_sha = str(result.get("head") or "").strip()
|
|
6461
|
+
if head_sha:
|
|
6462
|
+
try:
|
|
6463
|
+
detail = commit_detail(repo, sha=head_sha)
|
|
6464
|
+
except Exception:
|
|
6465
|
+
detail = {
|
|
6466
|
+
"sha": head_sha,
|
|
6467
|
+
"short_sha": head_sha[:7],
|
|
6468
|
+
"subject": commit_message,
|
|
6469
|
+
"parents": [],
|
|
6470
|
+
}
|
|
6471
|
+
else:
|
|
6472
|
+
detail = {
|
|
6473
|
+
"sha": None,
|
|
6474
|
+
"short_sha": None,
|
|
6475
|
+
"subject": commit_message,
|
|
6476
|
+
"parents": [],
|
|
6477
|
+
}
|
|
6478
|
+
else:
|
|
6479
|
+
detail = {
|
|
6480
|
+
"sha": result.get("head"),
|
|
6481
|
+
"short_sha": str(result.get("head") or "")[:7] or None,
|
|
6482
|
+
"subject": commit_message,
|
|
6483
|
+
"parents": [],
|
|
6484
|
+
}
|
|
6485
|
+
payload = {
|
|
6486
|
+
"committed": bool(result.get("committed")),
|
|
6487
|
+
"branch": result.get("branch"),
|
|
6488
|
+
"head": result.get("head"),
|
|
6489
|
+
"before_branch": before_branch,
|
|
6490
|
+
"before_head": before_head,
|
|
6491
|
+
"after_branch": result.get("branch"),
|
|
6492
|
+
"after_head": result.get("head"),
|
|
6493
|
+
"target_ref": result.get("branch") or before_branch,
|
|
6494
|
+
"stdout": result.get("stdout"),
|
|
6495
|
+
"stderr": result.get("stderr"),
|
|
6496
|
+
**detail,
|
|
6497
|
+
}
|
|
6498
|
+
self._append_git_event(quest_root, action=resolved_action, repo=repo, result=payload)
|
|
6499
|
+
self._record_git_operation_artifact(
|
|
6500
|
+
quest_root,
|
|
6501
|
+
repo=repo,
|
|
6502
|
+
action=resolved_action,
|
|
6503
|
+
payload=payload,
|
|
6504
|
+
)
|
|
6505
|
+
return {
|
|
6506
|
+
"ok": True,
|
|
6507
|
+
"action": resolved_action,
|
|
6508
|
+
"quest_id": quest_root.name,
|
|
6509
|
+
"current_ref": current_branch(repo),
|
|
6510
|
+
"head": head_commit(repo),
|
|
6511
|
+
"projection_refresh": projection_refresh,
|
|
6512
|
+
"result": payload,
|
|
6513
|
+
}
|
|
6514
|
+
|
|
6515
|
+
if resolved_action == "branch":
|
|
6516
|
+
branch_name = str(branch or "").strip()
|
|
6517
|
+
if not branch_name:
|
|
6518
|
+
return {"ok": False, "action": resolved_action, "message": "`branch` is required."}
|
|
6519
|
+
result = ensure_branch(repo, branch_name, start_point=create_from, checkout=checkout_new_branch)
|
|
6520
|
+
payload = {
|
|
6521
|
+
**result,
|
|
6522
|
+
"before_branch": before_branch,
|
|
6523
|
+
"before_head": before_head,
|
|
6524
|
+
"after_branch": current_branch(repo),
|
|
6525
|
+
"after_head": head_commit(repo),
|
|
6526
|
+
"target_ref": branch_name,
|
|
6527
|
+
"create_from": str(create_from or "").strip() or before_branch,
|
|
6528
|
+
}
|
|
6529
|
+
self._touch_quest_updated_at(quest_root)
|
|
6530
|
+
projection_refresh = self._refresh_git_surfaces(quest_root)
|
|
6531
|
+
self._append_git_event(quest_root, action=resolved_action, repo=repo, result=payload)
|
|
6532
|
+
self._record_git_operation_artifact(
|
|
6533
|
+
quest_root,
|
|
6534
|
+
repo=repo,
|
|
6535
|
+
action=resolved_action,
|
|
6536
|
+
payload=payload,
|
|
6537
|
+
)
|
|
6538
|
+
return {
|
|
6539
|
+
"ok": True,
|
|
6540
|
+
"action": resolved_action,
|
|
6541
|
+
"quest_id": quest_root.name,
|
|
6542
|
+
"current_ref": current_branch(repo),
|
|
6543
|
+
"head": head_commit(repo),
|
|
6544
|
+
"projection_refresh": projection_refresh,
|
|
6545
|
+
"result": payload,
|
|
6546
|
+
}
|
|
6547
|
+
|
|
6548
|
+
if resolved_action == "checkout":
|
|
6549
|
+
target = str(branch or ref or sha or head or "").strip()
|
|
6550
|
+
if not target:
|
|
6551
|
+
return {"ok": False, "action": resolved_action, "message": "One of `branch`, `ref`, `sha`, or `head` is required."}
|
|
6552
|
+
result = run_command(["git", "checkout", target], cwd=repo, check=False)
|
|
6553
|
+
payload = {
|
|
6554
|
+
"ok": result.returncode == 0,
|
|
6555
|
+
"target": target,
|
|
6556
|
+
"branch": current_branch(repo),
|
|
6557
|
+
"head": head_commit(repo),
|
|
6558
|
+
"before_branch": before_branch,
|
|
6559
|
+
"before_head": before_head,
|
|
6560
|
+
"after_branch": current_branch(repo),
|
|
6561
|
+
"after_head": head_commit(repo),
|
|
6562
|
+
"target_ref": target,
|
|
6563
|
+
"stdout": result.stdout,
|
|
6564
|
+
"stderr": result.stderr,
|
|
6565
|
+
}
|
|
6566
|
+
self._touch_quest_updated_at(quest_root)
|
|
6567
|
+
projection_refresh = self._refresh_git_surfaces(quest_root)
|
|
6568
|
+
self._append_git_event(quest_root, action=resolved_action, repo=repo, result=payload)
|
|
6569
|
+
self._record_git_operation_artifact(
|
|
6570
|
+
quest_root,
|
|
6571
|
+
repo=repo,
|
|
6572
|
+
action=resolved_action,
|
|
6573
|
+
payload=payload,
|
|
6574
|
+
)
|
|
6575
|
+
return {
|
|
6576
|
+
"ok": result.returncode == 0,
|
|
6577
|
+
"action": resolved_action,
|
|
6578
|
+
"quest_id": quest_root.name,
|
|
6579
|
+
"current_ref": current_branch(repo),
|
|
6580
|
+
"head": head_commit(repo),
|
|
6581
|
+
"projection_refresh": projection_refresh,
|
|
6582
|
+
"result": payload,
|
|
6583
|
+
}
|
|
6584
|
+
|
|
6585
|
+
if resolved_action == "log":
|
|
6586
|
+
target_ref = str(ref or branch or "").strip() or current_branch(repo)
|
|
6587
|
+
result = log_ref_history(repo, ref=target_ref, base=(base or "").strip() or None, limit=limit)
|
|
6588
|
+
return {
|
|
6589
|
+
"ok": True,
|
|
6590
|
+
"action": resolved_action,
|
|
6591
|
+
"quest_id": quest_root.name,
|
|
6592
|
+
"current_ref": current_branch(repo),
|
|
6593
|
+
"head": head_commit(repo),
|
|
6594
|
+
"projection_refresh": projection_refresh,
|
|
6595
|
+
"result": result,
|
|
6596
|
+
}
|
|
6597
|
+
|
|
6598
|
+
if resolved_action == "show":
|
|
6599
|
+
target_sha = str(sha or ref or head or "").strip()
|
|
6600
|
+
if not target_sha:
|
|
6601
|
+
return {"ok": False, "action": resolved_action, "message": "`sha` or `ref` is required."}
|
|
6602
|
+
result = commit_detail(repo, sha=target_sha)
|
|
6603
|
+
return {
|
|
6604
|
+
"ok": True,
|
|
6605
|
+
"action": resolved_action,
|
|
6606
|
+
"quest_id": quest_root.name,
|
|
6607
|
+
"current_ref": current_branch(repo),
|
|
6608
|
+
"head": head_commit(repo),
|
|
6609
|
+
"projection_refresh": projection_refresh,
|
|
6610
|
+
"result": result,
|
|
6611
|
+
}
|
|
6612
|
+
|
|
6613
|
+
if resolved_action == "diff":
|
|
6614
|
+
target_path = str(path or "").strip() or None
|
|
6615
|
+
if sha:
|
|
6616
|
+
result = commit_detail(repo, sha=sha) if not target_path else diff_file_for_commit(repo, sha=sha, path=target_path)
|
|
6617
|
+
elif base and head:
|
|
6618
|
+
result = (
|
|
6619
|
+
diff_file_between_refs(repo, base=base, head=head, path=target_path)
|
|
6620
|
+
if target_path
|
|
6621
|
+
else compare_refs(repo, base=base, head=head)
|
|
6622
|
+
)
|
|
6623
|
+
else:
|
|
6624
|
+
return {
|
|
6625
|
+
"ok": False,
|
|
6626
|
+
"action": resolved_action,
|
|
6627
|
+
"message": "Provide `sha` for commit diff or `base` and `head` for compare diff.",
|
|
6628
|
+
}
|
|
6629
|
+
return {
|
|
6630
|
+
"ok": True,
|
|
6631
|
+
"action": resolved_action,
|
|
6632
|
+
"quest_id": quest_root.name,
|
|
6633
|
+
"current_ref": current_branch(repo),
|
|
6634
|
+
"head": head_commit(repo),
|
|
6635
|
+
"projection_refresh": projection_refresh,
|
|
6636
|
+
"result": result,
|
|
6637
|
+
}
|
|
6638
|
+
|
|
6639
|
+
if resolved_action == "graph":
|
|
6640
|
+
projection_refresh = self._refresh_git_surfaces(quest_root)
|
|
6641
|
+
return {
|
|
6642
|
+
"ok": True,
|
|
6643
|
+
"action": resolved_action,
|
|
6644
|
+
"quest_id": quest_root.name,
|
|
6645
|
+
"current_ref": current_branch(repo),
|
|
6646
|
+
"head": head_commit(repo),
|
|
6647
|
+
"projection_refresh": projection_refresh,
|
|
6648
|
+
"result": self.quest_service.git_commit_canvas(quest_root.name),
|
|
6649
|
+
}
|
|
6650
|
+
|
|
6651
|
+
return {
|
|
6652
|
+
"ok": False,
|
|
6653
|
+
"action": resolved_action,
|
|
6654
|
+
"message": "Unsupported git action. Use status, commit, branch, checkout, log, show, diff, or graph.",
|
|
6655
|
+
}
|
|
6656
|
+
|
|
6160
6657
|
def prepare_branch(
|
|
6161
6658
|
self,
|
|
6162
6659
|
quest_root: Path,
|
|
@@ -6323,7 +6820,11 @@ class ArtifactService:
|
|
|
6323
6820
|
has_idea=bool(resolved_idea_id),
|
|
6324
6821
|
has_main_result=has_main_result,
|
|
6325
6822
|
)
|
|
6326
|
-
workspace_mode = self.
|
|
6823
|
+
workspace_mode, branch_mode = self._resolve_workspace_modes(
|
|
6824
|
+
state,
|
|
6825
|
+
branch_name=branch_name,
|
|
6826
|
+
has_idea=bool(resolved_idea_id),
|
|
6827
|
+
)
|
|
6327
6828
|
source_run_id = (
|
|
6328
6829
|
str(target.get("run_id") or "").strip()
|
|
6329
6830
|
or str(latest_main_run.get("run_id") or "").strip()
|
|
@@ -6360,7 +6861,7 @@ class ArtifactService:
|
|
|
6360
6861
|
"promote_to_head": bool(promote_to_head),
|
|
6361
6862
|
"worktree_created": worktree_created,
|
|
6362
6863
|
"next_anchor": next_anchor,
|
|
6363
|
-
"workspace_mode":
|
|
6864
|
+
"workspace_mode": branch_mode,
|
|
6364
6865
|
"latest_main_run_id": str(latest_main_run.get("run_id") or "").strip() or None,
|
|
6365
6866
|
"branch_kind": branch_kind,
|
|
6366
6867
|
"paper_parent_branch": source_parent_branch if branch_kind == "paper" else None,
|
|
@@ -6388,6 +6889,7 @@ class ArtifactService:
|
|
|
6388
6889
|
"paper_parent_run_id": source_run_id if branch_kind == "paper" else None,
|
|
6389
6890
|
"next_pending_slice_id": None,
|
|
6390
6891
|
"workspace_mode": workspace_mode,
|
|
6892
|
+
"workspace_branch_mode": branch_mode,
|
|
6391
6893
|
"last_flow_type": "branch_activation",
|
|
6392
6894
|
}
|
|
6393
6895
|
if promote_to_head:
|
|
@@ -6490,6 +6992,12 @@ class ArtifactService:
|
|
|
6490
6992
|
checkpoint=False,
|
|
6491
6993
|
workspace_root=workspace_root,
|
|
6492
6994
|
)
|
|
6995
|
+
current_state = self.quest_service.read_research_state(quest_root)
|
|
6996
|
+
workspace_mode, branch_mode = self._resolve_workspace_modes(
|
|
6997
|
+
current_state,
|
|
6998
|
+
branch_name=target_branch,
|
|
6999
|
+
has_idea=bool(idea_id),
|
|
7000
|
+
)
|
|
6493
7001
|
self.quest_service.update_research_state(
|
|
6494
7002
|
quest_root,
|
|
6495
7003
|
active_idea_id=idea_id,
|
|
@@ -6503,7 +7011,8 @@ class ArtifactService:
|
|
|
6503
7011
|
paper_parent_branch=None,
|
|
6504
7012
|
paper_parent_worktree_root=None,
|
|
6505
7013
|
paper_parent_run_id=None,
|
|
6506
|
-
workspace_mode=
|
|
7014
|
+
workspace_mode=workspace_mode,
|
|
7015
|
+
workspace_branch_mode=branch_mode,
|
|
6507
7016
|
last_flow_type="main_experiment_branch",
|
|
6508
7017
|
)
|
|
6509
7018
|
return target_branch, current_branch_name, True
|
|
@@ -6522,10 +7031,7 @@ class ArtifactService:
|
|
|
6522
7031
|
or current_branch(self._workspace_root_for(quest_root))
|
|
6523
7032
|
)
|
|
6524
7033
|
current_workspace_root = self._workspace_root_for(quest_root)
|
|
6525
|
-
if (
|
|
6526
|
-
str(state.get("workspace_mode") or "").strip() == "paper"
|
|
6527
|
-
and self._branch_kind_from_name(current_branch_name) == "paper"
|
|
6528
|
-
):
|
|
7034
|
+
if self._active_workspace_branch_mode(state, branch_name=current_branch_name) == "paper":
|
|
6529
7035
|
return {
|
|
6530
7036
|
"ok": True,
|
|
6531
7037
|
"branch": current_branch_name,
|
|
@@ -6632,9 +7138,9 @@ class ArtifactService:
|
|
|
6632
7138
|
risks = [str(item).strip() for item in (risks or []) if str(item).strip()]
|
|
6633
7139
|
next_target = str(next_target or "experiment").strip().lower() or "experiment"
|
|
6634
7140
|
normalized_lineage_intent = self._normalize_lineage_intent(lineage_intent)
|
|
6635
|
-
from ..prompts.builder import
|
|
7141
|
+
from ..prompts.builder import current_standard_skills
|
|
6636
7142
|
|
|
6637
|
-
next_anchor = next_target if next_target in
|
|
7143
|
+
next_anchor = next_target if next_target in current_standard_skills(repo_root()) else "experiment"
|
|
6638
7144
|
|
|
6639
7145
|
if normalized_mode == "create":
|
|
6640
7146
|
resolved_idea_id = str(idea_id or generate_id("idea")).strip()
|
|
@@ -6941,6 +7447,12 @@ class ArtifactService:
|
|
|
6941
7447
|
checkpoint=False,
|
|
6942
7448
|
workspace_root=worktree_root,
|
|
6943
7449
|
)
|
|
7450
|
+
current_state = self.quest_service.read_research_state(quest_root)
|
|
7451
|
+
workspace_mode, branch_mode = self._resolve_workspace_modes(
|
|
7452
|
+
current_state,
|
|
7453
|
+
branch_name=branch_name,
|
|
7454
|
+
has_idea=bool(resolved_idea_id),
|
|
7455
|
+
)
|
|
6944
7456
|
research_state = self.quest_service.update_research_state(
|
|
6945
7457
|
quest_root,
|
|
6946
7458
|
active_idea_id=resolved_idea_id,
|
|
@@ -6954,7 +7466,8 @@ class ArtifactService:
|
|
|
6954
7466
|
analysis_parent_branch=None,
|
|
6955
7467
|
analysis_parent_worktree_root=None,
|
|
6956
7468
|
next_pending_slice_id=None,
|
|
6957
|
-
workspace_mode=
|
|
7469
|
+
workspace_mode=workspace_mode,
|
|
7470
|
+
workspace_branch_mode=branch_mode,
|
|
6958
7471
|
last_flow_type="idea_submission",
|
|
6959
7472
|
)
|
|
6960
7473
|
self.quest_service.update_settings(quest_id, active_anchor=next_anchor)
|
|
@@ -7404,13 +7917,16 @@ class ArtifactService:
|
|
|
7404
7917
|
) -> dict[str, Any]:
|
|
7405
7918
|
self._require_baseline_gate_open(quest_root, action="record_main_experiment")
|
|
7406
7919
|
state = self.quest_service.read_research_state(quest_root)
|
|
7407
|
-
|
|
7408
|
-
|
|
7920
|
+
branch_mode = self._active_workspace_branch_mode(
|
|
7921
|
+
state,
|
|
7922
|
+
branch_name=str(state.get("current_workspace_branch") or "").strip(),
|
|
7923
|
+
)
|
|
7924
|
+
if branch_mode == "analysis":
|
|
7409
7925
|
raise ValueError(
|
|
7410
7926
|
"record_main_experiment cannot run while the active workspace is an analysis slice. "
|
|
7411
7927
|
"Finish or close the analysis campaign first."
|
|
7412
7928
|
)
|
|
7413
|
-
if
|
|
7929
|
+
if branch_mode == "paper":
|
|
7414
7930
|
raise ValueError(
|
|
7415
7931
|
"record_main_experiment cannot run while the active workspace is a paper branch. "
|
|
7416
7932
|
"Return to the source evidence branch or create a new run branch first."
|
|
@@ -7896,6 +8412,12 @@ class ArtifactService:
|
|
|
7896
8412
|
},
|
|
7897
8413
|
)
|
|
7898
8414
|
self.quest_service.update_settings(self._quest_id(quest_root), active_anchor="decision")
|
|
8415
|
+
current_state = self.quest_service.read_research_state(quest_root)
|
|
8416
|
+
workspace_mode, branch_mode = self._resolve_workspace_modes(
|
|
8417
|
+
current_state,
|
|
8418
|
+
branch_name=branch_name,
|
|
8419
|
+
has_idea=bool(active_idea_id),
|
|
8420
|
+
)
|
|
7899
8421
|
research_state = self.quest_service.update_research_state(
|
|
7900
8422
|
quest_root,
|
|
7901
8423
|
active_idea_id=active_idea_id,
|
|
@@ -7909,7 +8431,8 @@ class ArtifactService:
|
|
|
7909
8431
|
paper_parent_branch=None,
|
|
7910
8432
|
paper_parent_worktree_root=None,
|
|
7911
8433
|
paper_parent_run_id=None,
|
|
7912
|
-
workspace_mode=
|
|
8434
|
+
workspace_mode=workspace_mode,
|
|
8435
|
+
workspace_branch_mode=branch_mode,
|
|
7913
8436
|
last_flow_type="main_experiment_recorded",
|
|
7914
8437
|
)
|
|
7915
8438
|
return {
|
|
@@ -7994,7 +8517,10 @@ class ArtifactService:
|
|
|
7994
8517
|
or normalized_research_questions
|
|
7995
8518
|
or normalized_experimental_designs
|
|
7996
8519
|
or normalized_todo_items
|
|
7997
|
-
or
|
|
8520
|
+
or self._active_workspace_branch_mode(
|
|
8521
|
+
state,
|
|
8522
|
+
branch_name=str(state.get("current_workspace_branch") or "").strip(),
|
|
8523
|
+
) == "paper"
|
|
7998
8524
|
or active_anchor == "write"
|
|
7999
8525
|
or campaign_origin_kind in {"write", "paper", "rebuttal", "revision"}
|
|
8000
8526
|
)
|
|
@@ -8525,6 +9051,11 @@ class ArtifactService:
|
|
|
8525
9051
|
source_run_id=resolved_parent_run_id,
|
|
8526
9052
|
source_idea_id=active_idea_id,
|
|
8527
9053
|
)
|
|
9054
|
+
current_state = self.quest_service.read_research_state(quest_root)
|
|
9055
|
+
workspace_mode, branch_mode = self._resolve_workspace_modes(
|
|
9056
|
+
current_state,
|
|
9057
|
+
branch_name=first_slice["branch"],
|
|
9058
|
+
)
|
|
8528
9059
|
research_state = self.quest_service.update_research_state(
|
|
8529
9060
|
quest_root,
|
|
8530
9061
|
active_idea_id=active_idea_id,
|
|
@@ -8534,7 +9065,8 @@ class ArtifactService:
|
|
|
8534
9065
|
next_pending_slice_id=first_slice["slice_id"],
|
|
8535
9066
|
current_workspace_branch=first_slice["branch"],
|
|
8536
9067
|
current_workspace_root=first_slice["worktree_root"],
|
|
8537
|
-
workspace_mode=
|
|
9068
|
+
workspace_mode=workspace_mode,
|
|
9069
|
+
workspace_branch_mode=branch_mode,
|
|
8538
9070
|
last_flow_type="analysis_campaign",
|
|
8539
9071
|
)
|
|
8540
9072
|
baseline_inventory = self._upsert_analysis_baseline_inventory(quest_root, inventory_entries) if inventory_entries else None
|
|
@@ -9516,13 +10048,19 @@ class ArtifactService:
|
|
|
9516
10048
|
)
|
|
9517
10049
|
|
|
9518
10050
|
if next_slice is not None:
|
|
10051
|
+
current_state = self.quest_service.read_research_state(quest_root)
|
|
10052
|
+
workspace_mode, branch_mode = self._resolve_workspace_modes(
|
|
10053
|
+
current_state,
|
|
10054
|
+
branch_name=next_slice.get("branch"),
|
|
10055
|
+
)
|
|
9519
10056
|
research_state = self.quest_service.update_research_state(
|
|
9520
10057
|
quest_root,
|
|
9521
10058
|
active_analysis_campaign_id=campaign_id,
|
|
9522
10059
|
next_pending_slice_id=next_slice.get("slice_id"),
|
|
9523
10060
|
current_workspace_branch=next_slice.get("branch"),
|
|
9524
10061
|
current_workspace_root=next_slice.get("worktree_root"),
|
|
9525
|
-
workspace_mode=
|
|
10062
|
+
workspace_mode=workspace_mode,
|
|
10063
|
+
workspace_branch_mode=branch_mode,
|
|
9526
10064
|
last_flow_type="analysis_slice",
|
|
9527
10065
|
)
|
|
9528
10066
|
self.quest_service.update_settings(self._quest_id(quest_root), active_anchor="analysis-campaign")
|
|
@@ -9624,6 +10162,11 @@ class ArtifactService:
|
|
|
9624
10162
|
startup_contract = self._startup_contract(quest_root)
|
|
9625
10163
|
raw_need_research_paper = startup_contract.get("need_research_paper")
|
|
9626
10164
|
need_research_paper = raw_need_research_paper if isinstance(raw_need_research_paper, bool) else True
|
|
10165
|
+
current_state = self.quest_service.read_research_state(quest_root)
|
|
10166
|
+
workspace_mode, branch_mode = self._resolve_workspace_modes(
|
|
10167
|
+
current_state,
|
|
10168
|
+
branch_name=parent_branch,
|
|
10169
|
+
)
|
|
9627
10170
|
base_research_state = self.quest_service.update_research_state(
|
|
9628
10171
|
quest_root,
|
|
9629
10172
|
active_idea_id=restored_idea_id,
|
|
@@ -9636,7 +10179,8 @@ class ArtifactService:
|
|
|
9636
10179
|
next_pending_slice_id=None,
|
|
9637
10180
|
current_workspace_branch=parent_branch,
|
|
9638
10181
|
current_workspace_root=str(parent_worktree_root),
|
|
9639
|
-
workspace_mode=
|
|
10182
|
+
workspace_mode=workspace_mode,
|
|
10183
|
+
workspace_branch_mode=branch_mode,
|
|
9640
10184
|
last_flow_type="analysis_campaign_complete",
|
|
9641
10185
|
)
|
|
9642
10186
|
writing_workspace: dict[str, Any] | None = None
|
|
@@ -10382,11 +10926,12 @@ class ArtifactService:
|
|
|
10382
10926
|
}
|
|
10383
10927
|
suppress_resolved = (kind == "progress") if suppress_if_unchanged is None else bool(suppress_if_unchanged)
|
|
10384
10928
|
dedupe_key_resolved = str(dedupe_key or self._normalize_interaction_message(full_message)).strip() or None
|
|
10929
|
+
pending_user_message_count = int(self.quest_service.snapshot(self._quest_id(quest_root)).get("pending_user_message_count") or 0)
|
|
10385
10930
|
if (
|
|
10386
10931
|
kind == "progress"
|
|
10387
10932
|
and suppress_resolved
|
|
10388
10933
|
and dedupe_key_resolved
|
|
10389
|
-
and
|
|
10934
|
+
and pending_user_message_count == 0
|
|
10390
10935
|
):
|
|
10391
10936
|
prior_interaction = self._latest_duplicate_progress_interaction(
|
|
10392
10937
|
quest_root,
|
|
@@ -10433,6 +10978,57 @@ class ArtifactService:
|
|
|
10433
10978
|
"suppressed_reason": "unchanged_progress",
|
|
10434
10979
|
"dedupe_key": dedupe_key_resolved,
|
|
10435
10980
|
}
|
|
10981
|
+
if (
|
|
10982
|
+
kind == "answer"
|
|
10983
|
+
and not deliver_to_bound_conversations
|
|
10984
|
+
and dedupe_key_resolved
|
|
10985
|
+
and pending_user_message_count == 0
|
|
10986
|
+
):
|
|
10987
|
+
prior_answer = self._latest_duplicate_answer_fallback_interaction(
|
|
10988
|
+
quest_root,
|
|
10989
|
+
dedupe_key=dedupe_key_resolved,
|
|
10990
|
+
min_interval_seconds=120,
|
|
10991
|
+
)
|
|
10992
|
+
if prior_answer is not None:
|
|
10993
|
+
interaction_state = self._read_interaction_state(quest_root)
|
|
10994
|
+
waiting_requests = [
|
|
10995
|
+
dict(item)
|
|
10996
|
+
for item in (interaction_state.get("open_requests") or [])
|
|
10997
|
+
if str(item.get("status") or "") == "waiting"
|
|
10998
|
+
]
|
|
10999
|
+
return {
|
|
11000
|
+
"status": "suppressed_duplicate",
|
|
11001
|
+
"artifact_id": prior_answer.get("artifact_id"),
|
|
11002
|
+
"interaction_id": prior_answer.get("interaction_id"),
|
|
11003
|
+
"expects_reply": False,
|
|
11004
|
+
"reply_mode": "threaded",
|
|
11005
|
+
"surface_actions": [],
|
|
11006
|
+
"connector_hints": connector_hints_resolved,
|
|
11007
|
+
"normalized_attachments": attachments_resolved,
|
|
11008
|
+
"attachment_issues": attachment_issues,
|
|
11009
|
+
"delivered": False,
|
|
11010
|
+
"delivery_results": [],
|
|
11011
|
+
"response_phase": response_phase,
|
|
11012
|
+
"delivery_targets": [],
|
|
11013
|
+
"delivery_policy": self._delivery_policy(self._connectors_config()),
|
|
11014
|
+
"preferred_connector": self._preferred_connector(self._connectors_config()),
|
|
11015
|
+
"recent_inbound_messages": [],
|
|
11016
|
+
"delivery_batch": None,
|
|
11017
|
+
"recent_interaction_records": self.quest_service.latest_artifact_interaction_records(quest_root, limit=10),
|
|
11018
|
+
"agent_instruction": self.quest_service.localized_copy(
|
|
11019
|
+
quest_root=quest_root,
|
|
11020
|
+
zh="这一轮里相同内容的 answer 已经发出过一次,不要再为本地 fallback 额外创建第二条。",
|
|
11021
|
+
en="An identical answer was already emitted in this user turn. Do not create a second local-only fallback copy.",
|
|
11022
|
+
),
|
|
11023
|
+
"queued_message_count_before_delivery": 0,
|
|
11024
|
+
"queued_message_count_after_delivery": 0,
|
|
11025
|
+
"open_request_count": len(waiting_requests),
|
|
11026
|
+
"active_request": waiting_requests[-1] if waiting_requests else None,
|
|
11027
|
+
"default_reply_interaction_id": interaction_state.get("default_reply_interaction_id"),
|
|
11028
|
+
"guidance": "Duplicate answer fallback was suppressed because the same answer was already recorded in the current user turn.",
|
|
11029
|
+
"suppressed_reason": "duplicate_answer_fallback",
|
|
11030
|
+
"dedupe_key": dedupe_key_resolved,
|
|
11031
|
+
}
|
|
10436
11032
|
resolved_artifact_id = generate_id(durable_kind)
|
|
10437
11033
|
resolved_interaction_id = interaction_id or (
|
|
10438
11034
|
resolved_artifact_id if reply_mode_resolved != "none" or reply_to_interaction_id else None
|
|
@@ -10559,6 +11155,7 @@ class ArtifactService:
|
|
|
10559
11155
|
connector_hints=connector_hints_resolved,
|
|
10560
11156
|
created_at=(artifact.get("record") or {}).get("updated_at"),
|
|
10561
11157
|
counts_as_visible=counts_as_visible,
|
|
11158
|
+
deliver_to_bound_conversations=deliver_to_bound_conversations,
|
|
10562
11159
|
)
|
|
10563
11160
|
|
|
10564
11161
|
return {
|
|
@@ -10619,6 +11216,34 @@ class ArtifactService:
|
|
|
10619
11216
|
return dict(item)
|
|
10620
11217
|
return None
|
|
10621
11218
|
|
|
11219
|
+
def _latest_duplicate_answer_fallback_interaction(
|
|
11220
|
+
self,
|
|
11221
|
+
quest_root: Path,
|
|
11222
|
+
*,
|
|
11223
|
+
dedupe_key: str,
|
|
11224
|
+
min_interval_seconds: int | None,
|
|
11225
|
+
) -> dict[str, Any] | None:
|
|
11226
|
+
recent = self.quest_service.latest_artifact_interaction_records(quest_root, limit=40)
|
|
11227
|
+
for item in reversed(recent):
|
|
11228
|
+
record_type = str(item.get("type") or "").strip()
|
|
11229
|
+
if record_type == "user_inbound":
|
|
11230
|
+
return None
|
|
11231
|
+
if record_type != "artifact_outbound":
|
|
11232
|
+
continue
|
|
11233
|
+
if str(item.get("kind") or "").strip() != "answer":
|
|
11234
|
+
continue
|
|
11235
|
+
if not bool(item.get("deliver_to_bound_conversations")):
|
|
11236
|
+
continue
|
|
11237
|
+
previous_key = str(item.get("dedupe_key") or self._normalize_interaction_message(item.get("message") or "")).strip()
|
|
11238
|
+
if previous_key != dedupe_key:
|
|
11239
|
+
continue
|
|
11240
|
+
if min_interval_seconds:
|
|
11241
|
+
seconds_since = self.quest_service._seconds_since_iso_timestamp(item.get("created_at"))
|
|
11242
|
+
if seconds_since is not None and seconds_since > int(min_interval_seconds):
|
|
11243
|
+
return None
|
|
11244
|
+
return dict(item)
|
|
11245
|
+
return None
|
|
11246
|
+
|
|
10622
11247
|
def complete_quest(
|
|
10623
11248
|
self,
|
|
10624
11249
|
quest_root: Path,
|