@researai/deepscientist 1.5.7 → 1.5.9
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/LICENSE +186 -21
- package/README.md +8 -4
- package/bin/ds.js +224 -9
- package/docs/en/00_QUICK_START.md +2 -2
- package/docs/en/07_MEMORY_AND_MCP.md +40 -3
- package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
- package/docs/zh/00_QUICK_START.md +2 -2
- package/docs/zh/07_MEMORY_AND_MCP.md +40 -3
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +1 -0
- package/install.sh +34 -0
- package/package.json +2 -2
- package/pyproject.toml +2 -2
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/acp/envelope.py +1 -0
- package/src/deepscientist/artifact/metrics.py +814 -83
- package/src/deepscientist/artifact/schemas.py +1 -0
- package/src/deepscientist/artifact/service.py +2001 -229
- package/src/deepscientist/bash_exec/monitor.py +1 -1
- package/src/deepscientist/bash_exec/service.py +17 -9
- package/src/deepscientist/channels/qq.py +17 -0
- package/src/deepscientist/channels/relay.py +16 -0
- package/src/deepscientist/config/models.py +6 -0
- package/src/deepscientist/config/service.py +70 -2
- package/src/deepscientist/daemon/api/handlers.py +414 -14
- package/src/deepscientist/daemon/api/router.py +4 -0
- package/src/deepscientist/daemon/app.py +292 -21
- package/src/deepscientist/gitops/diff.py +6 -10
- package/src/deepscientist/mcp/server.py +191 -40
- package/src/deepscientist/prompts/builder.py +65 -19
- package/src/deepscientist/quest/node_traces.py +129 -2
- package/src/deepscientist/quest/service.py +140 -34
- package/src/deepscientist/quest/stage_views.py +175 -33
- package/src/deepscientist/registries/baseline.py +56 -4
- package/src/deepscientist/runners/codex.py +1 -1
- package/src/prompts/connectors/qq.md +1 -1
- package/src/prompts/contracts/shared_interaction.md +14 -0
- package/src/prompts/system.md +113 -32
- package/src/skills/analysis-campaign/SKILL.md +10 -14
- package/src/skills/baseline/SKILL.md +51 -38
- package/src/skills/baseline/references/baseline-plan-template.md +2 -0
- package/src/skills/decision/SKILL.md +12 -8
- package/src/skills/experiment/SKILL.md +28 -16
- package/src/skills/experiment/references/main-experiment-plan-template.md +2 -0
- package/src/skills/figure-polish/SKILL.md +1 -0
- package/src/skills/finalize/SKILL.md +3 -8
- package/src/skills/idea/SKILL.md +18 -8
- package/src/skills/idea/references/literature-survey-template.md +24 -0
- package/src/skills/idea/references/related-work-playbook.md +4 -0
- package/src/skills/idea/references/selection-gate.md +9 -0
- package/src/skills/intake-audit/SKILL.md +2 -8
- package/src/skills/rebuttal/SKILL.md +2 -8
- package/src/skills/review/SKILL.md +2 -8
- package/src/skills/scout/SKILL.md +2 -8
- package/src/skills/write/SKILL.md +53 -17
- package/src/skills/write/templates/DEEPSCIENTIST_NOTES.md +21 -0
- package/src/skills/write/templates/README.md +408 -0
- package/src/skills/write/templates/UPSTREAM_LICENSE.txt +21 -0
- package/src/skills/write/templates/aaai2026/README.md +534 -0
- package/src/skills/write/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
- package/src/skills/write/templates/aaai2026/aaai2026-unified-template.tex +952 -0
- package/src/skills/write/templates/aaai2026/aaai2026.bib +111 -0
- package/src/skills/write/templates/aaai2026/aaai2026.bst +1493 -0
- package/src/skills/write/templates/aaai2026/aaai2026.sty +315 -0
- package/src/skills/write/templates/acl/README.md +50 -0
- package/src/skills/write/templates/acl/acl.sty +312 -0
- package/src/skills/write/templates/acl/acl_latex.tex +377 -0
- package/src/skills/write/templates/acl/acl_lualatex.tex +101 -0
- package/src/skills/write/templates/acl/acl_natbib.bst +1940 -0
- package/src/skills/write/templates/acl/anthology.bib.txt +26 -0
- package/src/skills/write/templates/acl/custom.bib +70 -0
- package/src/skills/write/templates/acl/formatting.md +326 -0
- package/src/skills/write/templates/asplos2027/main.tex +459 -0
- package/src/skills/write/templates/asplos2027/references.bib +135 -0
- package/src/skills/write/templates/colm2025/README.md +3 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.bib +11 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.bst +1440 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.sty +218 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.tex +305 -0
- package/src/skills/write/templates/colm2025/fancyhdr.sty +485 -0
- package/src/skills/write/templates/colm2025/math_commands.tex +508 -0
- package/src/skills/write/templates/colm2025/natbib.sty +1246 -0
- package/src/skills/write/templates/iclr2026/fancyhdr.sty +485 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.bib +24 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.bst +1440 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.sty +246 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.tex +414 -0
- package/src/skills/write/templates/iclr2026/math_commands.tex +508 -0
- package/src/skills/write/templates/iclr2026/natbib.sty +1246 -0
- package/src/skills/write/templates/icml2026/algorithm.sty +79 -0
- package/src/skills/write/templates/icml2026/algorithmic.sty +201 -0
- package/src/skills/write/templates/icml2026/example_paper.bib +75 -0
- package/src/skills/write/templates/icml2026/example_paper.tex +662 -0
- package/src/skills/write/templates/icml2026/fancyhdr.sty +864 -0
- package/src/skills/write/templates/icml2026/icml2026.bst +1443 -0
- package/src/skills/write/templates/icml2026/icml2026.sty +767 -0
- package/src/skills/write/templates/neurips2025/Makefile +36 -0
- package/src/skills/write/templates/neurips2025/extra_pkgs.tex +53 -0
- package/src/skills/write/templates/neurips2025/main.tex +38 -0
- package/src/skills/write/templates/neurips2025/neurips.sty +382 -0
- package/src/skills/write/templates/nsdi2027/main.tex +426 -0
- package/src/skills/write/templates/nsdi2027/references.bib +151 -0
- package/src/skills/write/templates/nsdi2027/usenix-2020-09.sty +83 -0
- package/src/skills/write/templates/osdi2026/main.tex +429 -0
- package/src/skills/write/templates/osdi2026/references.bib +150 -0
- package/src/skills/write/templates/osdi2026/usenix-2020-09.sty +83 -0
- package/src/skills/write/templates/sosp2026/main.tex +532 -0
- package/src/skills/write/templates/sosp2026/references.bib +148 -0
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-BS3V4ZOk.js → AiManusChatView-BKZ103sn.js} +110 -14
- package/src/ui/dist/assets/{AnalysisPlugin-DLPXQsmr.js → AnalysisPlugin-mTTzGAlK.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-C-Fr9knQ.js → AutoFigurePlugin-C_wWw4AP.js} +5 -5
- package/src/ui/dist/assets/{CliPlugin-Dd8AHzFg.js → CliPlugin-BH58n3GY.js} +9 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-Dg-RepTl.js → CodeEditorPlugin-BKGRUH7e.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-D2J_3nyt.js → CodeViewerPlugin-BMADwFWJ.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-ChRLLKNb.js → DocViewerPlugin-ZOnTIHLN.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-DgHfcved.js → GitDiffViewerPlugin-CQ7h1Djm.js} +830 -86
- package/src/ui/dist/assets/{ImageViewerPlugin-C89GZMBy.js → ImageViewerPlugin-GVS5MsnC.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-BUfIwUcb.js → LabCopilotPanel-BZNv1JML.js} +10 -10
- package/src/ui/dist/assets/{LabPlugin-zvUmQUMq.js → LabPlugin-TWcJsdQA.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-C1SSNuWp.js → LatexPlugin-DIjHiR2x.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-D2Mf5tU5.js → MarkdownViewerPlugin-D3ooGAH0.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-CF4LgiS2.js → MarketplacePlugin-DfVfE9hN.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-BM7Bgwlv.js → NotebookEditor-DDl0_Mc0.js} +1 -1
- package/src/ui/dist/assets/{index-Be0NAmh8.js → NotebookEditor-s8JhzuX1.js} +12 -155
- package/src/ui/dist/assets/{PdfLoader-Bc5qfD-Z.js → PdfLoader-C2Sf6SJM.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-sh1-IRcp.js → PdfMarkdownPlugin-CXFLoIsa.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-C_a7CpWG.js → PdfViewerPlugin-BYTmz2fK.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-L4z3HcLf.js → SearchPlugin-CjWBI1O9.js} +1 -1
- package/src/ui/dist/assets/{Stepper-Dk4aQ3fN.js → Stepper-B0Dd8CxK.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-BsNtlKVo.js → TextViewerPlugin-DdOBU3-S.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-BpeDcZ5_.js → VNCViewer-B8HGgLwQ.js} +9 -9
- package/src/ui/dist/assets/{bibtex-C4QI-bbj.js → bibtex-CKaefIN2.js} +1 -1
- package/src/ui/dist/assets/{code-DuMINRsg.js → code-BWAY76JP.js} +1 -1
- package/src/ui/dist/assets/{file-content-C3N-432K.js → file-content-C1NwU5oQ.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-CffQ4ZMg.js → file-diff-panel-CywslwB9.js} +1 -1
- package/src/ui/dist/assets/{file-socket-CRH59PCO.js → file-socket-B4kzuOBQ.js} +1 -1
- package/src/ui/dist/assets/{file-utils-vYGtW2mI.js → file-utils-H2fjA46S.js} +1 -1
- package/src/ui/dist/assets/{image-DBVGaooo.js → image-D-NZM-6P.js} +1 -1
- package/src/ui/dist/assets/{index-B1P6hQRJ.js → index-7Chr1g9c.js} +3734 -1862
- package/src/ui/dist/assets/{index-DjSFDmgB.js → index-BdM1Gqfr.js} +2 -2
- package/src/ui/dist/assets/{index-BpjYH9Vg.js → index-CDxNdQdz.js} +1 -1
- package/src/ui/dist/assets/{index-Do9N28uB.css → index-DGIYDuTv.css} +163 -34
- package/src/ui/dist/assets/index-DHZJ_0TI.js +159 -0
- package/src/ui/dist/assets/{message-square-BsPDBhiY.js → message-square-BzjLiXir.js} +1 -1
- package/src/ui/dist/assets/{monaco-BTkdPojV.js → monaco-Cb2uKKe6.js} +1 -1
- package/src/ui/dist/assets/{popover-cWjCk-vc.js → popover-Bg72DGgT.js} +1 -1
- package/src/ui/dist/assets/{project-sync-CXn530xb.js → project-sync-Ce_0BglY.js} +1 -1
- package/src/ui/dist/assets/{sigma-04Jr12jg.js → sigma-DPaACDrh.js} +1 -1
- package/src/ui/dist/assets/{tooltip-BdVDl0G5.js → tooltip-C_mA6R0w.js} +1 -1
- package/src/ui/dist/assets/{trash-CB_GlQyC.js → trash-BvTgE5__.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-BL932NwS.js → useCliAccess-CgPeMOwP.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-B2WK7Tvq.js → useFileDiffOverlay-xPhz7P5B.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-YC68g12z.js → wrap-text-C3Un3YQr.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-C0RJvFiJ.js → zoom-out-BgxLa0Ri.js} +1 -1
- package/src/ui/dist/index.html +5 -2
- /package/src/ui/dist/assets/{index-CccQYZjX.css → NotebookEditor-CccQYZjX.css} +0 -0
|
@@ -10,6 +10,7 @@ from ..channels import get_channel_factory, register_builtin_channels
|
|
|
10
10
|
from ..config import ConfigManager
|
|
11
11
|
from ..connector_runtime import conversation_identity_key, infer_connector_transport, normalize_conversation_id
|
|
12
12
|
from ..gitops import (
|
|
13
|
+
branch_exists,
|
|
13
14
|
canonical_worktree_root,
|
|
14
15
|
checkpoint_repo,
|
|
15
16
|
create_worktree,
|
|
@@ -42,13 +43,18 @@ from .guidance import build_guidance_for_record, guidance_summary
|
|
|
42
43
|
from .metrics import (
|
|
43
44
|
baseline_metric_lines,
|
|
44
45
|
build_metrics_timeline,
|
|
46
|
+
canonicalize_baseline_submission,
|
|
45
47
|
compare_with_baseline,
|
|
46
48
|
compute_progress_eval,
|
|
49
|
+
MetricContractValidationError,
|
|
47
50
|
normalize_metric_contract,
|
|
51
|
+
normalize_metric_direction,
|
|
48
52
|
normalize_metric_rows,
|
|
49
53
|
normalize_metrics_summary,
|
|
50
54
|
selected_baseline_metrics,
|
|
51
55
|
to_number,
|
|
56
|
+
validate_baseline_metric_contract_submission,
|
|
57
|
+
validate_main_experiment_against_baseline_contract,
|
|
52
58
|
)
|
|
53
59
|
from .schemas import ARTIFACT_DIRS, guidance_for_kind, validate_artifact_payload
|
|
54
60
|
|
|
@@ -91,6 +97,16 @@ class ArtifactService:
|
|
|
91
97
|
self.baselines = BaselineRegistry(home)
|
|
92
98
|
self.quest_service = QuestService(home)
|
|
93
99
|
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _notification_text(value: object, *, limit: int = 220) -> str | None:
|
|
102
|
+
text = str(value or "").strip()
|
|
103
|
+
if not text:
|
|
104
|
+
return None
|
|
105
|
+
text = re.sub(r"\s+", " ", text)
|
|
106
|
+
if len(text) <= limit:
|
|
107
|
+
return text
|
|
108
|
+
return text[: limit - 1].rstrip() + "…"
|
|
109
|
+
|
|
94
110
|
def _normalize_evaluation_summary(self, payload: dict[str, Any] | None) -> dict[str, str] | None:
|
|
95
111
|
if not isinstance(payload, dict):
|
|
96
112
|
return None
|
|
@@ -126,6 +142,418 @@ class ArtifactService:
|
|
|
126
142
|
lines = [f"- {label}: {normalized[key]}" for key, label in labels if normalized.get(key)]
|
|
127
143
|
return lines or ["- Not recorded."]
|
|
128
144
|
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _format_route_label(value: object) -> str | None:
|
|
147
|
+
normalized = str(value or "").strip().replace("_", " ").replace("-", " ")
|
|
148
|
+
if not normalized:
|
|
149
|
+
return None
|
|
150
|
+
return " ".join(part.capitalize() for part in normalized.split())
|
|
151
|
+
|
|
152
|
+
def _format_foundation_label(self, foundation_ref: dict[str, Any] | None, *, fallback: str | None = None) -> str:
|
|
153
|
+
payload = dict(foundation_ref or {})
|
|
154
|
+
label = self._notification_text(payload.get("label"))
|
|
155
|
+
if label:
|
|
156
|
+
return label
|
|
157
|
+
kind = self._notification_text(payload.get("kind"))
|
|
158
|
+
ref = self._notification_text(payload.get("ref"))
|
|
159
|
+
branch = self._notification_text(payload.get("branch"))
|
|
160
|
+
if kind and ref:
|
|
161
|
+
return f"{kind} {ref}"
|
|
162
|
+
if branch:
|
|
163
|
+
return branch
|
|
164
|
+
return fallback or "current head"
|
|
165
|
+
|
|
166
|
+
def _build_idea_interaction_message(
|
|
167
|
+
self,
|
|
168
|
+
*,
|
|
169
|
+
action: str,
|
|
170
|
+
idea_id: str,
|
|
171
|
+
title: str | None,
|
|
172
|
+
problem: str | None,
|
|
173
|
+
hypothesis: str | None,
|
|
174
|
+
mechanism: str | None,
|
|
175
|
+
foundation_label: str | None,
|
|
176
|
+
branch_name: str,
|
|
177
|
+
next_target: str | None,
|
|
178
|
+
idea_md_rel_path: str | None,
|
|
179
|
+
draft_md_rel_path: str | None,
|
|
180
|
+
) -> str:
|
|
181
|
+
lead = "is now active" if action == "create" else "was revised"
|
|
182
|
+
lines = [f"Idea `{idea_id}` {lead} on branch `{branch_name}`."]
|
|
183
|
+
if self._notification_text(title):
|
|
184
|
+
lines.append(f"Title: {self._notification_text(title)}")
|
|
185
|
+
if self._notification_text(problem):
|
|
186
|
+
lines.append(f"Problem: {self._notification_text(problem)}")
|
|
187
|
+
if self._notification_text(hypothesis):
|
|
188
|
+
lines.append(f"Hypothesis: {self._notification_text(hypothesis)}")
|
|
189
|
+
if self._notification_text(mechanism):
|
|
190
|
+
lines.append(f"Mechanism: {self._notification_text(mechanism)}")
|
|
191
|
+
if foundation_label:
|
|
192
|
+
lines.append(f"Foundation: {foundation_label}")
|
|
193
|
+
if next_target:
|
|
194
|
+
lines.append(f"Next route: {self._format_route_label(next_target) or next_target}")
|
|
195
|
+
if idea_md_rel_path:
|
|
196
|
+
lines.append(f"Idea doc: `{idea_md_rel_path}`")
|
|
197
|
+
if draft_md_rel_path:
|
|
198
|
+
lines.append(f"Draft: `{draft_md_rel_path}`")
|
|
199
|
+
return "\n".join(lines)
|
|
200
|
+
|
|
201
|
+
def _build_main_experiment_interaction_message(
|
|
202
|
+
self,
|
|
203
|
+
*,
|
|
204
|
+
run_id: str,
|
|
205
|
+
branch_name: str,
|
|
206
|
+
verdict: str,
|
|
207
|
+
primary_metric_id: str | None,
|
|
208
|
+
primary_value: object,
|
|
209
|
+
primary_baseline: object,
|
|
210
|
+
primary_delta: object,
|
|
211
|
+
decimals: int | None,
|
|
212
|
+
conclusion: str | None,
|
|
213
|
+
evaluation_summary: dict[str, str] | None,
|
|
214
|
+
breakthrough_level: str | None,
|
|
215
|
+
recommended_next_route: str | None,
|
|
216
|
+
run_md_rel_path: str | None,
|
|
217
|
+
result_json_rel_path: str | None,
|
|
218
|
+
) -> str:
|
|
219
|
+
lines = [f"Main experiment `{run_id}` finished on branch `{branch_name}`."]
|
|
220
|
+
if primary_metric_id and primary_value is not None:
|
|
221
|
+
metric_text = f"{primary_metric_id}={self._format_metric_value(primary_value, decimals)}"
|
|
222
|
+
if primary_baseline is not None and primary_delta is not None:
|
|
223
|
+
metric_text += (
|
|
224
|
+
f", baseline={self._format_metric_value(primary_baseline, decimals)}, "
|
|
225
|
+
f"delta={self._format_metric_value(primary_delta, decimals)}"
|
|
226
|
+
)
|
|
227
|
+
lines.append(f"Metric: {metric_text}")
|
|
228
|
+
lines.append(f"Verdict: {self._format_route_label(verdict) or verdict}")
|
|
229
|
+
takeaway = (
|
|
230
|
+
self._notification_text((evaluation_summary or {}).get("takeaway"))
|
|
231
|
+
or self._notification_text(conclusion)
|
|
232
|
+
)
|
|
233
|
+
if takeaway:
|
|
234
|
+
lines.append(f"Takeaway: {takeaway}")
|
|
235
|
+
claim_update = self._notification_text((evaluation_summary or {}).get("claim_update"))
|
|
236
|
+
if claim_update:
|
|
237
|
+
lines.append(f"Claim update: {claim_update}")
|
|
238
|
+
if self._notification_text(breakthrough_level):
|
|
239
|
+
lines.append(f"Breakthrough level: {self._notification_text(breakthrough_level)}")
|
|
240
|
+
if recommended_next_route:
|
|
241
|
+
lines.append(
|
|
242
|
+
f"Recommended next route: {self._format_route_label(recommended_next_route) or recommended_next_route}"
|
|
243
|
+
)
|
|
244
|
+
if run_md_rel_path:
|
|
245
|
+
lines.append(f"Run log: `{run_md_rel_path}`")
|
|
246
|
+
if result_json_rel_path:
|
|
247
|
+
lines.append(f"Result: `{result_json_rel_path}`")
|
|
248
|
+
return "\n".join(lines)
|
|
249
|
+
|
|
250
|
+
def _build_outline_interaction_message(
|
|
251
|
+
self,
|
|
252
|
+
*,
|
|
253
|
+
action: str,
|
|
254
|
+
outline_id: str,
|
|
255
|
+
title: str | None,
|
|
256
|
+
selected_reason: str | None,
|
|
257
|
+
story: str | None,
|
|
258
|
+
research_questions: object,
|
|
259
|
+
experimental_designs: object,
|
|
260
|
+
selected_outline_rel_path: str | None,
|
|
261
|
+
outline_selection_rel_path: str | None,
|
|
262
|
+
revised_outline_rel_path: str | None = None,
|
|
263
|
+
) -> str:
|
|
264
|
+
verb = "selected" if action == "select" else "revised"
|
|
265
|
+
lines = [f"Paper outline `{outline_id}` was {verb} and promoted into the writing stage."]
|
|
266
|
+
if self._notification_text(title):
|
|
267
|
+
lines.append(f"Title: {self._notification_text(title)}")
|
|
268
|
+
if self._notification_text(selected_reason):
|
|
269
|
+
lines.append(f"Reason: {self._notification_text(selected_reason)}")
|
|
270
|
+
if self._notification_text(story):
|
|
271
|
+
lines.append(f"Story: {self._notification_text(story)}")
|
|
272
|
+
if research_questions:
|
|
273
|
+
lines.append(f"Research questions: {self._notification_text(research_questions)}")
|
|
274
|
+
if experimental_designs:
|
|
275
|
+
lines.append(f"Experimental designs: {self._notification_text(experimental_designs)}")
|
|
276
|
+
lines.append("Next route: Continue writing on the paper branch, or launch outline-bound analysis if evidence is still missing.")
|
|
277
|
+
if selected_outline_rel_path:
|
|
278
|
+
lines.append(f"Selected outline: `{selected_outline_rel_path}`")
|
|
279
|
+
if outline_selection_rel_path:
|
|
280
|
+
lines.append(f"Selection note: `{outline_selection_rel_path}`")
|
|
281
|
+
if revised_outline_rel_path:
|
|
282
|
+
lines.append(f"Revision record: `{revised_outline_rel_path}`")
|
|
283
|
+
return "\n".join(lines)
|
|
284
|
+
|
|
285
|
+
def _build_analysis_campaign_interaction_message(
|
|
286
|
+
self,
|
|
287
|
+
*,
|
|
288
|
+
campaign_id: str,
|
|
289
|
+
goal: str | None,
|
|
290
|
+
parent_branch: str,
|
|
291
|
+
selected_outline_ref: str | None,
|
|
292
|
+
first_slice: dict[str, Any],
|
|
293
|
+
todo_manifest_rel_path: str | None,
|
|
294
|
+
) -> str:
|
|
295
|
+
lines = [f"Analysis campaign `{campaign_id}` is ready from parent branch `{parent_branch}`."]
|
|
296
|
+
if self._notification_text(goal):
|
|
297
|
+
lines.append(f"Goal: {self._notification_text(goal)}")
|
|
298
|
+
if selected_outline_ref:
|
|
299
|
+
lines.append(f"Selected outline: `{selected_outline_ref}`")
|
|
300
|
+
lines.append(
|
|
301
|
+
f"Next slice: `{first_slice.get('slice_id')}` on branch `{first_slice.get('branch')}`"
|
|
302
|
+
)
|
|
303
|
+
if self._notification_text(first_slice.get("title")):
|
|
304
|
+
lines.append(f"Slice focus: {self._notification_text(first_slice.get('title'))}")
|
|
305
|
+
requirement = self._notification_text(first_slice.get("must_not_simplify") or first_slice.get("goal"))
|
|
306
|
+
if requirement:
|
|
307
|
+
lines.append(f"Core requirement: {requirement}")
|
|
308
|
+
if todo_manifest_rel_path:
|
|
309
|
+
lines.append(f"Todo manifest: `{todo_manifest_rel_path}`")
|
|
310
|
+
return "\n".join(lines)
|
|
311
|
+
|
|
312
|
+
def _build_analysis_slice_interaction_message(
|
|
313
|
+
self,
|
|
314
|
+
*,
|
|
315
|
+
campaign_id: str,
|
|
316
|
+
slice_id: str,
|
|
317
|
+
evaluation_summary: dict[str, str] | None,
|
|
318
|
+
claim_impact: str | None,
|
|
319
|
+
next_slice: dict[str, Any],
|
|
320
|
+
mirror_rel_path: str | None,
|
|
321
|
+
) -> str:
|
|
322
|
+
lines = [f"Analysis slice `{slice_id}` from campaign `{campaign_id}` is complete."]
|
|
323
|
+
takeaway = self._notification_text((evaluation_summary or {}).get("takeaway"))
|
|
324
|
+
if takeaway:
|
|
325
|
+
lines.append(f"Takeaway: {takeaway}")
|
|
326
|
+
if self._notification_text(claim_impact):
|
|
327
|
+
lines.append(f"Claim impact: {self._notification_text(claim_impact)}")
|
|
328
|
+
lines.append(
|
|
329
|
+
f"Next slice: `{next_slice.get('slice_id')}` on branch `{next_slice.get('branch')}`"
|
|
330
|
+
)
|
|
331
|
+
requirement = self._notification_text(next_slice.get("must_not_simplify") or next_slice.get("goal"))
|
|
332
|
+
if requirement:
|
|
333
|
+
lines.append(f"Core requirement: {requirement}")
|
|
334
|
+
if mirror_rel_path:
|
|
335
|
+
lines.append(f"Parent mirror: `{mirror_rel_path}`")
|
|
336
|
+
return "\n".join(lines)
|
|
337
|
+
|
|
338
|
+
def _build_analysis_complete_interaction_message(
|
|
339
|
+
self,
|
|
340
|
+
*,
|
|
341
|
+
campaign_id: str,
|
|
342
|
+
completed_slices: list[dict[str, Any]],
|
|
343
|
+
summary_rel_path: str | None,
|
|
344
|
+
writing_branch: str | None,
|
|
345
|
+
writing_worktree_rel_path: str | None,
|
|
346
|
+
) -> str:
|
|
347
|
+
lines = [f"Analysis campaign `{campaign_id}` is complete."]
|
|
348
|
+
lines.append(f"Completed slices: {len(completed_slices)}")
|
|
349
|
+
strongest_takeaway = next(
|
|
350
|
+
(
|
|
351
|
+
self._notification_text(
|
|
352
|
+
((item.get("evaluation_summary") or {}) if isinstance(item.get("evaluation_summary"), dict) else {}).get(
|
|
353
|
+
"takeaway"
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
for item in completed_slices
|
|
357
|
+
if self._notification_text(
|
|
358
|
+
((item.get("evaluation_summary") or {}) if isinstance(item.get("evaluation_summary"), dict) else {}).get(
|
|
359
|
+
"takeaway"
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
),
|
|
363
|
+
None,
|
|
364
|
+
)
|
|
365
|
+
if strongest_takeaway:
|
|
366
|
+
lines.append(f"Main takeaway: {strongest_takeaway}")
|
|
367
|
+
if summary_rel_path:
|
|
368
|
+
lines.append(f"Summary: `{summary_rel_path}`")
|
|
369
|
+
if writing_branch:
|
|
370
|
+
lines.append(f"Next route: writing is active on branch `{writing_branch}`")
|
|
371
|
+
if writing_worktree_rel_path:
|
|
372
|
+
lines.append(f"Writing workspace: `{writing_worktree_rel_path}`")
|
|
373
|
+
else:
|
|
374
|
+
lines.append("Next route: make the next durable decision from the merged analysis evidence.")
|
|
375
|
+
return "\n".join(lines)
|
|
376
|
+
|
|
377
|
+
def _load_metric_contract_payload(self, quest_root: Path, metric_contract_json_rel_path: str | None) -> dict[str, Any] | None:
|
|
378
|
+
rel_path = str(metric_contract_json_rel_path or "").strip()
|
|
379
|
+
if not rel_path:
|
|
380
|
+
return None
|
|
381
|
+
try:
|
|
382
|
+
resolved_path = resolve_within(quest_root, rel_path)
|
|
383
|
+
except ValueError:
|
|
384
|
+
return None
|
|
385
|
+
if not resolved_path.exists():
|
|
386
|
+
return None
|
|
387
|
+
payload = read_json(resolved_path, {})
|
|
388
|
+
return payload if isinstance(payload, dict) and payload else None
|
|
389
|
+
|
|
390
|
+
def _normalize_metric_directions(self, metric_directions: object) -> dict[str, str]:
|
|
391
|
+
if not isinstance(metric_directions, dict):
|
|
392
|
+
return {}
|
|
393
|
+
normalized: dict[str, str] = {}
|
|
394
|
+
for raw_metric_id, raw_direction in metric_directions.items():
|
|
395
|
+
metric_id = str(raw_metric_id or "").strip()
|
|
396
|
+
if not metric_id:
|
|
397
|
+
continue
|
|
398
|
+
normalized[metric_id] = normalize_metric_direction(raw_direction, metric_id=metric_id)
|
|
399
|
+
return normalized
|
|
400
|
+
|
|
401
|
+
def _apply_metric_directions_to_contract(
|
|
402
|
+
self,
|
|
403
|
+
*,
|
|
404
|
+
metric_contract: object,
|
|
405
|
+
metric_directions: object,
|
|
406
|
+
baseline_id: str | None = None,
|
|
407
|
+
metrics_summary: object = None,
|
|
408
|
+
metric_rows: object = None,
|
|
409
|
+
primary_metric: object = None,
|
|
410
|
+
baseline_variants: object = None,
|
|
411
|
+
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
412
|
+
normalized_contract = normalize_metric_contract(
|
|
413
|
+
metric_contract,
|
|
414
|
+
baseline_id=baseline_id,
|
|
415
|
+
metrics_summary=metrics_summary,
|
|
416
|
+
metric_rows=metric_rows,
|
|
417
|
+
primary_metric=primary_metric,
|
|
418
|
+
baseline_variants=baseline_variants,
|
|
419
|
+
)
|
|
420
|
+
normalized_primary_metric = dict(primary_metric or {}) if isinstance(primary_metric, dict) else None
|
|
421
|
+
overrides = self._normalize_metric_directions(metric_directions)
|
|
422
|
+
if not overrides:
|
|
423
|
+
return normalized_contract, normalized_primary_metric
|
|
424
|
+
|
|
425
|
+
metrics_by_id: dict[str, dict[str, Any]] = {}
|
|
426
|
+
ordered_metric_ids: list[str] = []
|
|
427
|
+
for raw_metric in normalized_contract.get("metrics", []):
|
|
428
|
+
if not isinstance(raw_metric, dict):
|
|
429
|
+
continue
|
|
430
|
+
metric_id = str(raw_metric.get("metric_id") or "").strip()
|
|
431
|
+
if not metric_id:
|
|
432
|
+
continue
|
|
433
|
+
metrics_by_id[metric_id] = dict(raw_metric)
|
|
434
|
+
ordered_metric_ids.append(metric_id)
|
|
435
|
+
for metric_id, direction in overrides.items():
|
|
436
|
+
current = metrics_by_id.get(metric_id)
|
|
437
|
+
if current is None:
|
|
438
|
+
current = {
|
|
439
|
+
"metric_id": metric_id,
|
|
440
|
+
"label": metric_id,
|
|
441
|
+
"direction": direction,
|
|
442
|
+
"unit": None,
|
|
443
|
+
"decimals": None,
|
|
444
|
+
"chart_group": "default",
|
|
445
|
+
}
|
|
446
|
+
ordered_metric_ids.append(metric_id)
|
|
447
|
+
else:
|
|
448
|
+
current = {
|
|
449
|
+
**current,
|
|
450
|
+
"direction": direction,
|
|
451
|
+
}
|
|
452
|
+
metrics_by_id[metric_id] = current
|
|
453
|
+
|
|
454
|
+
primary_metric_id = str(
|
|
455
|
+
(normalized_primary_metric or {}).get("metric_id")
|
|
456
|
+
or (normalized_primary_metric or {}).get("name")
|
|
457
|
+
or (normalized_primary_metric or {}).get("id")
|
|
458
|
+
or normalized_contract.get("primary_metric_id")
|
|
459
|
+
or ""
|
|
460
|
+
).strip()
|
|
461
|
+
if normalized_primary_metric and primary_metric_id in overrides:
|
|
462
|
+
normalized_primary_metric = {
|
|
463
|
+
**normalized_primary_metric,
|
|
464
|
+
"direction": overrides[primary_metric_id],
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
**normalized_contract,
|
|
469
|
+
"metrics": [metrics_by_id[metric_id] for metric_id in ordered_metric_ids if metric_id in metrics_by_id],
|
|
470
|
+
}, normalized_primary_metric
|
|
471
|
+
|
|
472
|
+
def _merge_run_metric_contract(
|
|
473
|
+
self,
|
|
474
|
+
*,
|
|
475
|
+
baseline_metric_contract: object,
|
|
476
|
+
baseline_primary_metric: object,
|
|
477
|
+
baseline_variants: object,
|
|
478
|
+
run_metric_contract: object,
|
|
479
|
+
metrics_summary: object,
|
|
480
|
+
metric_rows: object,
|
|
481
|
+
baseline_id: str | None = None,
|
|
482
|
+
) -> dict[str, Any]:
|
|
483
|
+
baseline_contract = normalize_metric_contract(
|
|
484
|
+
baseline_metric_contract,
|
|
485
|
+
baseline_id=baseline_id,
|
|
486
|
+
metrics_summary=metrics_summary,
|
|
487
|
+
metric_rows=metric_rows,
|
|
488
|
+
primary_metric=baseline_primary_metric,
|
|
489
|
+
baseline_variants=baseline_variants,
|
|
490
|
+
)
|
|
491
|
+
if not isinstance(run_metric_contract, dict) or not run_metric_contract:
|
|
492
|
+
return baseline_contract
|
|
493
|
+
|
|
494
|
+
overlay_contract = normalize_metric_contract(
|
|
495
|
+
run_metric_contract,
|
|
496
|
+
baseline_id=baseline_id,
|
|
497
|
+
metrics_summary=metrics_summary,
|
|
498
|
+
metric_rows=metric_rows,
|
|
499
|
+
primary_metric=baseline_contract.get("primary_metric_id"),
|
|
500
|
+
)
|
|
501
|
+
overlay_metrics: dict[str, dict[str, Any]] = {}
|
|
502
|
+
for raw_metric in overlay_contract.get("metrics", []):
|
|
503
|
+
if not isinstance(raw_metric, dict):
|
|
504
|
+
continue
|
|
505
|
+
metric_id = str(raw_metric.get("metric_id") or "").strip()
|
|
506
|
+
if metric_id:
|
|
507
|
+
overlay_metrics[metric_id] = raw_metric
|
|
508
|
+
|
|
509
|
+
merged_metrics: list[dict[str, Any]] = []
|
|
510
|
+
seen_metric_ids: set[str] = set()
|
|
511
|
+
for raw_metric in baseline_contract.get("metrics", []):
|
|
512
|
+
if not isinstance(raw_metric, dict):
|
|
513
|
+
continue
|
|
514
|
+
metric_id = str(raw_metric.get("metric_id") or "").strip()
|
|
515
|
+
if not metric_id:
|
|
516
|
+
continue
|
|
517
|
+
patch = overlay_metrics.get(metric_id) or {}
|
|
518
|
+
merged = dict(raw_metric)
|
|
519
|
+
for field in (
|
|
520
|
+
"label",
|
|
521
|
+
"unit",
|
|
522
|
+
"decimals",
|
|
523
|
+
"chart_group",
|
|
524
|
+
"description",
|
|
525
|
+
"derivation",
|
|
526
|
+
"source_ref",
|
|
527
|
+
"required",
|
|
528
|
+
"origin_path",
|
|
529
|
+
):
|
|
530
|
+
value = patch.get(field)
|
|
531
|
+
if value is None:
|
|
532
|
+
continue
|
|
533
|
+
if isinstance(value, str) and not value.strip():
|
|
534
|
+
continue
|
|
535
|
+
merged[field] = value
|
|
536
|
+
merged_metrics.append(merged)
|
|
537
|
+
seen_metric_ids.add(metric_id)
|
|
538
|
+
|
|
539
|
+
for metric_id, raw_metric in overlay_metrics.items():
|
|
540
|
+
if metric_id in seen_metric_ids:
|
|
541
|
+
continue
|
|
542
|
+
merged_metrics.append(dict(raw_metric))
|
|
543
|
+
|
|
544
|
+
merged_contract = {
|
|
545
|
+
**baseline_contract,
|
|
546
|
+
"metrics": merged_metrics,
|
|
547
|
+
}
|
|
548
|
+
if not merged_contract.get("evaluation_protocol") and overlay_contract.get("evaluation_protocol") is not None:
|
|
549
|
+
merged_contract["evaluation_protocol"] = overlay_contract.get("evaluation_protocol")
|
|
550
|
+
for key, value in overlay_contract.items():
|
|
551
|
+
if key in {"contract_id", "primary_metric_id", "metrics", "evaluation_protocol"}:
|
|
552
|
+
continue
|
|
553
|
+
if key not in merged_contract and value is not None:
|
|
554
|
+
merged_contract[key] = value
|
|
555
|
+
return merged_contract
|
|
556
|
+
|
|
129
557
|
def _workspace_root_for(self, quest_root: Path, workspace_root: Path | None = None) -> Path:
|
|
130
558
|
if workspace_root is not None:
|
|
131
559
|
return workspace_root
|
|
@@ -139,6 +567,73 @@ class ArtifactService:
|
|
|
139
567
|
except ValueError:
|
|
140
568
|
return str(path)
|
|
141
569
|
|
|
570
|
+
@staticmethod
|
|
571
|
+
def _branch_kind_from_name(branch_name: str | None) -> str:
|
|
572
|
+
normalized = str(branch_name or "").strip()
|
|
573
|
+
if normalized in {"main", "master"} or normalized.startswith("quest/"):
|
|
574
|
+
return "quest"
|
|
575
|
+
if normalized.startswith("idea/"):
|
|
576
|
+
return "idea"
|
|
577
|
+
if normalized.startswith("analysis/"):
|
|
578
|
+
return "analysis"
|
|
579
|
+
if normalized.startswith("paper/"):
|
|
580
|
+
return "paper"
|
|
581
|
+
if normalized.startswith("run/"):
|
|
582
|
+
return "run"
|
|
583
|
+
return "branch"
|
|
584
|
+
|
|
585
|
+
def _workspace_mode_for_branch(self, branch_name: str | None, *, has_idea: bool = False) -> str:
|
|
586
|
+
branch_kind = self._branch_kind_from_name(branch_name)
|
|
587
|
+
if branch_kind == "paper":
|
|
588
|
+
return "paper"
|
|
589
|
+
if branch_kind == "analysis":
|
|
590
|
+
return "analysis"
|
|
591
|
+
if branch_kind == "run":
|
|
592
|
+
return "run"
|
|
593
|
+
if branch_kind == "idea" or has_idea:
|
|
594
|
+
return "idea"
|
|
595
|
+
return "quest"
|
|
596
|
+
|
|
597
|
+
def _prepare_branch_worktree_root(
|
|
598
|
+
self,
|
|
599
|
+
quest_root: Path,
|
|
600
|
+
*,
|
|
601
|
+
branch_name: str,
|
|
602
|
+
branch_kind: str,
|
|
603
|
+
run_id: str | None = None,
|
|
604
|
+
idea_id: str | None = None,
|
|
605
|
+
) -> Path:
|
|
606
|
+
normalized_kind = str(branch_kind or "").strip().lower() or "run"
|
|
607
|
+
normalized_run_id = str(run_id or "").strip() or None
|
|
608
|
+
normalized_idea_id = str(idea_id or "").strip() or None
|
|
609
|
+
if normalized_kind == "idea" and normalized_idea_id:
|
|
610
|
+
return canonical_worktree_root(quest_root, f"idea-{normalized_idea_id}")
|
|
611
|
+
if normalized_kind == "paper":
|
|
612
|
+
return canonical_worktree_root(
|
|
613
|
+
quest_root,
|
|
614
|
+
f"paper-{normalized_run_id or slugify(branch_name, 'paper')}",
|
|
615
|
+
)
|
|
616
|
+
if normalized_kind == "run" and normalized_run_id:
|
|
617
|
+
return canonical_worktree_root(quest_root, normalized_run_id)
|
|
618
|
+
return canonical_worktree_root(quest_root, slugify(branch_name, "branch"))
|
|
619
|
+
|
|
620
|
+
def _latest_prepare_branch_record(self, quest_root: Path, branch_name: str) -> dict[str, Any]:
|
|
621
|
+
normalized_branch = str(branch_name or "").strip()
|
|
622
|
+
if not normalized_branch:
|
|
623
|
+
return {}
|
|
624
|
+
for item in reversed(self.quest_service._collect_artifacts(quest_root)):
|
|
625
|
+
payload = dict(item.get("payload") or {}) if isinstance(item.get("payload"), dict) else {}
|
|
626
|
+
if not payload:
|
|
627
|
+
continue
|
|
628
|
+
if str(payload.get("kind") or "").strip() != "decision":
|
|
629
|
+
continue
|
|
630
|
+
if str(payload.get("action") or "").strip() != "prepare_branch":
|
|
631
|
+
continue
|
|
632
|
+
if str(payload.get("branch") or "").strip() != normalized_branch:
|
|
633
|
+
continue
|
|
634
|
+
return payload
|
|
635
|
+
return {}
|
|
636
|
+
|
|
142
637
|
def _git_config(self) -> dict[str, Any]:
|
|
143
638
|
config = ConfigManager(self.home).load_named("config")
|
|
144
639
|
payload = config.get("git") if isinstance(config.get("git"), dict) else {}
|
|
@@ -623,43 +1118,91 @@ class ArtifactService:
|
|
|
623
1118
|
)
|
|
624
1119
|
return normalized
|
|
625
1120
|
|
|
626
|
-
def _paper_root(
|
|
627
|
-
|
|
1121
|
+
def _paper_root(
|
|
1122
|
+
self,
|
|
1123
|
+
quest_root: Path,
|
|
1124
|
+
*,
|
|
1125
|
+
workspace_root: Path | None = None,
|
|
1126
|
+
prefer_workspace: bool = True,
|
|
1127
|
+
create: bool = False,
|
|
1128
|
+
) -> Path:
|
|
1129
|
+
roots: list[Path] = []
|
|
1130
|
+
if prefer_workspace:
|
|
1131
|
+
roots.append(self._workspace_root_for(quest_root, workspace_root))
|
|
1132
|
+
roots.append(quest_root)
|
|
1133
|
+
seen: set[str] = set()
|
|
1134
|
+
first_candidate: Path | None = None
|
|
1135
|
+
for root in roots:
|
|
1136
|
+
key = str(root.resolve())
|
|
1137
|
+
if key in seen:
|
|
1138
|
+
continue
|
|
1139
|
+
seen.add(key)
|
|
1140
|
+
candidate = root / "paper"
|
|
1141
|
+
if first_candidate is None:
|
|
1142
|
+
first_candidate = candidate
|
|
1143
|
+
if candidate.exists():
|
|
1144
|
+
return candidate
|
|
1145
|
+
fallback = first_candidate or (quest_root / "paper")
|
|
1146
|
+
return ensure_dir(fallback) if create else fallback
|
|
628
1147
|
|
|
629
|
-
def _paper_outline_candidates_root(self, quest_root: Path) -> Path:
|
|
630
|
-
return ensure_dir(self._paper_root(quest_root) / "outlines" / "candidates")
|
|
1148
|
+
def _paper_outline_candidates_root(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
1149
|
+
return ensure_dir(self._paper_root(quest_root, workspace_root=workspace_root, create=True) / "outlines" / "candidates")
|
|
631
1150
|
|
|
632
|
-
def _paper_outline_revisions_root(self, quest_root: Path) -> Path:
|
|
633
|
-
return ensure_dir(self._paper_root(quest_root) / "outlines" / "revisions")
|
|
1151
|
+
def _paper_outline_revisions_root(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
1152
|
+
return ensure_dir(self._paper_root(quest_root, workspace_root=workspace_root, create=True) / "outlines" / "revisions")
|
|
634
1153
|
|
|
635
|
-
def _paper_selected_outline_path(self, quest_root: Path) -> Path:
|
|
636
|
-
return self._paper_root(quest_root) / "selected_outline.json"
|
|
1154
|
+
def _paper_selected_outline_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
1155
|
+
return self._paper_root(quest_root, workspace_root=workspace_root) / "selected_outline.json"
|
|
637
1156
|
|
|
638
|
-
def _paper_outline_selection_path(self, quest_root: Path) -> Path:
|
|
639
|
-
return self._paper_root(quest_root) / "outline_selection.md"
|
|
1157
|
+
def _paper_outline_selection_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
1158
|
+
return self._paper_root(quest_root, workspace_root=workspace_root, create=True) / "outline_selection.md"
|
|
640
1159
|
|
|
641
|
-
def _paper_bundle_manifest_path(self, quest_root: Path) -> Path:
|
|
642
|
-
return self._paper_root(quest_root) / "paper_bundle_manifest.json"
|
|
1160
|
+
def _paper_bundle_manifest_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
1161
|
+
return self._paper_root(quest_root, workspace_root=workspace_root, create=True) / "paper_bundle_manifest.json"
|
|
643
1162
|
|
|
644
|
-
def _paper_baseline_inventory_path(self, quest_root: Path) -> Path:
|
|
645
|
-
return self._paper_root(quest_root) / "baseline_inventory.json"
|
|
1163
|
+
def _paper_baseline_inventory_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
1164
|
+
return self._paper_root(quest_root, workspace_root=workspace_root, create=True) / "baseline_inventory.json"
|
|
646
1165
|
|
|
647
|
-
def _open_source_root(
|
|
648
|
-
|
|
1166
|
+
def _open_source_root(
|
|
1167
|
+
self,
|
|
1168
|
+
quest_root: Path,
|
|
1169
|
+
*,
|
|
1170
|
+
workspace_root: Path | None = None,
|
|
1171
|
+
prefer_workspace: bool = True,
|
|
1172
|
+
create: bool = False,
|
|
1173
|
+
) -> Path:
|
|
1174
|
+
roots: list[Path] = []
|
|
1175
|
+
if prefer_workspace:
|
|
1176
|
+
roots.append(self._workspace_root_for(quest_root, workspace_root))
|
|
1177
|
+
roots.append(quest_root)
|
|
1178
|
+
seen: set[str] = set()
|
|
1179
|
+
first_candidate: Path | None = None
|
|
1180
|
+
for root in roots:
|
|
1181
|
+
key = str(root.resolve())
|
|
1182
|
+
if key in seen:
|
|
1183
|
+
continue
|
|
1184
|
+
seen.add(key)
|
|
1185
|
+
candidate = root / "release" / "open_source"
|
|
1186
|
+
if first_candidate is None:
|
|
1187
|
+
first_candidate = candidate
|
|
1188
|
+
if candidate.exists():
|
|
1189
|
+
return candidate
|
|
1190
|
+
fallback = first_candidate or (quest_root / "release" / "open_source")
|
|
1191
|
+
return ensure_dir(fallback) if create else fallback
|
|
649
1192
|
|
|
650
|
-
def _open_source_manifest_path(self, quest_root: Path) -> Path:
|
|
651
|
-
return self._open_source_root(quest_root) / "manifest.json"
|
|
1193
|
+
def _open_source_manifest_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
1194
|
+
return self._open_source_root(quest_root, workspace_root=workspace_root, create=True) / "manifest.json"
|
|
652
1195
|
|
|
653
|
-
def _open_source_cleanup_plan_path(self, quest_root: Path) -> Path:
|
|
654
|
-
return self._open_source_root(quest_root) / "cleanup_plan.md"
|
|
1196
|
+
def _open_source_cleanup_plan_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
1197
|
+
return self._open_source_root(quest_root, workspace_root=workspace_root, create=True) / "cleanup_plan.md"
|
|
655
1198
|
|
|
656
|
-
def _open_source_include_paths_path(self, quest_root: Path) -> Path:
|
|
657
|
-
return self._open_source_root(quest_root) / "include_paths.json"
|
|
1199
|
+
def _open_source_include_paths_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
1200
|
+
return self._open_source_root(quest_root, workspace_root=workspace_root, create=True) / "include_paths.json"
|
|
658
1201
|
|
|
659
|
-
def _open_source_exclude_paths_path(self, quest_root: Path) -> Path:
|
|
660
|
-
return self._open_source_root(quest_root) / "exclude_paths.json"
|
|
1202
|
+
def _open_source_exclude_paths_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
1203
|
+
return self._open_source_root(quest_root, workspace_root=workspace_root, create=True) / "exclude_paths.json"
|
|
661
1204
|
|
|
662
|
-
def _write_paper_baseline_inventory(self, quest_root: Path) -> dict[str, Any]:
|
|
1205
|
+
def _write_paper_baseline_inventory(self, quest_root: Path, *, workspace_root: Path | None = None) -> dict[str, Any]:
|
|
663
1206
|
quest_yaml = self.quest_service.read_quest_yaml(quest_root)
|
|
664
1207
|
confirmed_baseline_ref = (
|
|
665
1208
|
dict(quest_yaml.get("confirmed_baseline_ref") or {})
|
|
@@ -675,22 +1218,23 @@ class ArtifactService:
|
|
|
675
1218
|
],
|
|
676
1219
|
"updated_at": utc_now(),
|
|
677
1220
|
}
|
|
678
|
-
write_json(self._paper_baseline_inventory_path(quest_root), payload)
|
|
1221
|
+
write_json(self._paper_baseline_inventory_path(quest_root, workspace_root=workspace_root), payload)
|
|
679
1222
|
return payload
|
|
680
1223
|
|
|
681
1224
|
def _ensure_open_source_prep(
|
|
682
1225
|
self,
|
|
683
1226
|
quest_root: Path,
|
|
684
1227
|
*,
|
|
1228
|
+
workspace_root: Path | None,
|
|
685
1229
|
source_branch: str | None,
|
|
686
1230
|
source_bundle_manifest_path: str,
|
|
687
1231
|
baseline_inventory_path: str,
|
|
688
1232
|
) -> dict[str, Any]:
|
|
689
|
-
root = self._open_source_root(quest_root)
|
|
690
|
-
cleanup_plan_path = self._open_source_cleanup_plan_path(quest_root)
|
|
691
|
-
include_paths_path = self._open_source_include_paths_path(quest_root)
|
|
692
|
-
exclude_paths_path = self._open_source_exclude_paths_path(quest_root)
|
|
693
|
-
manifest_path = self._open_source_manifest_path(quest_root)
|
|
1233
|
+
root = self._open_source_root(quest_root, workspace_root=workspace_root, create=True)
|
|
1234
|
+
cleanup_plan_path = self._open_source_cleanup_plan_path(quest_root, workspace_root=workspace_root)
|
|
1235
|
+
include_paths_path = self._open_source_include_paths_path(quest_root, workspace_root=workspace_root)
|
|
1236
|
+
exclude_paths_path = self._open_source_exclude_paths_path(quest_root, workspace_root=workspace_root)
|
|
1237
|
+
manifest_path = self._open_source_manifest_path(quest_root, workspace_root=workspace_root)
|
|
694
1238
|
if not cleanup_plan_path.exists():
|
|
695
1239
|
write_text(
|
|
696
1240
|
cleanup_plan_path,
|
|
@@ -737,11 +1281,17 @@ class ArtifactService:
|
|
|
737
1281
|
or source_bundle_manifest_path,
|
|
738
1282
|
"baseline_inventory_path": str(existing.get("baseline_inventory_path") or baseline_inventory_path or "").strip()
|
|
739
1283
|
or baseline_inventory_path,
|
|
740
|
-
"cleanup_plan_path": str(
|
|
1284
|
+
"cleanup_plan_path": str(
|
|
1285
|
+
existing.get("cleanup_plan_path") or self._workspace_relative(quest_root, cleanup_plan_path) or ""
|
|
1286
|
+
).strip()
|
|
741
1287
|
or "release/open_source/cleanup_plan.md",
|
|
742
|
-
"include_paths_path": str(
|
|
1288
|
+
"include_paths_path": str(
|
|
1289
|
+
existing.get("include_paths_path") or self._workspace_relative(quest_root, include_paths_path) or ""
|
|
1290
|
+
).strip()
|
|
743
1291
|
or "release/open_source/include_paths.json",
|
|
744
|
-
"exclude_paths_path": str(
|
|
1292
|
+
"exclude_paths_path": str(
|
|
1293
|
+
existing.get("exclude_paths_path") or self._workspace_relative(quest_root, exclude_paths_path) or ""
|
|
1294
|
+
).strip()
|
|
745
1295
|
or "release/open_source/exclude_paths.json",
|
|
746
1296
|
"created_at": existing.get("created_at") or utc_now(),
|
|
747
1297
|
"updated_at": utc_now(),
|
|
@@ -859,6 +1409,9 @@ class ArtifactService:
|
|
|
859
1409
|
continue
|
|
860
1410
|
seen_paths.add(key)
|
|
861
1411
|
payload = read_yaml(path, {})
|
|
1412
|
+
baseline_id = str(payload.get("source_baseline_id") or "").strip() if isinstance(payload, dict) else ""
|
|
1413
|
+
if baseline_id and self.baselines.is_deleted(baseline_id):
|
|
1414
|
+
continue
|
|
862
1415
|
if isinstance(payload, dict) and payload:
|
|
863
1416
|
attachments.append(payload)
|
|
864
1417
|
if not attachments:
|
|
@@ -871,6 +1424,48 @@ class ArtifactService:
|
|
|
871
1424
|
),
|
|
872
1425
|
)
|
|
873
1426
|
|
|
1427
|
+
def _baseline_workspace_roots(self, quest_root: Path) -> list[Path]:
|
|
1428
|
+
roots: list[Path] = [quest_root]
|
|
1429
|
+
research_state = read_json(quest_root / ".ds" / "research_state.json", {})
|
|
1430
|
+
if isinstance(research_state, dict):
|
|
1431
|
+
for key in (
|
|
1432
|
+
"research_head_worktree_root",
|
|
1433
|
+
"current_workspace_root",
|
|
1434
|
+
"analysis_parent_worktree_root",
|
|
1435
|
+
"paper_parent_worktree_root",
|
|
1436
|
+
):
|
|
1437
|
+
raw = str(research_state.get(key) or "").strip()
|
|
1438
|
+
if raw:
|
|
1439
|
+
roots.append(Path(raw))
|
|
1440
|
+
worktrees_root = quest_root / ".ds" / "worktrees"
|
|
1441
|
+
if worktrees_root.exists():
|
|
1442
|
+
roots.extend(path for path in sorted(worktrees_root.iterdir()) if path.is_dir())
|
|
1443
|
+
deduped: list[Path] = []
|
|
1444
|
+
seen: set[str] = set()
|
|
1445
|
+
for root in roots:
|
|
1446
|
+
key = str(root.resolve(strict=False))
|
|
1447
|
+
if key in seen:
|
|
1448
|
+
continue
|
|
1449
|
+
seen.add(key)
|
|
1450
|
+
deduped.append(root)
|
|
1451
|
+
return deduped
|
|
1452
|
+
|
|
1453
|
+
@staticmethod
|
|
1454
|
+
def _remove_baseline_materialization(root: Path, baseline_id: str) -> list[str]:
|
|
1455
|
+
deleted_paths: list[str] = []
|
|
1456
|
+
for candidate in (
|
|
1457
|
+
root / "baselines" / "imported" / baseline_id,
|
|
1458
|
+
root / "baselines" / "local" / baseline_id,
|
|
1459
|
+
):
|
|
1460
|
+
if not candidate.exists():
|
|
1461
|
+
continue
|
|
1462
|
+
if candidate.is_dir():
|
|
1463
|
+
shutil.rmtree(candidate)
|
|
1464
|
+
else:
|
|
1465
|
+
candidate.unlink()
|
|
1466
|
+
deleted_paths.append(str(candidate))
|
|
1467
|
+
return deleted_paths
|
|
1468
|
+
|
|
874
1469
|
def _resolve_baseline_path(
|
|
875
1470
|
self,
|
|
876
1471
|
quest_root: Path,
|
|
@@ -1064,6 +1659,7 @@ class ArtifactService:
|
|
|
1064
1659
|
"metric_contract": metric_contract,
|
|
1065
1660
|
"primary_metric": entry.get("primary_metric"),
|
|
1066
1661
|
"metrics_summary": metrics_summary,
|
|
1662
|
+
"metric_details": entry.get("metric_details") or [],
|
|
1067
1663
|
}
|
|
1068
1664
|
json_path = ensure_dir(baseline_root / "json") / "metric_contract.json"
|
|
1069
1665
|
write_json(json_path, payload)
|
|
@@ -1169,9 +1765,43 @@ class ArtifactService:
|
|
|
1169
1765
|
"Use `artifact.confirm_baseline(...)` or `artifact.waive_baseline(...)` first."
|
|
1170
1766
|
)
|
|
1171
1767
|
|
|
1768
|
+
@staticmethod
|
|
1769
|
+
def _artifact_record_identity(path: Path, payload: dict[str, Any], *, kind: str | None = None) -> str:
|
|
1770
|
+
normalized_kind = str(kind or payload.get("kind") or path.parent.name or "artifact").strip() or "artifact"
|
|
1771
|
+
branch_name = str(payload.get("branch") or "").strip()
|
|
1772
|
+
run_id = str(payload.get("run_id") or "").strip()
|
|
1773
|
+
if normalized_kind == "run" and run_id and branch_name:
|
|
1774
|
+
return f"{normalized_kind}:branch_run:{branch_name}:{run_id}"
|
|
1775
|
+
artifact_id = str(payload.get("artifact_id") or payload.get("id") or "").strip()
|
|
1776
|
+
if artifact_id:
|
|
1777
|
+
return f"{normalized_kind}:artifact:{artifact_id}"
|
|
1778
|
+
if normalized_kind == "run" and run_id:
|
|
1779
|
+
return f"{normalized_kind}:run:{run_id}"
|
|
1780
|
+
idea_id = str(payload.get("idea_id") or "").strip()
|
|
1781
|
+
if normalized_kind == "idea" and idea_id and branch_name:
|
|
1782
|
+
return f"{normalized_kind}:branch_idea:{branch_name}:{idea_id}"
|
|
1783
|
+
if normalized_kind == "idea" and idea_id:
|
|
1784
|
+
return f"{normalized_kind}:idea:{idea_id}"
|
|
1785
|
+
baseline_id = str(payload.get("baseline_id") or payload.get("entry_id") or "").strip()
|
|
1786
|
+
if baseline_id:
|
|
1787
|
+
return f"{normalized_kind}:baseline:{baseline_id}"
|
|
1788
|
+
interaction_id = str(payload.get("interaction_id") or "").strip()
|
|
1789
|
+
if interaction_id:
|
|
1790
|
+
return f"{normalized_kind}:interaction:{interaction_id}"
|
|
1791
|
+
return f"path:{path.resolve()}"
|
|
1792
|
+
|
|
1793
|
+
@staticmethod
|
|
1794
|
+
def _artifact_record_rank(payload: dict[str, Any], *, path: Path, mtime_ns: int) -> tuple[str, str, int, int, str]:
|
|
1795
|
+
return (
|
|
1796
|
+
str(payload.get("updated_at") or ""),
|
|
1797
|
+
str(payload.get("created_at") or ""),
|
|
1798
|
+
len(payload),
|
|
1799
|
+
mtime_ns,
|
|
1800
|
+
str(path),
|
|
1801
|
+
)
|
|
1802
|
+
|
|
1172
1803
|
def _main_run_artifacts(self, quest_root: Path) -> list[dict[str, Any]]:
|
|
1173
|
-
|
|
1174
|
-
seen_paths: set[str] = set()
|
|
1804
|
+
records_by_identity: dict[str, dict[str, Any]] = {}
|
|
1175
1805
|
for root in self.quest_service.workspace_roots(quest_root):
|
|
1176
1806
|
artifacts_root = root / "artifacts" / "runs"
|
|
1177
1807
|
if not artifacts_root.exists():
|
|
@@ -1179,10 +1809,6 @@ class ArtifactService:
|
|
|
1179
1809
|
for path in sorted(artifacts_root.glob("*.json")):
|
|
1180
1810
|
if not path.is_file():
|
|
1181
1811
|
continue
|
|
1182
|
-
key = str(path.resolve())
|
|
1183
|
-
if key in seen_paths:
|
|
1184
|
-
continue
|
|
1185
|
-
seen_paths.add(key)
|
|
1186
1812
|
payload = read_json(path, {})
|
|
1187
1813
|
if not isinstance(payload, dict) or not payload:
|
|
1188
1814
|
continue
|
|
@@ -1194,7 +1820,19 @@ class ArtifactService:
|
|
|
1194
1820
|
enriched["_artifact_mtime_ns"] = path.stat().st_mtime_ns
|
|
1195
1821
|
except OSError:
|
|
1196
1822
|
enriched["_artifact_mtime_ns"] = 0
|
|
1197
|
-
|
|
1823
|
+
identity = self._artifact_record_identity(path, enriched, kind="run")
|
|
1824
|
+
existing = records_by_identity.get(identity)
|
|
1825
|
+
if existing is None or self._artifact_record_rank(
|
|
1826
|
+
enriched,
|
|
1827
|
+
path=path,
|
|
1828
|
+
mtime_ns=int(enriched.get("_artifact_mtime_ns") or 0),
|
|
1829
|
+
) >= self._artifact_record_rank(
|
|
1830
|
+
existing,
|
|
1831
|
+
path=Path(str(existing.get("_artifact_path") or path)),
|
|
1832
|
+
mtime_ns=int(existing.get("_artifact_mtime_ns") or 0),
|
|
1833
|
+
):
|
|
1834
|
+
records_by_identity[identity] = enriched
|
|
1835
|
+
records = list(records_by_identity.values())
|
|
1198
1836
|
records.sort(
|
|
1199
1837
|
key=lambda item: (
|
|
1200
1838
|
str(item.get("updated_at") or item.get("created_at") or ""),
|
|
@@ -1277,6 +1915,173 @@ class ArtifactService:
|
|
|
1277
1915
|
continue
|
|
1278
1916
|
return None
|
|
1279
1917
|
|
|
1918
|
+
def _branch_activation_worktree_root(
|
|
1919
|
+
self,
|
|
1920
|
+
quest_root: Path,
|
|
1921
|
+
*,
|
|
1922
|
+
branch_name: str,
|
|
1923
|
+
idea_id: str | None = None,
|
|
1924
|
+
run_id: str | None = None,
|
|
1925
|
+
) -> Path:
|
|
1926
|
+
normalized_branch = str(branch_name or "").strip()
|
|
1927
|
+
branch_kind = self._branch_kind_from_name(normalized_branch)
|
|
1928
|
+
normalized_idea_id = str(idea_id or "").strip() or None
|
|
1929
|
+
if branch_kind == "paper":
|
|
1930
|
+
normalized_run_id = str(run_id or "").strip() or None
|
|
1931
|
+
return canonical_worktree_root(
|
|
1932
|
+
quest_root,
|
|
1933
|
+
f"paper-{normalized_run_id or slugify(normalized_branch, 'paper')}",
|
|
1934
|
+
)
|
|
1935
|
+
if normalized_idea_id and branch_kind == "idea":
|
|
1936
|
+
return canonical_worktree_root(quest_root, f"idea-{normalized_idea_id}")
|
|
1937
|
+
normalized_run_id = str(run_id or "").strip() or None
|
|
1938
|
+
if normalized_run_id and branch_kind == "run":
|
|
1939
|
+
return canonical_worktree_root(quest_root, normalized_run_id)
|
|
1940
|
+
return canonical_worktree_root(quest_root, f"branch-{slugify(normalized_branch, 'branch')}")
|
|
1941
|
+
|
|
1942
|
+
@staticmethod
|
|
1943
|
+
def _resolve_activate_branch_anchor(
|
|
1944
|
+
*,
|
|
1945
|
+
anchor: str | None,
|
|
1946
|
+
has_idea: bool,
|
|
1947
|
+
has_main_result: bool,
|
|
1948
|
+
) -> str:
|
|
1949
|
+
normalized_anchor = str(anchor or "auto").strip().lower() or "auto"
|
|
1950
|
+
if normalized_anchor == "auto":
|
|
1951
|
+
if has_main_result:
|
|
1952
|
+
return "decision"
|
|
1953
|
+
if has_idea:
|
|
1954
|
+
return "experiment"
|
|
1955
|
+
return "idea"
|
|
1956
|
+
aliases = {
|
|
1957
|
+
"analysis": "analysis-campaign",
|
|
1958
|
+
}
|
|
1959
|
+
resolved_anchor = aliases.get(normalized_anchor, normalized_anchor)
|
|
1960
|
+
allowed = {
|
|
1961
|
+
"scout",
|
|
1962
|
+
"baseline",
|
|
1963
|
+
"idea",
|
|
1964
|
+
"experiment",
|
|
1965
|
+
"analysis-campaign",
|
|
1966
|
+
"write",
|
|
1967
|
+
"finalize",
|
|
1968
|
+
"decision",
|
|
1969
|
+
}
|
|
1970
|
+
if resolved_anchor not in allowed:
|
|
1971
|
+
allowed_text = ", ".join(sorted(allowed | {"auto"}))
|
|
1972
|
+
raise ValueError(f"Unsupported activate_branch anchor `{anchor}`. Allowed values: {allowed_text}.")
|
|
1973
|
+
return resolved_anchor
|
|
1974
|
+
|
|
1975
|
+
def _resolve_branch_activation_target(
|
|
1976
|
+
self,
|
|
1977
|
+
quest_root: Path,
|
|
1978
|
+
*,
|
|
1979
|
+
branch: str | None = None,
|
|
1980
|
+
idea_id: str | None = None,
|
|
1981
|
+
run_id: str | None = None,
|
|
1982
|
+
) -> dict[str, Any]:
|
|
1983
|
+
provided = sum(
|
|
1984
|
+
1
|
|
1985
|
+
for value in (
|
|
1986
|
+
str(branch or "").strip(),
|
|
1987
|
+
str(idea_id or "").strip(),
|
|
1988
|
+
str(run_id or "").strip(),
|
|
1989
|
+
)
|
|
1990
|
+
if value
|
|
1991
|
+
)
|
|
1992
|
+
if provided != 1:
|
|
1993
|
+
raise ValueError("activate_branch requires exactly one of `branch`, `idea_id`, or `run_id`.")
|
|
1994
|
+
|
|
1995
|
+
latest_idea: dict[str, Any] | None = None
|
|
1996
|
+
latest_run: dict[str, Any] | None = None
|
|
1997
|
+
normalized_branch = str(branch or "").strip()
|
|
1998
|
+
normalized_idea_id = str(idea_id or "").strip()
|
|
1999
|
+
normalized_run_id = str(run_id or "").strip()
|
|
2000
|
+
|
|
2001
|
+
if normalized_idea_id:
|
|
2002
|
+
candidates = [
|
|
2003
|
+
item for item in self._idea_artifacts(quest_root) if str(item.get("idea_id") or "").strip() == normalized_idea_id
|
|
2004
|
+
]
|
|
2005
|
+
if not candidates:
|
|
2006
|
+
raise FileNotFoundError(f"Unknown idea `{normalized_idea_id}`.")
|
|
2007
|
+
latest_idea = candidates[-1]
|
|
2008
|
+
normalized_branch = str(latest_idea.get("branch") or "").strip()
|
|
2009
|
+
elif normalized_run_id:
|
|
2010
|
+
candidates = [
|
|
2011
|
+
item for item in self._main_run_artifacts(quest_root) if str(item.get("run_id") or "").strip() == normalized_run_id
|
|
2012
|
+
]
|
|
2013
|
+
if not candidates:
|
|
2014
|
+
raise FileNotFoundError(f"Unknown main run `{normalized_run_id}`.")
|
|
2015
|
+
latest_run = candidates[-1]
|
|
2016
|
+
normalized_branch = str(latest_run.get("branch") or "").strip()
|
|
2017
|
+
else:
|
|
2018
|
+
if normalized_branch.startswith("analysis/"):
|
|
2019
|
+
raise ValueError(
|
|
2020
|
+
"activate_branch only supports durable idea/main branches. "
|
|
2021
|
+
"Analysis slice branches remain managed by analysis campaigns."
|
|
2022
|
+
)
|
|
2023
|
+
if not branch_exists(quest_root, normalized_branch):
|
|
2024
|
+
raise FileNotFoundError(f"Unknown branch `{normalized_branch}`.")
|
|
2025
|
+
|
|
2026
|
+
if not normalized_branch:
|
|
2027
|
+
raise ValueError("Unable to resolve a durable branch to activate.")
|
|
2028
|
+
|
|
2029
|
+
prepare_record = self._latest_prepare_branch_record(quest_root, normalized_branch)
|
|
2030
|
+
prepare_details = dict(prepare_record.get("details") or {}) if isinstance(prepare_record.get("details"), dict) else {}
|
|
2031
|
+
recorded_parent_branch = (
|
|
2032
|
+
str(prepare_record.get("parent_branch") or prepare_details.get("parent_branch") or "").strip() or None
|
|
2033
|
+
)
|
|
2034
|
+
recorded_branch_kind = (
|
|
2035
|
+
str(prepare_record.get("branch_kind") or prepare_details.get("branch_kind") or "").strip().lower()
|
|
2036
|
+
or self._branch_kind_from_name(normalized_branch)
|
|
2037
|
+
)
|
|
2038
|
+
|
|
2039
|
+
latest_idea = latest_idea or self._latest_idea_for_branch(quest_root, normalized_branch)
|
|
2040
|
+
latest_run = latest_run or self._latest_main_run_for_branch(quest_root, normalized_branch)
|
|
2041
|
+
if not latest_run and recorded_branch_kind == "idea":
|
|
2042
|
+
latest_run = self._latest_child_main_run_for_branch(quest_root, normalized_branch)
|
|
2043
|
+
if not latest_run and recorded_parent_branch:
|
|
2044
|
+
latest_run = self._latest_main_run_for_branch(quest_root, recorded_parent_branch)
|
|
2045
|
+
resolved_idea_id = (
|
|
2046
|
+
normalized_idea_id
|
|
2047
|
+
or str((latest_run or {}).get("idea_id") or "").strip()
|
|
2048
|
+
or str((latest_idea or {}).get("idea_id") or "").strip()
|
|
2049
|
+
or str(prepare_record.get("idea_id") or "").strip()
|
|
2050
|
+
or self._latest_branch_idea_id(quest_root, normalized_branch)
|
|
2051
|
+
or None
|
|
2052
|
+
)
|
|
2053
|
+
idea_paths = dict((latest_idea or {}).get("paths") or {}) if isinstance((latest_idea or {}).get("paths"), dict) else {}
|
|
2054
|
+
recorded_root = (
|
|
2055
|
+
str((latest_idea or {}).get("worktree_root") or "").strip()
|
|
2056
|
+
or str((latest_run or {}).get("worktree_root") or "").strip()
|
|
2057
|
+
or str(prepare_record.get("worktree_root") or "").strip()
|
|
2058
|
+
or None
|
|
2059
|
+
)
|
|
2060
|
+
return {
|
|
2061
|
+
"branch": normalized_branch,
|
|
2062
|
+
"idea_id": resolved_idea_id,
|
|
2063
|
+
"run_id": normalized_run_id or str((latest_run or {}).get("run_id") or "").strip() or None,
|
|
2064
|
+
"has_main_result": bool((latest_run or {}).get("run_id")),
|
|
2065
|
+
"latest_idea": latest_idea,
|
|
2066
|
+
"latest_main_run": latest_run,
|
|
2067
|
+
"branch_kind": recorded_branch_kind,
|
|
2068
|
+
"parent_branch": recorded_parent_branch,
|
|
2069
|
+
"recorded_worktree_root": recorded_root,
|
|
2070
|
+
"idea_md_path": str(idea_paths.get("idea_md") or "").strip() or None,
|
|
2071
|
+
"idea_draft_path": str(idea_paths.get("idea_draft_md") or "").strip() or None,
|
|
2072
|
+
"suggested_worktree_root": self._branch_activation_worktree_root(
|
|
2073
|
+
quest_root,
|
|
2074
|
+
branch_name=normalized_branch,
|
|
2075
|
+
idea_id=resolved_idea_id,
|
|
2076
|
+
run_id=(
|
|
2077
|
+
normalized_run_id
|
|
2078
|
+
or str(prepare_record.get("run_id") or "").strip()
|
|
2079
|
+
or str((latest_run or {}).get("run_id") or "").strip()
|
|
2080
|
+
or None
|
|
2081
|
+
),
|
|
2082
|
+
),
|
|
2083
|
+
}
|
|
2084
|
+
|
|
1280
2085
|
def _normalize_foundation_ref(self, foundation_ref: dict[str, Any] | str | None) -> dict[str, Any]:
|
|
1281
2086
|
if foundation_ref is None:
|
|
1282
2087
|
return {"kind": "current_head", "ref": None}
|
|
@@ -1445,6 +2250,17 @@ class ArtifactService:
|
|
|
1445
2250
|
]
|
|
1446
2251
|
return candidates[-1] if candidates else None
|
|
1447
2252
|
|
|
2253
|
+
def _latest_child_main_run_for_branch(self, quest_root: Path, branch_name: str) -> dict[str, Any] | None:
|
|
2254
|
+
normalized_branch = str(branch_name or "").strip()
|
|
2255
|
+
if not normalized_branch:
|
|
2256
|
+
return None
|
|
2257
|
+
candidates = [
|
|
2258
|
+
item
|
|
2259
|
+
for item in self._main_run_artifacts(quest_root)
|
|
2260
|
+
if str(item.get("parent_branch") or "").strip() == normalized_branch
|
|
2261
|
+
]
|
|
2262
|
+
return candidates[-1] if candidates else None
|
|
2263
|
+
|
|
1448
2264
|
def _latest_idea_for_branch(self, quest_root: Path, branch_name: str) -> dict[str, Any] | None:
|
|
1449
2265
|
normalized_branch = str(branch_name or "").strip()
|
|
1450
2266
|
if not normalized_branch:
|
|
@@ -1506,8 +2322,19 @@ class ArtifactService:
|
|
|
1506
2322
|
) -> tuple[str, Path, str | None]:
|
|
1507
2323
|
current_root_raw = str(state.get("current_workspace_root") or "").strip()
|
|
1508
2324
|
head_root_raw = str(state.get("research_head_worktree_root") or "").strip()
|
|
2325
|
+
paper_parent_root_raw = str(state.get("paper_parent_worktree_root") or "").strip()
|
|
2326
|
+
current_branch_raw = str(state.get("current_workspace_branch") or "").strip()
|
|
2327
|
+
research_head_branch_raw = str(state.get("research_head_branch") or "").strip()
|
|
2328
|
+
paper_parent_branch_raw = str(state.get("paper_parent_branch") or "").strip()
|
|
2329
|
+
workspace_mode = str(state.get("workspace_mode") or "").strip().lower()
|
|
2330
|
+
prefer_paper_parent = workspace_mode == "paper" or self._branch_kind_from_name(current_branch_raw) == "paper"
|
|
1509
2331
|
parent_worktree_root: Path | None = None
|
|
1510
|
-
|
|
2332
|
+
root_candidates = (
|
|
2333
|
+
(paper_parent_root_raw, head_root_raw, current_root_raw)
|
|
2334
|
+
if prefer_paper_parent
|
|
2335
|
+
else (current_root_raw, head_root_raw, paper_parent_root_raw)
|
|
2336
|
+
)
|
|
2337
|
+
for raw in root_candidates:
|
|
1511
2338
|
if not raw:
|
|
1512
2339
|
continue
|
|
1513
2340
|
candidate = Path(raw)
|
|
@@ -1518,15 +2345,36 @@ class ArtifactService:
|
|
|
1518
2345
|
parent_worktree_root = self._workspace_root_for(quest_root)
|
|
1519
2346
|
|
|
1520
2347
|
parent_branch = (
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
2348
|
+
(
|
|
2349
|
+
paper_parent_branch_raw
|
|
2350
|
+
or research_head_branch_raw
|
|
2351
|
+
or current_branch_raw
|
|
2352
|
+
or current_branch(parent_worktree_root)
|
|
2353
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
2354
|
+
)
|
|
2355
|
+
if prefer_paper_parent
|
|
2356
|
+
else (
|
|
2357
|
+
current_branch_raw
|
|
2358
|
+
or research_head_branch_raw
|
|
2359
|
+
or paper_parent_branch_raw
|
|
2360
|
+
or current_branch(parent_worktree_root)
|
|
2361
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
2362
|
+
)
|
|
1525
2363
|
)
|
|
1526
2364
|
parent_branch = str(parent_branch or "").strip()
|
|
1527
2365
|
if not parent_branch:
|
|
1528
2366
|
raise ValueError("Unable to resolve a parent branch for the analysis campaign.")
|
|
1529
2367
|
|
|
2368
|
+
if self._branch_kind_from_name(parent_branch) == "idea":
|
|
2369
|
+
latest_child_run = self._latest_child_main_run_for_branch(quest_root, parent_branch)
|
|
2370
|
+
if isinstance(latest_child_run, dict) and str(latest_child_run.get("branch") or "").strip():
|
|
2371
|
+
parent_branch = str(latest_child_run.get("branch") or "").strip()
|
|
2372
|
+
recorded_worktree_root = str(latest_child_run.get("worktree_root") or "").strip()
|
|
2373
|
+
if recorded_worktree_root:
|
|
2374
|
+
candidate = Path(recorded_worktree_root)
|
|
2375
|
+
if candidate.exists():
|
|
2376
|
+
parent_worktree_root = candidate
|
|
2377
|
+
|
|
1530
2378
|
idea_id = self._latest_branch_idea_id(quest_root, parent_branch) or str(state.get("active_idea_id") or "").strip() or None
|
|
1531
2379
|
return parent_branch, parent_worktree_root, idea_id
|
|
1532
2380
|
|
|
@@ -1568,15 +2416,22 @@ class ArtifactService:
|
|
|
1568
2416
|
state=state,
|
|
1569
2417
|
foundation_ref={"kind": "idea", "ref": str(latest_idea.get("idea_id") or "").strip()},
|
|
1570
2418
|
)
|
|
2419
|
+
current_workspace_branch = str(state.get("current_workspace_branch") or "").strip()
|
|
2420
|
+
research_head_branch = str(state.get("research_head_branch") or "").strip()
|
|
1571
2421
|
active_branch = (
|
|
1572
|
-
|
|
1573
|
-
or
|
|
2422
|
+
current_workspace_branch
|
|
2423
|
+
or research_head_branch
|
|
2424
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
1574
2425
|
)
|
|
1575
2426
|
if normalized_branch and active_branch and normalized_branch == active_branch:
|
|
1576
2427
|
return self._resolve_idea_foundation(
|
|
1577
2428
|
quest_root,
|
|
1578
2429
|
state=state,
|
|
1579
|
-
foundation_ref=
|
|
2430
|
+
foundation_ref=(
|
|
2431
|
+
{"kind": "branch", "ref": normalized_branch}
|
|
2432
|
+
if current_workspace_branch and research_head_branch and current_workspace_branch != research_head_branch
|
|
2433
|
+
else None
|
|
2434
|
+
),
|
|
1580
2435
|
)
|
|
1581
2436
|
return self._resolve_idea_foundation(
|
|
1582
2437
|
quest_root,
|
|
@@ -1614,8 +2469,8 @@ class ArtifactService:
|
|
|
1614
2469
|
) -> tuple[str, str, dict[str, Any]]:
|
|
1615
2470
|
normalized_intent = self._normalize_lineage_intent(lineage_intent) or "continue_line"
|
|
1616
2471
|
active_branch = (
|
|
1617
|
-
str(state.get("
|
|
1618
|
-
or str(state.get("
|
|
2472
|
+
str(state.get("current_workspace_branch") or "").strip()
|
|
2473
|
+
or str(state.get("research_head_branch") or "").strip()
|
|
1619
2474
|
)
|
|
1620
2475
|
if not active_branch:
|
|
1621
2476
|
active_branch = current_branch(self._workspace_root_for(quest_root))
|
|
@@ -1643,6 +2498,7 @@ class ArtifactService:
|
|
|
1643
2498
|
def list_research_branches(self, quest_root: Path) -> dict[str, Any]:
|
|
1644
2499
|
state = self.quest_service.read_research_state(quest_root)
|
|
1645
2500
|
active_head_branch = str(state.get("research_head_branch") or "").strip() or None
|
|
2501
|
+
active_workspace_branch = str(state.get("current_workspace_branch") or "").strip() or None
|
|
1646
2502
|
idea_records = self._idea_artifacts(quest_root)
|
|
1647
2503
|
main_runs = self._main_run_artifacts(quest_root)
|
|
1648
2504
|
|
|
@@ -1709,6 +2565,7 @@ class ArtifactService:
|
|
|
1709
2565
|
"verdict": record.get("verdict"),
|
|
1710
2566
|
"status": record.get("status"),
|
|
1711
2567
|
"idea_id": record.get("idea_id"),
|
|
2568
|
+
"parent_branch": record.get("parent_branch"),
|
|
1712
2569
|
"primary_metric_id": details.get("primary_metric_id"),
|
|
1713
2570
|
"primary_value": details.get("primary_value"),
|
|
1714
2571
|
"delta_vs_baseline": details.get("delta_vs_baseline"),
|
|
@@ -1721,6 +2578,8 @@ class ArtifactService:
|
|
|
1721
2578
|
|
|
1722
2579
|
if active_head_branch:
|
|
1723
2580
|
ensure_branch_entry(active_head_branch)
|
|
2581
|
+
if active_workspace_branch:
|
|
2582
|
+
ensure_branch_entry(active_workspace_branch)
|
|
1724
2583
|
|
|
1725
2584
|
ordered_branches = sorted(
|
|
1726
2585
|
grouped.values(),
|
|
@@ -1756,10 +2615,15 @@ class ArtifactService:
|
|
|
1756
2615
|
else {}
|
|
1757
2616
|
)
|
|
1758
2617
|
parent_branch = str(latest_idea.get("parent_branch") or "").strip() or None
|
|
2618
|
+
experiment_parent_branch = (
|
|
2619
|
+
str((latest_experiment or {}).get("parent_branch") or "").strip()
|
|
2620
|
+
if isinstance(latest_experiment, dict)
|
|
2621
|
+
else None
|
|
2622
|
+
) or None
|
|
1759
2623
|
foundation_branch = (
|
|
1760
2624
|
str(latest_foundation.get("branch") or latest_foundation.get("ref") or "").strip() or None
|
|
1761
2625
|
)
|
|
1762
|
-
resolved_parent_branch = parent_branch or foundation_branch
|
|
2626
|
+
resolved_parent_branch = parent_branch or experiment_parent_branch or foundation_branch
|
|
1763
2627
|
has_main_result = isinstance(latest_experiment, dict) and bool(latest_experiment.get("run_id"))
|
|
1764
2628
|
numeric_branch_no = recorded_branch_numbers.get(branch_name)
|
|
1765
2629
|
if numeric_branch_no is None:
|
|
@@ -1774,7 +2638,8 @@ class ArtifactService:
|
|
|
1774
2638
|
"branch_name": branch_name,
|
|
1775
2639
|
"worktree_root": item.get("worktree_root"),
|
|
1776
2640
|
"is_active_head": branch_name == active_head_branch,
|
|
1777
|
-
"
|
|
2641
|
+
"is_active_workspace": branch_name == active_workspace_branch,
|
|
2642
|
+
"idea_id": latest_idea.get("idea_id") or (latest_experiment.get("idea_id") if isinstance(latest_experiment, dict) else None),
|
|
1778
2643
|
"idea_title": latest_idea.get("title"),
|
|
1779
2644
|
"idea_problem": latest_idea.get("problem"),
|
|
1780
2645
|
"next_target": latest_idea.get("next_target"),
|
|
@@ -1810,6 +2675,7 @@ class ArtifactService:
|
|
|
1810
2675
|
return {
|
|
1811
2676
|
"ok": True,
|
|
1812
2677
|
"active_head_branch": active_head_branch,
|
|
2678
|
+
"active_workspace_branch": active_workspace_branch,
|
|
1813
2679
|
"count": len(branches),
|
|
1814
2680
|
"branches": branches,
|
|
1815
2681
|
}
|
|
@@ -1819,9 +2685,10 @@ class ArtifactService:
|
|
|
1819
2685
|
snapshot = self.quest_service.snapshot(self._quest_id(quest_root))
|
|
1820
2686
|
active_campaign_id = str(state.get("active_analysis_campaign_id") or "").strip() or None
|
|
1821
2687
|
analysis_parent_branch = str(state.get("analysis_parent_branch") or "").strip() or None
|
|
2688
|
+
paper_parent_branch = str(state.get("paper_parent_branch") or "").strip() or None
|
|
1822
2689
|
current_workspace_branch = str(state.get("current_workspace_branch") or "").strip() or None
|
|
1823
2690
|
research_head_branch = str(state.get("research_head_branch") or "").strip() or None
|
|
1824
|
-
canonical_branch = analysis_parent_branch or current_workspace_branch or research_head_branch
|
|
2691
|
+
canonical_branch = analysis_parent_branch or paper_parent_branch or current_workspace_branch or research_head_branch
|
|
1825
2692
|
latest_main_run = self._latest_main_run_for_branch(quest_root, canonical_branch or "")
|
|
1826
2693
|
selected_outline = read_json(self._paper_selected_outline_path(quest_root), {})
|
|
1827
2694
|
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
@@ -1888,32 +2755,82 @@ class ArtifactService:
|
|
|
1888
2755
|
}
|
|
1889
2756
|
|
|
1890
2757
|
def list_paper_outlines(self, quest_root: Path) -> dict[str, Any]:
|
|
1891
|
-
|
|
2758
|
+
selected_outline_path = self._paper_selected_outline_path(quest_root)
|
|
2759
|
+
selected_outline = read_json(selected_outline_path, {})
|
|
1892
2760
|
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
(
|
|
1896
|
-
(
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
2761
|
+
if not selected_outline:
|
|
2762
|
+
fallback_selected_outline_path = quest_root / "paper" / "selected_outline.json"
|
|
2763
|
+
fallback_selected_outline = read_json(fallback_selected_outline_path, {})
|
|
2764
|
+
if isinstance(fallback_selected_outline, dict) and fallback_selected_outline:
|
|
2765
|
+
selected_outline = fallback_selected_outline
|
|
2766
|
+
selected_outline_path = fallback_selected_outline_path
|
|
2767
|
+
|
|
2768
|
+
selected_outline_id = str(selected_outline.get("outline_id") or "").strip()
|
|
2769
|
+
status_rank = {"candidate": 1, "revised": 2, "selected": 3}
|
|
2770
|
+
outlines_by_id: dict[str, dict[str, Any]] = {}
|
|
2771
|
+
seen_paper_roots: set[str] = set()
|
|
2772
|
+
paper_roots: list[Path] = []
|
|
2773
|
+
for root in (self._paper_root(quest_root), quest_root / "paper"):
|
|
2774
|
+
try:
|
|
2775
|
+
key = str(root.resolve())
|
|
2776
|
+
except FileNotFoundError:
|
|
2777
|
+
key = str(root)
|
|
2778
|
+
if key in seen_paper_roots:
|
|
2779
|
+
continue
|
|
2780
|
+
seen_paper_roots.add(key)
|
|
2781
|
+
paper_roots.append(root)
|
|
2782
|
+
|
|
2783
|
+
for paper_root in paper_roots:
|
|
2784
|
+
for default_status, relative_parts in (
|
|
2785
|
+
("candidate", ("outlines", "candidates")),
|
|
2786
|
+
("revised", ("outlines", "revisions")),
|
|
2787
|
+
):
|
|
2788
|
+
root = paper_root.joinpath(*relative_parts)
|
|
2789
|
+
if not root.exists():
|
|
1901
2790
|
continue
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2791
|
+
for path in sorted(root.glob("outline-*.json")):
|
|
2792
|
+
record = read_json(path, {})
|
|
2793
|
+
if not isinstance(record, dict) or not record:
|
|
2794
|
+
continue
|
|
2795
|
+
outline_id = str(record.get("outline_id") or path.stem).strip() or path.stem
|
|
2796
|
+
item = {
|
|
1905
2797
|
"outline_id": outline_id,
|
|
1906
2798
|
"title": str(record.get("title") or outline_id).strip() or outline_id,
|
|
1907
|
-
"status": str(record.get("status") or
|
|
2799
|
+
"status": str(record.get("status") or default_status).strip() or default_status,
|
|
1908
2800
|
"review_result": str(record.get("review_result") or "").strip() or None,
|
|
1909
2801
|
"path": str(path),
|
|
1910
|
-
"is_selected": outline_id ==
|
|
2802
|
+
"is_selected": outline_id == selected_outline_id,
|
|
1911
2803
|
}
|
|
1912
|
-
|
|
2804
|
+
current = outlines_by_id.get(outline_id)
|
|
2805
|
+
if current is None or status_rank.get(str(item.get("status") or ""), 0) >= status_rank.get(
|
|
2806
|
+
str(current.get("status") or ""),
|
|
2807
|
+
0,
|
|
2808
|
+
):
|
|
2809
|
+
outlines_by_id[outline_id] = item
|
|
2810
|
+
|
|
2811
|
+
if selected_outline_id:
|
|
2812
|
+
selected_item = {
|
|
2813
|
+
"outline_id": selected_outline_id,
|
|
2814
|
+
"title": str(selected_outline.get("title") or selected_outline_id).strip() or selected_outline_id,
|
|
2815
|
+
"status": str(selected_outline.get("status") or "selected").strip() or "selected",
|
|
2816
|
+
"review_result": str(selected_outline.get("review_result") or "").strip() or None,
|
|
2817
|
+
"path": str(selected_outline_path),
|
|
2818
|
+
"is_selected": True,
|
|
2819
|
+
}
|
|
2820
|
+
current = outlines_by_id.get(selected_outline_id)
|
|
2821
|
+
if current is None or status_rank.get(str(selected_item.get("status") or ""), 0) >= status_rank.get(
|
|
2822
|
+
str(current.get("status") or ""),
|
|
2823
|
+
0,
|
|
2824
|
+
):
|
|
2825
|
+
outlines_by_id[selected_outline_id] = selected_item
|
|
2826
|
+
else:
|
|
2827
|
+
current["is_selected"] = True
|
|
2828
|
+
|
|
2829
|
+
outlines = list(outlines_by_id.values())
|
|
1913
2830
|
outlines.sort(key=lambda item: (str(item.get("outline_id") or ""), str(item.get("status") or "")))
|
|
1914
2831
|
return {
|
|
1915
2832
|
"ok": True,
|
|
1916
|
-
"selected_outline_ref":
|
|
2833
|
+
"selected_outline_ref": selected_outline_id or None,
|
|
1917
2834
|
"selected_outline": selected_outline or None,
|
|
1918
2835
|
"count": len(outlines),
|
|
1919
2836
|
"outlines": outlines,
|
|
@@ -2139,65 +3056,438 @@ class ArtifactService:
|
|
|
2139
3056
|
self._touch_quest_updated_at(quest_root)
|
|
2140
3057
|
return {
|
|
2141
3058
|
"ok": True,
|
|
2142
|
-
"message": message,
|
|
2143
|
-
"guidance": "Checkpoint created. Continue from the updated quest branch state.",
|
|
2144
|
-
**result,
|
|
3059
|
+
"message": message,
|
|
3060
|
+
"guidance": "Checkpoint created. Continue from the updated quest branch state.",
|
|
3061
|
+
**result,
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
def prepare_branch(
|
|
3065
|
+
self,
|
|
3066
|
+
quest_root: Path,
|
|
3067
|
+
*,
|
|
3068
|
+
run_id: str | None = None,
|
|
3069
|
+
idea_id: str | None = None,
|
|
3070
|
+
branch: str | None = None,
|
|
3071
|
+
branch_kind: str = "run",
|
|
3072
|
+
create_worktree_flag: bool = True,
|
|
3073
|
+
start_point: str | None = None,
|
|
3074
|
+
) -> dict:
|
|
3075
|
+
state = self.quest_service.read_research_state(quest_root)
|
|
3076
|
+
parent_branch = (
|
|
3077
|
+
str(start_point or "").strip()
|
|
3078
|
+
or str(state.get("current_workspace_branch") or "").strip()
|
|
3079
|
+
or str(state.get("research_head_branch") or "").strip()
|
|
3080
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
3081
|
+
or current_branch(quest_root)
|
|
3082
|
+
)
|
|
3083
|
+
start_ref = start_point or parent_branch
|
|
3084
|
+
branch_name = branch or self._default_branch_name(quest_root, run_id=run_id, idea_id=idea_id, branch_kind=branch_kind)
|
|
3085
|
+
branch_result = ensure_branch(quest_root, branch_name, start_point=start_ref, checkout=False)
|
|
3086
|
+
worktree_result = None
|
|
3087
|
+
worktree_root = None
|
|
3088
|
+
if create_worktree_flag:
|
|
3089
|
+
worktree_root = self._prepare_branch_worktree_root(
|
|
3090
|
+
quest_root,
|
|
3091
|
+
branch_name=branch_name,
|
|
3092
|
+
branch_kind=branch_kind,
|
|
3093
|
+
run_id=run_id,
|
|
3094
|
+
idea_id=idea_id,
|
|
3095
|
+
)
|
|
3096
|
+
worktree_result = create_worktree(
|
|
3097
|
+
quest_root,
|
|
3098
|
+
branch=branch_name,
|
|
3099
|
+
worktree_root=worktree_root,
|
|
3100
|
+
start_point=start_ref,
|
|
3101
|
+
)
|
|
3102
|
+
artifact_result = self.record(
|
|
3103
|
+
quest_root,
|
|
3104
|
+
{
|
|
3105
|
+
"kind": "decision",
|
|
3106
|
+
"status": "prepared",
|
|
3107
|
+
"verdict": "prepared",
|
|
3108
|
+
"action": "prepare_branch",
|
|
3109
|
+
"reason": f"Prepared branch `{branch_name}` for the next quest step.",
|
|
3110
|
+
"branch": branch_name,
|
|
3111
|
+
"run_id": run_id,
|
|
3112
|
+
"idea_id": idea_id,
|
|
3113
|
+
"branch_kind": branch_kind,
|
|
3114
|
+
"parent_branch": parent_branch,
|
|
3115
|
+
"start_point": start_ref,
|
|
3116
|
+
"worktree_root": str(worktree_root) if worktree_root else None,
|
|
3117
|
+
"workspace_mode": self._workspace_mode_for_branch(branch_name, has_idea=bool(idea_id)),
|
|
3118
|
+
"source": {"kind": "system", "role": "artifact"},
|
|
3119
|
+
},
|
|
3120
|
+
checkpoint=False,
|
|
3121
|
+
workspace_root=worktree_root if worktree_root else None,
|
|
3122
|
+
)
|
|
3123
|
+
return {
|
|
3124
|
+
"ok": True,
|
|
3125
|
+
"branch": branch_name,
|
|
3126
|
+
"branch_result": branch_result,
|
|
3127
|
+
"worktree": worktree_result,
|
|
3128
|
+
"worktree_root": str(worktree_root) if worktree_root else None,
|
|
3129
|
+
"parent_branch": parent_branch,
|
|
3130
|
+
"start_point": start_ref,
|
|
3131
|
+
"guidance": "Use this branch/worktree for the isolated idea or run. Keep durable outputs under quest_root.",
|
|
3132
|
+
"artifact": artifact_result,
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
def activate_branch(
|
|
3136
|
+
self,
|
|
3137
|
+
quest_root: Path,
|
|
3138
|
+
*,
|
|
3139
|
+
branch: str | None = None,
|
|
3140
|
+
idea_id: str | None = None,
|
|
3141
|
+
run_id: str | None = None,
|
|
3142
|
+
anchor: str | None = "auto",
|
|
3143
|
+
promote_to_head: bool = False,
|
|
3144
|
+
create_worktree_if_missing: bool = True,
|
|
3145
|
+
) -> dict[str, Any]:
|
|
3146
|
+
state = self.quest_service.read_research_state(quest_root)
|
|
3147
|
+
active_campaign_id = str(state.get("active_analysis_campaign_id") or "").strip() or None
|
|
3148
|
+
if active_campaign_id:
|
|
3149
|
+
raise ValueError(
|
|
3150
|
+
"activate_branch cannot run while an analysis campaign is active. "
|
|
3151
|
+
"Finish or close the campaign first."
|
|
3152
|
+
)
|
|
3153
|
+
|
|
3154
|
+
target = self._resolve_branch_activation_target(
|
|
3155
|
+
quest_root,
|
|
3156
|
+
branch=branch,
|
|
3157
|
+
idea_id=idea_id,
|
|
3158
|
+
run_id=run_id,
|
|
3159
|
+
)
|
|
3160
|
+
branch_name = str(target.get("branch") or "").strip()
|
|
3161
|
+
if str(target.get("branch_kind") or self._branch_kind_from_name(branch_name)).strip().lower() != "paper":
|
|
3162
|
+
self._require_baseline_gate_open(quest_root, action="activate_branch")
|
|
3163
|
+
resolved_idea_id = str(target.get("idea_id") or "").strip() or None
|
|
3164
|
+
latest_main_run = (
|
|
3165
|
+
dict(target.get("latest_main_run") or {})
|
|
3166
|
+
if isinstance(target.get("latest_main_run"), dict)
|
|
3167
|
+
else {}
|
|
3168
|
+
)
|
|
3169
|
+
latest_idea = (
|
|
3170
|
+
dict(target.get("latest_idea") or {})
|
|
3171
|
+
if isinstance(target.get("latest_idea"), dict)
|
|
3172
|
+
else {}
|
|
3173
|
+
)
|
|
3174
|
+
branch_kind = str(target.get("branch_kind") or self._branch_kind_from_name(branch_name)).strip().lower() or "branch"
|
|
3175
|
+
source_parent_branch = str(target.get("parent_branch") or "").strip() or None
|
|
3176
|
+
|
|
3177
|
+
workspace_root = self._branch_workspace_root(quest_root, branch_name)
|
|
3178
|
+
worktree_result = None
|
|
3179
|
+
worktree_created = False
|
|
3180
|
+
if workspace_root is None:
|
|
3181
|
+
recorded_root = str(target.get("recorded_worktree_root") or "").strip()
|
|
3182
|
+
if recorded_root:
|
|
3183
|
+
candidate = Path(recorded_root)
|
|
3184
|
+
if candidate.exists():
|
|
3185
|
+
workspace_root = candidate
|
|
3186
|
+
if workspace_root is None:
|
|
3187
|
+
if not create_worktree_if_missing:
|
|
3188
|
+
raise FileNotFoundError(
|
|
3189
|
+
f"No existing worktree is available for branch `{branch_name}` and create_worktree_if_missing=False."
|
|
3190
|
+
)
|
|
3191
|
+
workspace_root = Path(target.get("suggested_worktree_root") or "")
|
|
3192
|
+
worktree_result = create_worktree(
|
|
3193
|
+
quest_root,
|
|
3194
|
+
branch=branch_name,
|
|
3195
|
+
worktree_root=workspace_root,
|
|
3196
|
+
start_point=branch_name,
|
|
3197
|
+
)
|
|
3198
|
+
if not bool(worktree_result.get("ok")):
|
|
3199
|
+
raise RuntimeError(
|
|
3200
|
+
f"Failed to activate branch `{branch_name}`: {worktree_result.get('stderr') or 'worktree creation failed.'}"
|
|
3201
|
+
)
|
|
3202
|
+
worktree_created = True
|
|
3203
|
+
|
|
3204
|
+
resolved_workspace_root = workspace_root or quest_root
|
|
3205
|
+
idea_md_path = (
|
|
3206
|
+
str(target.get("idea_md_path") or "").strip()
|
|
3207
|
+
or str((dict(latest_idea.get("paths") or {}) if isinstance(latest_idea.get("paths"), dict) else {}).get("idea_md") or "").strip()
|
|
3208
|
+
or (str(resolved_workspace_root / "memory" / "ideas" / resolved_idea_id / "idea.md") if resolved_idea_id else "")
|
|
3209
|
+
)
|
|
3210
|
+
idea_draft_path = (
|
|
3211
|
+
str(target.get("idea_draft_path") or "").strip()
|
|
3212
|
+
or str((dict(latest_idea.get("paths") or {}) if isinstance(latest_idea.get("paths"), dict) else {}).get("idea_draft_md") or "").strip()
|
|
3213
|
+
or (str(resolved_workspace_root / "memory" / "ideas" / resolved_idea_id / "draft.md") if resolved_idea_id else "")
|
|
3214
|
+
)
|
|
3215
|
+
resolved_idea_md_path = idea_md_path if resolved_idea_id else None
|
|
3216
|
+
resolved_idea_draft_path = idea_draft_path if resolved_idea_id else None
|
|
3217
|
+
has_main_result = bool(latest_main_run.get("run_id"))
|
|
3218
|
+
if branch_kind == "paper":
|
|
3219
|
+
next_anchor = "write" if str(anchor or "auto").strip().lower() == "auto" else self._resolve_activate_branch_anchor(
|
|
3220
|
+
anchor=anchor,
|
|
3221
|
+
has_idea=bool(resolved_idea_id),
|
|
3222
|
+
has_main_result=has_main_result,
|
|
3223
|
+
)
|
|
3224
|
+
else:
|
|
3225
|
+
next_anchor = self._resolve_activate_branch_anchor(
|
|
3226
|
+
anchor=anchor,
|
|
3227
|
+
has_idea=bool(resolved_idea_id),
|
|
3228
|
+
has_main_result=has_main_result,
|
|
3229
|
+
)
|
|
3230
|
+
workspace_mode = self._workspace_mode_for_branch(branch_name, has_idea=bool(resolved_idea_id))
|
|
3231
|
+
source_run_id = (
|
|
3232
|
+
str(target.get("run_id") or "").strip()
|
|
3233
|
+
or str(latest_main_run.get("run_id") or "").strip()
|
|
3234
|
+
or None
|
|
3235
|
+
)
|
|
3236
|
+
|
|
3237
|
+
artifact = self.record(
|
|
3238
|
+
quest_root,
|
|
3239
|
+
{
|
|
3240
|
+
"kind": "decision",
|
|
3241
|
+
"status": "completed",
|
|
3242
|
+
"verdict": "continue",
|
|
3243
|
+
"action": "activate_branch",
|
|
3244
|
+
"summary": f"Activated durable branch `{branch_name}` as the current workspace.",
|
|
3245
|
+
"reason": (
|
|
3246
|
+
"Return to an existing research branch without creating a new lineage node, "
|
|
3247
|
+
"so follow-up experiments or decisions continue from the correct historical context."
|
|
3248
|
+
),
|
|
3249
|
+
"idea_id": resolved_idea_id,
|
|
3250
|
+
"run_id": str(latest_main_run.get("run_id") or "").strip() or None,
|
|
3251
|
+
"branch": branch_name,
|
|
3252
|
+
"worktree_root": str(resolved_workspace_root),
|
|
3253
|
+
"worktree_rel_path": self._workspace_relative(quest_root, resolved_workspace_root),
|
|
3254
|
+
"flow_type": "branch_activation",
|
|
3255
|
+
"protocol_step": "activate",
|
|
3256
|
+
"details": {
|
|
3257
|
+
"activate_branch_by": (
|
|
3258
|
+
"idea_id"
|
|
3259
|
+
if str(idea_id or "").strip()
|
|
3260
|
+
else "run_id"
|
|
3261
|
+
if str(run_id or "").strip()
|
|
3262
|
+
else "branch"
|
|
3263
|
+
),
|
|
3264
|
+
"promote_to_head": bool(promote_to_head),
|
|
3265
|
+
"worktree_created": worktree_created,
|
|
3266
|
+
"next_anchor": next_anchor,
|
|
3267
|
+
"workspace_mode": workspace_mode,
|
|
3268
|
+
"latest_main_run_id": str(latest_main_run.get("run_id") or "").strip() or None,
|
|
3269
|
+
"branch_kind": branch_kind,
|
|
3270
|
+
"paper_parent_branch": source_parent_branch if branch_kind == "paper" else None,
|
|
3271
|
+
},
|
|
3272
|
+
},
|
|
3273
|
+
checkpoint=False,
|
|
3274
|
+
workspace_root=resolved_workspace_root,
|
|
3275
|
+
)
|
|
3276
|
+
|
|
3277
|
+
research_state_updates: dict[str, Any] = {
|
|
3278
|
+
"active_idea_id": resolved_idea_id,
|
|
3279
|
+
"current_workspace_branch": branch_name,
|
|
3280
|
+
"current_workspace_root": str(resolved_workspace_root),
|
|
3281
|
+
"active_idea_md_path": resolved_idea_md_path,
|
|
3282
|
+
"active_idea_draft_path": resolved_idea_draft_path,
|
|
3283
|
+
"active_analysis_campaign_id": None,
|
|
3284
|
+
"analysis_parent_branch": None,
|
|
3285
|
+
"analysis_parent_worktree_root": None,
|
|
3286
|
+
"paper_parent_branch": source_parent_branch if branch_kind == "paper" else None,
|
|
3287
|
+
"paper_parent_worktree_root": (
|
|
3288
|
+
str(self._branch_workspace_root(quest_root, source_parent_branch))
|
|
3289
|
+
if branch_kind == "paper" and source_parent_branch and self._branch_workspace_root(quest_root, source_parent_branch)
|
|
3290
|
+
else None
|
|
3291
|
+
),
|
|
3292
|
+
"paper_parent_run_id": source_run_id if branch_kind == "paper" else None,
|
|
3293
|
+
"next_pending_slice_id": None,
|
|
3294
|
+
"workspace_mode": workspace_mode,
|
|
3295
|
+
"last_flow_type": "branch_activation",
|
|
3296
|
+
}
|
|
3297
|
+
if promote_to_head:
|
|
3298
|
+
research_state_updates["research_head_branch"] = branch_name
|
|
3299
|
+
research_state_updates["research_head_worktree_root"] = str(resolved_workspace_root)
|
|
3300
|
+
research_state = self.quest_service.update_research_state(quest_root, **research_state_updates)
|
|
3301
|
+
self.quest_service.update_settings(self._quest_id(quest_root), active_anchor=next_anchor)
|
|
3302
|
+
|
|
3303
|
+
interaction = self.interact(
|
|
3304
|
+
quest_root,
|
|
3305
|
+
kind="milestone",
|
|
3306
|
+
message=(
|
|
3307
|
+
f"Activated branch `{branch_name}`.\n"
|
|
3308
|
+
f"- Worktree: `{resolved_workspace_root}`\n"
|
|
3309
|
+
f"- Active idea: `{resolved_idea_id or 'none'}`\n"
|
|
3310
|
+
f"- Latest main run: `{str(latest_main_run.get('run_id') or '').strip() or 'none'}`\n"
|
|
3311
|
+
f"- Promoted to head: `{bool(promote_to_head)}`\n"
|
|
3312
|
+
f"- Next anchor: `{next_anchor}`"
|
|
3313
|
+
),
|
|
3314
|
+
deliver_to_bound_conversations=True,
|
|
3315
|
+
include_recent_inbound_messages=False,
|
|
3316
|
+
attachments=[
|
|
3317
|
+
{
|
|
3318
|
+
"kind": "branch_activation",
|
|
3319
|
+
"branch": branch_name,
|
|
3320
|
+
"worktree_root": str(resolved_workspace_root),
|
|
3321
|
+
"idea_id": resolved_idea_id,
|
|
3322
|
+
"latest_main_run_id": str(latest_main_run.get("run_id") or "").strip() or None,
|
|
3323
|
+
"next_anchor": next_anchor,
|
|
3324
|
+
"promote_to_head": bool(promote_to_head),
|
|
3325
|
+
}
|
|
3326
|
+
],
|
|
3327
|
+
)
|
|
3328
|
+
return {
|
|
3329
|
+
"ok": True,
|
|
3330
|
+
"branch": branch_name,
|
|
3331
|
+
"worktree_root": str(resolved_workspace_root),
|
|
3332
|
+
"idea_id": resolved_idea_id,
|
|
3333
|
+
"latest_main_run_id": str(latest_main_run.get("run_id") or "").strip() or None,
|
|
3334
|
+
"branch_kind": branch_kind,
|
|
3335
|
+
"source_parent_branch": source_parent_branch,
|
|
3336
|
+
"idea_md_path": resolved_idea_md_path,
|
|
3337
|
+
"idea_draft_path": resolved_idea_draft_path,
|
|
3338
|
+
"workspace_mode": workspace_mode,
|
|
3339
|
+
"next_anchor": next_anchor,
|
|
3340
|
+
"promote_to_head": bool(promote_to_head),
|
|
3341
|
+
"worktree_created": worktree_created,
|
|
3342
|
+
"worktree": worktree_result,
|
|
3343
|
+
"artifact": artifact,
|
|
3344
|
+
"interaction": interaction,
|
|
3345
|
+
"research_state": research_state,
|
|
2145
3346
|
}
|
|
2146
3347
|
|
|
2147
|
-
def
|
|
3348
|
+
def _promote_workspace_to_run_branch(
|
|
2148
3349
|
self,
|
|
2149
3350
|
quest_root: Path,
|
|
2150
3351
|
*,
|
|
2151
|
-
run_id: str
|
|
2152
|
-
idea_id: str | None
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
branch_result = ensure_branch(quest_root, branch_name, start_point=start_ref, checkout=False)
|
|
2162
|
-
worktree_result = None
|
|
2163
|
-
worktree_root = None
|
|
2164
|
-
if create_worktree_flag:
|
|
2165
|
-
worktree_root = canonical_worktree_root(quest_root, run_id or branch_name)
|
|
2166
|
-
worktree_result = create_worktree(
|
|
2167
|
-
quest_root,
|
|
2168
|
-
branch=branch_name,
|
|
2169
|
-
worktree_root=worktree_root,
|
|
2170
|
-
start_point=start_ref,
|
|
3352
|
+
run_id: str,
|
|
3353
|
+
idea_id: str | None,
|
|
3354
|
+
workspace_root: Path,
|
|
3355
|
+
current_branch_name: str,
|
|
3356
|
+
) -> tuple[str, str | None, bool]:
|
|
3357
|
+
branch_kind = self._branch_kind_from_name(current_branch_name)
|
|
3358
|
+
if branch_kind == "paper":
|
|
3359
|
+
raise ValueError(
|
|
3360
|
+
"record_main_experiment cannot run while the active workspace is a paper branch. "
|
|
3361
|
+
"Return to the evidence branch or create a new run branch first."
|
|
2171
3362
|
)
|
|
2172
|
-
|
|
3363
|
+
if branch_kind == "run":
|
|
3364
|
+
prepare_record = self._latest_prepare_branch_record(quest_root, current_branch_name)
|
|
3365
|
+
parent_branch = str(prepare_record.get("parent_branch") or "").strip() or None
|
|
3366
|
+
return current_branch_name, parent_branch, False
|
|
3367
|
+
|
|
3368
|
+
target_branch = self._default_branch_name(quest_root, run_id=run_id, idea_id=idea_id, branch_kind="run")
|
|
3369
|
+
if branch_exists(quest_root, target_branch):
|
|
3370
|
+
raise ValueError(
|
|
3371
|
+
f"Run branch `{target_branch}` already exists. Reuse that run branch or choose a new `run_id`."
|
|
3372
|
+
)
|
|
3373
|
+
|
|
3374
|
+
ensure_branch(quest_root, target_branch, start_point=current_branch_name, checkout=False)
|
|
3375
|
+
run_command(["git", "switch", target_branch], cwd=workspace_root, check=True)
|
|
3376
|
+
self.record(
|
|
2173
3377
|
quest_root,
|
|
2174
3378
|
{
|
|
2175
3379
|
"kind": "decision",
|
|
2176
3380
|
"status": "prepared",
|
|
2177
3381
|
"verdict": "prepared",
|
|
2178
3382
|
"action": "prepare_branch",
|
|
2179
|
-
"reason": f"
|
|
2180
|
-
"branch":
|
|
3383
|
+
"reason": f"Materialized a dedicated main-experiment branch `{target_branch}` before durable recording.",
|
|
3384
|
+
"branch": target_branch,
|
|
2181
3385
|
"run_id": run_id,
|
|
2182
3386
|
"idea_id": idea_id,
|
|
2183
|
-
"branch_kind":
|
|
2184
|
-
"parent_branch":
|
|
2185
|
-
"start_point":
|
|
2186
|
-
"worktree_root": str(
|
|
3387
|
+
"branch_kind": "run",
|
|
3388
|
+
"parent_branch": current_branch_name,
|
|
3389
|
+
"start_point": current_branch_name,
|
|
3390
|
+
"worktree_root": str(workspace_root),
|
|
3391
|
+
"workspace_mode": "run",
|
|
2187
3392
|
"source": {"kind": "system", "role": "artifact"},
|
|
2188
3393
|
},
|
|
2189
3394
|
checkpoint=False,
|
|
3395
|
+
workspace_root=workspace_root,
|
|
3396
|
+
)
|
|
3397
|
+
self.quest_service.update_research_state(
|
|
3398
|
+
quest_root,
|
|
3399
|
+
active_idea_id=idea_id,
|
|
3400
|
+
current_workspace_branch=target_branch,
|
|
3401
|
+
current_workspace_root=str(workspace_root),
|
|
3402
|
+
research_head_branch=target_branch,
|
|
3403
|
+
research_head_worktree_root=str(workspace_root),
|
|
3404
|
+
active_analysis_campaign_id=None,
|
|
3405
|
+
analysis_parent_branch=None,
|
|
3406
|
+
analysis_parent_worktree_root=None,
|
|
3407
|
+
paper_parent_branch=None,
|
|
3408
|
+
paper_parent_worktree_root=None,
|
|
3409
|
+
paper_parent_run_id=None,
|
|
3410
|
+
workspace_mode="run",
|
|
3411
|
+
last_flow_type="main_experiment_branch",
|
|
3412
|
+
)
|
|
3413
|
+
return target_branch, current_branch_name, True
|
|
3414
|
+
|
|
3415
|
+
def _ensure_active_paper_workspace(
|
|
3416
|
+
self,
|
|
3417
|
+
quest_root: Path,
|
|
3418
|
+
*,
|
|
3419
|
+
source_branch: str | None = None,
|
|
3420
|
+
source_run_id: str | None = None,
|
|
3421
|
+
source_idea_id: str | None = None,
|
|
3422
|
+
) -> dict[str, Any]:
|
|
3423
|
+
state = self.quest_service.read_research_state(quest_root)
|
|
3424
|
+
current_branch_name = (
|
|
3425
|
+
str(state.get("current_workspace_branch") or "").strip()
|
|
3426
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
3427
|
+
)
|
|
3428
|
+
current_workspace_root = self._workspace_root_for(quest_root)
|
|
3429
|
+
if (
|
|
3430
|
+
str(state.get("workspace_mode") or "").strip() == "paper"
|
|
3431
|
+
and self._branch_kind_from_name(current_branch_name) == "paper"
|
|
3432
|
+
):
|
|
3433
|
+
return {
|
|
3434
|
+
"ok": True,
|
|
3435
|
+
"branch": current_branch_name,
|
|
3436
|
+
"worktree_root": str(current_workspace_root),
|
|
3437
|
+
"source_branch": str(state.get("paper_parent_branch") or "").strip() or None,
|
|
3438
|
+
"source_run_id": str(state.get("paper_parent_run_id") or "").strip() or None,
|
|
3439
|
+
"source_idea_id": str(state.get("active_idea_id") or "").strip() or None,
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
resolved_source_branch = (
|
|
3443
|
+
str(source_branch or "").strip()
|
|
3444
|
+
or str(state.get("paper_parent_branch") or "").strip()
|
|
3445
|
+
or str(state.get("current_workspace_branch") or "").strip()
|
|
3446
|
+
or str(state.get("research_head_branch") or "").strip()
|
|
3447
|
+
or current_branch(current_workspace_root)
|
|
3448
|
+
)
|
|
3449
|
+
if not resolved_source_branch:
|
|
3450
|
+
raise ValueError("Unable to resolve the source branch for the paper workspace.")
|
|
3451
|
+
|
|
3452
|
+
latest_main_run = self._latest_main_run_for_branch(quest_root, resolved_source_branch)
|
|
3453
|
+
resolved_run_id = (
|
|
3454
|
+
str(source_run_id or "").strip()
|
|
3455
|
+
or str((latest_main_run or {}).get("run_id") or "").strip()
|
|
3456
|
+
or None
|
|
3457
|
+
)
|
|
3458
|
+
resolved_idea_id = (
|
|
3459
|
+
str(source_idea_id or "").strip()
|
|
3460
|
+
or str((latest_main_run or {}).get("idea_id") or "").strip()
|
|
3461
|
+
or str(state.get("active_idea_id") or "").strip()
|
|
3462
|
+
or None
|
|
3463
|
+
)
|
|
3464
|
+
paper_branch = (
|
|
3465
|
+
self._default_branch_name(quest_root, run_id=resolved_run_id, idea_id=resolved_idea_id, branch_kind="paper")
|
|
3466
|
+
if resolved_run_id
|
|
3467
|
+
else f"paper/{slugify(resolved_source_branch, 'paper')}"
|
|
3468
|
+
)
|
|
3469
|
+
if not branch_exists(quest_root, paper_branch):
|
|
3470
|
+
self.prepare_branch(
|
|
3471
|
+
quest_root,
|
|
3472
|
+
run_id=resolved_run_id,
|
|
3473
|
+
idea_id=resolved_idea_id,
|
|
3474
|
+
branch=paper_branch,
|
|
3475
|
+
branch_kind="paper",
|
|
3476
|
+
create_worktree_flag=True,
|
|
3477
|
+
start_point=resolved_source_branch,
|
|
3478
|
+
)
|
|
3479
|
+
activated = self.activate_branch(
|
|
3480
|
+
quest_root,
|
|
3481
|
+
branch=paper_branch,
|
|
3482
|
+
anchor="write",
|
|
3483
|
+
promote_to_head=False,
|
|
3484
|
+
create_worktree_if_missing=True,
|
|
2190
3485
|
)
|
|
2191
3486
|
return {
|
|
2192
|
-
|
|
2193
|
-
"
|
|
2194
|
-
"
|
|
2195
|
-
"
|
|
2196
|
-
"worktree_root": str(worktree_root) if worktree_root else None,
|
|
2197
|
-
"parent_branch": parent_branch,
|
|
2198
|
-
"start_point": start_ref,
|
|
2199
|
-
"guidance": "Use this branch/worktree for the isolated idea or run. Keep durable outputs under quest_root.",
|
|
2200
|
-
"artifact": artifact_result,
|
|
3487
|
+
**activated,
|
|
3488
|
+
"source_branch": resolved_source_branch,
|
|
3489
|
+
"source_run_id": resolved_run_id,
|
|
3490
|
+
"source_idea_id": resolved_idea_id,
|
|
2201
3491
|
}
|
|
2202
3492
|
|
|
2203
3493
|
def submit_idea(
|
|
@@ -2235,8 +3525,8 @@ class ArtifactService:
|
|
|
2235
3525
|
if normalized_mode == "create":
|
|
2236
3526
|
resolved_idea_id = str(idea_id or generate_id("idea")).strip()
|
|
2237
3527
|
active_branch = (
|
|
2238
|
-
str(state.get("
|
|
2239
|
-
or str(state.get("
|
|
3528
|
+
str(state.get("current_workspace_branch") or "").strip()
|
|
3529
|
+
or str(state.get("research_head_branch") or "").strip()
|
|
2240
3530
|
or current_branch(self._workspace_root_for(quest_root))
|
|
2241
3531
|
)
|
|
2242
3532
|
active_parent_branch = self._idea_parent_branch(self._latest_idea_for_branch(quest_root, active_branch))
|
|
@@ -2376,19 +3666,26 @@ class ArtifactService:
|
|
|
2376
3666
|
worktree_root,
|
|
2377
3667
|
message=f"idea: create {resolved_idea_id}",
|
|
2378
3668
|
)
|
|
3669
|
+
idea_md_rel_path = self._workspace_relative(quest_root, idea_md_path)
|
|
3670
|
+
idea_draft_rel_path = self._workspace_relative(quest_root, idea_draft_path)
|
|
2379
3671
|
interaction = self.interact(
|
|
2380
3672
|
quest_root,
|
|
2381
3673
|
kind="milestone",
|
|
2382
|
-
message=(
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
3674
|
+
message=self._build_idea_interaction_message(
|
|
3675
|
+
action="create",
|
|
3676
|
+
idea_id=resolved_idea_id,
|
|
3677
|
+
title=title,
|
|
3678
|
+
problem=problem,
|
|
3679
|
+
hypothesis=hypothesis,
|
|
3680
|
+
mechanism=mechanism,
|
|
3681
|
+
foundation_label=self._format_foundation_label(
|
|
3682
|
+
foundation,
|
|
3683
|
+
fallback=foundation.get("branch") or "current head",
|
|
3684
|
+
),
|
|
3685
|
+
branch_name=branch_name,
|
|
3686
|
+
next_target=next_target,
|
|
3687
|
+
idea_md_rel_path=idea_md_rel_path,
|
|
3688
|
+
draft_md_rel_path=idea_draft_rel_path,
|
|
2392
3689
|
),
|
|
2393
3690
|
deliver_to_bound_conversations=True,
|
|
2394
3691
|
include_recent_inbound_messages=False,
|
|
@@ -2441,9 +3738,17 @@ class ArtifactService:
|
|
|
2441
3738
|
raise ValueError("submit_idea(mode='revise') requires an existing active `idea_id`.")
|
|
2442
3739
|
if normalized_lineage_intent:
|
|
2443
3740
|
raise ValueError("submit_idea(mode='revise') does not accept `lineage_intent`; use mode='create' for new branch lineage.")
|
|
2444
|
-
branch_name = str(
|
|
3741
|
+
branch_name = str(
|
|
3742
|
+
state.get("current_workspace_branch")
|
|
3743
|
+
or state.get("research_head_branch")
|
|
3744
|
+
or f"idea/{quest_id}-{resolved_idea_id}"
|
|
3745
|
+
).strip()
|
|
2445
3746
|
worktree_root = Path(
|
|
2446
|
-
str(
|
|
3747
|
+
str(
|
|
3748
|
+
state.get("current_workspace_root")
|
|
3749
|
+
or state.get("research_head_worktree_root")
|
|
3750
|
+
or canonical_worktree_root(quest_root, f"idea-{resolved_idea_id}")
|
|
3751
|
+
)
|
|
2447
3752
|
)
|
|
2448
3753
|
ensure_dir(worktree_root / "memory" / "ideas" / resolved_idea_id)
|
|
2449
3754
|
idea_md_path = worktree_root / "memory" / "ideas" / resolved_idea_id / "idea.md"
|
|
@@ -2545,34 +3850,48 @@ class ArtifactService:
|
|
|
2545
3850
|
checkpoint=False,
|
|
2546
3851
|
workspace_root=worktree_root,
|
|
2547
3852
|
)
|
|
3853
|
+
research_state_updates: dict[str, Any] = {
|
|
3854
|
+
"active_idea_id": resolved_idea_id,
|
|
3855
|
+
"current_workspace_branch": branch_name,
|
|
3856
|
+
"current_workspace_root": str(worktree_root),
|
|
3857
|
+
"active_idea_md_path": str(idea_md_path),
|
|
3858
|
+
"active_idea_draft_path": str(idea_draft_path),
|
|
3859
|
+
"workspace_mode": "idea",
|
|
3860
|
+
"last_flow_type": "idea_revision",
|
|
3861
|
+
}
|
|
3862
|
+
current_head_branch = str(state.get("research_head_branch") or "").strip()
|
|
3863
|
+
if not current_head_branch or current_head_branch == branch_name:
|
|
3864
|
+
research_state_updates["research_head_branch"] = branch_name
|
|
3865
|
+
research_state_updates["research_head_worktree_root"] = str(worktree_root)
|
|
2548
3866
|
research_state = self.quest_service.update_research_state(
|
|
2549
3867
|
quest_root,
|
|
2550
|
-
|
|
2551
|
-
research_head_branch=branch_name,
|
|
2552
|
-
research_head_worktree_root=str(worktree_root),
|
|
2553
|
-
current_workspace_branch=branch_name,
|
|
2554
|
-
current_workspace_root=str(worktree_root),
|
|
2555
|
-
active_idea_md_path=str(idea_md_path),
|
|
2556
|
-
active_idea_draft_path=str(idea_draft_path),
|
|
2557
|
-
workspace_mode="idea",
|
|
2558
|
-
last_flow_type="idea_revision",
|
|
3868
|
+
**research_state_updates,
|
|
2559
3869
|
)
|
|
2560
3870
|
self.quest_service.update_settings(quest_id, active_anchor="experiment")
|
|
2561
3871
|
checkpoint_result = self._checkpoint_with_optional_push(
|
|
2562
3872
|
worktree_root,
|
|
2563
3873
|
message=f"idea: revise {resolved_idea_id}",
|
|
2564
3874
|
)
|
|
3875
|
+
idea_md_rel_path = self._workspace_relative(quest_root, idea_md_path)
|
|
3876
|
+
idea_draft_rel_path = self._workspace_relative(quest_root, idea_draft_path)
|
|
2565
3877
|
interaction = self.interact(
|
|
2566
3878
|
quest_root,
|
|
2567
3879
|
kind="progress",
|
|
2568
|
-
message=(
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
3880
|
+
message=self._build_idea_interaction_message(
|
|
3881
|
+
action="revise",
|
|
3882
|
+
idea_id=resolved_idea_id,
|
|
3883
|
+
title=title,
|
|
3884
|
+
problem=problem,
|
|
3885
|
+
hypothesis=hypothesis,
|
|
3886
|
+
mechanism=mechanism,
|
|
3887
|
+
foundation_label=self._format_foundation_label(
|
|
3888
|
+
existing_foundation_ref,
|
|
3889
|
+
fallback=(existing_foundation_ref or {}).get("branch") or "current head",
|
|
3890
|
+
),
|
|
3891
|
+
branch_name=branch_name,
|
|
3892
|
+
next_target=next_target,
|
|
3893
|
+
idea_md_rel_path=idea_md_rel_path,
|
|
3894
|
+
draft_md_rel_path=idea_draft_rel_path,
|
|
2576
3895
|
),
|
|
2577
3896
|
deliver_to_bound_conversations=True,
|
|
2578
3897
|
include_recent_inbound_messages=False,
|
|
@@ -2712,14 +4031,21 @@ class ArtifactService:
|
|
|
2712
4031
|
baseline_id: str | None = None,
|
|
2713
4032
|
baseline_variant_id: str | None = None,
|
|
2714
4033
|
evaluation_summary: dict[str, Any] | None = None,
|
|
4034
|
+
strict_metric_contract: bool = False,
|
|
2715
4035
|
) -> dict[str, Any]:
|
|
2716
4036
|
self._require_baseline_gate_open(quest_root, action="record_main_experiment")
|
|
2717
4037
|
state = self.quest_service.read_research_state(quest_root)
|
|
2718
|
-
|
|
4038
|
+
workspace_mode = str(state.get("workspace_mode") or "").strip()
|
|
4039
|
+
if workspace_mode == "analysis":
|
|
2719
4040
|
raise ValueError(
|
|
2720
4041
|
"record_main_experiment cannot run while the active workspace is an analysis slice. "
|
|
2721
4042
|
"Finish or close the analysis campaign first."
|
|
2722
4043
|
)
|
|
4044
|
+
if workspace_mode == "paper":
|
|
4045
|
+
raise ValueError(
|
|
4046
|
+
"record_main_experiment cannot run while the active workspace is a paper branch. "
|
|
4047
|
+
"Return to the source evidence branch or create a new run branch first."
|
|
4048
|
+
)
|
|
2723
4049
|
|
|
2724
4050
|
run_identifier = str(run_id or "").strip()
|
|
2725
4051
|
if not run_identifier:
|
|
@@ -2727,7 +4053,18 @@ class ArtifactService:
|
|
|
2727
4053
|
|
|
2728
4054
|
active_idea_id = str(state.get("active_idea_id") or "").strip() or None
|
|
2729
4055
|
workspace_root = self._workspace_root_for(quest_root)
|
|
2730
|
-
|
|
4056
|
+
current_branch_name = str(
|
|
4057
|
+
state.get("current_workspace_branch")
|
|
4058
|
+
or state.get("research_head_branch")
|
|
4059
|
+
or current_branch(workspace_root)
|
|
4060
|
+
).strip()
|
|
4061
|
+
branch_name, parent_branch, auto_promoted_run_branch = self._promote_workspace_to_run_branch(
|
|
4062
|
+
quest_root,
|
|
4063
|
+
run_id=run_identifier,
|
|
4064
|
+
idea_id=active_idea_id,
|
|
4065
|
+
workspace_root=workspace_root,
|
|
4066
|
+
current_branch_name=current_branch_name,
|
|
4067
|
+
)
|
|
2731
4068
|
attachment = self._active_baseline_attachment(quest_root, workspace_root=workspace_root)
|
|
2732
4069
|
baseline_entry = dict(attachment.get("entry") or {}) if isinstance(attachment, dict) else {}
|
|
2733
4070
|
selected_variant = dict(attachment.get("selected_variant") or {}) if isinstance(attachment, dict) else {}
|
|
@@ -2757,16 +4094,49 @@ class ArtifactService:
|
|
|
2757
4094
|
for item in normalized_metric_rows
|
|
2758
4095
|
if str(item.get("metric_id") or "").strip()
|
|
2759
4096
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
4097
|
+
baseline_contract_payload = self._load_metric_contract_payload(quest_root, metric_contract_json_rel_path)
|
|
4098
|
+
baseline_metric_contract = baseline_entry.get("metric_contract")
|
|
4099
|
+
baseline_primary_metric = baseline_entry.get("primary_metric")
|
|
4100
|
+
if isinstance(baseline_contract_payload, dict) and baseline_contract_payload:
|
|
4101
|
+
payload_metric_contract = baseline_contract_payload.get("metric_contract")
|
|
4102
|
+
if isinstance(payload_metric_contract, dict) and payload_metric_contract:
|
|
4103
|
+
baseline_metric_contract = payload_metric_contract
|
|
4104
|
+
payload_primary_metric = baseline_contract_payload.get("primary_metric")
|
|
4105
|
+
if isinstance(payload_primary_metric, dict) and payload_primary_metric:
|
|
4106
|
+
baseline_primary_metric = payload_primary_metric
|
|
4107
|
+
effective_metric_contract = (
|
|
4108
|
+
self._merge_run_metric_contract(
|
|
4109
|
+
baseline_metric_contract=baseline_metric_contract,
|
|
4110
|
+
baseline_primary_metric=baseline_primary_metric,
|
|
4111
|
+
baseline_variants=baseline_entry.get("baseline_variants"),
|
|
4112
|
+
run_metric_contract=metric_contract,
|
|
4113
|
+
metrics_summary=normalized_metrics_summary,
|
|
4114
|
+
metric_rows=normalized_metric_rows,
|
|
4115
|
+
baseline_id=resolved_baseline_id,
|
|
4116
|
+
)
|
|
4117
|
+
if isinstance(baseline_metric_contract, dict) and baseline_metric_contract
|
|
4118
|
+
else normalize_metric_contract(
|
|
4119
|
+
metric_contract or baseline_entry.get("metric_contract"),
|
|
4120
|
+
baseline_id=resolved_baseline_id,
|
|
4121
|
+
metrics_summary=normalized_metrics_summary,
|
|
4122
|
+
metric_rows=normalized_metric_rows,
|
|
4123
|
+
primary_metric=baseline_primary_metric,
|
|
4124
|
+
baseline_variants=baseline_entry.get("baseline_variants"),
|
|
4125
|
+
)
|
|
2766
4126
|
)
|
|
4127
|
+
metric_validation: dict[str, Any] | None = None
|
|
4128
|
+
if strict_metric_contract:
|
|
4129
|
+
metric_validation = validate_main_experiment_against_baseline_contract(
|
|
4130
|
+
baseline_contract_payload=baseline_contract_payload,
|
|
4131
|
+
run_metric_contract=effective_metric_contract,
|
|
4132
|
+
metric_rows=normalized_metric_rows,
|
|
4133
|
+
metrics_summary=normalized_metrics_summary,
|
|
4134
|
+
dataset_scope=dataset_scope,
|
|
4135
|
+
)
|
|
2767
4136
|
baseline_metrics = selected_baseline_metrics(baseline_entry, resolved_variant_id)
|
|
2768
4137
|
comparisons = compare_with_baseline(
|
|
2769
4138
|
metrics_summary=normalized_metrics_summary,
|
|
4139
|
+
metric_rows=normalized_metric_rows,
|
|
2770
4140
|
metric_contract=effective_metric_contract,
|
|
2771
4141
|
baseline_metrics=baseline_metrics,
|
|
2772
4142
|
)
|
|
@@ -2827,6 +4197,7 @@ class ArtifactService:
|
|
|
2827
4197
|
"",
|
|
2828
4198
|
f"- Run id: `{run_identifier}`",
|
|
2829
4199
|
f"- Branch: `{branch_name}`",
|
|
4200
|
+
f"- Parent branch: `{parent_branch or 'none'}`",
|
|
2830
4201
|
f"- Worktree: `{workspace_root}`",
|
|
2831
4202
|
f"- Idea: `{active_idea_id or 'none'}`",
|
|
2832
4203
|
f"- Baseline: `{resolved_baseline_id or 'none'}`",
|
|
@@ -2924,6 +4295,7 @@ class ArtifactService:
|
|
|
2924
4295
|
"verdict": verdict,
|
|
2925
4296
|
"idea_id": active_idea_id,
|
|
2926
4297
|
"branch": branch_name,
|
|
4298
|
+
"parent_branch": parent_branch,
|
|
2927
4299
|
"worktree_root": str(workspace_root),
|
|
2928
4300
|
"head_commit": head_commit(workspace_root),
|
|
2929
4301
|
"baseline_ref": {
|
|
@@ -2956,6 +4328,7 @@ class ArtifactService:
|
|
|
2956
4328
|
"evidence_paths": resolved_evidence_paths,
|
|
2957
4329
|
"files_changed": resolved_changed_files,
|
|
2958
4330
|
"run_md_path": str(run_md_path),
|
|
4331
|
+
"metric_validation": metric_validation,
|
|
2959
4332
|
}
|
|
2960
4333
|
write_json(result_json_path, result_payload)
|
|
2961
4334
|
|
|
@@ -2970,6 +4343,7 @@ class ArtifactService:
|
|
|
2970
4343
|
"reason": conclusion.strip() or progress_eval.get("reason") or "Main experiment result recorded.",
|
|
2971
4344
|
"idea_id": active_idea_id,
|
|
2972
4345
|
"branch": branch_name,
|
|
4346
|
+
"parent_branch": parent_branch,
|
|
2973
4347
|
"worktree_root": str(workspace_root),
|
|
2974
4348
|
"worktree_rel_path": self._workspace_relative(quest_root, workspace_root),
|
|
2975
4349
|
"flow_type": "main_experiment",
|
|
@@ -2989,6 +4363,7 @@ class ArtifactService:
|
|
|
2989
4363
|
"breakthrough_level": progress_eval.get("breakthrough_level"),
|
|
2990
4364
|
"need_research_paper": delivery_policy.get("need_research_paper"),
|
|
2991
4365
|
"recommended_next_route": delivery_policy.get("recommended_next_route"),
|
|
4366
|
+
"auto_promoted_run_branch": auto_promoted_run_branch,
|
|
2992
4367
|
"changed_file_count": len(resolved_changed_files),
|
|
2993
4368
|
"evidence_count": len(resolved_evidence_paths),
|
|
2994
4369
|
"evaluation_summary": normalized_evaluation_summary,
|
|
@@ -3008,6 +4383,7 @@ class ArtifactService:
|
|
|
3008
4383
|
},
|
|
3009
4384
|
"progress_eval": progress_eval,
|
|
3010
4385
|
"evaluation_summary": normalized_evaluation_summary,
|
|
4386
|
+
"metric_validation": metric_validation,
|
|
3011
4387
|
"files_changed": resolved_changed_files,
|
|
3012
4388
|
"evidence_paths": resolved_evidence_paths,
|
|
3013
4389
|
"verdict": verdict,
|
|
@@ -3018,14 +4394,21 @@ class ArtifactService:
|
|
|
3018
4394
|
interaction = self.interact(
|
|
3019
4395
|
quest_root,
|
|
3020
4396
|
kind="milestone",
|
|
3021
|
-
message=(
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
4397
|
+
message=self._build_main_experiment_interaction_message(
|
|
4398
|
+
run_id=run_identifier,
|
|
4399
|
+
branch_name=branch_name,
|
|
4400
|
+
verdict=verdict,
|
|
4401
|
+
primary_metric_id=primary_metric_id,
|
|
4402
|
+
primary_value=primary_value,
|
|
4403
|
+
primary_baseline=primary_baseline,
|
|
4404
|
+
primary_delta=primary_delta,
|
|
4405
|
+
decimals=decimals if isinstance(decimals, int) else None,
|
|
4406
|
+
conclusion=conclusion.strip() or progress_eval.get("reason"),
|
|
4407
|
+
evaluation_summary=normalized_evaluation_summary,
|
|
4408
|
+
breakthrough_level=str(progress_eval.get("breakthrough_level") or "").strip() or None,
|
|
4409
|
+
recommended_next_route=str(delivery_policy.get("recommended_next_route") or "").strip() or None,
|
|
4410
|
+
run_md_rel_path=self._workspace_relative(quest_root, run_md_path),
|
|
4411
|
+
result_json_rel_path=self._workspace_relative(quest_root, result_json_path),
|
|
3029
4412
|
),
|
|
3030
4413
|
deliver_to_bound_conversations=True,
|
|
3031
4414
|
include_recent_inbound_messages=False,
|
|
@@ -3049,6 +4432,22 @@ class ArtifactService:
|
|
|
3049
4432
|
],
|
|
3050
4433
|
)
|
|
3051
4434
|
self.quest_service.update_settings(self._quest_id(quest_root), active_anchor="decision")
|
|
4435
|
+
research_state = self.quest_service.update_research_state(
|
|
4436
|
+
quest_root,
|
|
4437
|
+
active_idea_id=active_idea_id,
|
|
4438
|
+
current_workspace_branch=branch_name,
|
|
4439
|
+
current_workspace_root=str(workspace_root),
|
|
4440
|
+
research_head_branch=branch_name,
|
|
4441
|
+
research_head_worktree_root=str(workspace_root),
|
|
4442
|
+
active_analysis_campaign_id=None,
|
|
4443
|
+
analysis_parent_branch=None,
|
|
4444
|
+
analysis_parent_worktree_root=None,
|
|
4445
|
+
paper_parent_branch=None,
|
|
4446
|
+
paper_parent_worktree_root=None,
|
|
4447
|
+
paper_parent_run_id=None,
|
|
4448
|
+
workspace_mode="run",
|
|
4449
|
+
last_flow_type="main_experiment_recorded",
|
|
4450
|
+
)
|
|
3052
4451
|
return {
|
|
3053
4452
|
"ok": True,
|
|
3054
4453
|
"guidance": artifact.get("guidance"),
|
|
@@ -3058,10 +4457,14 @@ class ArtifactService:
|
|
|
3058
4457
|
"suggested_artifact_calls": artifact.get("suggested_artifact_calls"),
|
|
3059
4458
|
"next_instruction": artifact.get("next_instruction"),
|
|
3060
4459
|
"run_id": run_identifier,
|
|
4460
|
+
"branch": branch_name,
|
|
4461
|
+
"parent_branch": parent_branch,
|
|
4462
|
+
"auto_promoted_run_branch": auto_promoted_run_branch,
|
|
3061
4463
|
"run_md_path": str(run_md_path),
|
|
3062
4464
|
"result_json_path": str(result_json_path),
|
|
3063
4465
|
"artifact": artifact,
|
|
3064
4466
|
"interaction": interaction,
|
|
4467
|
+
"research_state": research_state,
|
|
3065
4468
|
"metrics_summary": normalized_metrics_summary,
|
|
3066
4469
|
"baseline_comparisons": {
|
|
3067
4470
|
key: value for key, value in comparisons.items() if key != "primary"
|
|
@@ -3069,6 +4472,7 @@ class ArtifactService:
|
|
|
3069
4472
|
"progress_eval": progress_eval,
|
|
3070
4473
|
"evaluation_summary": normalized_evaluation_summary,
|
|
3071
4474
|
"delivery_policy": delivery_policy,
|
|
4475
|
+
"metric_validation": metric_validation,
|
|
3072
4476
|
}
|
|
3073
4477
|
|
|
3074
4478
|
def create_analysis_campaign(
|
|
@@ -3091,6 +4495,14 @@ class ArtifactService:
|
|
|
3091
4495
|
quest_root,
|
|
3092
4496
|
state=state,
|
|
3093
4497
|
)
|
|
4498
|
+
runtime_refs = self.resolve_runtime_refs(quest_root)
|
|
4499
|
+
resolved_parent_run_id = (
|
|
4500
|
+
str(parent_run_id or "").strip()
|
|
4501
|
+
or str(state.get("paper_parent_run_id") or "").strip()
|
|
4502
|
+
or str((self._latest_main_run_for_branch(quest_root, parent_branch) or {}).get("run_id") or "").strip()
|
|
4503
|
+
or str(runtime_refs.get("latest_main_run_id") or "").strip()
|
|
4504
|
+
or None
|
|
4505
|
+
)
|
|
3094
4506
|
active_idea_id = str(resolved_idea_id or "").strip()
|
|
3095
4507
|
if not active_idea_id:
|
|
3096
4508
|
raise ValueError("An active idea is required before starting an analysis campaign.")
|
|
@@ -3104,6 +4516,54 @@ class ArtifactService:
|
|
|
3104
4516
|
normalized_research_questions = self._normalize_string_list(research_questions)
|
|
3105
4517
|
normalized_experimental_designs = self._normalize_string_list(experimental_designs)
|
|
3106
4518
|
normalized_todo_items = self._normalize_campaign_todo_items(todo_items)
|
|
4519
|
+
quest_data = self.quest_service.read_quest_yaml(quest_root)
|
|
4520
|
+
active_anchor = str(quest_data.get("active_anchor") or "").strip().lower()
|
|
4521
|
+
campaign_origin_kind = (
|
|
4522
|
+
str(normalized_campaign_origin.get("kind") or "").strip().lower()
|
|
4523
|
+
if isinstance(normalized_campaign_origin, dict)
|
|
4524
|
+
else ""
|
|
4525
|
+
)
|
|
4526
|
+
writing_facing = bool(
|
|
4527
|
+
resolved_outline_ref
|
|
4528
|
+
or normalized_research_questions
|
|
4529
|
+
or normalized_experimental_designs
|
|
4530
|
+
or normalized_todo_items
|
|
4531
|
+
or str(state.get("workspace_mode") or "").strip().lower() == "paper"
|
|
4532
|
+
or active_anchor == "write"
|
|
4533
|
+
or campaign_origin_kind in {"write", "paper", "rebuttal", "revision"}
|
|
4534
|
+
)
|
|
4535
|
+
if writing_facing:
|
|
4536
|
+
if not resolved_outline_ref:
|
|
4537
|
+
raise ValueError(
|
|
4538
|
+
"Writing-facing analysis campaigns require `selected_outline_ref` before slices can be launched."
|
|
4539
|
+
)
|
|
4540
|
+
if not normalized_research_questions:
|
|
4541
|
+
raise ValueError(
|
|
4542
|
+
"Writing-facing analysis campaigns require non-empty `research_questions`."
|
|
4543
|
+
)
|
|
4544
|
+
if not normalized_experimental_designs:
|
|
4545
|
+
raise ValueError(
|
|
4546
|
+
"Writing-facing analysis campaigns require non-empty `experimental_designs`."
|
|
4547
|
+
)
|
|
4548
|
+
if not normalized_todo_items:
|
|
4549
|
+
raise ValueError(
|
|
4550
|
+
"Writing-facing analysis campaigns require non-empty `todo_items`."
|
|
4551
|
+
)
|
|
4552
|
+
todo_slice_ids = {
|
|
4553
|
+
str(item.get("slice_id") or "").strip()
|
|
4554
|
+
for item in normalized_todo_items
|
|
4555
|
+
if str(item.get("slice_id") or "").strip()
|
|
4556
|
+
}
|
|
4557
|
+
missing_slice_ids = [
|
|
4558
|
+
str(raw.get("slice_id") or "").strip()
|
|
4559
|
+
for raw in slices
|
|
4560
|
+
if str(raw.get("slice_id") or "").strip() and str(raw.get("slice_id") or "").strip() not in todo_slice_ids
|
|
4561
|
+
]
|
|
4562
|
+
if missing_slice_ids:
|
|
4563
|
+
raise ValueError(
|
|
4564
|
+
"Writing-facing analysis campaigns require one todo item per slice. "
|
|
4565
|
+
f"Missing todo items for: {', '.join(missing_slice_ids)}."
|
|
4566
|
+
)
|
|
3107
4567
|
slice_contexts: list[dict[str, Any]] = []
|
|
3108
4568
|
inventory_entries: list[dict[str, Any]] = []
|
|
3109
4569
|
for index, raw in enumerate(slices, start=1):
|
|
@@ -3366,7 +4826,7 @@ class ArtifactService:
|
|
|
3366
4826
|
{
|
|
3367
4827
|
"title": campaign_title,
|
|
3368
4828
|
"goal": campaign_goal,
|
|
3369
|
-
"parent_run_id":
|
|
4829
|
+
"parent_run_id": resolved_parent_run_id,
|
|
3370
4830
|
"active_idea_id": active_idea_id,
|
|
3371
4831
|
"parent_branch": parent_branch,
|
|
3372
4832
|
"parent_worktree_root": str(parent_worktree_root),
|
|
@@ -3441,7 +4901,7 @@ class ArtifactService:
|
|
|
3441
4901
|
"details": {
|
|
3442
4902
|
"campaign_title": campaign_title,
|
|
3443
4903
|
"campaign_goal": campaign_goal,
|
|
3444
|
-
"parent_run_id":
|
|
4904
|
+
"parent_run_id": resolved_parent_run_id,
|
|
3445
4905
|
"campaign_origin": normalized_campaign_origin,
|
|
3446
4906
|
"selected_outline_ref": resolved_outline_ref,
|
|
3447
4907
|
"todo_manifest_path": str(todo_manifest_path),
|
|
@@ -3493,14 +4953,13 @@ class ArtifactService:
|
|
|
3493
4953
|
interaction = self.interact(
|
|
3494
4954
|
quest_root,
|
|
3495
4955
|
kind="milestone",
|
|
3496
|
-
message=(
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
f"- Core requirement: {first_slice['must_not_simplify'] or 'Follow the full evaluation protocol.'}"
|
|
4956
|
+
message=self._build_analysis_campaign_interaction_message(
|
|
4957
|
+
campaign_id=campaign_id,
|
|
4958
|
+
goal=campaign_goal,
|
|
4959
|
+
parent_branch=parent_branch,
|
|
4960
|
+
selected_outline_ref=resolved_outline_ref,
|
|
4961
|
+
first_slice=first_slice,
|
|
4962
|
+
todo_manifest_rel_path=self._workspace_relative(quest_root, todo_manifest_path),
|
|
3504
4963
|
),
|
|
3505
4964
|
deliver_to_bound_conversations=True,
|
|
3506
4965
|
include_recent_inbound_messages=False,
|
|
@@ -3560,11 +5019,32 @@ class ArtifactService:
|
|
|
3560
5019
|
if normalized_mode not in {"candidate", "select", "revise"}:
|
|
3561
5020
|
raise ValueError("submit_paper_outline mode must be `candidate`, `select`, or `revise`.")
|
|
3562
5021
|
|
|
3563
|
-
|
|
5022
|
+
paper_context = (
|
|
5023
|
+
self._ensure_active_paper_workspace(quest_root)
|
|
5024
|
+
if normalized_mode in {"select", "revise"}
|
|
5025
|
+
else {
|
|
5026
|
+
"worktree_root": str(self._workspace_root_for(quest_root)),
|
|
5027
|
+
"branch": str(self.quest_service.read_research_state(quest_root).get("current_workspace_branch") or "").strip() or None,
|
|
5028
|
+
}
|
|
5029
|
+
)
|
|
5030
|
+
workspace_root = Path(str(paper_context.get("worktree_root") or self._workspace_root_for(quest_root)))
|
|
5031
|
+
paper_root = (
|
|
5032
|
+
ensure_dir(workspace_root / "paper")
|
|
5033
|
+
if normalized_mode in {"select", "revise"}
|
|
5034
|
+
else self._paper_root(quest_root, workspace_root=workspace_root, create=True)
|
|
5035
|
+
)
|
|
5036
|
+
if normalized_mode in {"select", "revise"}:
|
|
5037
|
+
selected_outline_path = paper_root / "selected_outline.json"
|
|
5038
|
+
else:
|
|
5039
|
+
selected_outline_path = self._paper_selected_outline_path(quest_root, workspace_root=workspace_root)
|
|
5040
|
+
existing_selected = read_json(selected_outline_path, {})
|
|
5041
|
+
if not isinstance(existing_selected, dict) or not existing_selected:
|
|
5042
|
+
existing_selected = read_json(quest_root / "paper" / "selected_outline.json", {})
|
|
3564
5043
|
existing_selected = existing_selected if isinstance(existing_selected, dict) else {}
|
|
3565
5044
|
if normalized_mode == "candidate":
|
|
3566
5045
|
resolved_outline_id = str(outline_id or self._next_paper_outline_id(quest_root)).strip()
|
|
3567
|
-
candidate_path = self._paper_outline_candidates_root(quest_root) / f"{resolved_outline_id}.json"
|
|
5046
|
+
candidate_path = self._paper_outline_candidates_root(quest_root, workspace_root=workspace_root) / f"{resolved_outline_id}.json"
|
|
5047
|
+
canonical_candidate_path = quest_root / "paper" / "outlines" / "candidates" / f"{resolved_outline_id}.json"
|
|
3568
5048
|
existing = read_json(candidate_path, {})
|
|
3569
5049
|
existing = existing if isinstance(existing, dict) else {}
|
|
3570
5050
|
record = self._normalize_paper_outline_record(
|
|
@@ -3579,6 +5059,8 @@ class ArtifactService:
|
|
|
3579
5059
|
created_at=str(existing.get("created_at") or "") or None,
|
|
3580
5060
|
)
|
|
3581
5061
|
write_json(candidate_path, record)
|
|
5062
|
+
if canonical_candidate_path.resolve() != candidate_path.resolve():
|
|
5063
|
+
write_json(canonical_candidate_path, record)
|
|
3582
5064
|
artifact = self.record(
|
|
3583
5065
|
quest_root,
|
|
3584
5066
|
{
|
|
@@ -3599,7 +5081,7 @@ class ArtifactService:
|
|
|
3599
5081
|
},
|
|
3600
5082
|
},
|
|
3601
5083
|
checkpoint=False,
|
|
3602
|
-
workspace_root=
|
|
5084
|
+
workspace_root=workspace_root,
|
|
3603
5085
|
)
|
|
3604
5086
|
return {
|
|
3605
5087
|
"ok": True,
|
|
@@ -3613,8 +5095,13 @@ class ArtifactService:
|
|
|
3613
5095
|
source_outline_id = str(outline_id or existing_selected.get("outline_id") or "").strip()
|
|
3614
5096
|
if not source_outline_id:
|
|
3615
5097
|
raise ValueError("submit_paper_outline(select/revise) requires an existing `outline_id` or selected outline.")
|
|
3616
|
-
source_candidate_path =
|
|
5098
|
+
source_candidate_path = paper_root / "outlines" / "candidates" / f"{source_outline_id}.json"
|
|
3617
5099
|
source_record = read_json(source_candidate_path, {})
|
|
5100
|
+
if not isinstance(source_record, dict) or not source_record:
|
|
5101
|
+
fallback_candidate_path = quest_root / "paper" / "outlines" / "candidates" / f"{source_outline_id}.json"
|
|
5102
|
+
source_record = read_json(fallback_candidate_path, {})
|
|
5103
|
+
if isinstance(source_record, dict) and source_record:
|
|
5104
|
+
source_candidate_path = fallback_candidate_path
|
|
3618
5105
|
if not isinstance(source_record, dict) or not source_record:
|
|
3619
5106
|
source_record = existing_selected if str(existing_selected.get("outline_id") or "").strip() == source_outline_id else {}
|
|
3620
5107
|
if not source_record:
|
|
@@ -3632,18 +5119,23 @@ class ArtifactService:
|
|
|
3632
5119
|
created_at=str(source_record.get("created_at") or "") or None,
|
|
3633
5120
|
)
|
|
3634
5121
|
|
|
3635
|
-
selected_outline_path = self._paper_selected_outline_path(quest_root)
|
|
3636
5122
|
write_json(selected_outline_path, resolved_record)
|
|
5123
|
+
canonical_selected_outline_path = quest_root / "paper" / "selected_outline.json"
|
|
5124
|
+
if canonical_selected_outline_path.resolve() != selected_outline_path.resolve():
|
|
5125
|
+
write_json(canonical_selected_outline_path, resolved_record)
|
|
3637
5126
|
if source_candidate_path.exists():
|
|
3638
5127
|
source_record["status"] = "selected" if normalized_mode == "select" else "revised"
|
|
3639
5128
|
source_record["updated_at"] = utc_now()
|
|
3640
5129
|
write_json(source_candidate_path, source_record)
|
|
3641
5130
|
revised_outline_path = None
|
|
3642
5131
|
if normalized_mode == "revise":
|
|
3643
|
-
revised_outline_path =
|
|
5132
|
+
revised_outline_path = ensure_dir(paper_root / "outlines" / "revisions") / f"{source_outline_id}.json"
|
|
3644
5133
|
write_json(revised_outline_path, resolved_record)
|
|
5134
|
+
canonical_revised_outline_path = quest_root / "paper" / "outlines" / "revisions" / f"{source_outline_id}.json"
|
|
5135
|
+
if canonical_revised_outline_path.resolve() != revised_outline_path.resolve():
|
|
5136
|
+
write_json(canonical_revised_outline_path, resolved_record)
|
|
3645
5137
|
|
|
3646
|
-
outline_selection_path =
|
|
5138
|
+
outline_selection_path = paper_root / "outline_selection.md"
|
|
3647
5139
|
action_label = "selected" if normalized_mode == "select" else "revised"
|
|
3648
5140
|
selection_lines = [
|
|
3649
5141
|
f"# Outline {normalized_mode.capitalize()}",
|
|
@@ -3682,7 +5174,47 @@ class ArtifactService:
|
|
|
3682
5174
|
},
|
|
3683
5175
|
},
|
|
3684
5176
|
checkpoint=False,
|
|
3685
|
-
workspace_root=
|
|
5177
|
+
workspace_root=workspace_root,
|
|
5178
|
+
)
|
|
5179
|
+
selected_outline_rel_path = self._workspace_relative(quest_root, selected_outline_path)
|
|
5180
|
+
outline_selection_rel_path = self._workspace_relative(quest_root, outline_selection_path)
|
|
5181
|
+
revised_outline_rel_path = self._workspace_relative(quest_root, revised_outline_path) if revised_outline_path else None
|
|
5182
|
+
interaction = self.interact(
|
|
5183
|
+
quest_root,
|
|
5184
|
+
kind="milestone" if normalized_mode == "select" else "progress",
|
|
5185
|
+
message=self._build_outline_interaction_message(
|
|
5186
|
+
action=normalized_mode,
|
|
5187
|
+
outline_id=source_outline_id,
|
|
5188
|
+
title=str(resolved_record.get("title") or "").strip() or source_outline_id,
|
|
5189
|
+
selected_reason=selected_reason or note,
|
|
5190
|
+
story=str(resolved_record.get("story") or "").strip() or None,
|
|
5191
|
+
research_questions=(
|
|
5192
|
+
(resolved_record.get("detailed_outline") or {})
|
|
5193
|
+
if isinstance(resolved_record.get("detailed_outline"), dict)
|
|
5194
|
+
else {}
|
|
5195
|
+
).get("research_questions"),
|
|
5196
|
+
experimental_designs=(
|
|
5197
|
+
(resolved_record.get("detailed_outline") or {})
|
|
5198
|
+
if isinstance(resolved_record.get("detailed_outline"), dict)
|
|
5199
|
+
else {}
|
|
5200
|
+
).get("experimental_designs"),
|
|
5201
|
+
selected_outline_rel_path=selected_outline_rel_path,
|
|
5202
|
+
outline_selection_rel_path=outline_selection_rel_path,
|
|
5203
|
+
revised_outline_rel_path=revised_outline_rel_path,
|
|
5204
|
+
),
|
|
5205
|
+
deliver_to_bound_conversations=True,
|
|
5206
|
+
include_recent_inbound_messages=False,
|
|
5207
|
+
attachments=[
|
|
5208
|
+
{
|
|
5209
|
+
"kind": "paper_outline_selected" if normalized_mode == "select" else "paper_outline_revised",
|
|
5210
|
+
"outline_id": source_outline_id,
|
|
5211
|
+
"title": resolved_record.get("title"),
|
|
5212
|
+
"selected_reason": selected_reason,
|
|
5213
|
+
"selected_outline_path": str(selected_outline_path),
|
|
5214
|
+
"outline_selection_path": str(outline_selection_path),
|
|
5215
|
+
"revised_outline_path": str(revised_outline_path) if revised_outline_path else None,
|
|
5216
|
+
}
|
|
5217
|
+
],
|
|
3686
5218
|
)
|
|
3687
5219
|
return {
|
|
3688
5220
|
"ok": True,
|
|
@@ -3693,6 +5225,7 @@ class ArtifactService:
|
|
|
3693
5225
|
"revised_outline_path": str(revised_outline_path) if revised_outline_path else None,
|
|
3694
5226
|
"record": resolved_record,
|
|
3695
5227
|
"artifact": artifact,
|
|
5228
|
+
"interaction": interaction,
|
|
3696
5229
|
}
|
|
3697
5230
|
|
|
3698
5231
|
def submit_paper_bundle(
|
|
@@ -3710,24 +5243,44 @@ class ArtifactService:
|
|
|
3710
5243
|
pdf_path: str | None = None,
|
|
3711
5244
|
latex_root_path: str | None = None,
|
|
3712
5245
|
) -> dict[str, Any]:
|
|
3713
|
-
|
|
5246
|
+
paper_context = self._ensure_active_paper_workspace(quest_root)
|
|
5247
|
+
workspace_root = Path(str(paper_context.get("worktree_root") or self._workspace_root_for(quest_root)))
|
|
5248
|
+
paper_root = self._paper_root(quest_root, workspace_root=workspace_root, create=True)
|
|
5249
|
+
selected_outline_path = self._paper_selected_outline_path(quest_root, workspace_root=workspace_root)
|
|
3714
5250
|
selected_outline = read_json(selected_outline_path, {})
|
|
5251
|
+
if not isinstance(selected_outline, dict) or not selected_outline:
|
|
5252
|
+
fallback_selected_outline_path = quest_root / "paper" / "selected_outline.json"
|
|
5253
|
+
selected_outline = read_json(fallback_selected_outline_path, {})
|
|
5254
|
+
if isinstance(selected_outline, dict) and selected_outline:
|
|
5255
|
+
selected_outline_path = fallback_selected_outline_path
|
|
3715
5256
|
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
3716
5257
|
if not selected_outline and not str(outline_path or "").strip():
|
|
3717
5258
|
raise ValueError("submit_paper_bundle requires a selected outline or explicit `outline_path`.")
|
|
3718
5259
|
|
|
3719
|
-
manifest_path = self._paper_bundle_manifest_path(quest_root)
|
|
3720
|
-
baseline_inventory = self._write_paper_baseline_inventory(quest_root)
|
|
3721
|
-
baseline_inventory_path = self._paper_baseline_inventory_path(quest_root)
|
|
3722
|
-
source_branch = (
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
)
|
|
5260
|
+
manifest_path = self._paper_bundle_manifest_path(quest_root, workspace_root=workspace_root)
|
|
5261
|
+
baseline_inventory = self._write_paper_baseline_inventory(quest_root, workspace_root=workspace_root)
|
|
5262
|
+
baseline_inventory_path = self._paper_baseline_inventory_path(quest_root, workspace_root=workspace_root)
|
|
5263
|
+
source_branch = str(paper_context.get("source_branch") or "").strip() or None
|
|
5264
|
+
paper_branch = str(paper_context.get("branch") or "").strip() or current_branch(workspace_root)
|
|
5265
|
+
source_run_id = str(paper_context.get("source_run_id") or "").strip() or None
|
|
5266
|
+
source_idea_id = str(paper_context.get("source_idea_id") or "").strip() or None
|
|
5267
|
+
paper_manifest_rel = self._workspace_relative(quest_root, manifest_path) or "paper/paper_bundle_manifest.json"
|
|
5268
|
+
paper_inventory_rel = self._workspace_relative(quest_root, baseline_inventory_path) or "paper/baseline_inventory.json"
|
|
3726
5269
|
open_source_manifest = self._ensure_open_source_prep(
|
|
3727
5270
|
quest_root,
|
|
5271
|
+
workspace_root=workspace_root,
|
|
3728
5272
|
source_branch=source_branch,
|
|
3729
|
-
source_bundle_manifest_path=
|
|
3730
|
-
baseline_inventory_path=
|
|
5273
|
+
source_bundle_manifest_path=paper_manifest_rel,
|
|
5274
|
+
baseline_inventory_path=paper_inventory_rel,
|
|
5275
|
+
)
|
|
5276
|
+
default_draft_path = self._workspace_relative(quest_root, paper_root / "draft.md") or "paper/draft.md"
|
|
5277
|
+
default_writing_plan_path = self._workspace_relative(quest_root, paper_root / "writing_plan.md") or "paper/writing_plan.md"
|
|
5278
|
+
default_references_path = self._workspace_relative(quest_root, paper_root / "references.bib") or "paper/references.bib"
|
|
5279
|
+
default_claim_map_path = (
|
|
5280
|
+
self._workspace_relative(quest_root, paper_root / "claim_evidence_map.json") or "paper/claim_evidence_map.json"
|
|
5281
|
+
)
|
|
5282
|
+
default_compile_report_path = (
|
|
5283
|
+
self._workspace_relative(quest_root, paper_root / "build" / "compile_report.json") or "paper/build/compile_report.json"
|
|
3731
5284
|
)
|
|
3732
5285
|
manifest = {
|
|
3733
5286
|
"schema_version": 1,
|
|
@@ -3740,15 +5293,23 @@ class ArtifactService:
|
|
|
3740
5293
|
or "paper",
|
|
3741
5294
|
"summary": str(summary or "").strip() or None,
|
|
3742
5295
|
"outline_path": str(outline_path or selected_outline_path).strip() or None,
|
|
3743
|
-
"
|
|
3744
|
-
"
|
|
3745
|
-
"
|
|
3746
|
-
"
|
|
3747
|
-
"
|
|
5296
|
+
"paper_branch": paper_branch,
|
|
5297
|
+
"source_branch": source_branch,
|
|
5298
|
+
"source_run_id": source_run_id,
|
|
5299
|
+
"source_idea_id": source_idea_id,
|
|
5300
|
+
"draft_path": str(draft_path or default_draft_path).strip() or None,
|
|
5301
|
+
"writing_plan_path": str(writing_plan_path or default_writing_plan_path).strip() or None,
|
|
5302
|
+
"references_path": str(references_path or default_references_path).strip() or None,
|
|
5303
|
+
"claim_evidence_map_path": str(claim_evidence_map_path or default_claim_map_path).strip() or None,
|
|
5304
|
+
"compile_report_path": str(compile_report_path or default_compile_report_path).strip() or None,
|
|
3748
5305
|
"pdf_path": str(pdf_path or "").strip() or None,
|
|
3749
5306
|
"latex_root_path": str(latex_root_path or "").strip() or None,
|
|
3750
|
-
"baseline_inventory_path":
|
|
3751
|
-
"open_source_manifest_path":
|
|
5307
|
+
"baseline_inventory_path": paper_inventory_rel,
|
|
5308
|
+
"open_source_manifest_path": self._workspace_relative(
|
|
5309
|
+
quest_root,
|
|
5310
|
+
self._open_source_manifest_path(quest_root, workspace_root=workspace_root),
|
|
5311
|
+
)
|
|
5312
|
+
or "release/open_source/manifest.json",
|
|
3752
5313
|
"open_source_cleanup_plan_path": str(open_source_manifest.get("cleanup_plan_path") or "").strip()
|
|
3753
5314
|
or "release/open_source/cleanup_plan.md",
|
|
3754
5315
|
"selected_outline_ref": str(selected_outline.get("outline_id") or "").strip() or None,
|
|
@@ -3773,24 +5334,27 @@ class ArtifactService:
|
|
|
3773
5334
|
"draft_path": manifest.get("draft_path"),
|
|
3774
5335
|
"pdf_path": manifest.get("pdf_path"),
|
|
3775
5336
|
"baseline_inventory_path": str(baseline_inventory_path),
|
|
3776
|
-
"open_source_manifest_path": str(self._open_source_manifest_path(quest_root)),
|
|
5337
|
+
"open_source_manifest_path": str(self._open_source_manifest_path(quest_root, workspace_root=workspace_root)),
|
|
3777
5338
|
},
|
|
3778
5339
|
"details": {
|
|
3779
5340
|
"title": manifest.get("title"),
|
|
3780
5341
|
"selected_outline_ref": manifest.get("selected_outline_ref"),
|
|
3781
5342
|
"baseline_inventory_count": len(baseline_inventory.get("supplementary_baselines") or []),
|
|
3782
5343
|
"open_source_status": open_source_manifest.get("status"),
|
|
5344
|
+
"paper_branch": paper_branch,
|
|
5345
|
+
"source_branch": source_branch,
|
|
5346
|
+
"source_run_id": source_run_id,
|
|
3783
5347
|
},
|
|
3784
5348
|
},
|
|
3785
5349
|
checkpoint=False,
|
|
3786
|
-
workspace_root=
|
|
5350
|
+
workspace_root=workspace_root,
|
|
3787
5351
|
)
|
|
3788
5352
|
return {
|
|
3789
5353
|
"ok": True,
|
|
3790
5354
|
"manifest_path": str(manifest_path),
|
|
3791
5355
|
"manifest": manifest,
|
|
3792
5356
|
"baseline_inventory_path": str(baseline_inventory_path),
|
|
3793
|
-
"open_source_manifest_path": str(self._open_source_manifest_path(quest_root)),
|
|
5357
|
+
"open_source_manifest_path": str(self._open_source_manifest_path(quest_root, workspace_root=workspace_root)),
|
|
3794
5358
|
"artifact": artifact,
|
|
3795
5359
|
}
|
|
3796
5360
|
|
|
@@ -3828,7 +5392,17 @@ class ArtifactService:
|
|
|
3828
5392
|
|
|
3829
5393
|
evidence_paths = [str(item).strip() for item in (evidence_paths or []) if str(item).strip()]
|
|
3830
5394
|
deviations = [str(item).strip() for item in (deviations or []) if str(item).strip()]
|
|
3831
|
-
|
|
5395
|
+
normalized_metric_rows = normalize_metric_rows(metric_rows or [])
|
|
5396
|
+
normalized_metrics_summary = {
|
|
5397
|
+
str(item.get("metric_id") or "").strip(): item.get("value")
|
|
5398
|
+
for item in normalized_metric_rows
|
|
5399
|
+
if str(item.get("metric_id") or "").strip()
|
|
5400
|
+
}
|
|
5401
|
+
normalized_metric_contract = normalize_metric_contract(
|
|
5402
|
+
{},
|
|
5403
|
+
metrics_summary=normalized_metrics_summary,
|
|
5404
|
+
metric_rows=normalized_metric_rows,
|
|
5405
|
+
)
|
|
3832
5406
|
normalized_comparison_baselines = self._normalize_comparison_baselines(quest_root, comparison_baselines)
|
|
3833
5407
|
normalized_claim_impact = str(claim_impact or "").strip() or None
|
|
3834
5408
|
normalized_reviewer_resolution = str(reviewer_resolution or "").strip() or None
|
|
@@ -3896,9 +5470,9 @@ class ArtifactService:
|
|
|
3896
5470
|
result_lines.extend([f"- `{item}`" for item in evidence_paths])
|
|
3897
5471
|
else:
|
|
3898
5472
|
result_lines.append("- None recorded.")
|
|
3899
|
-
if
|
|
5473
|
+
if normalized_metric_rows:
|
|
3900
5474
|
result_lines.extend(["", "## Metric Rows", ""])
|
|
3901
|
-
for row in
|
|
5475
|
+
for row in normalized_metric_rows:
|
|
3902
5476
|
result_lines.append(f"- `{row}`")
|
|
3903
5477
|
result_lines.extend(["", "## Comparison Baselines", ""])
|
|
3904
5478
|
if normalized_comparison_baselines:
|
|
@@ -3918,16 +5492,6 @@ class ArtifactService:
|
|
|
3918
5492
|
result_lines.extend(["", "## Subset Approval", "", f"`{subset_approval_ref}`"])
|
|
3919
5493
|
write_text(result_path, "\n".join(result_lines).rstrip() + "\n")
|
|
3920
5494
|
|
|
3921
|
-
metrics_summary: dict[str, Any] = {}
|
|
3922
|
-
for row in metric_rows:
|
|
3923
|
-
name = str(row.get("name") or row.get("metric") or "").strip()
|
|
3924
|
-
if name:
|
|
3925
|
-
metrics_summary[name] = row.get("value")
|
|
3926
|
-
continue
|
|
3927
|
-
keys = [key for key in row.keys() if key not in {"split", "seed", "note", "notes"}]
|
|
3928
|
-
if len(keys) == 1:
|
|
3929
|
-
metrics_summary[keys[0]] = row.get(keys[0])
|
|
3930
|
-
|
|
3931
5495
|
result_payload = {
|
|
3932
5496
|
"schema_version": 1,
|
|
3933
5497
|
"result_kind": "analysis_slice",
|
|
@@ -3939,8 +5503,9 @@ class ArtifactService:
|
|
|
3939
5503
|
"run_kind": target.get("run_kind"),
|
|
3940
5504
|
"required_baselines": target.get("required_baselines") or [],
|
|
3941
5505
|
"comparison_baselines": normalized_comparison_baselines,
|
|
3942
|
-
"metrics_summary":
|
|
3943
|
-
"metric_rows":
|
|
5506
|
+
"metrics_summary": normalized_metrics_summary,
|
|
5507
|
+
"metric_rows": normalized_metric_rows,
|
|
5508
|
+
"metric_contract": normalized_metric_contract,
|
|
3944
5509
|
"dataset_scope": normalized_scope,
|
|
3945
5510
|
"subset_approval_ref": subset_approval_ref,
|
|
3946
5511
|
"setup": setup.strip() or None,
|
|
@@ -4026,7 +5591,11 @@ class ArtifactService:
|
|
|
4026
5591
|
"parent_branch": parent_branch,
|
|
4027
5592
|
"worktree_root": str(slice_worktree_root),
|
|
4028
5593
|
"worktree_rel_path": self._workspace_relative(quest_root, slice_worktree_root),
|
|
4029
|
-
"metrics_summary":
|
|
5594
|
+
"metrics_summary": normalized_metrics_summary,
|
|
5595
|
+
"metric_rows": normalized_metric_rows,
|
|
5596
|
+
"metric_contract": normalized_metric_contract,
|
|
5597
|
+
"comparison_baselines": normalized_comparison_baselines,
|
|
5598
|
+
"evidence_paths": evidence_paths,
|
|
4030
5599
|
"flow_type": "analysis_slice",
|
|
4031
5600
|
"protocol_step": "record",
|
|
4032
5601
|
"paths": {
|
|
@@ -4040,7 +5609,7 @@ class ArtifactService:
|
|
|
4040
5609
|
"must_not_simplify": target.get("must_not_simplify"),
|
|
4041
5610
|
"dataset_scope": normalized_scope,
|
|
4042
5611
|
"subset_approval_ref": subset_approval_ref,
|
|
4043
|
-
"metric_rows":
|
|
5612
|
+
"metric_rows": normalized_metric_rows,
|
|
4044
5613
|
"claim_impact": normalized_claim_impact,
|
|
4045
5614
|
"reviewer_resolution": normalized_reviewer_resolution,
|
|
4046
5615
|
"manuscript_update_hint": normalized_manuscript_update_hint,
|
|
@@ -4080,6 +5649,8 @@ class ArtifactService:
|
|
|
4080
5649
|
updated["reviewer_resolution"] = normalized_reviewer_resolution
|
|
4081
5650
|
updated["manuscript_update_hint"] = normalized_manuscript_update_hint
|
|
4082
5651
|
updated["next_recommendation"] = normalized_next_recommendation
|
|
5652
|
+
updated["metrics_summary"] = normalized_metrics_summary
|
|
5653
|
+
updated["metric_rows"] = normalized_metric_rows
|
|
4083
5654
|
updated["comparison_baselines"] = normalized_comparison_baselines
|
|
4084
5655
|
updated["evaluation_summary"] = normalized_evaluation_summary
|
|
4085
5656
|
updated_slices.append(updated)
|
|
@@ -4137,13 +5708,13 @@ class ArtifactService:
|
|
|
4137
5708
|
interaction = self.interact(
|
|
4138
5709
|
quest_root,
|
|
4139
5710
|
kind="progress",
|
|
4140
|
-
message=(
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
5711
|
+
message=self._build_analysis_slice_interaction_message(
|
|
5712
|
+
campaign_id=campaign_id,
|
|
5713
|
+
slice_id=slice_id,
|
|
5714
|
+
evaluation_summary=normalized_evaluation_summary,
|
|
5715
|
+
claim_impact=normalized_claim_impact,
|
|
5716
|
+
next_slice=next_slice,
|
|
5717
|
+
mirror_rel_path=self._workspace_relative(quest_root, mirror_path),
|
|
4147
5718
|
),
|
|
4148
5719
|
deliver_to_bound_conversations=True,
|
|
4149
5720
|
include_recent_inbound_messages=False,
|
|
@@ -4229,26 +5800,55 @@ class ArtifactService:
|
|
|
4229
5800
|
message=f"analysis: summarize {campaign_id}",
|
|
4230
5801
|
)
|
|
4231
5802
|
restored_idea_id = self._latest_branch_idea_id(quest_root, parent_branch) or str(manifest.get("active_idea_id") or "").strip() or None
|
|
4232
|
-
|
|
5803
|
+
startup_contract = self._startup_contract(quest_root)
|
|
5804
|
+
raw_need_research_paper = startup_contract.get("need_research_paper")
|
|
5805
|
+
need_research_paper = raw_need_research_paper if isinstance(raw_need_research_paper, bool) else True
|
|
5806
|
+
base_research_state = self.quest_service.update_research_state(
|
|
4233
5807
|
quest_root,
|
|
4234
5808
|
active_idea_id=restored_idea_id,
|
|
4235
5809
|
active_analysis_campaign_id=None,
|
|
5810
|
+
analysis_parent_branch=None,
|
|
5811
|
+
analysis_parent_worktree_root=None,
|
|
5812
|
+
paper_parent_branch=None,
|
|
5813
|
+
paper_parent_worktree_root=None,
|
|
5814
|
+
paper_parent_run_id=None,
|
|
4236
5815
|
next_pending_slice_id=None,
|
|
4237
5816
|
current_workspace_branch=parent_branch,
|
|
4238
5817
|
current_workspace_root=str(parent_worktree_root),
|
|
4239
|
-
workspace_mode="idea",
|
|
5818
|
+
workspace_mode="run" if self._branch_kind_from_name(parent_branch) == "run" else "idea",
|
|
4240
5819
|
last_flow_type="analysis_campaign_complete",
|
|
4241
5820
|
)
|
|
4242
|
-
|
|
5821
|
+
writing_workspace: dict[str, Any] | None = None
|
|
5822
|
+
if need_research_paper:
|
|
5823
|
+
try:
|
|
5824
|
+
writing_workspace = self._ensure_active_paper_workspace(
|
|
5825
|
+
quest_root,
|
|
5826
|
+
source_branch=parent_branch,
|
|
5827
|
+
source_run_id=str(manifest.get("parent_run_id") or "").strip() or None,
|
|
5828
|
+
source_idea_id=restored_idea_id,
|
|
5829
|
+
)
|
|
5830
|
+
except Exception:
|
|
5831
|
+
writing_workspace = None
|
|
5832
|
+
|
|
5833
|
+
if writing_workspace:
|
|
5834
|
+
research_state = self.quest_service.read_research_state(quest_root)
|
|
5835
|
+
self.quest_service.update_settings(self._quest_id(quest_root), active_anchor="write")
|
|
5836
|
+
else:
|
|
5837
|
+
research_state = base_research_state
|
|
5838
|
+
self.quest_service.update_settings(self._quest_id(quest_root), active_anchor="decision")
|
|
4243
5839
|
interaction = self.interact(
|
|
4244
5840
|
quest_root,
|
|
4245
5841
|
kind="milestone",
|
|
4246
|
-
message=(
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
5842
|
+
message=self._build_analysis_complete_interaction_message(
|
|
5843
|
+
campaign_id=campaign_id,
|
|
5844
|
+
completed_slices=updated_slices,
|
|
5845
|
+
summary_rel_path=self._workspace_relative(quest_root, summary_path),
|
|
5846
|
+
writing_branch=writing_workspace.get("branch") if writing_workspace else None,
|
|
5847
|
+
writing_worktree_rel_path=(
|
|
5848
|
+
self._workspace_relative(quest_root, Path(str(writing_workspace.get("worktree_root"))))
|
|
5849
|
+
if writing_workspace and str(writing_workspace.get("worktree_root") or "").strip()
|
|
5850
|
+
else None
|
|
5851
|
+
),
|
|
4252
5852
|
),
|
|
4253
5853
|
deliver_to_bound_conversations=True,
|
|
4254
5854
|
include_recent_inbound_messages=False,
|
|
@@ -4259,6 +5859,8 @@ class ArtifactService:
|
|
|
4259
5859
|
"parent_branch": parent_branch,
|
|
4260
5860
|
"parent_worktree_root": str(parent_worktree_root),
|
|
4261
5861
|
"summary_path": str(summary_path),
|
|
5862
|
+
"writing_branch": writing_workspace.get("branch") if writing_workspace else None,
|
|
5863
|
+
"writing_worktree_root": writing_workspace.get("worktree_root") if writing_workspace else None,
|
|
4262
5864
|
}
|
|
4263
5865
|
],
|
|
4264
5866
|
)
|
|
@@ -4284,6 +5886,8 @@ class ArtifactService:
|
|
|
4284
5886
|
"completed": True,
|
|
4285
5887
|
"returned_to_branch": parent_branch,
|
|
4286
5888
|
"returned_to_worktree_root": str(parent_worktree_root),
|
|
5889
|
+
"writing_branch": writing_workspace.get("branch") if writing_workspace else None,
|
|
5890
|
+
"writing_worktree_root": writing_workspace.get("worktree_root") if writing_workspace else None,
|
|
4287
5891
|
}
|
|
4288
5892
|
|
|
4289
5893
|
def publish_baseline(self, quest_root: Path, payload: dict) -> dict:
|
|
@@ -4334,6 +5938,81 @@ class ArtifactService:
|
|
|
4334
5938
|
"guidance": "The selected baseline is now attached under baselines/imported. Reuse it before considering a fresh reproduction.",
|
|
4335
5939
|
}
|
|
4336
5940
|
|
|
5941
|
+
def delete_baseline(self, baseline_id: str) -> dict[str, Any]:
|
|
5942
|
+
existing = self.baselines.get(baseline_id, include_deleted=True)
|
|
5943
|
+
if existing is None:
|
|
5944
|
+
raise FileNotFoundError(f"Unknown baseline: {baseline_id}")
|
|
5945
|
+
|
|
5946
|
+
normalized_baseline_id = str(existing.get("baseline_id") or existing.get("entry_id") or baseline_id).strip()
|
|
5947
|
+
already_deleted = self.baselines.is_deleted(normalized_baseline_id)
|
|
5948
|
+
deleted_entry = self.baselines.delete(normalized_baseline_id) if not already_deleted else dict(existing)
|
|
5949
|
+
|
|
5950
|
+
affected_quest_ids: list[str] = []
|
|
5951
|
+
cleared_requested_refs = 0
|
|
5952
|
+
cleared_confirmed_refs = 0
|
|
5953
|
+
deleted_paths: list[str] = []
|
|
5954
|
+
warnings: list[str] = []
|
|
5955
|
+
quests_root = self.home / "quests"
|
|
5956
|
+
|
|
5957
|
+
for quest_yaml in sorted(quests_root.glob("*/quest.yaml")):
|
|
5958
|
+
quest_root = quest_yaml.parent
|
|
5959
|
+
quest_id = quest_root.name
|
|
5960
|
+
quest_touched = False
|
|
5961
|
+
quest_payload = self.quest_service.read_quest_yaml(quest_root)
|
|
5962
|
+
|
|
5963
|
+
requested_ref = (
|
|
5964
|
+
dict(quest_payload.get("requested_baseline_ref") or {})
|
|
5965
|
+
if isinstance(quest_payload.get("requested_baseline_ref"), dict)
|
|
5966
|
+
else {}
|
|
5967
|
+
)
|
|
5968
|
+
if str(requested_ref.get("baseline_id") or "").strip() == normalized_baseline_id:
|
|
5969
|
+
self.quest_service.update_startup_context(quest_root, requested_baseline_ref=None)
|
|
5970
|
+
cleared_requested_refs += 1
|
|
5971
|
+
quest_touched = True
|
|
5972
|
+
|
|
5973
|
+
confirmed_ref = (
|
|
5974
|
+
dict(quest_payload.get("confirmed_baseline_ref") or {})
|
|
5975
|
+
if isinstance(quest_payload.get("confirmed_baseline_ref"), dict)
|
|
5976
|
+
else {}
|
|
5977
|
+
)
|
|
5978
|
+
if str(confirmed_ref.get("baseline_id") or "").strip() == normalized_baseline_id:
|
|
5979
|
+
self.quest_service.update_baseline_state(
|
|
5980
|
+
quest_root,
|
|
5981
|
+
baseline_gate="pending",
|
|
5982
|
+
confirmed_baseline_ref=None,
|
|
5983
|
+
active_anchor="baseline",
|
|
5984
|
+
)
|
|
5985
|
+
cleared_confirmed_refs += 1
|
|
5986
|
+
quest_touched = True
|
|
5987
|
+
|
|
5988
|
+
for root in self._baseline_workspace_roots(quest_root):
|
|
5989
|
+
try:
|
|
5990
|
+
removed = self._remove_baseline_materialization(root, normalized_baseline_id)
|
|
5991
|
+
except OSError as exc:
|
|
5992
|
+
warnings.append(
|
|
5993
|
+
f"Unable to remove baseline materialization under `{root}` for quest `{quest_id}`: {exc}"
|
|
5994
|
+
)
|
|
5995
|
+
continue
|
|
5996
|
+
if removed:
|
|
5997
|
+
deleted_paths.extend(removed)
|
|
5998
|
+
quest_touched = True
|
|
5999
|
+
|
|
6000
|
+
if quest_touched:
|
|
6001
|
+
affected_quest_ids.append(quest_id)
|
|
6002
|
+
|
|
6003
|
+
return {
|
|
6004
|
+
"ok": True,
|
|
6005
|
+
"baseline_id": normalized_baseline_id,
|
|
6006
|
+
"deleted": not already_deleted,
|
|
6007
|
+
"already_deleted": already_deleted,
|
|
6008
|
+
"baseline_registry_entry": deleted_entry,
|
|
6009
|
+
"affected_quest_ids": affected_quest_ids,
|
|
6010
|
+
"cleared_requested_refs": cleared_requested_refs,
|
|
6011
|
+
"cleared_confirmed_refs": cleared_confirmed_refs,
|
|
6012
|
+
"deleted_paths": deleted_paths,
|
|
6013
|
+
"warnings": warnings,
|
|
6014
|
+
}
|
|
6015
|
+
|
|
4337
6016
|
def confirm_baseline(
|
|
4338
6017
|
self,
|
|
4339
6018
|
quest_root: Path,
|
|
@@ -4345,9 +6024,11 @@ class ArtifactService:
|
|
|
4345
6024
|
summary: str | None = None,
|
|
4346
6025
|
baseline_kind: str | None = None,
|
|
4347
6026
|
metric_contract: dict[str, Any] | None = None,
|
|
6027
|
+
metric_directions: dict[str, str] | None = None,
|
|
4348
6028
|
metrics_summary: dict[str, Any] | None = None,
|
|
4349
6029
|
primary_metric: dict[str, Any] | None = None,
|
|
4350
6030
|
auto_advance: bool = True,
|
|
6031
|
+
strict_metric_contract: bool = False,
|
|
4351
6032
|
) -> dict[str, Any]:
|
|
4352
6033
|
resolved = self._resolve_baseline_path(quest_root, baseline_path, baseline_id=baseline_id)
|
|
4353
6034
|
resolved_baseline_id = str(resolved["baseline_id"] or "").strip()
|
|
@@ -4439,6 +6120,85 @@ class ArtifactService:
|
|
|
4439
6120
|
or ""
|
|
4440
6121
|
).strip() or None
|
|
4441
6122
|
|
|
6123
|
+
source_metrics_summary = (
|
|
6124
|
+
selected_variant.get("metrics_summary")
|
|
6125
|
+
if isinstance(selected_variant, dict) and selected_variant.get("metrics_summary") is not None
|
|
6126
|
+
else entry.get("metrics_summary")
|
|
6127
|
+
)
|
|
6128
|
+
entry_metric_contract, entry_primary_metric = self._apply_metric_directions_to_contract(
|
|
6129
|
+
metric_contract=entry.get("metric_contract"),
|
|
6130
|
+
metric_directions=metric_directions,
|
|
6131
|
+
baseline_id=resolved_baseline_id,
|
|
6132
|
+
metrics_summary=source_metrics_summary,
|
|
6133
|
+
primary_metric=entry.get("primary_metric"),
|
|
6134
|
+
baseline_variants=entry.get("baseline_variants"),
|
|
6135
|
+
)
|
|
6136
|
+
entry = {
|
|
6137
|
+
**entry,
|
|
6138
|
+
"metric_contract": entry_metric_contract,
|
|
6139
|
+
"primary_metric": entry_primary_metric or entry.get("primary_metric"),
|
|
6140
|
+
}
|
|
6141
|
+
canonical_baseline = (
|
|
6142
|
+
validate_baseline_metric_contract_submission(
|
|
6143
|
+
metric_contract=entry.get("metric_contract"),
|
|
6144
|
+
metrics_summary=source_metrics_summary,
|
|
6145
|
+
primary_metric=entry.get("primary_metric"),
|
|
6146
|
+
)
|
|
6147
|
+
if strict_metric_contract
|
|
6148
|
+
else canonicalize_baseline_submission(
|
|
6149
|
+
metric_contract=entry.get("metric_contract"),
|
|
6150
|
+
metrics_summary=source_metrics_summary,
|
|
6151
|
+
primary_metric=entry.get("primary_metric"),
|
|
6152
|
+
)
|
|
6153
|
+
)
|
|
6154
|
+
entry = {
|
|
6155
|
+
**entry,
|
|
6156
|
+
"metrics_summary": canonical_baseline["metrics_summary"],
|
|
6157
|
+
"metric_contract": canonical_baseline["metric_contract"],
|
|
6158
|
+
"metric_details": canonical_baseline["metric_details"],
|
|
6159
|
+
}
|
|
6160
|
+
if isinstance(selected_variant, dict):
|
|
6161
|
+
selected_variant = {
|
|
6162
|
+
**selected_variant,
|
|
6163
|
+
"metrics_summary": canonical_baseline["metrics_summary"],
|
|
6164
|
+
}
|
|
6165
|
+
if isinstance(entry.get("baseline_variants"), list):
|
|
6166
|
+
entry["baseline_variants"] = [
|
|
6167
|
+
(
|
|
6168
|
+
{
|
|
6169
|
+
**variant,
|
|
6170
|
+
"metrics_summary": canonical_baseline["metrics_summary"],
|
|
6171
|
+
}
|
|
6172
|
+
if isinstance(variant, dict)
|
|
6173
|
+
and str(variant.get("variant_id") or "").strip() == str(resolved_variant_id or "").strip()
|
|
6174
|
+
else variant
|
|
6175
|
+
)
|
|
6176
|
+
for variant in entry.get("baseline_variants", [])
|
|
6177
|
+
]
|
|
6178
|
+
primary_metric_id = str(
|
|
6179
|
+
(entry.get("primary_metric") or {}).get("metric_id")
|
|
6180
|
+
or (entry.get("primary_metric") or {}).get("name")
|
|
6181
|
+
or (entry.get("primary_metric") or {}).get("id")
|
|
6182
|
+
or (canonical_baseline["metric_contract"] or {}).get("primary_metric_id")
|
|
6183
|
+
or ""
|
|
6184
|
+
).strip()
|
|
6185
|
+
if primary_metric_id and primary_metric_id in canonical_baseline["metrics_summary"]:
|
|
6186
|
+
primary_metric_meta = next(
|
|
6187
|
+
(
|
|
6188
|
+
item
|
|
6189
|
+
for item in (canonical_baseline["metric_contract"] or {}).get("metrics", [])
|
|
6190
|
+
if isinstance(item, dict) and str(item.get("metric_id") or "").strip() == primary_metric_id
|
|
6191
|
+
),
|
|
6192
|
+
{},
|
|
6193
|
+
)
|
|
6194
|
+
entry["primary_metric"] = {
|
|
6195
|
+
**(dict(entry.get("primary_metric") or {}) if isinstance(entry.get("primary_metric"), dict) else {}),
|
|
6196
|
+
"metric_id": primary_metric_id,
|
|
6197
|
+
"value": canonical_baseline["metrics_summary"][primary_metric_id],
|
|
6198
|
+
"direction": primary_metric_meta.get("direction")
|
|
6199
|
+
or (entry.get("primary_metric") or {}).get("direction"),
|
|
6200
|
+
}
|
|
6201
|
+
|
|
4442
6202
|
metric_contract_json = self._write_baseline_metric_contract_json(
|
|
4443
6203
|
quest_root,
|
|
4444
6204
|
baseline_root=resolved_root,
|
|
@@ -4540,7 +6300,10 @@ class ArtifactService:
|
|
|
4540
6300
|
"artifact": artifact,
|
|
4541
6301
|
"baseline_registry_entry": registry_entry,
|
|
4542
6302
|
"snapshot": self.quest_service.snapshot(self._quest_id(quest_root)),
|
|
6303
|
+
"metric_details": canonical_baseline["metric_details"],
|
|
4543
6304
|
"legacy_guidance": "Baseline gate confirmed. Idea selection is now the default next anchor.",
|
|
6305
|
+
"metric_contract_json_path": str(metric_contract_json.get("path") or ""),
|
|
6306
|
+
"metric_contract_json_rel_path": str(metric_contract_json.get("rel_path") or ""),
|
|
4544
6307
|
}
|
|
4545
6308
|
|
|
4546
6309
|
def waive_baseline(
|
|
@@ -5159,6 +6922,8 @@ class ArtifactService:
|
|
|
5159
6922
|
return f"idea/{quest_id}-{idea_id}"
|
|
5160
6923
|
if branch_kind == "quest":
|
|
5161
6924
|
return f"quest/{quest_id}"
|
|
6925
|
+
if branch_kind == "paper":
|
|
6926
|
+
return f"paper/{run_id or generate_id('paper')}"
|
|
5162
6927
|
return f"run/{run_id or generate_id('run')}"
|
|
5163
6928
|
|
|
5164
6929
|
def _bound_conversations(self, quest_root: Path) -> list[str]:
|
|
@@ -5187,7 +6952,14 @@ class ArtifactService:
|
|
|
5187
6952
|
return targets
|
|
5188
6953
|
|
|
5189
6954
|
def _connectors_config(self) -> dict[str, Any]:
|
|
5190
|
-
|
|
6955
|
+
manager = ConfigManager(self.home)
|
|
6956
|
+
connectors = manager.load_named_normalized("connectors")
|
|
6957
|
+
for name, config in list(connectors.items()):
|
|
6958
|
+
if str(name).startswith("_") or not isinstance(config, dict):
|
|
6959
|
+
continue
|
|
6960
|
+
if not manager.is_connector_system_enabled(str(name)):
|
|
6961
|
+
config["enabled"] = False
|
|
6962
|
+
return connectors
|
|
5191
6963
|
|
|
5192
6964
|
@staticmethod
|
|
5193
6965
|
def _delivery_policy(connectors: dict[str, Any]) -> str:
|