@researai/deepscientist 1.5.15 → 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 -98
- package/bin/ds.js +691 -91
- package/docs/en/00_QUICK_START.md +36 -15
- package/docs/en/01_SETTINGS_REFERENCE.md +33 -0
- 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 +11 -5
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
- 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 +18 -0
- package/docs/zh/00_QUICK_START.md +36 -15
- package/docs/zh/01_SETTINGS_REFERENCE.md +33 -0
- 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 +11 -5
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
- 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 +18 -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/service.py +647 -22
- package/src/deepscientist/bash_exec/service.py +234 -9
- package/src/deepscientist/cli.py +115 -19
- package/src/deepscientist/codex_cli_compat.py +232 -0
- package/src/deepscientist/config/models.py +2 -1
- package/src/deepscientist/config/service.py +31 -9
- package/src/deepscientist/daemon/api/handlers.py +125 -6
- package/src/deepscientist/daemon/api/router.py +4 -0
- package/src/deepscientist/daemon/app.py +715 -98
- 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 +255 -32
- package/src/deepscientist/quest/layout.py +15 -2
- package/src/deepscientist/quest/service.py +295 -43
- package/src/deepscientist/quest/stage_views.py +6 -1
- package/src/deepscientist/runners/codex.py +86 -31
- 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 +12 -1
- package/src/prompts/system.md +10 -5
- package/src/prompts/system_copilot.md +43 -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-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-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
|
@@ -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,
|
|
@@ -6,30 +6,21 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
from ..connector_runtime import normalize_conversation_id, parse_conversation_id
|
|
8
8
|
from ..config import ConfigManager
|
|
9
|
+
from ..home import repo_root
|
|
9
10
|
from ..memory import MemoryService
|
|
10
11
|
from ..memory.frontmatter import load_markdown_document
|
|
11
12
|
from ..quest import QuestService
|
|
12
13
|
from ..registries import BaselineRegistry
|
|
13
14
|
from ..shared import read_json, read_text, read_yaml
|
|
15
|
+
from ..skills import SkillInstaller, companion_skill_ids, stage_skill_ids
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"write",
|
|
23
|
-
"finalize",
|
|
24
|
-
"decision",
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
COMPANION_SKILLS = (
|
|
28
|
-
"figure-polish",
|
|
29
|
-
"intake-audit",
|
|
30
|
-
"review",
|
|
31
|
-
"rebuttal",
|
|
32
|
-
)
|
|
17
|
+
# Backward-compatible snapshots for modules or tests that still import these names directly.
|
|
18
|
+
# Runtime routing should call `current_standard_skills(...)` / `current_companion_skills(...)`.
|
|
19
|
+
STANDARD_SKILLS = stage_skill_ids(repo_root())
|
|
20
|
+
|
|
21
|
+
_AUTO_CONTINUE_MONITOR_INTERVAL_SECONDS = 240
|
|
22
|
+
|
|
23
|
+
COMPANION_SKILLS = companion_skill_ids(repo_root())
|
|
33
24
|
|
|
34
25
|
STAGE_MEMORY_PLAN = {
|
|
35
26
|
"scout": {
|
|
@@ -71,6 +62,14 @@ STAGE_MEMORY_PLAN = {
|
|
|
71
62
|
}
|
|
72
63
|
|
|
73
64
|
|
|
65
|
+
def current_standard_skills(repo_root_path: Path | None = None) -> tuple[str, ...]:
|
|
66
|
+
return stage_skill_ids(repo_root_path or repo_root())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def current_companion_skills(repo_root_path: Path | None = None) -> tuple[str, ...]:
|
|
70
|
+
return companion_skill_ids(repo_root_path or repo_root())
|
|
71
|
+
|
|
72
|
+
|
|
74
73
|
def classify_turn_intent(user_message: str) -> str:
|
|
75
74
|
text = str(user_message or "").strip()
|
|
76
75
|
if not text:
|
|
@@ -104,13 +103,15 @@ def classify_turn_intent(user_message: str) -> str:
|
|
|
104
103
|
|
|
105
104
|
|
|
106
105
|
class PromptBuilder:
|
|
107
|
-
def __init__(self, repo_root: Path, home: Path) -> None:
|
|
106
|
+
def __init__(self, repo_root: Path, home: Path, *, prompt_version_selection: str | None = None) -> None:
|
|
108
107
|
self.repo_root = repo_root
|
|
109
108
|
self.home = home
|
|
110
109
|
self.quest_service = QuestService(home)
|
|
111
110
|
self.memory_service = MemoryService(home)
|
|
112
111
|
self.baseline_registry = BaselineRegistry(home)
|
|
113
112
|
self.config_manager = ConfigManager(home)
|
|
113
|
+
self.skill_installer = SkillInstaller(repo_root, home)
|
|
114
|
+
self.prompt_version_selection = str(prompt_version_selection or "").strip() or None
|
|
114
115
|
|
|
115
116
|
def build(
|
|
116
117
|
self,
|
|
@@ -128,9 +129,14 @@ class PromptBuilder:
|
|
|
128
129
|
runtime_config = self.config_manager.load_named("config")
|
|
129
130
|
connectors_config = self.config_manager.load_named_normalized("connectors")
|
|
130
131
|
quest_root = Path(snapshot["quest_root"])
|
|
132
|
+
self.skill_installer.sync_quest_prompts(quest_root)
|
|
131
133
|
active_anchor = str(snapshot.get("active_anchor") or skill_id)
|
|
132
134
|
default_locale = str(runtime_config.get("default_locale") or "en-US")
|
|
133
|
-
|
|
135
|
+
workspace_mode = self._workspace_mode(snapshot)
|
|
136
|
+
system_block = self._prompt_fragment(
|
|
137
|
+
"system_copilot.md" if workspace_mode == "copilot" else "system.md",
|
|
138
|
+
quest_root=quest_root,
|
|
139
|
+
)
|
|
134
140
|
shared_interaction_block = self._prompt_fragment(
|
|
135
141
|
Path("contracts") / "shared_interaction.md",
|
|
136
142
|
quest_root=quest_root,
|
|
@@ -159,7 +165,7 @@ class PromptBuilder:
|
|
|
159
165
|
f"conversation_id: quest:{quest_id}",
|
|
160
166
|
f"default_locale: {default_locale}",
|
|
161
167
|
"built_in_mcp_namespaces: memory, artifact, bash_exec",
|
|
162
|
-
"mcp_namespace_note: any shell-like command execution must use bash_exec
|
|
168
|
+
"mcp_namespace_note: **any shell-like command execution must use `bash_exec(...)`, including curl/python/bash/node/git/npm/uv and similar CLI tools; do not use native `shell_command` / `command_execution`.**",
|
|
163
169
|
"",
|
|
164
170
|
"Canonical stage skills root:",
|
|
165
171
|
str((self.repo_root / "src" / "skills").resolve()),
|
|
@@ -229,6 +235,14 @@ class PromptBuilder:
|
|
|
229
235
|
"## Recovery Resume Packet",
|
|
230
236
|
self._recovery_resume_block(snapshot=snapshot, turn_reason=turn_reason),
|
|
231
237
|
"",
|
|
238
|
+
"## Resume Context Spine",
|
|
239
|
+
self._resume_context_spine_block(
|
|
240
|
+
quest_id=quest_id,
|
|
241
|
+
quest_root=quest_root,
|
|
242
|
+
snapshot=snapshot,
|
|
243
|
+
turn_reason=turn_reason,
|
|
244
|
+
),
|
|
245
|
+
"",
|
|
232
246
|
"## Interaction Style",
|
|
233
247
|
self._interaction_style_block(default_locale=default_locale, user_message=user_message, snapshot=snapshot),
|
|
234
248
|
"",
|
|
@@ -486,8 +500,14 @@ class PromptBuilder:
|
|
|
486
500
|
]
|
|
487
501
|
)
|
|
488
502
|
if str(turn_reason or "").strip() == "auto_continue":
|
|
489
|
-
lines.
|
|
490
|
-
|
|
503
|
+
lines.extend(
|
|
504
|
+
[
|
|
505
|
+
"- auto_continue_rule: this turn has no new user message; continue from the active requirements, durable artifacts, current quest state, and resume context spine instead of replaying the previous user message",
|
|
506
|
+
f"- auto_continue_interval_rule: when a real long-running external task is already active, background-progress auto-continue becomes a low-frequency monitoring pass, about every {_AUTO_CONTINUE_MONITOR_INTERVAL_SECONDS} seconds rather than sub-minute polling",
|
|
507
|
+
"- auto_continue_fast_prepare_rule: in autonomous mode before a real external long-running task exists, auto-continue may advance quickly, around 0.2 seconds between turns, so the agent can keep preparing or launching the real work without idling",
|
|
508
|
+
"- autonomous_prepare_rule: in autonomous mode, if no real long-running external task is active yet, use the next turns to keep preparing, launching, or durably deciding the next real unit of work instead of parking idly",
|
|
509
|
+
"- copilot_park_rule: in copilot mode, once the current requested unit is complete, it is normal to park and wait for the next user message or `/resume` instead of continuing autonomously",
|
|
510
|
+
]
|
|
491
511
|
)
|
|
492
512
|
else:
|
|
493
513
|
lines.append(
|
|
@@ -614,6 +634,76 @@ class PromptBuilder:
|
|
|
614
634
|
lines.append(f"- remaining_attachment_count: {len(attachments) - 6}")
|
|
615
635
|
return "\n".join(lines)
|
|
616
636
|
|
|
637
|
+
def _resume_context_spine_block(self, *, quest_id: str, quest_root: Path, snapshot: dict, turn_reason: str) -> str:
|
|
638
|
+
if str(turn_reason or "").strip() != "auto_continue":
|
|
639
|
+
return "- none"
|
|
640
|
+
lines = [
|
|
641
|
+
"- resume_spine_rule: on auto_continue turns, first continue from the latest durable user requirement, the latest assistant checkpoint, the latest run summary, and recent memory cues instead of reconstructing intent from scratch",
|
|
642
|
+
]
|
|
643
|
+
bash_running_count = int(((snapshot.get("counts") or {}).get("bash_running_count")) or 0)
|
|
644
|
+
latest_bash_session = (
|
|
645
|
+
dict((snapshot.get("summary") or {}).get("latest_bash_session") or {})
|
|
646
|
+
if isinstance((snapshot.get("summary") or {}).get("latest_bash_session"), dict)
|
|
647
|
+
else {}
|
|
648
|
+
)
|
|
649
|
+
lines.append(f"- active_bash_exec_run_count: {bash_running_count}")
|
|
650
|
+
if latest_bash_session:
|
|
651
|
+
command_preview = " ".join(str(latest_bash_session.get("command") or "").split())
|
|
652
|
+
if len(command_preview) > 180:
|
|
653
|
+
command_preview = command_preview[:177].rstrip() + "..."
|
|
654
|
+
lines.append(
|
|
655
|
+
f"- latest_bash_exec_session: bash_id={str(latest_bash_session.get('bash_id') or 'none')} | "
|
|
656
|
+
f"status={str(latest_bash_session.get('status') or 'unknown')} | "
|
|
657
|
+
f"command={command_preview or 'none'}"
|
|
658
|
+
)
|
|
659
|
+
latest_user = self._latest_user_message(quest_id)
|
|
660
|
+
if latest_user is not None:
|
|
661
|
+
preview = " ".join(str(latest_user.get("content") or "").split())
|
|
662
|
+
if len(preview) > 320:
|
|
663
|
+
preview = preview[:317].rstrip() + "..."
|
|
664
|
+
lines.append(
|
|
665
|
+
f"- latest_user_message: {str(latest_user.get('created_at') or 'unknown')} | "
|
|
666
|
+
f"source={str(latest_user.get('source') or 'unknown')} | "
|
|
667
|
+
f"reply_to={str(latest_user.get('reply_to_interaction_id') or 'none')} | "
|
|
668
|
+
f"preview={preview or 'none'}"
|
|
669
|
+
)
|
|
670
|
+
latest_assistant = self._latest_assistant_message(quest_id)
|
|
671
|
+
if latest_assistant is not None:
|
|
672
|
+
preview = " ".join(str(latest_assistant.get("content") or "").split())
|
|
673
|
+
if len(preview) > 360:
|
|
674
|
+
preview = preview[:357].rstrip() + "..."
|
|
675
|
+
lines.append(
|
|
676
|
+
f"- latest_assistant_checkpoint: {str(latest_assistant.get('created_at') or 'unknown')} | "
|
|
677
|
+
f"skill={str(latest_assistant.get('skill_id') or 'none')} | "
|
|
678
|
+
f"run_id={str(latest_assistant.get('run_id') or 'none')} | "
|
|
679
|
+
f"preview={preview or 'none'}"
|
|
680
|
+
)
|
|
681
|
+
latest_run = self._latest_run_result(quest_root)
|
|
682
|
+
if latest_run is not None:
|
|
683
|
+
preview = " ".join(str(latest_run.get("preview") or "").split())
|
|
684
|
+
if len(preview) > 360:
|
|
685
|
+
preview = preview[:357].rstrip() + "..."
|
|
686
|
+
lines.append(
|
|
687
|
+
f"- latest_run_result: {str(latest_run.get('completed_at') or 'unknown')} | "
|
|
688
|
+
f"run_id={str(latest_run.get('run_id') or 'none')} | "
|
|
689
|
+
f"exit_code={latest_run.get('exit_code') if latest_run.get('exit_code') is not None else 'none'} | "
|
|
690
|
+
f"preview={preview or 'none'}"
|
|
691
|
+
)
|
|
692
|
+
recent_memory = self.memory_service.list_recent(scope="quest", quest_root=quest_root, limit=3)
|
|
693
|
+
if recent_memory:
|
|
694
|
+
lines.append("- recent_memory_cues:")
|
|
695
|
+
for item in recent_memory:
|
|
696
|
+
title = str(item.get("title") or "memory").strip() or "memory"
|
|
697
|
+
card_type = str(item.get("type") or "memory").strip() or "memory"
|
|
698
|
+
excerpt = " ".join(str(item.get("excerpt") or "").split())
|
|
699
|
+
if len(excerpt) > 200:
|
|
700
|
+
excerpt = excerpt[:197].rstrip() + "..."
|
|
701
|
+
lines.append(f" - [{card_type}] {title}: {excerpt or 'no excerpt'}")
|
|
702
|
+
else:
|
|
703
|
+
lines.append("- recent_memory_cues: none")
|
|
704
|
+
lines.append("- resume_spine_conflict_rule: if these spine items conflict with newer durable files or artifacts, trust the newer durable state and update the summary rather than replaying the older plan verbatim")
|
|
705
|
+
return "\n".join(lines)
|
|
706
|
+
|
|
617
707
|
def _retry_recovery_block(self, retry_context: dict | None) -> str:
|
|
618
708
|
if not isinstance(retry_context, dict) or not retry_context:
|
|
619
709
|
return "- none"
|
|
@@ -726,6 +816,19 @@ class PromptBuilder:
|
|
|
726
816
|
def _prompt_path(self, relative_path: str | Path, *, quest_root: Path | None = None) -> Path:
|
|
727
817
|
normalized = Path(relative_path)
|
|
728
818
|
if quest_root is not None:
|
|
819
|
+
selected_version = str(self.prompt_version_selection or "").strip()
|
|
820
|
+
if selected_version and selected_version not in {"latest", "current", "active"}:
|
|
821
|
+
selected_root = self.skill_installer.resolve_prompt_version_root(quest_root, selected_version)
|
|
822
|
+
if selected_root is None:
|
|
823
|
+
raise FileNotFoundError(
|
|
824
|
+
f"Prompt version `{selected_version}` is unavailable for quest `{quest_root.name}`."
|
|
825
|
+
)
|
|
826
|
+
selected_path = selected_root / normalized
|
|
827
|
+
if not selected_path.exists():
|
|
828
|
+
raise FileNotFoundError(
|
|
829
|
+
f"Prompt version `{selected_version}` does not include `{normalized.as_posix()}` for quest `{quest_root.name}`."
|
|
830
|
+
)
|
|
831
|
+
return selected_path
|
|
729
832
|
quest_path = quest_root / ".codex" / "prompts" / normalized
|
|
730
833
|
if quest_path.exists():
|
|
731
834
|
return quest_path
|
|
@@ -737,16 +840,45 @@ class PromptBuilder:
|
|
|
737
840
|
return item
|
|
738
841
|
return None
|
|
739
842
|
|
|
843
|
+
def _latest_assistant_message(self, quest_id: str) -> dict | None:
|
|
844
|
+
for item in reversed(self.quest_service.history(quest_id, limit=120)):
|
|
845
|
+
if str(item.get("role") or "") == "assistant":
|
|
846
|
+
return item
|
|
847
|
+
return None
|
|
848
|
+
|
|
849
|
+
@staticmethod
|
|
850
|
+
def _latest_run_result(quest_root: Path) -> dict[str, object] | None:
|
|
851
|
+
runs_root = quest_root / ".ds" / "runs"
|
|
852
|
+
if not runs_root.exists():
|
|
853
|
+
return None
|
|
854
|
+
candidates = [path for path in runs_root.glob("*/result.json") if path.is_file()]
|
|
855
|
+
if not candidates:
|
|
856
|
+
return None
|
|
857
|
+
latest = max(candidates, key=lambda path: path.stat().st_mtime)
|
|
858
|
+
payload = read_json(latest, {})
|
|
859
|
+
if not isinstance(payload, dict):
|
|
860
|
+
return None
|
|
861
|
+
preview = (
|
|
862
|
+
str(payload.get("output_text") or "").strip()
|
|
863
|
+
or str(payload.get("stderr_text") or "").strip()
|
|
864
|
+
)
|
|
865
|
+
return {
|
|
866
|
+
"run_id": latest.parent.name,
|
|
867
|
+
"completed_at": str(payload.get("completed_at") or "").strip() or None,
|
|
868
|
+
"exit_code": payload.get("exit_code"),
|
|
869
|
+
"preview": preview,
|
|
870
|
+
}
|
|
871
|
+
|
|
740
872
|
def _skill_paths_block(self) -> str:
|
|
741
873
|
lines = []
|
|
742
|
-
for skill_id in
|
|
874
|
+
for skill_id in current_standard_skills(self.repo_root):
|
|
743
875
|
primary = (self.repo_root / "src" / "skills" / skill_id / "SKILL.md").resolve()
|
|
744
876
|
lines.append(f"- {skill_id}: primary={primary}")
|
|
745
877
|
return "\n".join(lines)
|
|
746
878
|
|
|
747
879
|
def _companion_skill_paths_block(self) -> str:
|
|
748
880
|
lines = []
|
|
749
|
-
for skill_id in
|
|
881
|
+
for skill_id in current_companion_skills(self.repo_root):
|
|
750
882
|
primary = (self.repo_root / "src" / "skills" / skill_id / "SKILL.md").resolve()
|
|
751
883
|
lines.append(f"- {skill_id}: primary={primary}")
|
|
752
884
|
return "\n".join(lines)
|
|
@@ -760,6 +892,18 @@ class PromptBuilder:
|
|
|
760
892
|
return value
|
|
761
893
|
return True
|
|
762
894
|
|
|
895
|
+
@staticmethod
|
|
896
|
+
def _workspace_mode(snapshot: dict) -> str:
|
|
897
|
+
value = str(snapshot.get("workspace_mode") or "").strip().lower()
|
|
898
|
+
if value in {"copilot", "autonomous"}:
|
|
899
|
+
return value
|
|
900
|
+
startup_contract = snapshot.get("startup_contract")
|
|
901
|
+
if isinstance(startup_contract, dict):
|
|
902
|
+
value = str(startup_contract.get("workspace_mode") or "").strip().lower()
|
|
903
|
+
if value in {"copilot", "autonomous"}:
|
|
904
|
+
return value
|
|
905
|
+
return "autonomous"
|
|
906
|
+
|
|
763
907
|
@staticmethod
|
|
764
908
|
def _decision_policy(snapshot: dict) -> str:
|
|
765
909
|
startup_contract = snapshot.get("startup_contract")
|
|
@@ -824,6 +968,18 @@ class PromptBuilder:
|
|
|
824
968
|
return "none"
|
|
825
969
|
|
|
826
970
|
def _research_delivery_policy_block(self, snapshot: dict) -> str:
|
|
971
|
+
if self._workspace_mode(snapshot) == "copilot":
|
|
972
|
+
return "\n".join(
|
|
973
|
+
[
|
|
974
|
+
"- workspace_mode: copilot",
|
|
975
|
+
"- delivery_goal: complete the user-requested unit of work instead of forcing the full research graph by default.",
|
|
976
|
+
"- task_scope_rule: arbitrary research tasks such as reading, coding, debugging, experiment design, run inspection, analysis, writing, and planning can all be handled directly in this mode.",
|
|
977
|
+
"- autonomy_boundary: only expand into longer autonomous continuation when the user explicitly asks for end-to-end or unattended progress.",
|
|
978
|
+
"- routing_rule: open only the skills actually needed for the current request.",
|
|
979
|
+
"- durability_rule: keep important plan, evidence, decisions, and outputs durable in quest files or artifacts so later turns can resume cleanly.",
|
|
980
|
+
"- completion_rule: after the requested unit is complete, summarize what changed and stop instead of auto-continuing.",
|
|
981
|
+
]
|
|
982
|
+
)
|
|
827
983
|
need_research_paper = self._need_research_paper(snapshot)
|
|
828
984
|
launch_mode = self._launch_mode(snapshot)
|
|
829
985
|
standard_profile = self._standard_profile(snapshot)
|
|
@@ -1029,6 +1185,30 @@ class PromptBuilder:
|
|
|
1029
1185
|
def _interaction_style_block(self, *, default_locale: str, user_message: str, snapshot: dict) -> str:
|
|
1030
1186
|
normalized_locale = str(default_locale or "").lower()
|
|
1031
1187
|
chinese_turn = normalized_locale.startswith("zh") or bool(re.search(r"[\u4e00-\u9fff]", user_message))
|
|
1188
|
+
if self._workspace_mode(snapshot) == "copilot":
|
|
1189
|
+
lines = [
|
|
1190
|
+
f"- configured_default_locale: {default_locale}",
|
|
1191
|
+
f"- current_turn_language_bias: {'zh' if chinese_turn else 'en'}",
|
|
1192
|
+
"- collaboration_mode: user-directed copilot",
|
|
1193
|
+
"- freeform_task_rule: if the user asks for a concrete research task, solve that task directly before introducing stage-routing language.",
|
|
1194
|
+
"- requested_skill_hint_rule: in copilot mode, treat `requested_skill` as a lightweight routing hint, not as an instruction to default into `decision` for ordinary direct tasks.",
|
|
1195
|
+
"- response_pattern: say what changed -> say what it means -> say what happens next",
|
|
1196
|
+
"- mailbox_protocol: artifact.interact(include_recent_inbound_messages=True) remains the queued human-message mailbox and should be checked whenever human continuity matters.",
|
|
1197
|
+
"- planning_rule: before non-trivial execution, make the immediate plan explicit and keep the first step small.",
|
|
1198
|
+
"- tool_rule: use memory for durable recall, artifact for quest state and git-aware research operations, and bash_exec for terminal execution.",
|
|
1199
|
+
"- copilot_sop_rule: classify the request first, choose the narrowest correct tool path, execute the smallest useful unit, persist the important result, then answer plainly.",
|
|
1200
|
+
"- shell_tool_mandate: **for any shell, CLI, Python, bash, node, git, npm, uv, or environment command execution, use `bash_exec(...)`; do not use native `shell_command` or Codex `command_execution`.**",
|
|
1201
|
+
"- git_tool_mandate: for git work inside the current quest repository or worktree, prefer `artifact.git(...)` before raw shell git commands.",
|
|
1202
|
+
"- git_test_rule: if the user wants a generic git smoke test rather than a quest-repo mutation, use `bash_exec(...)` in an isolated scratch repository.",
|
|
1203
|
+
"- decision_entry_rule: use `decision` only for real route, scope, cost, branch, or scientific-direction judgments; do not default to it for ordinary repo, code, environment, or execution tasks.",
|
|
1204
|
+
"- stop_rule: once the current requested unit is done, send a concise update and wait for the next message or `/resume`.",
|
|
1205
|
+
"- escalation_rule: if a route change materially affects cost, scope, or scientific direction, ask before proceeding.",
|
|
1206
|
+
]
|
|
1207
|
+
if chinese_turn:
|
|
1208
|
+
lines.append("- tone_hint: 使用自然、礼貌、专业的中文,先解释结论,再说明下一步。")
|
|
1209
|
+
else:
|
|
1210
|
+
lines.append("- tone_hint: use concise, natural, professional English and lead with the conclusion.")
|
|
1211
|
+
return "\n".join(lines)
|
|
1032
1212
|
bound_conversations = snapshot.get("bound_conversations") or []
|
|
1033
1213
|
need_research_paper = self._need_research_paper(snapshot)
|
|
1034
1214
|
decision_policy = self._decision_policy(snapshot)
|
|
@@ -1047,6 +1227,7 @@ class PromptBuilder:
|
|
|
1047
1227
|
"- response_pattern: say what changed -> say what it means -> say what happens next",
|
|
1048
1228
|
"- interaction_protocol: first message may be plain conversation; after that, treat artifact.interact threads and mailbox polls as the main continuity spine across TUI, web, and connectors",
|
|
1049
1229
|
"- shared_interaction_contract_precedence: use the shared interaction contract as the default user-facing cadence; the rules below add runtime-specific execution behavior instead of restating the same chat cadence",
|
|
1230
|
+
"- shell_tool_mandate: **native `shell_command` / `command_execution` is forbidden; all shell-like execution must use `bash_exec(...)`.**",
|
|
1050
1231
|
"- mailbox_protocol: artifact.interact(include_recent_inbound_messages=True) is the queued human-message mailbox; when it returns user text, treat that input as higher priority than background subtasks until it has been acknowledged",
|
|
1051
1232
|
"- acknowledgment_protocol: after artifact.interact returns any human message, immediately send one substantive artifact.interact(...) follow-up; if the active connector runtime already emitted a transport-level receipt acknowledgement, do not send a redundant receipt-only message; if answerable, answer directly, otherwise state the short plan, nearest checkpoint, and that the current background subtask is paused",
|
|
1052
1233
|
"- subtask_boundary_protocol: send a user-visible update whenever the active subtask changes materially, especially across intake -> audit, audit -> experiment planning, experiment planning -> run launch, run result -> drafting, or drafting -> review/rebuttal",
|
|
@@ -1055,6 +1236,10 @@ class PromptBuilder:
|
|
|
1055
1236
|
"- long_run_reporting_protocol: inspect real logs/status after each meaningful await cycle and at least once every 30 minutes at worst, but only send a user-visible update when there is a human-meaningful delta, blocker, recovery, route change, or the visibility bound would otherwise be exceeded",
|
|
1056
1237
|
"- intervention_threshold_protocol: do not kill or restart a run merely because a short watch window passed without final completion; intervene only on explicit failure, clear invalidity, process exit, or no meaningful delta across a sufficiently long observation window",
|
|
1057
1238
|
"- timeout_protocol: before using bash_exec(mode='await', ...), estimate whether the command can finish within the selected wait window; if runtime is uncertain or likely longer, use bash_exec(mode='detach', ...) and monitor instead of guessing a fake deadline",
|
|
1239
|
+
f"- auto_continue_monitoring_protocol: if the runtime schedules background-progress auto_continue turns while a real external task is already active, treat them as low-frequency monitoring passes roughly every {_AUTO_CONTINUE_MONITOR_INTERVAL_SECONDS} seconds rather than as a fast polling loop",
|
|
1240
|
+
"- auto_continue_prepare_protocol: in autonomous mode before a real long-running external task exists, rapid auto-continue passes around 0.2 seconds apart are acceptable only for active preparation, launch, or durable route closure work; they are not a substitute for starting the real task",
|
|
1241
|
+
"- long_run_ownership_protocol: real long-running execution should stay alive in detached bash_exec sessions or the runtime process it launched; do not rely on repeated model turns to simulate continuous execution",
|
|
1242
|
+
"- auto_continue_resume_protocol: on auto_continue turns, read the resume context spine first and continue from the latest durable user requirement, latest assistant checkpoint, latest run summary, recent memory cues, and current bash_exec state before changing route",
|
|
1058
1243
|
"- blocking_protocol: use reply_mode='blocking' only for true unresolved user decisions; ordinary progress updates should stay threaded and non-blocking",
|
|
1059
1244
|
"- credential_blocking_protocol: if continuation requires user-supplied external credentials or secrets such as an API key, GitHub key/token, or Hugging Face key/token, emit one structured blocking decision request that asks the user to provide the credential or choose an alternative route; do not invent placeholders or silently skip the blocked step",
|
|
1060
1245
|
"- credential_wait_protocol: if that credential request remains unanswered, keep the quest waiting rather than self-resolving; if you are resumed without new credentials and no other work is possible, a long low-frequency park such as `bash_exec(command='sleep 3600', mode='await', timeout_seconds=3700)` is acceptable to avoid busy-looping",
|
|
@@ -1212,13 +1397,51 @@ class PromptBuilder:
|
|
|
1212
1397
|
plan = STAGE_MEMORY_PLAN.get(stage, STAGE_MEMORY_PLAN["decision"])
|
|
1213
1398
|
quest_kinds = ", ".join(plan.get("quest", ())) or "none"
|
|
1214
1399
|
global_kinds = ", ".join(plan.get("global", ())) or "none"
|
|
1215
|
-
|
|
1216
|
-
[
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
)
|
|
1400
|
+
lines = [
|
|
1401
|
+
f"- stage_memory_rule: for `{stage}`, prefer quest memory kinds [{quest_kinds}] and global memory kinds [{global_kinds}] when memory lookup is needed.",
|
|
1402
|
+
"- memory_lookup_tool: call memory.list_recent(...) to recover context after pause/restart and memory.search(...) before repeating prior work.",
|
|
1403
|
+
"- memory_injection_rule: keep the injected memory compact, but do not drop all continuity on auto_continue turns; reuse a few recent durable cues directly when they materially anchor the next action.",
|
|
1404
|
+
]
|
|
1405
|
+
selected: list[dict] = []
|
|
1406
|
+
seen_paths: set[str] = set()
|
|
1407
|
+
for kind in plan.get("quest", ())[:2]:
|
|
1408
|
+
for card in self.memory_service.list_recent(scope="quest", quest_root=quest_root, limit=2, kind=kind)[:1]:
|
|
1409
|
+
self._append_priority_memory(
|
|
1410
|
+
selected,
|
|
1411
|
+
seen_paths,
|
|
1412
|
+
card=card,
|
|
1413
|
+
scope="quest",
|
|
1414
|
+
quest_root=quest_root,
|
|
1415
|
+
reason=f"recent quest memory for stage `{stage}`",
|
|
1416
|
+
)
|
|
1417
|
+
for kind in plan.get("global", ())[:2]:
|
|
1418
|
+
for card in self.memory_service.list_recent(scope="global", limit=2, kind=kind)[:1]:
|
|
1419
|
+
self._append_priority_memory(
|
|
1420
|
+
selected,
|
|
1421
|
+
seen_paths,
|
|
1422
|
+
card=card,
|
|
1423
|
+
scope="global",
|
|
1424
|
+
quest_root=quest_root,
|
|
1425
|
+
reason=f"recent global memory for stage `{stage}`",
|
|
1426
|
+
)
|
|
1427
|
+
for query in self._memory_queries(user_message)[:2]:
|
|
1428
|
+
for scope in ("quest", "global"):
|
|
1429
|
+
for card in self.memory_service.search(
|
|
1430
|
+
query,
|
|
1431
|
+
scope=scope if scope == "global" else "quest",
|
|
1432
|
+
quest_root=quest_root if scope == "quest" else None,
|
|
1433
|
+
limit=1,
|
|
1434
|
+
):
|
|
1435
|
+
self._append_priority_memory(
|
|
1436
|
+
selected,
|
|
1437
|
+
seen_paths,
|
|
1438
|
+
card=card,
|
|
1439
|
+
scope=scope,
|
|
1440
|
+
quest_root=quest_root,
|
|
1441
|
+
reason=f"matched current-turn query `{query}`",
|
|
1442
|
+
)
|
|
1443
|
+
lines.extend(["- selected_memory:", self._format_priority_memory(selected)])
|
|
1444
|
+
return "\n".join(lines)
|
|
1222
1445
|
|
|
1223
1446
|
def _append_priority_memory(
|
|
1224
1447
|
self,
|