@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
|
@@ -52,11 +52,17 @@ def initial_quest_yaml(
|
|
|
52
52
|
startup_contract: dict | None = None,
|
|
53
53
|
) -> dict:
|
|
54
54
|
timestamp = utc_now()
|
|
55
|
+
workspace_mode = (
|
|
56
|
+
str((startup_contract or {}).get("workspace_mode") or "").strip().lower()
|
|
57
|
+
if isinstance(startup_contract, dict)
|
|
58
|
+
else ""
|
|
59
|
+
)
|
|
60
|
+
initial_status_value = "idle" if workspace_mode == "copilot" else "active"
|
|
55
61
|
return {
|
|
56
62
|
"quest_id": quest_id,
|
|
57
63
|
"title": title or goal,
|
|
58
64
|
"quest_root": str(quest_root.resolve()),
|
|
59
|
-
"status":
|
|
65
|
+
"status": initial_status_value,
|
|
60
66
|
"active_anchor": "baseline",
|
|
61
67
|
"baseline_gate": "pending",
|
|
62
68
|
"confirmed_baseline_ref": None,
|
|
@@ -100,7 +106,14 @@ def initial_plan() -> str:
|
|
|
100
106
|
)
|
|
101
107
|
|
|
102
108
|
|
|
103
|
-
def initial_status() -> str:
|
|
109
|
+
def initial_status(startup_contract: dict | None = None) -> str:
|
|
110
|
+
workspace_mode = (
|
|
111
|
+
str((startup_contract or {}).get("workspace_mode") or "").strip().lower()
|
|
112
|
+
if isinstance(startup_contract, dict)
|
|
113
|
+
else ""
|
|
114
|
+
)
|
|
115
|
+
if workspace_mode == "copilot":
|
|
116
|
+
return "# Status\n\nReady for your first instruction.\n"
|
|
104
117
|
return "# Status\n\nQuest created. Waiting for baseline setup or reuse.\n"
|
|
105
118
|
|
|
106
119
|
|
|
@@ -24,7 +24,7 @@ from ..artifact.metrics import build_baseline_compare_payload, build_metrics_tim
|
|
|
24
24
|
from ..config import ConfigManager
|
|
25
25
|
from ..connector_runtime import conversation_identity_key, normalize_conversation_id, parse_conversation_id
|
|
26
26
|
from ..file_lock import advisory_file_lock
|
|
27
|
-
from ..gitops import current_branch, export_git_graph, head_commit, init_repo, list_branch_canvas
|
|
27
|
+
from ..gitops import current_branch, export_git_graph, head_commit, init_repo, list_branch_canvas, list_commit_canvas
|
|
28
28
|
from ..home import repo_root
|
|
29
29
|
from ..registries import BaselineRegistry
|
|
30
30
|
from ..shared import append_jsonl, ensure_dir, generate_id, iter_jsonl, read_json, read_jsonl, read_jsonl_tail, read_text, read_yaml, resolve_within, run_command, sha256_text, slugify, utc_now, write_json, write_text, write_yaml
|
|
@@ -172,6 +172,127 @@ def _iter_jsonl_records_safely(
|
|
|
172
172
|
yield payload
|
|
173
173
|
|
|
174
174
|
|
|
175
|
+
def _parse_jsonl_record_line_safely(
|
|
176
|
+
raw_line: bytes,
|
|
177
|
+
*,
|
|
178
|
+
oversized_line_bytes: int = _EVENTS_OVERSIZED_LINE_BYTES,
|
|
179
|
+
) -> dict[str, Any] | None:
|
|
180
|
+
raw = bytes(raw_line).strip()
|
|
181
|
+
if not raw:
|
|
182
|
+
return None
|
|
183
|
+
line_bytes = len(raw)
|
|
184
|
+
if line_bytes > oversized_line_bytes:
|
|
185
|
+
return _oversized_event_placeholder(
|
|
186
|
+
prefix=raw[:_OVERSIZED_EVENT_PREFIX_BYTES],
|
|
187
|
+
line_bytes=line_bytes,
|
|
188
|
+
)
|
|
189
|
+
try:
|
|
190
|
+
payload = json.loads(raw)
|
|
191
|
+
except json.JSONDecodeError:
|
|
192
|
+
return None
|
|
193
|
+
return payload if isinstance(payload, dict) else None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _tail_jsonl_records_safely(
|
|
197
|
+
path: Path,
|
|
198
|
+
*,
|
|
199
|
+
limit: int,
|
|
200
|
+
oversized_line_bytes: int = _EVENTS_OVERSIZED_LINE_BYTES,
|
|
201
|
+
) -> tuple[list[tuple[int, dict[str, Any]]], int]:
|
|
202
|
+
normalized_limit = max(int(limit or 0), 0)
|
|
203
|
+
if normalized_limit <= 0 or not path.exists():
|
|
204
|
+
return [], 0
|
|
205
|
+
total = _count_jsonl_lines_fast(path)
|
|
206
|
+
if total <= 0:
|
|
207
|
+
return [], 0
|
|
208
|
+
|
|
209
|
+
raw_tail = _read_jsonl_tail_lines_fast(path, normalized_limit)
|
|
210
|
+
if not raw_tail:
|
|
211
|
+
return [], total
|
|
212
|
+
|
|
213
|
+
cursor_start = max(total - len(raw_tail) + 1, 1)
|
|
214
|
+
parsed: list[tuple[int, dict[str, Any]]] = []
|
|
215
|
+
for cursor, raw_line in enumerate(raw_tail, start=cursor_start):
|
|
216
|
+
payload = _parse_jsonl_record_line_safely(
|
|
217
|
+
raw_line,
|
|
218
|
+
oversized_line_bytes=oversized_line_bytes,
|
|
219
|
+
)
|
|
220
|
+
if isinstance(payload, dict):
|
|
221
|
+
parsed.append((cursor, payload))
|
|
222
|
+
return parsed, total
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _count_jsonl_lines_fast(path: Path, *, chunk_size: int = 1024 * 1024) -> int:
|
|
226
|
+
if not path.exists():
|
|
227
|
+
return 0
|
|
228
|
+
total = 0
|
|
229
|
+
last_byte = b""
|
|
230
|
+
with path.open("rb") as handle:
|
|
231
|
+
while True:
|
|
232
|
+
chunk = handle.read(chunk_size)
|
|
233
|
+
if not chunk:
|
|
234
|
+
break
|
|
235
|
+
total += chunk.count(b"\n")
|
|
236
|
+
last_byte = chunk[-1:]
|
|
237
|
+
if total == 0 and last_byte:
|
|
238
|
+
return 1
|
|
239
|
+
if last_byte not in {b"", b"\n"}:
|
|
240
|
+
total += 1
|
|
241
|
+
return total
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _read_jsonl_tail_lines_fast(path: Path, limit: int, *, chunk_size: int = 1024 * 1024) -> list[bytes]:
|
|
245
|
+
normalized_limit = max(int(limit or 0), 0)
|
|
246
|
+
if normalized_limit <= 0 or not path.exists():
|
|
247
|
+
return []
|
|
248
|
+
|
|
249
|
+
size = path.stat().st_size
|
|
250
|
+
if size <= 0:
|
|
251
|
+
return []
|
|
252
|
+
|
|
253
|
+
lines: deque[bytes] = deque()
|
|
254
|
+
remainder = b""
|
|
255
|
+
with path.open("rb") as handle:
|
|
256
|
+
position = size
|
|
257
|
+
while position > 0 and len(lines) < normalized_limit:
|
|
258
|
+
read_size = min(chunk_size, position)
|
|
259
|
+
position -= read_size
|
|
260
|
+
handle.seek(position)
|
|
261
|
+
chunk = handle.read(read_size)
|
|
262
|
+
payload = chunk + remainder
|
|
263
|
+
parts = payload.split(b"\n")
|
|
264
|
+
remainder = parts[0]
|
|
265
|
+
for raw_line in reversed(parts[1:]):
|
|
266
|
+
stripped = raw_line.rstrip(b"\r")
|
|
267
|
+
if not stripped.strip():
|
|
268
|
+
continue
|
|
269
|
+
lines.appendleft(stripped)
|
|
270
|
+
if len(lines) >= normalized_limit:
|
|
271
|
+
break
|
|
272
|
+
if len(lines) < normalized_limit and remainder.strip():
|
|
273
|
+
lines.appendleft(remainder.rstrip(b"\r"))
|
|
274
|
+
return list(lines)[-normalized_limit:]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _iter_jsonl_records_from_offset_safely(
|
|
278
|
+
path: Path,
|
|
279
|
+
*,
|
|
280
|
+
start_offset: int,
|
|
281
|
+
oversized_line_bytes: int = _EVENTS_OVERSIZED_LINE_BYTES,
|
|
282
|
+
):
|
|
283
|
+
if not path.exists():
|
|
284
|
+
return
|
|
285
|
+
with path.open("rb") as handle:
|
|
286
|
+
handle.seek(max(int(start_offset or 0), 0))
|
|
287
|
+
for raw_line in handle:
|
|
288
|
+
payload = _parse_jsonl_record_line_safely(
|
|
289
|
+
raw_line,
|
|
290
|
+
oversized_line_bytes=oversized_line_bytes,
|
|
291
|
+
)
|
|
292
|
+
if isinstance(payload, dict):
|
|
293
|
+
yield payload
|
|
294
|
+
|
|
295
|
+
|
|
175
296
|
class QuestService:
|
|
176
297
|
def __init__(self, home: Path, skill_installer: SkillInstaller | None = None) -> None:
|
|
177
298
|
self.home = home
|
|
@@ -182,6 +303,7 @@ class QuestService:
|
|
|
182
303
|
self._file_cache: dict[str, dict[str, Any]] = {}
|
|
183
304
|
self._jsonl_cache_lock = threading.Lock()
|
|
184
305
|
self._jsonl_cache: dict[str, dict[str, Any]] = {}
|
|
306
|
+
self._jsonl_tail_cache: dict[str, dict[str, Any]] = {}
|
|
185
307
|
self._snapshot_cache_lock = threading.Lock()
|
|
186
308
|
self._snapshot_cache: dict[str, dict[str, Any]] = {}
|
|
187
309
|
self._codex_history_cache_lock = threading.Lock()
|
|
@@ -288,6 +410,13 @@ class QuestService:
|
|
|
288
410
|
return quest_root / ".ds" / "lab_canvas_state.json"
|
|
289
411
|
|
|
290
412
|
def _default_research_state(self, quest_root: Path) -> dict[str, Any]:
|
|
413
|
+
quest_yaml = self.read_quest_yaml(quest_root)
|
|
414
|
+
startup_contract = (
|
|
415
|
+
dict(quest_yaml.get("startup_contract") or {})
|
|
416
|
+
if isinstance(quest_yaml.get("startup_contract"), dict)
|
|
417
|
+
else {}
|
|
418
|
+
)
|
|
419
|
+
workspace_mode = str(startup_contract.get("workspace_mode") or "").strip().lower() or "quest"
|
|
291
420
|
return {
|
|
292
421
|
"version": 1,
|
|
293
422
|
"active_idea_id": None,
|
|
@@ -304,7 +433,7 @@ class QuestService:
|
|
|
304
433
|
"paper_parent_worktree_root": None,
|
|
305
434
|
"paper_parent_run_id": None,
|
|
306
435
|
"next_pending_slice_id": None,
|
|
307
|
-
"workspace_mode":
|
|
436
|
+
"workspace_mode": workspace_mode,
|
|
308
437
|
"last_flow_type": None,
|
|
309
438
|
"updated_at": utc_now(),
|
|
310
439
|
}
|
|
@@ -354,7 +483,7 @@ class QuestService:
|
|
|
354
483
|
continue
|
|
355
484
|
current[key] = str(value) if isinstance(value, Path) else value
|
|
356
485
|
payload = self.write_research_state(quest_root, current)
|
|
357
|
-
self.schedule_projection_refresh(quest_root, kinds=("details", "canvas"))
|
|
486
|
+
self.schedule_projection_refresh(quest_root, kinds=("details", "canvas", "git_canvas"))
|
|
358
487
|
return payload
|
|
359
488
|
|
|
360
489
|
def read_lab_canvas_state(self, quest_root: Path) -> dict[str, Any]:
|
|
@@ -971,6 +1100,8 @@ class QuestService:
|
|
|
971
1100
|
return self._details_projection_state(quest_root)
|
|
972
1101
|
if kind == "canvas":
|
|
973
1102
|
return self._canvas_projection_state(quest_root)
|
|
1103
|
+
if kind == "git_canvas":
|
|
1104
|
+
return self._canvas_projection_state(quest_root)
|
|
974
1105
|
raise ValueError(f"Unsupported projection kind `{kind}`.")
|
|
975
1106
|
|
|
976
1107
|
def _projection_source_signature(self, quest_root: Path, kind: str) -> str:
|
|
@@ -1434,6 +1565,17 @@ class QuestService:
|
|
|
1434
1565
|
update_progress(2, "Computing branch canvas")
|
|
1435
1566
|
return list_branch_canvas(quest_root, quest_id=quest_root.name)
|
|
1436
1567
|
|
|
1568
|
+
def _build_git_canvas_projection_payload(
|
|
1569
|
+
self,
|
|
1570
|
+
quest_root: Path,
|
|
1571
|
+
*,
|
|
1572
|
+
source_signature: str,
|
|
1573
|
+
update_progress: Any,
|
|
1574
|
+
) -> dict[str, Any]:
|
|
1575
|
+
update_progress(1, "Scanning commit history")
|
|
1576
|
+
update_progress(2, "Computing commit canvas")
|
|
1577
|
+
return list_commit_canvas(quest_root, quest_id=quest_root.name)
|
|
1578
|
+
|
|
1437
1579
|
def _build_projection_payload(
|
|
1438
1580
|
self,
|
|
1439
1581
|
quest_root: Path,
|
|
@@ -1454,6 +1596,12 @@ class QuestService:
|
|
|
1454
1596
|
source_signature=source_signature,
|
|
1455
1597
|
update_progress=update_progress,
|
|
1456
1598
|
)
|
|
1599
|
+
if kind == "git_canvas":
|
|
1600
|
+
return self._build_git_canvas_projection_payload(
|
|
1601
|
+
quest_root,
|
|
1602
|
+
source_signature=source_signature,
|
|
1603
|
+
update_progress=update_progress,
|
|
1604
|
+
)
|
|
1457
1605
|
raise ValueError(f"Unsupported projection kind `{kind}`.")
|
|
1458
1606
|
|
|
1459
1607
|
def _placeholder_workflow_payload(self, quest_id: str, quest_root: Path) -> dict[str, Any]:
|
|
@@ -1486,6 +1634,17 @@ class QuestService:
|
|
|
1486
1634
|
},
|
|
1487
1635
|
}
|
|
1488
1636
|
|
|
1637
|
+
def _placeholder_git_canvas_payload(self, quest_id: str, quest_root: Path) -> dict[str, Any]:
|
|
1638
|
+
research_state = self.read_research_state(quest_root)
|
|
1639
|
+
return {
|
|
1640
|
+
"quest_id": quest_id,
|
|
1641
|
+
"workspace_mode": str(research_state.get("workspace_mode") or "copilot").strip() or "copilot",
|
|
1642
|
+
"head": head_commit(quest_root),
|
|
1643
|
+
"current_ref": current_branch(quest_root),
|
|
1644
|
+
"nodes": [],
|
|
1645
|
+
"edges": [],
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1489
1648
|
def _projected_payload(self, quest_id: str, kind: str) -> dict[str, Any]:
|
|
1490
1649
|
quest_root = self._quest_root(quest_id)
|
|
1491
1650
|
source_signature = self._projection_source_signature(quest_root, kind)
|
|
@@ -1510,11 +1669,12 @@ class QuestService:
|
|
|
1510
1669
|
else None
|
|
1511
1670
|
)
|
|
1512
1671
|
if payload is None:
|
|
1513
|
-
|
|
1514
|
-
self._placeholder_workflow_payload(quest_id, quest_root)
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1672
|
+
if kind == "details":
|
|
1673
|
+
payload = self._placeholder_workflow_payload(quest_id, quest_root)
|
|
1674
|
+
elif kind == "git_canvas":
|
|
1675
|
+
payload = self._placeholder_git_canvas_payload(quest_id, quest_root)
|
|
1676
|
+
else:
|
|
1677
|
+
payload = self._placeholder_canvas_payload(quest_id, quest_root)
|
|
1518
1678
|
payload["projection_status"] = status
|
|
1519
1679
|
return payload
|
|
1520
1680
|
|
|
@@ -1535,8 +1695,8 @@ class QuestService:
|
|
|
1535
1695
|
) -> None:
|
|
1536
1696
|
resolved_kinds = [
|
|
1537
1697
|
str(kind).strip()
|
|
1538
|
-
for kind in (kinds or ("details", "canvas"))
|
|
1539
|
-
if str(kind).strip() in {"details", "canvas"}
|
|
1698
|
+
for kind in (kinds or ("details", "canvas", "git_canvas"))
|
|
1699
|
+
if str(kind).strip() in {"details", "canvas", "git_canvas"}
|
|
1540
1700
|
]
|
|
1541
1701
|
if not resolved_kinds:
|
|
1542
1702
|
return
|
|
@@ -1563,6 +1723,9 @@ class QuestService:
|
|
|
1563
1723
|
def git_branch_canvas(self, quest_id: str) -> dict[str, Any]:
|
|
1564
1724
|
return self._projected_payload(quest_id, "canvas")
|
|
1565
1725
|
|
|
1726
|
+
def git_commit_canvas(self, quest_id: str) -> dict[str, Any]:
|
|
1727
|
+
return self._projected_payload(quest_id, "git_canvas")
|
|
1728
|
+
|
|
1566
1729
|
def _active_baseline_attachment(self, quest_root: Path, workspace_root: Path) -> dict[str, Any] | None:
|
|
1567
1730
|
attachments: list[dict[str, Any]] = []
|
|
1568
1731
|
seen_paths: set[str] = set()
|
|
@@ -2602,7 +2765,7 @@ class QuestService:
|
|
|
2602
2765
|
)
|
|
2603
2766
|
write_text(quest_root / "brief.md", initial_brief(goal))
|
|
2604
2767
|
write_text(quest_root / "plan.md", initial_plan())
|
|
2605
|
-
write_text(quest_root / "status.md", initial_status())
|
|
2768
|
+
write_text(quest_root / "status.md", initial_status(startup_contract))
|
|
2606
2769
|
write_text(quest_root / "SUMMARY.md", initial_summary())
|
|
2607
2770
|
write_text(quest_root / ".gitignore", gitignore())
|
|
2608
2771
|
self._write_active_user_requirements(
|
|
@@ -2790,6 +2953,7 @@ class QuestService:
|
|
|
2790
2953
|
"research_head_worktree_root": research_state.get("research_head_worktree_root"),
|
|
2791
2954
|
"current_workspace_branch": research_state.get("current_workspace_branch"),
|
|
2792
2955
|
"current_workspace_root": research_state.get("current_workspace_root"),
|
|
2956
|
+
"workspace_mode": research_state.get("workspace_mode") or "quest",
|
|
2793
2957
|
"active_idea_id": research_state.get("active_idea_id"),
|
|
2794
2958
|
"active_baseline_id": active_baseline_id,
|
|
2795
2959
|
"active_baseline_variant_id": active_baseline_variant_id,
|
|
@@ -2876,8 +3040,8 @@ class QuestService:
|
|
|
2876
3040
|
}
|
|
2877
3041
|
return items
|
|
2878
3042
|
|
|
2879
|
-
@staticmethod
|
|
2880
3043
|
def _read_jsonl_cursor_slice(
|
|
3044
|
+
self,
|
|
2881
3045
|
path: Path,
|
|
2882
3046
|
*,
|
|
2883
3047
|
after: int = 0,
|
|
@@ -2886,7 +3050,10 @@ class QuestService:
|
|
|
2886
3050
|
tail: bool = False,
|
|
2887
3051
|
) -> tuple[list[tuple[int, dict[str, Any]]], int, bool]:
|
|
2888
3052
|
normalized_limit = max(int(limit or 0), 0)
|
|
3053
|
+
cache_key = self._cache_key_for_path(path)
|
|
2889
3054
|
if not path.exists():
|
|
3055
|
+
with self._jsonl_cache_lock:
|
|
3056
|
+
self._jsonl_tail_cache.pop(cache_key, None)
|
|
2890
3057
|
return [], 0, False
|
|
2891
3058
|
if normalized_limit <= 0:
|
|
2892
3059
|
total = sum(1 for _ in _iter_jsonl_records_safely(path))
|
|
@@ -2905,11 +3072,71 @@ class QuestService:
|
|
|
2905
3072
|
return list(window), total, has_more
|
|
2906
3073
|
|
|
2907
3074
|
if tail:
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
3075
|
+
state = self._path_state(path)
|
|
3076
|
+
cached_tail: dict[str, Any] | None = None
|
|
3077
|
+
with self._jsonl_cache_lock:
|
|
3078
|
+
candidate = self._jsonl_tail_cache.get(cache_key)
|
|
3079
|
+
if isinstance(candidate, dict):
|
|
3080
|
+
cached_tail = dict(candidate)
|
|
3081
|
+
|
|
3082
|
+
if cached_tail and cached_tail.get("state") == state:
|
|
3083
|
+
cached_limit = int(cached_tail.get("limit") or 0)
|
|
3084
|
+
cached_records = list(cached_tail.get("records") or [])
|
|
3085
|
+
cached_total = int(cached_tail.get("total") or 0)
|
|
3086
|
+
if cached_limit >= normalized_limit and cached_records:
|
|
3087
|
+
window = cached_records[-normalized_limit:]
|
|
3088
|
+
has_more = cached_total > len(window)
|
|
3089
|
+
return window, cached_total, has_more
|
|
3090
|
+
|
|
3091
|
+
if (
|
|
3092
|
+
cached_tail
|
|
3093
|
+
and state is not None
|
|
3094
|
+
and cached_tail.get("state")
|
|
3095
|
+
and tuple(cached_tail.get("state"))[0] == state[0]
|
|
3096
|
+
and state[2] >= tuple(cached_tail.get("state"))[2]
|
|
3097
|
+
):
|
|
3098
|
+
cached_state = tuple(cached_tail.get("state"))
|
|
3099
|
+
cached_limit = int(cached_tail.get("limit") or 0)
|
|
3100
|
+
cached_total = int(cached_tail.get("total") or 0)
|
|
3101
|
+
max_limit = max(normalized_limit, cached_limit)
|
|
3102
|
+
window = deque(
|
|
3103
|
+
list(cached_tail.get("records") or []),
|
|
3104
|
+
maxlen=max_limit,
|
|
3105
|
+
)
|
|
3106
|
+
appended_records = list(
|
|
3107
|
+
_iter_jsonl_records_from_offset_safely(
|
|
3108
|
+
path,
|
|
3109
|
+
start_offset=int(cached_state[2]),
|
|
3110
|
+
)
|
|
3111
|
+
)
|
|
3112
|
+
if appended_records:
|
|
3113
|
+
next_cursor = cached_total + 1
|
|
3114
|
+
for payload in appended_records:
|
|
3115
|
+
window.append((next_cursor, payload))
|
|
3116
|
+
next_cursor += 1
|
|
3117
|
+
total = cached_total + len(appended_records)
|
|
3118
|
+
else:
|
|
3119
|
+
total = cached_total
|
|
3120
|
+
stored_records = list(window)
|
|
3121
|
+
with self._jsonl_cache_lock:
|
|
3122
|
+
self._jsonl_tail_cache[cache_key] = {
|
|
3123
|
+
"state": state,
|
|
3124
|
+
"limit": max_limit,
|
|
3125
|
+
"total": total,
|
|
3126
|
+
"records": stored_records,
|
|
3127
|
+
}
|
|
3128
|
+
selected = stored_records[-normalized_limit:]
|
|
3129
|
+
has_more = total > len(selected)
|
|
3130
|
+
return selected, total, has_more
|
|
3131
|
+
|
|
3132
|
+
window, total = _tail_jsonl_records_safely(path, limit=normalized_limit)
|
|
3133
|
+
with self._jsonl_cache_lock:
|
|
3134
|
+
self._jsonl_tail_cache[cache_key] = {
|
|
3135
|
+
"state": state,
|
|
3136
|
+
"limit": normalized_limit,
|
|
3137
|
+
"total": total,
|
|
3138
|
+
"records": list(window),
|
|
3139
|
+
}
|
|
2913
3140
|
has_more = total > len(window)
|
|
2914
3141
|
return list(window), total, has_more
|
|
2915
3142
|
|
|
@@ -2945,6 +3172,14 @@ class QuestService:
|
|
|
2945
3172
|
except FileNotFoundError:
|
|
2946
3173
|
return str(path.absolute())
|
|
2947
3174
|
|
|
3175
|
+
def jsonl_tail_cache_entry(self, path: Path) -> dict[str, Any] | None:
|
|
3176
|
+
cache_key = self._cache_key_for_path(path)
|
|
3177
|
+
with self._jsonl_cache_lock:
|
|
3178
|
+
candidate = self._jsonl_tail_cache.get(cache_key)
|
|
3179
|
+
if isinstance(candidate, dict):
|
|
3180
|
+
return dict(candidate)
|
|
3181
|
+
return None
|
|
3182
|
+
|
|
2948
3183
|
def _read_cached_path(
|
|
2949
3184
|
self,
|
|
2950
3185
|
path: Path,
|
|
@@ -3610,10 +3845,11 @@ class QuestService:
|
|
|
3610
3845
|
normalized_anchor = str(active_anchor).strip()
|
|
3611
3846
|
if not normalized_anchor:
|
|
3612
3847
|
raise ValueError("`active_anchor` cannot be empty.")
|
|
3613
|
-
from ..prompts.builder import
|
|
3848
|
+
from ..prompts.builder import current_standard_skills
|
|
3614
3849
|
|
|
3615
|
-
|
|
3616
|
-
|
|
3850
|
+
available_stage_skills = current_standard_skills(repo_root())
|
|
3851
|
+
if normalized_anchor not in available_stage_skills:
|
|
3852
|
+
allowed = ", ".join(available_stage_skills)
|
|
3617
3853
|
raise ValueError(f"Unsupported active anchor `{normalized_anchor}`. Allowed values: {allowed}.")
|
|
3618
3854
|
if quest_data.get("active_anchor") != normalized_anchor:
|
|
3619
3855
|
quest_data["active_anchor"] = normalized_anchor
|
|
@@ -3670,10 +3906,11 @@ class QuestService:
|
|
|
3670
3906
|
normalized_anchor = str(active_anchor or "").strip()
|
|
3671
3907
|
if not normalized_anchor:
|
|
3672
3908
|
raise ValueError("`active_anchor` cannot be empty.")
|
|
3673
|
-
from ..prompts.builder import
|
|
3909
|
+
from ..prompts.builder import current_standard_skills
|
|
3674
3910
|
|
|
3675
|
-
|
|
3676
|
-
|
|
3911
|
+
available_stage_skills = current_standard_skills(repo_root())
|
|
3912
|
+
if normalized_anchor not in available_stage_skills:
|
|
3913
|
+
allowed = ", ".join(available_stage_skills)
|
|
3677
3914
|
raise ValueError(f"Unsupported active anchor `{normalized_anchor}`. Allowed values: {allowed}.")
|
|
3678
3915
|
if quest_data.get("active_anchor") != normalized_anchor:
|
|
3679
3916
|
quest_data["active_anchor"] = normalized_anchor
|
|
@@ -4285,23 +4522,7 @@ class QuestService:
|
|
|
4285
4522
|
},
|
|
4286
4523
|
}
|
|
4287
4524
|
|
|
4288
|
-
|
|
4289
|
-
quest_root
|
|
4290
|
-
if document_id.startswith(("questpath::", "memory::"))
|
|
4291
|
-
else workspace_root
|
|
4292
|
-
)
|
|
4293
|
-
try:
|
|
4294
|
-
path, writable, scope, source_kind = self._resolve_document(resolution_root, document_id)
|
|
4295
|
-
except FileNotFoundError:
|
|
4296
|
-
legacy_relative = None
|
|
4297
|
-
if document_id.startswith("path::"):
|
|
4298
|
-
legacy_relative = document_id.split("::", 1)[1].lstrip("/")
|
|
4299
|
-
if legacy_relative and legacy_relative.startswith("literature/arxiv/"):
|
|
4300
|
-
path, writable, scope, source_kind = self._resolve_document(
|
|
4301
|
-
quest_root, f"questpath::{legacy_relative}"
|
|
4302
|
-
)
|
|
4303
|
-
else:
|
|
4304
|
-
raise
|
|
4525
|
+
path, writable, scope, source_kind = self.resolve_document(quest_id, document_id)
|
|
4305
4526
|
renderer_hint, mime_type = self._renderer_hint_for(path)
|
|
4306
4527
|
is_text = self._is_text_document(path, mime_type, renderer_hint)
|
|
4307
4528
|
content = read_text(path) if is_text else ""
|
|
@@ -4329,6 +4550,24 @@ class QuestService:
|
|
|
4329
4550
|
},
|
|
4330
4551
|
}
|
|
4331
4552
|
|
|
4553
|
+
def resolve_document(self, quest_id: str, document_id: str) -> tuple[Path, bool, str, str]:
|
|
4554
|
+
quest_root = self._quest_root(quest_id)
|
|
4555
|
+
workspace_root = self.active_workspace_root(quest_root)
|
|
4556
|
+
resolution_root = self._document_resolution_root(
|
|
4557
|
+
quest_root=quest_root,
|
|
4558
|
+
workspace_root=workspace_root,
|
|
4559
|
+
document_id=document_id,
|
|
4560
|
+
)
|
|
4561
|
+
try:
|
|
4562
|
+
return self._resolve_document(resolution_root, document_id)
|
|
4563
|
+
except FileNotFoundError:
|
|
4564
|
+
legacy_relative = None
|
|
4565
|
+
if document_id.startswith("path::"):
|
|
4566
|
+
legacy_relative = document_id.split("::", 1)[1].lstrip("/")
|
|
4567
|
+
if legacy_relative and legacy_relative.startswith("literature/arxiv/"):
|
|
4568
|
+
return self._resolve_document(quest_root, f"questpath::{legacy_relative}")
|
|
4569
|
+
raise
|
|
4570
|
+
|
|
4332
4571
|
def save_document(self, quest_id: str, document_id: str, content: str, previous_revision: str | None = None) -> dict:
|
|
4333
4572
|
current = self.open_document(quest_id, document_id)
|
|
4334
4573
|
if not current.get("writable", False):
|
|
@@ -4828,10 +5067,11 @@ class QuestService:
|
|
|
4828
5067
|
if continuation_anchor is not _UNSET:
|
|
4829
5068
|
normalized_anchor = str(continuation_anchor or "").strip() or None
|
|
4830
5069
|
if normalized_anchor is not None:
|
|
4831
|
-
from ..prompts.builder import
|
|
5070
|
+
from ..prompts.builder import current_standard_skills
|
|
4832
5071
|
|
|
4833
|
-
|
|
4834
|
-
|
|
5072
|
+
available_stage_skills = current_standard_skills(repo_root())
|
|
5073
|
+
if normalized_anchor not in available_stage_skills:
|
|
5074
|
+
allowed = ", ".join(available_stage_skills)
|
|
4835
5075
|
raise ValueError(
|
|
4836
5076
|
f"Unsupported continuation anchor `{normalized_anchor}`. Allowed values: {allowed}."
|
|
4837
5077
|
)
|
|
@@ -5132,6 +5372,7 @@ class QuestService:
|
|
|
5132
5372
|
connector_hints: dict[str, Any] | None = None,
|
|
5133
5373
|
created_at: str | None = None,
|
|
5134
5374
|
counts_as_visible: bool = True,
|
|
5375
|
+
deliver_to_bound_conversations: bool | None = None,
|
|
5135
5376
|
) -> dict[str, Any]:
|
|
5136
5377
|
timestamp = created_at or utc_now()
|
|
5137
5378
|
payload = {
|
|
@@ -5148,6 +5389,11 @@ class QuestService:
|
|
|
5148
5389
|
"reply_mode": reply_mode,
|
|
5149
5390
|
"surface_actions": [dict(item) for item in (surface_actions or []) if isinstance(item, dict)],
|
|
5150
5391
|
"connector_hints": dict(connector_hints) if isinstance(connector_hints, dict) else {},
|
|
5392
|
+
"deliver_to_bound_conversations": (
|
|
5393
|
+
bool(deliver_to_bound_conversations)
|
|
5394
|
+
if deliver_to_bound_conversations is not None
|
|
5395
|
+
else None
|
|
5396
|
+
),
|
|
5151
5397
|
"created_at": timestamp,
|
|
5152
5398
|
}
|
|
5153
5399
|
append_jsonl(self._interaction_journal_path(quest_root), payload)
|
|
@@ -5397,6 +5643,12 @@ class QuestService:
|
|
|
5397
5643
|
"queued_message_count_after_delivery": len(queue_payload.get("pending") or []),
|
|
5398
5644
|
}
|
|
5399
5645
|
|
|
5646
|
+
@staticmethod
|
|
5647
|
+
def _document_resolution_root(quest_root: Path, workspace_root: Path, document_id: str) -> Path:
|
|
5648
|
+
if document_id.startswith(("questpath::", "memory::")):
|
|
5649
|
+
return quest_root
|
|
5650
|
+
return workspace_root
|
|
5651
|
+
|
|
5400
5652
|
@staticmethod
|
|
5401
5653
|
def _resolve_document(quest_root: Path, document_id: str) -> tuple[Path, bool, str, str]:
|
|
5402
5654
|
if document_id.startswith("memory::"):
|
|
@@ -234,10 +234,11 @@ class QuestStageViewBuilder:
|
|
|
234
234
|
|
|
235
235
|
def build(self) -> dict[str, Any]:
|
|
236
236
|
selection_type = str(self.selection.get("selection_type") or "").strip()
|
|
237
|
+
explicit_stage_key = str(self.selection.get("stage_key") or "").strip()
|
|
237
238
|
self.stage_key = self._resolve_effective_stage_key()
|
|
238
239
|
if selection_type == "idea_candidate":
|
|
239
240
|
return self._build_idea_candidate()
|
|
240
|
-
if selection_type == "branch_node" and
|
|
241
|
+
if selection_type == "branch_node" and not explicit_stage_key:
|
|
241
242
|
return self._build_branch()
|
|
242
243
|
if self.stage_key == "baseline":
|
|
243
244
|
return self._build_baseline()
|
|
@@ -1288,11 +1289,15 @@ class QuestStageViewBuilder:
|
|
|
1288
1289
|
for item in self.artifacts
|
|
1289
1290
|
if self._branch_matches(self._payload(item), allow_parent=True, include_unscoped=False)
|
|
1290
1291
|
]
|
|
1292
|
+
latest_branch_payload = self._payload(branch_items[-1] if branch_items else {})
|
|
1291
1293
|
note = (
|
|
1292
1294
|
str(
|
|
1293
1295
|
latest_experiment_payload.get("summary")
|
|
1294
1296
|
or latest_idea_payload.get("summary")
|
|
1295
1297
|
or latest_idea_payload.get("reason")
|
|
1298
|
+
or latest_branch_payload.get("summary")
|
|
1299
|
+
or latest_branch_payload.get("message")
|
|
1300
|
+
or latest_branch_payload.get("reason")
|
|
1296
1301
|
or self.trace.get("summary")
|
|
1297
1302
|
or self.selection.get("summary")
|
|
1298
1303
|
or ""
|