@researai/deepscientist 1.5.0 → 1.5.1
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/AGENTS.md +26 -0
- package/README.md +19 -179
- package/assets/connectors/lingzhu/openclaw-bridge/README.md +124 -0
- package/assets/connectors/lingzhu/openclaw-bridge/index.ts +162 -0
- package/assets/connectors/lingzhu/openclaw-bridge/openclaw.plugin.json +145 -0
- package/assets/connectors/lingzhu/openclaw-bridge/package.json +35 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/cli.ts +180 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/config.ts +196 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/debug-log.ts +111 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/events.ts +4 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/http-handler.ts +1133 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/image-cache.ts +75 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/lingzhu-tools.ts +246 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/transform.ts +541 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/types.ts +131 -0
- package/assets/connectors/lingzhu/openclaw-bridge/tsconfig.json +14 -0
- package/assets/connectors/lingzhu/openclaw.lingzhu.config.template.json +39 -0
- package/bin/ds.js +233 -53
- package/docs/en/00_QUICK_START.md +134 -0
- package/docs/en/01_SETTINGS_REFERENCE.md +1104 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +404 -0
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +325 -0
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +216 -0
- package/docs/en/05_TUI_GUIDE.md +141 -0
- package/docs/en/06_RUNTIME_AND_CANVAS.md +679 -0
- package/docs/en/07_MEMORY_AND_MCP.md +253 -0
- package/docs/en/08_FIGURE_STYLE_GUIDE.md +97 -0
- package/docs/en/09_DOCTOR.md +108 -0
- package/docs/en/90_ARCHITECTURE.md +245 -0
- package/docs/en/91_DEVELOPMENT.md +195 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +29 -0
- package/docs/zh/00_QUICK_START.md +134 -0
- package/docs/zh/01_SETTINGS_REFERENCE.md +1137 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +414 -0
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +324 -0
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +230 -0
- package/docs/zh/05_TUI_GUIDE.md +128 -0
- package/docs/zh/06_RUNTIME_AND_CANVAS.md +271 -0
- package/docs/zh/07_MEMORY_AND_MCP.md +235 -0
- package/docs/zh/08_FIGURE_STYLE_GUIDE.md +97 -0
- package/docs/zh/09_DOCTOR.md +112 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +29 -0
- package/install.sh +32 -8
- package/package.json +4 -2
- package/pyproject.toml +1 -1
- package/src/deepscientist/artifact/guidance.py +9 -2
- package/src/deepscientist/artifact/service.py +482 -22
- package/src/deepscientist/bash_exec/monitor.py +27 -5
- package/src/deepscientist/bash_exec/runtime.py +639 -0
- package/src/deepscientist/bash_exec/service.py +99 -16
- package/src/deepscientist/bridges/base.py +3 -0
- package/src/deepscientist/bridges/connectors.py +292 -13
- package/src/deepscientist/channels/qq.py +19 -2
- package/src/deepscientist/channels/relay.py +1 -0
- package/src/deepscientist/cli.py +32 -25
- package/src/deepscientist/config/models.py +28 -2
- package/src/deepscientist/config/service.py +201 -6
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +50 -5
- package/src/deepscientist/daemon/api/router.py +1 -0
- package/src/deepscientist/daemon/app.py +442 -15
- package/src/deepscientist/doctor.py +444 -0
- package/src/deepscientist/home.py +1 -0
- package/src/deepscientist/latex_runtime.py +17 -4
- package/src/deepscientist/lingzhu_support.py +182 -0
- package/src/deepscientist/mcp/server.py +49 -2
- package/src/deepscientist/prompts/builder.py +181 -58
- package/src/deepscientist/quest/layout.py +1 -0
- package/src/deepscientist/quest/service.py +63 -2
- package/src/deepscientist/quest/stage_views.py +19 -1
- package/src/deepscientist/runtime_tools/__init__.py +16 -0
- package/src/deepscientist/runtime_tools/builtins.py +19 -0
- package/src/deepscientist/runtime_tools/models.py +29 -0
- package/src/deepscientist/runtime_tools/registry.py +40 -0
- package/src/deepscientist/runtime_tools/service.py +59 -0
- package/src/deepscientist/runtime_tools/tinytex.py +25 -0
- package/src/deepscientist/tinytex.py +276 -0
- package/src/prompts/connectors/lingzhu.md +12 -0
- package/src/prompts/connectors/qq.md +121 -0
- package/src/prompts/system.md +177 -33
- package/src/skills/analysis-campaign/SKILL.md +22 -6
- package/src/skills/baseline/SKILL.md +5 -4
- package/src/skills/decision/SKILL.md +4 -3
- package/src/skills/experiment/SKILL.md +5 -4
- package/src/skills/finalize/SKILL.md +5 -4
- package/src/skills/idea/SKILL.md +5 -4
- package/src/skills/intake-audit/SKILL.md +277 -0
- package/src/skills/intake-audit/references/state-audit-template.md +41 -0
- package/src/skills/rebuttal/SKILL.md +407 -0
- package/src/skills/rebuttal/references/action-plan-template.md +63 -0
- package/src/skills/rebuttal/references/evidence-update-template.md +30 -0
- package/src/skills/rebuttal/references/response-letter-template.md +113 -0
- package/src/skills/rebuttal/references/review-matrix-template.md +55 -0
- package/src/skills/review/SKILL.md +293 -0
- package/src/skills/review/references/experiment-todo-template.md +29 -0
- package/src/skills/review/references/review-report-template.md +83 -0
- package/src/skills/review/references/revision-log-template.md +40 -0
- package/src/skills/scout/SKILL.md +5 -4
- package/src/skills/write/SKILL.md +7 -3
- package/src/tui/dist/components/WelcomePanel.js +17 -43
- package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -2
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-7v-dHngU.js → AiManusChatView-w5lF2Ttt.js} +109 -575
- package/src/ui/dist/assets/{AnalysisPlugin-B_Xmz-KE.js → AnalysisPlugin-DJOED79I.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-Cko-0tm1.js → AutoFigurePlugin-DaG61Y0M.js} +63 -8
- package/src/ui/dist/assets/{CliPlugin-BsU0ht7q.js → CliPlugin-CV4LqUB_.js} +43 -609
- package/src/ui/dist/assets/{CodeEditorPlugin-DcMMP0Rt.js → CodeEditorPlugin-DylfAea4.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-BqoQ5QyY.js → CodeViewerPlugin-F7saY0LM.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-D7eHNhU6.js → DocViewerPlugin-COP0c7jf.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-DLJN42T5.js → GitDiffViewerPlugin-CAS05pT9.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-gJMV7MOu.js → ImageViewerPlugin-Bco1CN_w.js} +5 -6
- package/src/ui/dist/assets/{LabCopilotPanel-B857sfxP.js → LabCopilotPanel-CvMlCD99.js} +12 -15
- package/src/ui/dist/assets/LabPlugin-BYankkE4.js +2676 -0
- package/src/ui/dist/assets/LabPlugin-D9jVIo0A.css +2698 -0
- package/src/ui/dist/assets/{LatexPlugin-DWKEo-Wj.js → LatexPlugin-LDSMR-t-.js} +16 -16
- package/src/ui/dist/assets/{MarkdownViewerPlugin-DBzoEmhv.js → MarkdownViewerPlugin-B7o80jgm.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DoHc-8vo.js → MarketplacePlugin-CM6ZOcpC.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-CKjKH-yS.js → NotebookEditor-Dc61cXmK.js} +3 -3
- package/src/ui/dist/assets/{PdfLoader-zFoL0VPo.js → PdfLoader-DWowuQwx.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DXPaL9Nt.js → PdfMarkdownPlugin-BsJM1q_a.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-DhK8qCFp.js → PdfViewerPlugin-DB2eEEFQ.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-CdSi6krf.js → SearchPlugin-CraThSvt.js} +1 -1
- package/src/ui/dist/assets/{Stepper-V-WiDQJl.js → Stepper-CgocRTPq.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-hIs1Efiu.js → TextViewerPlugin-B1JGhKtd.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-DG8b0q2X.js → VNCViewer-CclFC7FM.js} +9 -10
- package/src/ui/dist/assets/{bibtex-HDac6fVW.js → bibtex-D3IKsMl7.js} +1 -1
- package/src/ui/dist/assets/{code-BnBeNxBc.js → code-BP37Xx0p.js} +1 -1
- package/src/ui/dist/assets/{file-content-IRQ3jHb8.js → file-content-BAJSu-9r.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DZoQ9I6r.js → file-diff-panel-DUGeCTuy.js} +1 -1
- package/src/ui/dist/assets/{file-socket-BMCdLc-P.js → file-socket-CXc1Ojf7.js} +1 -1
- package/src/ui/dist/assets/{file-utils-CltILB3w.js → file-utils-2J21jt7M.js} +1 -1
- package/src/ui/dist/assets/{image-Boe6ffhu.js → image-CMMmgvcn.js} +1 -1
- package/src/ui/dist/assets/{index-BlplpvE1.js → index-BaVumsQT.js} +2 -2
- package/src/ui/dist/assets/{index-DZqJ-qAM.js → index-CWgMgpow.js} +60 -2154
- package/src/ui/dist/assets/{index-DO43pFZP.js → index-DmwmJmbW.js} +6372 -8434
- package/src/ui/dist/assets/{index-Bq2bvfkl.css → index-KGt-z-dD.css} +225 -2920
- package/src/ui/dist/assets/{index-2Zf65FZt.js → index-s7aHnNQ4.js} +1 -1
- package/src/ui/dist/assets/{message-square-mUHn_Ssb.js → message-square-CQRfX0Am.js} +1 -1
- package/src/ui/dist/assets/{monaco-fe0arNEU.js → monaco-B4TbdsrF.js} +1 -1
- package/src/ui/dist/assets/{popover-D_7i19qU.js → popover-B8Rokodk.js} +1 -1
- package/src/ui/dist/assets/{project-sync-DyVGrU7H.js → project-sync-D_i96KH4.js} +2 -8
- package/src/ui/dist/assets/{sigma-BzazRyxQ.js → sigma-D12PnzCN.js} +1 -1
- package/src/ui/dist/assets/{tooltip-DN_yjHFH.js → tooltip-B6YrI4aJ.js} +1 -1
- package/src/ui/dist/assets/trash-Bc8jGp0V.js +32 -0
- package/src/ui/dist/assets/{useCliAccess-DV2L2Qxy.js → useCliAccess-mXVCYSZ-.js} +12 -42
- package/src/ui/dist/assets/{useFileDiffOverlay-DyTj-p_V.js → useFileDiffOverlay-Bg6b9H9K.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-ozYHtUwq.js → wrap-text-Drh5GEnL.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-BN9MUyCQ.js → zoom-out-CJj9DZLn.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/assets/fonts/Inter-Variable.ttf +0 -0
- package/assets/fonts/NotoSerifSC-Regular-C94HN_ZN.ttf +0 -0
- package/assets/fonts/NunitoSans-Variable.ttf +0 -0
- package/assets/fonts/Satoshi-Medium-ByP-Zb-9.woff2 +0 -0
- package/assets/fonts/SourceSans3-Variable.ttf +0 -0
- package/assets/fonts/ds-fonts.css +0 -83
- package/src/ui/dist/assets/Inter-Variable-VF2RPR_K.ttf +0 -0
- package/src/ui/dist/assets/LabPlugin-bL7rpic8.js +0 -43
- package/src/ui/dist/assets/NotoSerifSC-Regular-C94HN_ZN-C94HN_ZN.ttf +0 -0
- package/src/ui/dist/assets/NunitoSans-Variable-B_ZymHAd.ttf +0 -0
- package/src/ui/dist/assets/Satoshi-Medium-ByP-Zb-9-GkA34YXu.woff2 +0 -0
- package/src/ui/dist/assets/SourceSans3-Variable-CD-WOsSK.ttf +0 -0
- package/src/ui/dist/assets/info-CcsK_htA.js +0 -18
- package/src/ui/dist/assets/user-plus-BusDx-hF.js +0 -79
|
@@ -422,6 +422,45 @@ class ArtifactService:
|
|
|
422
422
|
def _normalize_string_list(values: list[object] | None) -> list[str]:
|
|
423
423
|
return [str(item).strip() for item in (values or []) if str(item).strip()]
|
|
424
424
|
|
|
425
|
+
def _normalize_campaign_origin(self, payload: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
426
|
+
if not isinstance(payload, dict):
|
|
427
|
+
return None
|
|
428
|
+
origin_kind = str(payload.get("kind") or "analysis").strip().lower() or "analysis"
|
|
429
|
+
normalized = {
|
|
430
|
+
"kind": origin_kind,
|
|
431
|
+
"reason": str(payload.get("reason") or "").strip() or None,
|
|
432
|
+
"source_artifact_id": str(payload.get("source_artifact_id") or "").strip() or None,
|
|
433
|
+
"source_outline_ref": str(payload.get("source_outline_ref") or "").strip() or None,
|
|
434
|
+
"source_review_round": str(payload.get("source_review_round") or "").strip() or None,
|
|
435
|
+
"reviewer_item_ids": self._normalize_string_list(payload.get("reviewer_item_ids")),
|
|
436
|
+
}
|
|
437
|
+
if not any(value for key, value in normalized.items() if key != "kind"):
|
|
438
|
+
normalized["reason"] = None
|
|
439
|
+
return normalized
|
|
440
|
+
|
|
441
|
+
def _normalize_campaign_todo_items(self, todo_items: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
|
|
442
|
+
normalized_items: list[dict[str, Any]] = []
|
|
443
|
+
for raw in todo_items or []:
|
|
444
|
+
if not isinstance(raw, dict):
|
|
445
|
+
continue
|
|
446
|
+
normalized_items.append(
|
|
447
|
+
{
|
|
448
|
+
"todo_id": str(raw.get("todo_id") or raw.get("slice_id") or "").strip() or None,
|
|
449
|
+
"slice_id": str(raw.get("slice_id") or "").strip() or None,
|
|
450
|
+
"title": str(raw.get("title") or "").strip() or None,
|
|
451
|
+
"status": str(raw.get("status") or "pending").strip() or "pending",
|
|
452
|
+
"research_question": str(raw.get("research_question") or "").strip() or None,
|
|
453
|
+
"experimental_design": str(raw.get("experimental_design") or "").strip() or None,
|
|
454
|
+
"completion_condition": str(raw.get("completion_condition") or "").strip() or None,
|
|
455
|
+
"why_now": str(raw.get("why_now") or "").strip() or None,
|
|
456
|
+
"success_criteria": str(raw.get("success_criteria") or "").strip() or None,
|
|
457
|
+
"abandonment_criteria": str(raw.get("abandonment_criteria") or "").strip() or None,
|
|
458
|
+
"reviewer_item_ids": self._normalize_string_list(raw.get("reviewer_item_ids")),
|
|
459
|
+
"manuscript_targets": self._normalize_string_list(raw.get("manuscript_targets")),
|
|
460
|
+
}
|
|
461
|
+
)
|
|
462
|
+
return normalized_items
|
|
463
|
+
|
|
425
464
|
def _normalize_paper_outline_record(
|
|
426
465
|
self,
|
|
427
466
|
*,
|
|
@@ -1073,6 +1112,80 @@ class ArtifactService:
|
|
|
1073
1112
|
]
|
|
1074
1113
|
return candidates[-1] if candidates else None
|
|
1075
1114
|
|
|
1115
|
+
def _latest_branch_idea_id(self, quest_root: Path, branch_name: str) -> str | None:
|
|
1116
|
+
normalized_branch = str(branch_name or "").strip()
|
|
1117
|
+
if not normalized_branch:
|
|
1118
|
+
return None
|
|
1119
|
+
latest_idea = self._latest_idea_for_branch(quest_root, normalized_branch)
|
|
1120
|
+
if isinstance(latest_idea, dict):
|
|
1121
|
+
candidate = str(latest_idea.get("idea_id") or "").strip()
|
|
1122
|
+
if candidate:
|
|
1123
|
+
return candidate
|
|
1124
|
+
latest_main_run = self._latest_main_run_for_branch(quest_root, normalized_branch)
|
|
1125
|
+
if isinstance(latest_main_run, dict):
|
|
1126
|
+
candidate = str(latest_main_run.get("idea_id") or "").strip()
|
|
1127
|
+
if candidate:
|
|
1128
|
+
return candidate
|
|
1129
|
+
latest_match: tuple[str, int, str] | None = None
|
|
1130
|
+
latest_candidate: str | None = None
|
|
1131
|
+
for item in self.quest_service._collect_artifacts(quest_root):
|
|
1132
|
+
payload = dict(item.get("payload") or {}) if isinstance(item.get("payload"), dict) else {}
|
|
1133
|
+
if not payload:
|
|
1134
|
+
continue
|
|
1135
|
+
if str(payload.get("branch") or "").strip() != normalized_branch:
|
|
1136
|
+
continue
|
|
1137
|
+
candidate = str(payload.get("idea_id") or "").strip()
|
|
1138
|
+
if not candidate:
|
|
1139
|
+
continue
|
|
1140
|
+
artifact_path = str(item.get("path") or "")
|
|
1141
|
+
try:
|
|
1142
|
+
artifact_mtime_ns = Path(artifact_path).stat().st_mtime_ns if artifact_path else 0
|
|
1143
|
+
except OSError:
|
|
1144
|
+
artifact_mtime_ns = 0
|
|
1145
|
+
sort_key = (
|
|
1146
|
+
str(payload.get("updated_at") or payload.get("created_at") or ""),
|
|
1147
|
+
artifact_mtime_ns,
|
|
1148
|
+
artifact_path,
|
|
1149
|
+
)
|
|
1150
|
+
if latest_match is None or sort_key > latest_match:
|
|
1151
|
+
latest_match = sort_key
|
|
1152
|
+
latest_candidate = candidate
|
|
1153
|
+
if latest_match is not None and latest_candidate:
|
|
1154
|
+
return latest_candidate
|
|
1155
|
+
return None
|
|
1156
|
+
|
|
1157
|
+
def _resolve_analysis_parent_context(
|
|
1158
|
+
self,
|
|
1159
|
+
quest_root: Path,
|
|
1160
|
+
*,
|
|
1161
|
+
state: dict[str, Any],
|
|
1162
|
+
) -> tuple[str, Path, str | None]:
|
|
1163
|
+
current_root_raw = str(state.get("current_workspace_root") or "").strip()
|
|
1164
|
+
head_root_raw = str(state.get("research_head_worktree_root") or "").strip()
|
|
1165
|
+
parent_worktree_root: Path | None = None
|
|
1166
|
+
for raw in (current_root_raw, head_root_raw):
|
|
1167
|
+
if not raw:
|
|
1168
|
+
continue
|
|
1169
|
+
candidate = Path(raw)
|
|
1170
|
+
if candidate.exists():
|
|
1171
|
+
parent_worktree_root = candidate
|
|
1172
|
+
break
|
|
1173
|
+
if parent_worktree_root is None:
|
|
1174
|
+
parent_worktree_root = self._workspace_root_for(quest_root)
|
|
1175
|
+
|
|
1176
|
+
parent_branch = (
|
|
1177
|
+
str(state.get("current_workspace_branch") or "").strip()
|
|
1178
|
+
or str(state.get("research_head_branch") or "").strip()
|
|
1179
|
+
or current_branch(parent_worktree_root)
|
|
1180
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
1181
|
+
)
|
|
1182
|
+
parent_branch = str(parent_branch or "").strip()
|
|
1183
|
+
if not parent_branch:
|
|
1184
|
+
raise ValueError("Unable to resolve a parent branch for the analysis campaign.")
|
|
1185
|
+
|
|
1186
|
+
idea_id = self._latest_branch_idea_id(quest_root, parent_branch) or str(state.get("active_idea_id") or "").strip() or None
|
|
1187
|
+
return parent_branch, parent_worktree_root, idea_id
|
|
1188
|
+
|
|
1076
1189
|
def _idea_parent_branch(self, record: dict[str, Any] | None) -> str | None:
|
|
1077
1190
|
if not isinstance(record, dict) or not record:
|
|
1078
1191
|
return None
|
|
@@ -1357,6 +1470,111 @@ class ArtifactService:
|
|
|
1357
1470
|
"branches": branches,
|
|
1358
1471
|
}
|
|
1359
1472
|
|
|
1473
|
+
def resolve_runtime_refs(self, quest_root: Path) -> dict[str, Any]:
|
|
1474
|
+
state = self.quest_service.read_research_state(quest_root)
|
|
1475
|
+
snapshot = self.quest_service.snapshot(self._quest_id(quest_root))
|
|
1476
|
+
active_campaign_id = str(state.get("active_analysis_campaign_id") or "").strip() or None
|
|
1477
|
+
analysis_parent_branch = str(state.get("analysis_parent_branch") or "").strip() or None
|
|
1478
|
+
current_workspace_branch = str(state.get("current_workspace_branch") or "").strip() or None
|
|
1479
|
+
research_head_branch = str(state.get("research_head_branch") or "").strip() or None
|
|
1480
|
+
canonical_branch = analysis_parent_branch or current_workspace_branch or research_head_branch
|
|
1481
|
+
latest_main_run = self._latest_main_run_for_branch(quest_root, canonical_branch or "")
|
|
1482
|
+
selected_outline = read_json(self._paper_selected_outline_path(quest_root), {})
|
|
1483
|
+
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
1484
|
+
active_campaign = (
|
|
1485
|
+
self._read_analysis_manifest(quest_root, active_campaign_id)
|
|
1486
|
+
if active_campaign_id
|
|
1487
|
+
else {}
|
|
1488
|
+
)
|
|
1489
|
+
active_campaign = active_campaign if isinstance(active_campaign, dict) else {}
|
|
1490
|
+
latest_paths = (
|
|
1491
|
+
dict(latest_main_run.get("paths") or {})
|
|
1492
|
+
if isinstance(latest_main_run, dict) and isinstance(latest_main_run.get("paths"), dict)
|
|
1493
|
+
else {}
|
|
1494
|
+
)
|
|
1495
|
+
return {
|
|
1496
|
+
"ok": True,
|
|
1497
|
+
"active_idea_id": str(state.get("active_idea_id") or "").strip() or None,
|
|
1498
|
+
"research_head_branch": research_head_branch,
|
|
1499
|
+
"research_head_worktree_root": str(state.get("research_head_worktree_root") or "").strip() or None,
|
|
1500
|
+
"current_workspace_branch": current_workspace_branch,
|
|
1501
|
+
"current_workspace_root": str(state.get("current_workspace_root") or "").strip() or None,
|
|
1502
|
+
"analysis_parent_branch": analysis_parent_branch,
|
|
1503
|
+
"analysis_parent_worktree_root": str(state.get("analysis_parent_worktree_root") or "").strip() or None,
|
|
1504
|
+
"current_canonical_branch": canonical_branch,
|
|
1505
|
+
"active_analysis_campaign_id": active_campaign_id,
|
|
1506
|
+
"active_campaign_title": str(active_campaign.get("title") or "").strip() or None,
|
|
1507
|
+
"next_pending_slice_id": str(state.get("next_pending_slice_id") or "").strip() or None,
|
|
1508
|
+
"latest_main_run_id": str((latest_main_run or {}).get("run_id") or "").strip() or None,
|
|
1509
|
+
"latest_main_run_branch": str((latest_main_run or {}).get("branch") or "").strip() or None,
|
|
1510
|
+
"latest_main_result_json": str(latest_paths.get("result_json") or "").strip() or None,
|
|
1511
|
+
"selected_outline_ref": str(selected_outline.get("outline_id") or "").strip() or None,
|
|
1512
|
+
"default_reply_interaction_id": str(snapshot.get("default_reply_interaction_id") or "").strip() or None,
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
def get_analysis_campaign(self, quest_root: Path, campaign_id: str | None = None) -> dict[str, Any]:
|
|
1516
|
+
resolved_campaign_id = str(campaign_id or "").strip()
|
|
1517
|
+
if not resolved_campaign_id or resolved_campaign_id == "active":
|
|
1518
|
+
state = self.quest_service.read_research_state(quest_root)
|
|
1519
|
+
resolved_campaign_id = str(state.get("active_analysis_campaign_id") or "").strip()
|
|
1520
|
+
if not resolved_campaign_id:
|
|
1521
|
+
raise ValueError("No active analysis campaign is available.")
|
|
1522
|
+
manifest = self._read_analysis_manifest(quest_root, resolved_campaign_id)
|
|
1523
|
+
slices = [dict(item) for item in (manifest.get("slices") or []) if isinstance(item, dict)]
|
|
1524
|
+
pending_slices = [item for item in slices if str(item.get("status") or "pending").strip() == "pending"]
|
|
1525
|
+
completed_slices = [item for item in slices if str(item.get("status") or "").strip() != "pending"]
|
|
1526
|
+
next_pending_slice = pending_slices[0] if pending_slices else None
|
|
1527
|
+
return {
|
|
1528
|
+
"ok": True,
|
|
1529
|
+
"campaign_id": resolved_campaign_id,
|
|
1530
|
+
"title": str(manifest.get("title") or "").strip() or None,
|
|
1531
|
+
"goal": str(manifest.get("goal") or "").strip() or None,
|
|
1532
|
+
"active_idea_id": str(manifest.get("active_idea_id") or "").strip() or None,
|
|
1533
|
+
"parent_run_id": str(manifest.get("parent_run_id") or "").strip() or None,
|
|
1534
|
+
"parent_branch": str(manifest.get("parent_branch") or "").strip() or None,
|
|
1535
|
+
"parent_worktree_root": str(manifest.get("parent_worktree_root") or "").strip() or None,
|
|
1536
|
+
"selected_outline_ref": str(manifest.get("selected_outline_ref") or "").strip() or None,
|
|
1537
|
+
"campaign_origin": dict(manifest.get("campaign_origin") or {}) if isinstance(manifest.get("campaign_origin"), dict) else None,
|
|
1538
|
+
"todo_items": [dict(item) for item in (manifest.get("todo_items") or []) if isinstance(item, dict)],
|
|
1539
|
+
"slices": slices,
|
|
1540
|
+
"next_pending_slice_id": str((next_pending_slice or {}).get("slice_id") or "").strip() or None,
|
|
1541
|
+
"pending_slice_count": len(pending_slices),
|
|
1542
|
+
"completed_slice_count": len(completed_slices),
|
|
1543
|
+
"manifest": manifest,
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
def list_paper_outlines(self, quest_root: Path) -> dict[str, Any]:
|
|
1547
|
+
selected_outline = read_json(self._paper_selected_outline_path(quest_root), {})
|
|
1548
|
+
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
1549
|
+
outlines: list[dict[str, Any]] = []
|
|
1550
|
+
for status, root in (
|
|
1551
|
+
("candidate", self._paper_outline_candidates_root(quest_root)),
|
|
1552
|
+
("revised", self._paper_outline_revisions_root(quest_root)),
|
|
1553
|
+
):
|
|
1554
|
+
for path in sorted(root.glob("outline-*.json")):
|
|
1555
|
+
record = read_json(path, {})
|
|
1556
|
+
if not isinstance(record, dict) or not record:
|
|
1557
|
+
continue
|
|
1558
|
+
outline_id = str(record.get("outline_id") or path.stem).strip() or path.stem
|
|
1559
|
+
outlines.append(
|
|
1560
|
+
{
|
|
1561
|
+
"outline_id": outline_id,
|
|
1562
|
+
"title": str(record.get("title") or outline_id).strip() or outline_id,
|
|
1563
|
+
"status": str(record.get("status") or status).strip() or status,
|
|
1564
|
+
"review_result": str(record.get("review_result") or "").strip() or None,
|
|
1565
|
+
"path": str(path),
|
|
1566
|
+
"is_selected": outline_id == str(selected_outline.get("outline_id") or "").strip(),
|
|
1567
|
+
}
|
|
1568
|
+
)
|
|
1569
|
+
outlines.sort(key=lambda item: (str(item.get("outline_id") or ""), str(item.get("status") or "")))
|
|
1570
|
+
return {
|
|
1571
|
+
"ok": True,
|
|
1572
|
+
"selected_outline_ref": str(selected_outline.get("outline_id") or "").strip() or None,
|
|
1573
|
+
"selected_outline": selected_outline or None,
|
|
1574
|
+
"count": len(outlines),
|
|
1575
|
+
"outlines": outlines,
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1360
1578
|
def _previous_primary_best(
|
|
1361
1579
|
self,
|
|
1362
1580
|
quest_root: Path,
|
|
@@ -2508,6 +2726,7 @@ class ArtifactService:
|
|
|
2508
2726
|
campaign_goal: str,
|
|
2509
2727
|
parent_run_id: str | None = None,
|
|
2510
2728
|
slices: list[dict[str, Any]],
|
|
2729
|
+
campaign_origin: dict[str, Any] | None = None,
|
|
2511
2730
|
selected_outline_ref: str | None = None,
|
|
2512
2731
|
research_questions: list[str] | None = None,
|
|
2513
2732
|
experimental_designs: list[str] | None = None,
|
|
@@ -2515,20 +2734,23 @@ class ArtifactService:
|
|
|
2515
2734
|
) -> dict[str, Any]:
|
|
2516
2735
|
self._require_baseline_gate_open(quest_root, action="create_analysis_campaign")
|
|
2517
2736
|
state = self.quest_service.read_research_state(quest_root)
|
|
2518
|
-
|
|
2737
|
+
parent_branch, parent_worktree_root, resolved_idea_id = self._resolve_analysis_parent_context(
|
|
2738
|
+
quest_root,
|
|
2739
|
+
state=state,
|
|
2740
|
+
)
|
|
2741
|
+
active_idea_id = str(resolved_idea_id or "").strip()
|
|
2519
2742
|
if not active_idea_id:
|
|
2520
2743
|
raise ValueError("An active idea is required before starting an analysis campaign.")
|
|
2521
2744
|
if not slices:
|
|
2522
2745
|
raise ValueError("At least one analysis slice is required.")
|
|
2523
|
-
parent_branch = str(state.get("research_head_branch") or current_branch(self._workspace_root_for(quest_root))).strip()
|
|
2524
|
-
parent_worktree_root = Path(str(state.get("research_head_worktree_root") or self._workspace_root_for(quest_root)))
|
|
2525
2746
|
campaign_id = generate_id("analysis")
|
|
2526
2747
|
charter_dir = ensure_dir(parent_worktree_root / "experiments" / "analysis-results" / campaign_id)
|
|
2527
2748
|
charter_path = charter_dir / "campaign.md"
|
|
2749
|
+
normalized_campaign_origin = self._normalize_campaign_origin(campaign_origin)
|
|
2528
2750
|
resolved_outline_ref = str(selected_outline_ref or "").strip() or None
|
|
2529
2751
|
normalized_research_questions = self._normalize_string_list(research_questions)
|
|
2530
2752
|
normalized_experimental_designs = self._normalize_string_list(experimental_designs)
|
|
2531
|
-
normalized_todo_items =
|
|
2753
|
+
normalized_todo_items = self._normalize_campaign_todo_items(todo_items)
|
|
2532
2754
|
slice_contexts: list[dict[str, Any]] = []
|
|
2533
2755
|
for index, raw in enumerate(slices, start=1):
|
|
2534
2756
|
slice_id = str(raw.get("slice_id") or generate_id("slice")).strip()
|
|
@@ -2550,6 +2772,17 @@ class ArtifactService:
|
|
|
2550
2772
|
worktree_root=worktree_root,
|
|
2551
2773
|
start_point=parent_branch,
|
|
2552
2774
|
)
|
|
2775
|
+
reviewer_item_ids = self._normalize_string_list(
|
|
2776
|
+
raw.get("reviewer_item_ids") or matched_todo.get("reviewer_item_ids")
|
|
2777
|
+
)
|
|
2778
|
+
manuscript_targets = self._normalize_string_list(
|
|
2779
|
+
raw.get("manuscript_targets") or matched_todo.get("manuscript_targets")
|
|
2780
|
+
)
|
|
2781
|
+
why_now = str(raw.get("why_now") or matched_todo.get("why_now") or "").strip()
|
|
2782
|
+
success_criteria = str(raw.get("success_criteria") or matched_todo.get("success_criteria") or "").strip()
|
|
2783
|
+
abandonment_criteria = str(
|
|
2784
|
+
raw.get("abandonment_criteria") or matched_todo.get("abandonment_criteria") or ""
|
|
2785
|
+
).strip()
|
|
2553
2786
|
plan_dir = ensure_dir(worktree_root / "experiments" / "analysis" / campaign_id / slice_id)
|
|
2554
2787
|
plan_path = plan_dir / "plan.md"
|
|
2555
2788
|
requirement_lines = [
|
|
@@ -2567,6 +2800,10 @@ class ArtifactService:
|
|
|
2567
2800
|
"",
|
|
2568
2801
|
str(raw.get("experimental_design") or matched_todo.get("experimental_design") or "").strip() or "TBD",
|
|
2569
2802
|
"",
|
|
2803
|
+
"## Why Now",
|
|
2804
|
+
"",
|
|
2805
|
+
why_now or "TBD",
|
|
2806
|
+
"",
|
|
2570
2807
|
"## Hypothesis",
|
|
2571
2808
|
"",
|
|
2572
2809
|
str(raw.get("hypothesis") or "").strip() or "TBD",
|
|
@@ -2587,6 +2824,14 @@ class ArtifactService:
|
|
|
2587
2824
|
"",
|
|
2588
2825
|
str(raw.get("must_not_simplify") or "").strip() or "Full dataset / full protocol only unless explicitly approved.",
|
|
2589
2826
|
"",
|
|
2827
|
+
"## Success Criteria",
|
|
2828
|
+
"",
|
|
2829
|
+
success_criteria or "TBD",
|
|
2830
|
+
"",
|
|
2831
|
+
"## Abandonment Criteria",
|
|
2832
|
+
"",
|
|
2833
|
+
abandonment_criteria or "TBD",
|
|
2834
|
+
"",
|
|
2590
2835
|
"## Completion Condition",
|
|
2591
2836
|
"",
|
|
2592
2837
|
str(raw.get("completion_condition") or matched_todo.get("completion_condition") or "").strip()
|
|
@@ -2594,6 +2839,17 @@ class ArtifactService:
|
|
|
2594
2839
|
or "Complete the planned analysis slice and mirror the durable result back to the parent branch.",
|
|
2595
2840
|
"",
|
|
2596
2841
|
]
|
|
2842
|
+
requirement_lines.extend(["## Reviewer Item IDs", ""])
|
|
2843
|
+
if reviewer_item_ids:
|
|
2844
|
+
requirement_lines.extend([f"- `{item}`" for item in reviewer_item_ids])
|
|
2845
|
+
else:
|
|
2846
|
+
requirement_lines.append("- None recorded.")
|
|
2847
|
+
requirement_lines.extend(["", "## Manuscript Targets", ""])
|
|
2848
|
+
if manuscript_targets:
|
|
2849
|
+
requirement_lines.extend([f"- {item}" for item in manuscript_targets])
|
|
2850
|
+
else:
|
|
2851
|
+
requirement_lines.append("- None recorded.")
|
|
2852
|
+
requirement_lines.append("")
|
|
2597
2853
|
write_text(plan_path, "\n".join(requirement_lines))
|
|
2598
2854
|
slice_contexts.append(
|
|
2599
2855
|
{
|
|
@@ -2612,20 +2868,26 @@ class ArtifactService:
|
|
|
2612
2868
|
"experimental_design": str(
|
|
2613
2869
|
raw.get("experimental_design") or matched_todo.get("experimental_design") or ""
|
|
2614
2870
|
).strip(),
|
|
2871
|
+
"why_now": why_now,
|
|
2615
2872
|
"hypothesis": str(raw.get("hypothesis") or "").strip(),
|
|
2616
2873
|
"required_changes": str(raw.get("required_changes") or "").strip(),
|
|
2617
2874
|
"metric_contract": str(raw.get("metric_contract") or "").strip(),
|
|
2618
2875
|
"environment_notes": str(raw.get("environment_notes") or "").strip(),
|
|
2619
2876
|
"must_not_simplify": str(raw.get("must_not_simplify") or "").strip(),
|
|
2877
|
+
"success_criteria": success_criteria,
|
|
2878
|
+
"abandonment_criteria": abandonment_criteria,
|
|
2620
2879
|
"completion_condition": str(
|
|
2621
2880
|
raw.get("completion_condition") or matched_todo.get("completion_condition") or ""
|
|
2622
2881
|
).strip(),
|
|
2882
|
+
"reviewer_item_ids": reviewer_item_ids,
|
|
2883
|
+
"manuscript_targets": manuscript_targets,
|
|
2623
2884
|
}
|
|
2624
2885
|
)
|
|
2625
2886
|
|
|
2626
2887
|
todo_manifest = {
|
|
2627
2888
|
"schema_version": 1,
|
|
2628
2889
|
"campaign_id": campaign_id,
|
|
2890
|
+
"campaign_origin": normalized_campaign_origin,
|
|
2629
2891
|
"selected_outline_ref": resolved_outline_ref,
|
|
2630
2892
|
"research_questions": normalized_research_questions,
|
|
2631
2893
|
"experimental_designs": normalized_experimental_designs,
|
|
@@ -2635,9 +2897,14 @@ class ArtifactService:
|
|
|
2635
2897
|
"slice_id": context["slice_id"],
|
|
2636
2898
|
"title": str(item.get("title") or context["title"]).strip() or context["title"],
|
|
2637
2899
|
"status": str(item.get("status") or "pending").strip() or "pending",
|
|
2638
|
-
"research_question":
|
|
2639
|
-
"experimental_design":
|
|
2640
|
-
"completion_condition":
|
|
2900
|
+
"research_question": item.get("research_question") or context.get("research_question"),
|
|
2901
|
+
"experimental_design": item.get("experimental_design") or context.get("experimental_design"),
|
|
2902
|
+
"completion_condition": item.get("completion_condition") or context.get("completion_condition") or context.get("must_not_simplify"),
|
|
2903
|
+
"why_now": item.get("why_now") or context.get("why_now"),
|
|
2904
|
+
"success_criteria": item.get("success_criteria") or context.get("success_criteria"),
|
|
2905
|
+
"abandonment_criteria": item.get("abandonment_criteria") or context.get("abandonment_criteria"),
|
|
2906
|
+
"reviewer_item_ids": item.get("reviewer_item_ids") or context.get("reviewer_item_ids") or [],
|
|
2907
|
+
"manuscript_targets": item.get("manuscript_targets") or context.get("manuscript_targets") or [],
|
|
2641
2908
|
}
|
|
2642
2909
|
for context, item in zip(slice_contexts, normalized_todo_items + [{}] * max(0, len(slice_contexts) - len(normalized_todo_items)))
|
|
2643
2910
|
],
|
|
@@ -2666,6 +2933,14 @@ class ArtifactService:
|
|
|
2666
2933
|
"",
|
|
2667
2934
|
f"`{resolved_outline_ref or 'none'}`",
|
|
2668
2935
|
"",
|
|
2936
|
+
"## Campaign Origin",
|
|
2937
|
+
"",
|
|
2938
|
+
f"- Kind: `{(normalized_campaign_origin or {}).get('kind') or 'analysis'}`",
|
|
2939
|
+
f"- Reason: {str((normalized_campaign_origin or {}).get('reason') or 'Not recorded')}",
|
|
2940
|
+
f"- Source Artifact: `{str((normalized_campaign_origin or {}).get('source_artifact_id') or 'none')}`",
|
|
2941
|
+
f"- Source Outline: `{str((normalized_campaign_origin or {}).get('source_outline_ref') or 'none')}`",
|
|
2942
|
+
f"- Source Review Round: `{str((normalized_campaign_origin or {}).get('source_review_round') or 'none')}`",
|
|
2943
|
+
"",
|
|
2669
2944
|
"## Slices",
|
|
2670
2945
|
"",
|
|
2671
2946
|
]
|
|
@@ -2681,8 +2956,13 @@ class ArtifactService:
|
|
|
2681
2956
|
f"- Goal: {item['goal'] or 'TBD'}",
|
|
2682
2957
|
f"- Research question: {item['research_question'] or 'TBD'}",
|
|
2683
2958
|
f"- Experimental design: {item['experimental_design'] or 'TBD'}",
|
|
2959
|
+
f"- Why now: {item['why_now'] or 'TBD'}",
|
|
2960
|
+
f"- Success criteria: {item['success_criteria'] or 'TBD'}",
|
|
2961
|
+
f"- Abandonment criteria: {item['abandonment_criteria'] or 'TBD'}",
|
|
2684
2962
|
f"- Completion condition: {item['completion_condition'] or item['must_not_simplify'] or 'TBD'}",
|
|
2685
2963
|
f"- Requirement: {item['must_not_simplify'] or 'TBD'}",
|
|
2964
|
+
f"- Reviewer items: {', '.join(item['reviewer_item_ids']) or 'none'}",
|
|
2965
|
+
f"- Manuscript targets: {', '.join(item['manuscript_targets']) or 'none'}",
|
|
2686
2966
|
"",
|
|
2687
2967
|
]
|
|
2688
2968
|
)
|
|
@@ -2697,6 +2977,7 @@ class ArtifactService:
|
|
|
2697
2977
|
"active_idea_id": active_idea_id,
|
|
2698
2978
|
"parent_branch": parent_branch,
|
|
2699
2979
|
"parent_worktree_root": str(parent_worktree_root),
|
|
2980
|
+
"campaign_origin": normalized_campaign_origin,
|
|
2700
2981
|
"selected_outline_ref": resolved_outline_ref,
|
|
2701
2982
|
"research_questions": normalized_research_questions,
|
|
2702
2983
|
"experimental_designs": normalized_experimental_designs,
|
|
@@ -2707,6 +2988,44 @@ class ArtifactService:
|
|
|
2707
2988
|
"created_at": utc_now(),
|
|
2708
2989
|
},
|
|
2709
2990
|
)
|
|
2991
|
+
for item in slice_contexts:
|
|
2992
|
+
self.record(
|
|
2993
|
+
quest_root,
|
|
2994
|
+
{
|
|
2995
|
+
"kind": "milestone",
|
|
2996
|
+
"status": "prepared",
|
|
2997
|
+
"summary": f"Analysis slice `{item['slice_id']}` prepared as a child branch.",
|
|
2998
|
+
"reason": "Expose the pending follow-up branch durably so Canvas and Git lineage stay visible before execution.",
|
|
2999
|
+
"idea_id": active_idea_id,
|
|
3000
|
+
"campaign_id": campaign_id,
|
|
3001
|
+
"slice_id": item["slice_id"],
|
|
3002
|
+
"branch": item["branch"],
|
|
3003
|
+
"parent_branch": parent_branch,
|
|
3004
|
+
"worktree_root": item["worktree_root"],
|
|
3005
|
+
"worktree_rel_path": self._workspace_relative(quest_root, Path(item["worktree_root"])),
|
|
3006
|
+
"flow_type": "analysis_slice",
|
|
3007
|
+
"protocol_step": "prepare",
|
|
3008
|
+
"paths": {
|
|
3009
|
+
"plan_md": item["plan_path"],
|
|
3010
|
+
},
|
|
3011
|
+
"details": {
|
|
3012
|
+
"title": item["title"],
|
|
3013
|
+
"goal": item["goal"],
|
|
3014
|
+
"run_kind": item["run_kind"],
|
|
3015
|
+
"research_question": item["research_question"],
|
|
3016
|
+
"experimental_design": item["experimental_design"],
|
|
3017
|
+
"why_now": item["why_now"],
|
|
3018
|
+
"completion_condition": item["completion_condition"] or item["must_not_simplify"],
|
|
3019
|
+
"must_not_simplify": item["must_not_simplify"],
|
|
3020
|
+
"success_criteria": item["success_criteria"],
|
|
3021
|
+
"abandonment_criteria": item["abandonment_criteria"],
|
|
3022
|
+
"reviewer_item_ids": item["reviewer_item_ids"],
|
|
3023
|
+
"manuscript_targets": item["manuscript_targets"],
|
|
3024
|
+
},
|
|
3025
|
+
},
|
|
3026
|
+
checkpoint=False,
|
|
3027
|
+
workspace_root=Path(item["worktree_root"]),
|
|
3028
|
+
)
|
|
2710
3029
|
first_slice = slice_contexts[0]
|
|
2711
3030
|
artifact = self.record(
|
|
2712
3031
|
quest_root,
|
|
@@ -2729,6 +3048,7 @@ class ArtifactService:
|
|
|
2729
3048
|
"campaign_title": campaign_title,
|
|
2730
3049
|
"campaign_goal": campaign_goal,
|
|
2731
3050
|
"parent_run_id": parent_run_id,
|
|
3051
|
+
"campaign_origin": normalized_campaign_origin,
|
|
2732
3052
|
"selected_outline_ref": resolved_outline_ref,
|
|
2733
3053
|
"todo_manifest_path": str(todo_manifest_path),
|
|
2734
3054
|
"slice_count": len(slice_contexts),
|
|
@@ -2742,8 +3062,13 @@ class ArtifactService:
|
|
|
2742
3062
|
"goal": item["goal"],
|
|
2743
3063
|
"research_question": item["research_question"],
|
|
2744
3064
|
"experimental_design": item["experimental_design"],
|
|
3065
|
+
"why_now": item["why_now"],
|
|
2745
3066
|
"completion_condition": item["completion_condition"] or item["must_not_simplify"],
|
|
2746
3067
|
"must_not_simplify": item["must_not_simplify"],
|
|
3068
|
+
"success_criteria": item["success_criteria"],
|
|
3069
|
+
"abandonment_criteria": item["abandonment_criteria"],
|
|
3070
|
+
"reviewer_item_ids": item["reviewer_item_ids"],
|
|
3071
|
+
"manuscript_targets": item["manuscript_targets"],
|
|
2747
3072
|
}
|
|
2748
3073
|
for item in slice_contexts
|
|
2749
3074
|
],
|
|
@@ -2754,6 +3079,7 @@ class ArtifactService:
|
|
|
2754
3079
|
)
|
|
2755
3080
|
research_state = self.quest_service.update_research_state(
|
|
2756
3081
|
quest_root,
|
|
3082
|
+
active_idea_id=active_idea_id,
|
|
2757
3083
|
active_analysis_campaign_id=campaign_id,
|
|
2758
3084
|
analysis_parent_branch=parent_branch,
|
|
2759
3085
|
analysis_parent_worktree_root=str(parent_worktree_root),
|
|
@@ -2785,16 +3111,17 @@ class ArtifactService:
|
|
|
2785
3111
|
attachments=[
|
|
2786
3112
|
{
|
|
2787
3113
|
"kind": "analysis_campaign",
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3114
|
+
"campaign_id": campaign_id,
|
|
3115
|
+
"parent_branch": parent_branch,
|
|
3116
|
+
"parent_worktree_root": str(parent_worktree_root),
|
|
3117
|
+
"campaign_origin": normalized_campaign_origin,
|
|
3118
|
+
"selected_outline_ref": resolved_outline_ref,
|
|
3119
|
+
"todo_manifest_path": str(todo_manifest_path),
|
|
3120
|
+
"next_slice": first_slice,
|
|
3121
|
+
"todo_items": todo_manifest["todo_items"],
|
|
3122
|
+
"slices": slice_contexts,
|
|
3123
|
+
}
|
|
3124
|
+
],
|
|
2798
3125
|
)
|
|
2799
3126
|
return {
|
|
2800
3127
|
"ok": True,
|
|
@@ -2807,6 +3134,7 @@ class ArtifactService:
|
|
|
2807
3134
|
"campaign_id": campaign_id,
|
|
2808
3135
|
"parent_branch": parent_branch,
|
|
2809
3136
|
"parent_worktree_root": str(parent_worktree_root),
|
|
3137
|
+
"campaign_origin": normalized_campaign_origin,
|
|
2810
3138
|
"charter_path": str(charter_path),
|
|
2811
3139
|
"slices": slice_contexts,
|
|
2812
3140
|
"manifest": manifest,
|
|
@@ -3060,6 +3388,10 @@ class ArtifactService:
|
|
|
3060
3388
|
evidence_paths: list[str] | None = None,
|
|
3061
3389
|
metric_rows: list[dict[str, Any]] | None = None,
|
|
3062
3390
|
deviations: list[str] | None = None,
|
|
3391
|
+
claim_impact: str | None = None,
|
|
3392
|
+
reviewer_resolution: str | None = None,
|
|
3393
|
+
manuscript_update_hint: str | None = None,
|
|
3394
|
+
next_recommendation: str | None = None,
|
|
3063
3395
|
dataset_scope: str = "full",
|
|
3064
3396
|
subset_approval_ref: str | None = None,
|
|
3065
3397
|
) -> dict[str, Any]:
|
|
@@ -3076,6 +3408,10 @@ class ArtifactService:
|
|
|
3076
3408
|
evidence_paths = [str(item).strip() for item in (evidence_paths or []) if str(item).strip()]
|
|
3077
3409
|
deviations = [str(item).strip() for item in (deviations or []) if str(item).strip()]
|
|
3078
3410
|
metric_rows = [item for item in (metric_rows or []) if isinstance(item, dict)]
|
|
3411
|
+
normalized_claim_impact = str(claim_impact or "").strip() or None
|
|
3412
|
+
normalized_reviewer_resolution = str(reviewer_resolution or "").strip() or None
|
|
3413
|
+
normalized_manuscript_update_hint = str(manuscript_update_hint or "").strip() or None
|
|
3414
|
+
normalized_next_recommendation = str(next_recommendation or "").strip() or None
|
|
3079
3415
|
slice_worktree_root = Path(str(target.get("worktree_root") or ""))
|
|
3080
3416
|
parent_worktree_root = Path(str(manifest.get("parent_worktree_root") or ""))
|
|
3081
3417
|
parent_branch = str(manifest.get("parent_branch") or "")
|
|
@@ -3104,6 +3440,22 @@ class ArtifactService:
|
|
|
3104
3440
|
"",
|
|
3105
3441
|
results.strip() or "TBD",
|
|
3106
3442
|
"",
|
|
3443
|
+
"## Claim Impact",
|
|
3444
|
+
"",
|
|
3445
|
+
normalized_claim_impact or "Not recorded.",
|
|
3446
|
+
"",
|
|
3447
|
+
"## Reviewer Resolution",
|
|
3448
|
+
"",
|
|
3449
|
+
normalized_reviewer_resolution or "Not recorded.",
|
|
3450
|
+
"",
|
|
3451
|
+
"## Manuscript Update Hint",
|
|
3452
|
+
"",
|
|
3453
|
+
normalized_manuscript_update_hint or "Not recorded.",
|
|
3454
|
+
"",
|
|
3455
|
+
"## Next Recommendation",
|
|
3456
|
+
"",
|
|
3457
|
+
normalized_next_recommendation or "Not recorded.",
|
|
3458
|
+
"",
|
|
3107
3459
|
"## Deviations",
|
|
3108
3460
|
"",
|
|
3109
3461
|
]
|
|
@@ -3164,6 +3516,14 @@ class ArtifactService:
|
|
|
3164
3516
|
"",
|
|
3165
3517
|
results.strip() or "TBD",
|
|
3166
3518
|
"",
|
|
3519
|
+
"## Claim Impact",
|
|
3520
|
+
"",
|
|
3521
|
+
normalized_claim_impact or "Not recorded.",
|
|
3522
|
+
"",
|
|
3523
|
+
"## Manuscript Update Hint",
|
|
3524
|
+
"",
|
|
3525
|
+
normalized_manuscript_update_hint or "Not recorded.",
|
|
3526
|
+
"",
|
|
3167
3527
|
]
|
|
3168
3528
|
write_text(mirror_path, "\n".join(mirror_lines).rstrip() + "\n")
|
|
3169
3529
|
|
|
@@ -3197,6 +3557,10 @@ class ArtifactService:
|
|
|
3197
3557
|
"dataset_scope": normalized_scope,
|
|
3198
3558
|
"subset_approval_ref": subset_approval_ref,
|
|
3199
3559
|
"metric_rows": metric_rows,
|
|
3560
|
+
"claim_impact": normalized_claim_impact,
|
|
3561
|
+
"reviewer_resolution": normalized_reviewer_resolution,
|
|
3562
|
+
"manuscript_update_hint": normalized_manuscript_update_hint,
|
|
3563
|
+
"next_recommendation": normalized_next_recommendation,
|
|
3200
3564
|
"deviations": deviations,
|
|
3201
3565
|
"evidence_paths": evidence_paths,
|
|
3202
3566
|
},
|
|
@@ -3223,6 +3587,10 @@ class ArtifactService:
|
|
|
3223
3587
|
updated["completed_at"] = utc_now()
|
|
3224
3588
|
updated["result_path"] = str(result_path)
|
|
3225
3589
|
updated["mirror_path"] = str(mirror_path)
|
|
3590
|
+
updated["claim_impact"] = normalized_claim_impact
|
|
3591
|
+
updated["reviewer_resolution"] = normalized_reviewer_resolution
|
|
3592
|
+
updated["manuscript_update_hint"] = normalized_manuscript_update_hint
|
|
3593
|
+
updated["next_recommendation"] = normalized_next_recommendation
|
|
3226
3594
|
updated_slices.append(updated)
|
|
3227
3595
|
next_slice = next((item for item in updated_slices if str(item.get("status") or "") == "pending"), None)
|
|
3228
3596
|
manifest = self._write_analysis_manifest(
|
|
@@ -3336,12 +3704,14 @@ class ArtifactService:
|
|
|
3336
3704
|
parent_worktree_root,
|
|
3337
3705
|
message=f"analysis: summarize {campaign_id}",
|
|
3338
3706
|
)
|
|
3707
|
+
restored_idea_id = self._latest_branch_idea_id(quest_root, parent_branch) or str(manifest.get("active_idea_id") or "").strip() or None
|
|
3339
3708
|
research_state = self.quest_service.update_research_state(
|
|
3340
3709
|
quest_root,
|
|
3710
|
+
active_idea_id=restored_idea_id,
|
|
3341
3711
|
active_analysis_campaign_id=None,
|
|
3342
3712
|
next_pending_slice_id=None,
|
|
3343
|
-
current_workspace_branch=
|
|
3344
|
-
current_workspace_root=
|
|
3713
|
+
current_workspace_branch=parent_branch,
|
|
3714
|
+
current_workspace_root=str(parent_worktree_root),
|
|
3345
3715
|
workspace_mode="idea",
|
|
3346
3716
|
last_flow_type="analysis_campaign_complete",
|
|
3347
3717
|
)
|
|
@@ -3789,6 +4159,8 @@ class ArtifactService:
|
|
|
3789
4159
|
expects_reply: bool | None = None,
|
|
3790
4160
|
reply_mode: str | None = None,
|
|
3791
4161
|
options: list[dict[str, Any]] | None = None,
|
|
4162
|
+
surface_actions: list[dict[str, Any]] | None = None,
|
|
4163
|
+
connector_hints: dict[str, Any] | None = None,
|
|
3792
4164
|
allow_free_text: bool = True,
|
|
3793
4165
|
reply_schema: dict[str, Any] | None = None,
|
|
3794
4166
|
reply_to_interaction_id: str | None = None,
|
|
@@ -3801,6 +4173,9 @@ class ArtifactService:
|
|
|
3801
4173
|
"approval_result": "approval",
|
|
3802
4174
|
}.get(kind, "progress")
|
|
3803
4175
|
options_resolved = options or []
|
|
4176
|
+
surface_actions_resolved = [dict(item) for item in (surface_actions or []) if isinstance(item, dict)]
|
|
4177
|
+
connector_hints_resolved = self._normalize_connector_hints(connector_hints)
|
|
4178
|
+
attachments_resolved, attachment_issues = self._normalize_interaction_attachments(quest_root, attachments)
|
|
3804
4179
|
reply_schema_resolved = reply_schema if isinstance(reply_schema, dict) else {}
|
|
3805
4180
|
reply_mode_resolved = str(
|
|
3806
4181
|
reply_mode
|
|
@@ -3860,10 +4235,14 @@ class ArtifactService:
|
|
|
3860
4235
|
"expects_reply": False,
|
|
3861
4236
|
"reply_mode": "none",
|
|
3862
4237
|
"delivered": False,
|
|
4238
|
+
"delivery_results": [],
|
|
3863
4239
|
"response_phase": response_phase,
|
|
3864
4240
|
"delivery_targets": [],
|
|
3865
4241
|
"delivery_policy": self._delivery_policy(self._connectors_config()),
|
|
3866
4242
|
"preferred_connector": self._preferred_connector(self._connectors_config()),
|
|
4243
|
+
"connector_hints": connector_hints_resolved,
|
|
4244
|
+
"normalized_attachments": attachments_resolved,
|
|
4245
|
+
"attachment_issues": attachment_issues,
|
|
3867
4246
|
"recent_inbound_messages": mailbox_payload.get("recent_inbound_messages") or [],
|
|
3868
4247
|
"delivery_batch": mailbox_payload.get("delivery_batch"),
|
|
3869
4248
|
"recent_interaction_records": mailbox_payload.get("recent_interaction_records") or [],
|
|
@@ -3889,11 +4268,13 @@ class ArtifactService:
|
|
|
3889
4268
|
"summary": message,
|
|
3890
4269
|
"interaction_phase": "request" if kind == "decision_request" else response_phase,
|
|
3891
4270
|
"importance": importance,
|
|
3892
|
-
"attachments":
|
|
4271
|
+
"attachments": attachments_resolved,
|
|
3893
4272
|
"interaction_id": resolved_interaction_id,
|
|
3894
4273
|
"expects_reply": expects_reply_resolved,
|
|
3895
4274
|
"reply_mode": reply_mode_resolved,
|
|
3896
4275
|
"options": options_resolved,
|
|
4276
|
+
"surface_actions": surface_actions_resolved,
|
|
4277
|
+
"connector_hints": connector_hints_resolved,
|
|
3897
4278
|
"allow_free_text": allow_free_text,
|
|
3898
4279
|
"reply_schema": reply_schema_resolved,
|
|
3899
4280
|
"reply_to_interaction_id": reply_to_interaction_id,
|
|
@@ -3929,6 +4310,7 @@ class ArtifactService:
|
|
|
3929
4310
|
)
|
|
3930
4311
|
delivery_targets: list[str] = []
|
|
3931
4312
|
delivered = False
|
|
4313
|
+
delivery_results: list[dict[str, Any]] = []
|
|
3932
4314
|
if deliver_to_bound_conversations:
|
|
3933
4315
|
connectors = self._connectors_config()
|
|
3934
4316
|
targets = self._select_delivery_targets(
|
|
@@ -3950,12 +4332,17 @@ class ArtifactService:
|
|
|
3950
4332
|
"expects_reply": expects_reply_resolved,
|
|
3951
4333
|
"reply_mode": reply_mode_resolved,
|
|
3952
4334
|
"options": options_resolved,
|
|
4335
|
+
"surface_actions": surface_actions_resolved,
|
|
4336
|
+
"connector_hints": connector_hints_resolved,
|
|
3953
4337
|
"allow_free_text": allow_free_text,
|
|
3954
4338
|
"reply_schema": reply_schema_resolved,
|
|
3955
4339
|
"reply_to_interaction_id": reply_to_interaction_id,
|
|
3956
|
-
"attachments":
|
|
4340
|
+
"attachments": attachments_resolved,
|
|
3957
4341
|
}
|
|
3958
|
-
|
|
4342
|
+
delivery_result = self._deliver_to_channel(channel_name, payload, connectors=connectors)
|
|
4343
|
+
delivery_result["conversation_id"] = target
|
|
4344
|
+
delivery_results.append(delivery_result)
|
|
4345
|
+
if delivery_result.get("ok", False) or delivery_result.get("queued", False):
|
|
3959
4346
|
delivery_targets.append(target)
|
|
3960
4347
|
delivered = True
|
|
3961
4348
|
|
|
@@ -3985,6 +4372,8 @@ class ArtifactService:
|
|
|
3985
4372
|
message=message,
|
|
3986
4373
|
response_phase=response_phase,
|
|
3987
4374
|
reply_mode=reply_mode_resolved,
|
|
4375
|
+
surface_actions=surface_actions_resolved,
|
|
4376
|
+
connector_hints=connector_hints_resolved,
|
|
3988
4377
|
created_at=(artifact.get("record") or {}).get("updated_at"),
|
|
3989
4378
|
)
|
|
3990
4379
|
|
|
@@ -3994,7 +4383,12 @@ class ArtifactService:
|
|
|
3994
4383
|
"interaction_id": request_state.get("interaction_id"),
|
|
3995
4384
|
"expects_reply": expects_reply_resolved,
|
|
3996
4385
|
"reply_mode": reply_mode_resolved,
|
|
4386
|
+
"surface_actions": surface_actions_resolved,
|
|
4387
|
+
"connector_hints": connector_hints_resolved,
|
|
4388
|
+
"normalized_attachments": attachments_resolved,
|
|
4389
|
+
"attachment_issues": attachment_issues,
|
|
3997
4390
|
"delivered": delivered,
|
|
4391
|
+
"delivery_results": delivery_results,
|
|
3998
4392
|
"response_phase": response_phase,
|
|
3999
4393
|
"delivery_targets": delivery_targets,
|
|
4000
4394
|
"delivery_policy": self._delivery_policy(self._connectors_config()),
|
|
@@ -4296,6 +4690,67 @@ class ArtifactService:
|
|
|
4296
4690
|
return enabled[0]
|
|
4297
4691
|
return None
|
|
4298
4692
|
|
|
4693
|
+
@staticmethod
|
|
4694
|
+
def _normalize_connector_hints(connector_hints: dict[str, Any] | None) -> dict[str, Any]:
|
|
4695
|
+
if not isinstance(connector_hints, dict):
|
|
4696
|
+
return {}
|
|
4697
|
+
normalized: dict[str, Any] = {}
|
|
4698
|
+
for key, value in connector_hints.items():
|
|
4699
|
+
name = str(key or "").strip().lower()
|
|
4700
|
+
if not name or not isinstance(value, dict):
|
|
4701
|
+
continue
|
|
4702
|
+
normalized[name] = dict(value)
|
|
4703
|
+
return normalized
|
|
4704
|
+
|
|
4705
|
+
def _normalize_interaction_attachments(
|
|
4706
|
+
self,
|
|
4707
|
+
quest_root: Path,
|
|
4708
|
+
attachments: list[dict[str, Any]] | None,
|
|
4709
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
4710
|
+
normalized: list[dict[str, Any]] = []
|
|
4711
|
+
issues: list[dict[str, Any]] = []
|
|
4712
|
+
for index, raw_item in enumerate(attachments or [], start=1):
|
|
4713
|
+
if not isinstance(raw_item, dict):
|
|
4714
|
+
issues.append(
|
|
4715
|
+
{
|
|
4716
|
+
"attachment_index": index,
|
|
4717
|
+
"error": "attachment must be an object",
|
|
4718
|
+
}
|
|
4719
|
+
)
|
|
4720
|
+
continue
|
|
4721
|
+
item = dict(raw_item)
|
|
4722
|
+
path_value = str(item.get("path") or "").strip()
|
|
4723
|
+
if path_value:
|
|
4724
|
+
resolved_path = Path(path_value).expanduser()
|
|
4725
|
+
if not resolved_path.is_absolute():
|
|
4726
|
+
resolved_path = (quest_root / resolved_path).resolve()
|
|
4727
|
+
else:
|
|
4728
|
+
resolved_path = resolved_path.resolve()
|
|
4729
|
+
item["path"] = str(resolved_path)
|
|
4730
|
+
if not resolved_path.exists():
|
|
4731
|
+
item["path_error"] = "path_not_found"
|
|
4732
|
+
issues.append(
|
|
4733
|
+
{
|
|
4734
|
+
"attachment_index": index,
|
|
4735
|
+
"path": str(resolved_path),
|
|
4736
|
+
"error": "attachment path does not exist",
|
|
4737
|
+
}
|
|
4738
|
+
)
|
|
4739
|
+
connector_delivery = item.get("connector_delivery")
|
|
4740
|
+
if isinstance(connector_delivery, dict):
|
|
4741
|
+
normalized_delivery: dict[str, Any] = {}
|
|
4742
|
+
for key, value in connector_delivery.items():
|
|
4743
|
+
name = str(key or "").strip().lower()
|
|
4744
|
+
if not name or not isinstance(value, dict):
|
|
4745
|
+
continue
|
|
4746
|
+
normalized_delivery[name] = dict(value)
|
|
4747
|
+
if normalized_delivery:
|
|
4748
|
+
item["connector_delivery"] = normalized_delivery
|
|
4749
|
+
else:
|
|
4750
|
+
item.pop("connector_delivery", None)
|
|
4751
|
+
normalized.append(item)
|
|
4752
|
+
return normalized, issues
|
|
4753
|
+
|
|
4299
4754
|
def _select_delivery_targets(self, targets: list[str], *, connectors: dict[str, Any]) -> list[str]:
|
|
4300
4755
|
if not targets:
|
|
4301
4756
|
return ["local:default"]
|
|
@@ -4445,6 +4900,11 @@ class ArtifactService:
|
|
|
4445
4900
|
"transport": transport,
|
|
4446
4901
|
"response_phase": str(payload.get("response_phase") or "").strip() or None,
|
|
4447
4902
|
"importance": str(payload.get("importance") or "").strip() or None,
|
|
4903
|
+
"artifact_id": str(payload.get("artifact_id") or "").strip() or None,
|
|
4904
|
+
"interaction_id": str(payload.get("interaction_id") or "").strip() or None,
|
|
4905
|
+
"surface_actions": payload.get("surface_actions") if isinstance(payload.get("surface_actions"), list) else [],
|
|
4906
|
+
"connector_hints": payload.get("connector_hints") if isinstance(payload.get("connector_hints"), dict) else {},
|
|
4907
|
+
"delivery_parts": delivery.get("parts") if isinstance(delivery.get("parts"), list) else [],
|
|
4448
4908
|
"error": str(result.get("error") or delivery.get("error") or "").strip() or None,
|
|
4449
4909
|
"created_at": utc_now(),
|
|
4450
4910
|
},
|