@researai/deepscientist 1.5.7 → 1.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/bin/ds.js +220 -5
- package/docs/en/07_MEMORY_AND_MCP.md +40 -3
- package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
- 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 +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/acp/envelope.py +1 -0
- package/src/deepscientist/artifact/metrics.py +813 -80
- package/src/deepscientist/artifact/schemas.py +1 -0
- package/src/deepscientist/artifact/service.py +1101 -99
- 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 +284 -14
- package/src/deepscientist/daemon/api/router.py +1 -0
- package/src/deepscientist/daemon/app.py +291 -20
- package/src/deepscientist/gitops/diff.py +6 -10
- package/src/deepscientist/mcp/server.py +188 -39
- package/src/deepscientist/prompts/builder.py +51 -18
- package/src/deepscientist/quest/service.py +83 -34
- package/src/deepscientist/quest/stage_views.py +74 -29
- 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 +106 -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 +2 -8
- 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 +52 -16
- 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-m2FNtwbn.js} +110 -14
- package/src/ui/dist/assets/{AnalysisPlugin-DLPXQsmr.js → AnalysisPlugin-BMTF8EGL.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-C-Fr9knQ.js → AutoFigurePlugin-DxPdMUNb.js} +5 -5
- package/src/ui/dist/assets/{CliPlugin-Dd8AHzFg.js → CliPlugin-BEOWgxCI.js} +9 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-Dg-RepTl.js → CodeEditorPlugin-BCXvjqmb.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-D2J_3nyt.js → CodeViewerPlugin-DaJcy3nD.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-ChRLLKNb.js → DocViewerPlugin-ByfeIq4K.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-DgHfcved.js → GitDiffViewerPlugin-Cksf3VZ-.js} +830 -86
- package/src/ui/dist/assets/{ImageViewerPlugin-C89GZMBy.js → ImageViewerPlugin-CFz-OsTS.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-BUfIwUcb.js → LabCopilotPanel-CJ1cJzoX.js} +10 -10
- package/src/ui/dist/assets/{LabPlugin-zvUmQUMq.js → LabPlugin-BF3dVJwa.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-C1SSNuWp.js → LatexPlugin-DDkwZ6Sj.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-D2Mf5tU5.js → MarkdownViewerPlugin-HAuvurcT.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-CF4LgiS2.js → MarketplacePlugin-BtoTYy2C.js} +3 -3
- package/src/ui/dist/assets/{index-Be0NAmh8.js → NotebookEditor-CSJYx7b-.js} +12 -155
- package/src/ui/dist/assets/{NotebookEditor-BM7Bgwlv.js → NotebookEditor-DQgRezm_.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-Bc5qfD-Z.js → PdfLoader-DPa_-fv6.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-sh1-IRcp.js → PdfMarkdownPlugin-BZpXOEjm.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-C_a7CpWG.js → PdfViewerPlugin-BT8a6wGR.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-L4z3HcLf.js → SearchPlugin-D_blveZi.js} +1 -1
- package/src/ui/dist/assets/{Stepper-Dk4aQ3fN.js → Stepper-DH2k75Vo.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-BsNtlKVo.js → TextViewerPlugin-Btx0M3hX.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-BpeDcZ5_.js → VNCViewer-DImJO4rO.js} +9 -9
- package/src/ui/dist/assets/{bibtex-C4QI-bbj.js → bibtex-B-Hqu0Sg.js} +1 -1
- package/src/ui/dist/assets/{code-DuMINRsg.js → code-BUfXGJSl.js} +1 -1
- package/src/ui/dist/assets/{file-content-C3N-432K.js → file-content-VqamwI3X.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-CffQ4ZMg.js → file-diff-panel-C_wOoS7a.js} +1 -1
- package/src/ui/dist/assets/{file-socket-CRH59PCO.js → file-socket-D2bTuMVP.js} +1 -1
- package/src/ui/dist/assets/{file-utils-vYGtW2mI.js → file-utils--zJCPN1i.js} +1 -1
- package/src/ui/dist/assets/{image-DBVGaooo.js → image-BZkGJ4mM.js} +1 -1
- package/src/ui/dist/assets/{index-DjSFDmgB.js → index-CxkvSeKw.js} +2 -2
- package/src/ui/dist/assets/{index-BpjYH9Vg.js → index-D9QIGcmc.js} +1 -1
- package/src/ui/dist/assets/{index-Do9N28uB.css → index-DXZ1daiJ.css} +163 -34
- package/src/ui/dist/assets/index-DdRW6RMJ.js +159 -0
- package/src/ui/dist/assets/{index-B1P6hQRJ.js → index-DjggJovS.js} +3029 -1780
- package/src/ui/dist/assets/{message-square-BsPDBhiY.js → message-square-FUIPIhU2.js} +1 -1
- package/src/ui/dist/assets/{monaco-BTkdPojV.js → monaco-DHMc7kKM.js} +1 -1
- package/src/ui/dist/assets/{popover-cWjCk-vc.js → popover-B85oCgCS.js} +1 -1
- package/src/ui/dist/assets/{project-sync-CXn530xb.js → project-sync-DOMCcPac.js} +1 -1
- package/src/ui/dist/assets/{sigma-04Jr12jg.js → sigma-BO2rQrl3.js} +1 -1
- package/src/ui/dist/assets/{tooltip-BdVDl0G5.js → tooltip-B1OspAkx.js} +1 -1
- package/src/ui/dist/assets/{trash-CB_GlQyC.js → trash-BsVEH_dV.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-BL932NwS.js → useCliAccess-b8L6JuZm.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-B2WK7Tvq.js → useFileDiffOverlay-BY7uA9hV.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-YC68g12z.js → wrap-text-BwyVuUIK.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-C0RJvFiJ.js → zoom-out-RDpLugQP.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,17 @@ 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,
|
|
48
51
|
normalize_metric_rows,
|
|
49
52
|
normalize_metrics_summary,
|
|
50
53
|
selected_baseline_metrics,
|
|
51
54
|
to_number,
|
|
55
|
+
validate_baseline_metric_contract_submission,
|
|
56
|
+
validate_main_experiment_against_baseline_contract,
|
|
52
57
|
)
|
|
53
58
|
from .schemas import ARTIFACT_DIRS, guidance_for_kind, validate_artifact_payload
|
|
54
59
|
|
|
@@ -126,6 +131,19 @@ class ArtifactService:
|
|
|
126
131
|
lines = [f"- {label}: {normalized[key]}" for key, label in labels if normalized.get(key)]
|
|
127
132
|
return lines or ["- Not recorded."]
|
|
128
133
|
|
|
134
|
+
def _load_metric_contract_payload(self, quest_root: Path, metric_contract_json_rel_path: str | None) -> dict[str, Any] | None:
|
|
135
|
+
rel_path = str(metric_contract_json_rel_path or "").strip()
|
|
136
|
+
if not rel_path:
|
|
137
|
+
return None
|
|
138
|
+
try:
|
|
139
|
+
resolved_path = resolve_within(quest_root, rel_path)
|
|
140
|
+
except ValueError:
|
|
141
|
+
return None
|
|
142
|
+
if not resolved_path.exists():
|
|
143
|
+
return None
|
|
144
|
+
payload = read_json(resolved_path, {})
|
|
145
|
+
return payload if isinstance(payload, dict) and payload else None
|
|
146
|
+
|
|
129
147
|
def _workspace_root_for(self, quest_root: Path, workspace_root: Path | None = None) -> Path:
|
|
130
148
|
if workspace_root is not None:
|
|
131
149
|
return workspace_root
|
|
@@ -139,6 +157,73 @@ class ArtifactService:
|
|
|
139
157
|
except ValueError:
|
|
140
158
|
return str(path)
|
|
141
159
|
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _branch_kind_from_name(branch_name: str | None) -> str:
|
|
162
|
+
normalized = str(branch_name or "").strip()
|
|
163
|
+
if normalized in {"main", "master"} or normalized.startswith("quest/"):
|
|
164
|
+
return "quest"
|
|
165
|
+
if normalized.startswith("idea/"):
|
|
166
|
+
return "idea"
|
|
167
|
+
if normalized.startswith("analysis/"):
|
|
168
|
+
return "analysis"
|
|
169
|
+
if normalized.startswith("paper/"):
|
|
170
|
+
return "paper"
|
|
171
|
+
if normalized.startswith("run/"):
|
|
172
|
+
return "run"
|
|
173
|
+
return "branch"
|
|
174
|
+
|
|
175
|
+
def _workspace_mode_for_branch(self, branch_name: str | None, *, has_idea: bool = False) -> str:
|
|
176
|
+
branch_kind = self._branch_kind_from_name(branch_name)
|
|
177
|
+
if branch_kind == "paper":
|
|
178
|
+
return "paper"
|
|
179
|
+
if branch_kind == "analysis":
|
|
180
|
+
return "analysis"
|
|
181
|
+
if branch_kind == "run":
|
|
182
|
+
return "run"
|
|
183
|
+
if branch_kind == "idea" or has_idea:
|
|
184
|
+
return "idea"
|
|
185
|
+
return "quest"
|
|
186
|
+
|
|
187
|
+
def _prepare_branch_worktree_root(
|
|
188
|
+
self,
|
|
189
|
+
quest_root: Path,
|
|
190
|
+
*,
|
|
191
|
+
branch_name: str,
|
|
192
|
+
branch_kind: str,
|
|
193
|
+
run_id: str | None = None,
|
|
194
|
+
idea_id: str | None = None,
|
|
195
|
+
) -> Path:
|
|
196
|
+
normalized_kind = str(branch_kind or "").strip().lower() or "run"
|
|
197
|
+
normalized_run_id = str(run_id or "").strip() or None
|
|
198
|
+
normalized_idea_id = str(idea_id or "").strip() or None
|
|
199
|
+
if normalized_kind == "idea" and normalized_idea_id:
|
|
200
|
+
return canonical_worktree_root(quest_root, f"idea-{normalized_idea_id}")
|
|
201
|
+
if normalized_kind == "paper":
|
|
202
|
+
return canonical_worktree_root(
|
|
203
|
+
quest_root,
|
|
204
|
+
f"paper-{normalized_run_id or slugify(branch_name, 'paper')}",
|
|
205
|
+
)
|
|
206
|
+
if normalized_kind == "run" and normalized_run_id:
|
|
207
|
+
return canonical_worktree_root(quest_root, normalized_run_id)
|
|
208
|
+
return canonical_worktree_root(quest_root, slugify(branch_name, "branch"))
|
|
209
|
+
|
|
210
|
+
def _latest_prepare_branch_record(self, quest_root: Path, branch_name: str) -> dict[str, Any]:
|
|
211
|
+
normalized_branch = str(branch_name or "").strip()
|
|
212
|
+
if not normalized_branch:
|
|
213
|
+
return {}
|
|
214
|
+
for item in reversed(self.quest_service._collect_artifacts(quest_root)):
|
|
215
|
+
payload = dict(item.get("payload") or {}) if isinstance(item.get("payload"), dict) else {}
|
|
216
|
+
if not payload:
|
|
217
|
+
continue
|
|
218
|
+
if str(payload.get("kind") or "").strip() != "decision":
|
|
219
|
+
continue
|
|
220
|
+
if str(payload.get("action") or "").strip() != "prepare_branch":
|
|
221
|
+
continue
|
|
222
|
+
if str(payload.get("branch") or "").strip() != normalized_branch:
|
|
223
|
+
continue
|
|
224
|
+
return payload
|
|
225
|
+
return {}
|
|
226
|
+
|
|
142
227
|
def _git_config(self) -> dict[str, Any]:
|
|
143
228
|
config = ConfigManager(self.home).load_named("config")
|
|
144
229
|
payload = config.get("git") if isinstance(config.get("git"), dict) else {}
|
|
@@ -623,43 +708,91 @@ class ArtifactService:
|
|
|
623
708
|
)
|
|
624
709
|
return normalized
|
|
625
710
|
|
|
626
|
-
def _paper_root(
|
|
627
|
-
|
|
711
|
+
def _paper_root(
|
|
712
|
+
self,
|
|
713
|
+
quest_root: Path,
|
|
714
|
+
*,
|
|
715
|
+
workspace_root: Path | None = None,
|
|
716
|
+
prefer_workspace: bool = True,
|
|
717
|
+
create: bool = False,
|
|
718
|
+
) -> Path:
|
|
719
|
+
roots: list[Path] = []
|
|
720
|
+
if prefer_workspace:
|
|
721
|
+
roots.append(self._workspace_root_for(quest_root, workspace_root))
|
|
722
|
+
roots.append(quest_root)
|
|
723
|
+
seen: set[str] = set()
|
|
724
|
+
first_candidate: Path | None = None
|
|
725
|
+
for root in roots:
|
|
726
|
+
key = str(root.resolve())
|
|
727
|
+
if key in seen:
|
|
728
|
+
continue
|
|
729
|
+
seen.add(key)
|
|
730
|
+
candidate = root / "paper"
|
|
731
|
+
if first_candidate is None:
|
|
732
|
+
first_candidate = candidate
|
|
733
|
+
if candidate.exists():
|
|
734
|
+
return candidate
|
|
735
|
+
fallback = first_candidate or (quest_root / "paper")
|
|
736
|
+
return ensure_dir(fallback) if create else fallback
|
|
628
737
|
|
|
629
|
-
def _paper_outline_candidates_root(self, quest_root: Path) -> Path:
|
|
630
|
-
return ensure_dir(self._paper_root(quest_root) / "outlines" / "candidates")
|
|
738
|
+
def _paper_outline_candidates_root(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
739
|
+
return ensure_dir(self._paper_root(quest_root, workspace_root=workspace_root, create=True) / "outlines" / "candidates")
|
|
631
740
|
|
|
632
|
-
def _paper_outline_revisions_root(self, quest_root: Path) -> Path:
|
|
633
|
-
return ensure_dir(self._paper_root(quest_root) / "outlines" / "revisions")
|
|
741
|
+
def _paper_outline_revisions_root(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
742
|
+
return ensure_dir(self._paper_root(quest_root, workspace_root=workspace_root, create=True) / "outlines" / "revisions")
|
|
634
743
|
|
|
635
|
-
def _paper_selected_outline_path(self, quest_root: Path) -> Path:
|
|
636
|
-
return self._paper_root(quest_root) / "selected_outline.json"
|
|
744
|
+
def _paper_selected_outline_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
745
|
+
return self._paper_root(quest_root, workspace_root=workspace_root) / "selected_outline.json"
|
|
637
746
|
|
|
638
|
-
def _paper_outline_selection_path(self, quest_root: Path) -> Path:
|
|
639
|
-
return self._paper_root(quest_root) / "outline_selection.md"
|
|
747
|
+
def _paper_outline_selection_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
748
|
+
return self._paper_root(quest_root, workspace_root=workspace_root, create=True) / "outline_selection.md"
|
|
640
749
|
|
|
641
|
-
def _paper_bundle_manifest_path(self, quest_root: Path) -> Path:
|
|
642
|
-
return self._paper_root(quest_root) / "paper_bundle_manifest.json"
|
|
750
|
+
def _paper_bundle_manifest_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
751
|
+
return self._paper_root(quest_root, workspace_root=workspace_root, create=True) / "paper_bundle_manifest.json"
|
|
643
752
|
|
|
644
|
-
def _paper_baseline_inventory_path(self, quest_root: Path) -> Path:
|
|
645
|
-
return self._paper_root(quest_root) / "baseline_inventory.json"
|
|
753
|
+
def _paper_baseline_inventory_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
754
|
+
return self._paper_root(quest_root, workspace_root=workspace_root, create=True) / "baseline_inventory.json"
|
|
646
755
|
|
|
647
|
-
def _open_source_root(
|
|
648
|
-
|
|
756
|
+
def _open_source_root(
|
|
757
|
+
self,
|
|
758
|
+
quest_root: Path,
|
|
759
|
+
*,
|
|
760
|
+
workspace_root: Path | None = None,
|
|
761
|
+
prefer_workspace: bool = True,
|
|
762
|
+
create: bool = False,
|
|
763
|
+
) -> Path:
|
|
764
|
+
roots: list[Path] = []
|
|
765
|
+
if prefer_workspace:
|
|
766
|
+
roots.append(self._workspace_root_for(quest_root, workspace_root))
|
|
767
|
+
roots.append(quest_root)
|
|
768
|
+
seen: set[str] = set()
|
|
769
|
+
first_candidate: Path | None = None
|
|
770
|
+
for root in roots:
|
|
771
|
+
key = str(root.resolve())
|
|
772
|
+
if key in seen:
|
|
773
|
+
continue
|
|
774
|
+
seen.add(key)
|
|
775
|
+
candidate = root / "release" / "open_source"
|
|
776
|
+
if first_candidate is None:
|
|
777
|
+
first_candidate = candidate
|
|
778
|
+
if candidate.exists():
|
|
779
|
+
return candidate
|
|
780
|
+
fallback = first_candidate or (quest_root / "release" / "open_source")
|
|
781
|
+
return ensure_dir(fallback) if create else fallback
|
|
649
782
|
|
|
650
|
-
def _open_source_manifest_path(self, quest_root: Path) -> Path:
|
|
651
|
-
return self._open_source_root(quest_root) / "manifest.json"
|
|
783
|
+
def _open_source_manifest_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
784
|
+
return self._open_source_root(quest_root, workspace_root=workspace_root, create=True) / "manifest.json"
|
|
652
785
|
|
|
653
|
-
def _open_source_cleanup_plan_path(self, quest_root: Path) -> Path:
|
|
654
|
-
return self._open_source_root(quest_root) / "cleanup_plan.md"
|
|
786
|
+
def _open_source_cleanup_plan_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
787
|
+
return self._open_source_root(quest_root, workspace_root=workspace_root, create=True) / "cleanup_plan.md"
|
|
655
788
|
|
|
656
|
-
def _open_source_include_paths_path(self, quest_root: Path) -> Path:
|
|
657
|
-
return self._open_source_root(quest_root) / "include_paths.json"
|
|
789
|
+
def _open_source_include_paths_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
790
|
+
return self._open_source_root(quest_root, workspace_root=workspace_root, create=True) / "include_paths.json"
|
|
658
791
|
|
|
659
|
-
def _open_source_exclude_paths_path(self, quest_root: Path) -> Path:
|
|
660
|
-
return self._open_source_root(quest_root) / "exclude_paths.json"
|
|
792
|
+
def _open_source_exclude_paths_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
793
|
+
return self._open_source_root(quest_root, workspace_root=workspace_root, create=True) / "exclude_paths.json"
|
|
661
794
|
|
|
662
|
-
def _write_paper_baseline_inventory(self, quest_root: Path) -> dict[str, Any]:
|
|
795
|
+
def _write_paper_baseline_inventory(self, quest_root: Path, *, workspace_root: Path | None = None) -> dict[str, Any]:
|
|
663
796
|
quest_yaml = self.quest_service.read_quest_yaml(quest_root)
|
|
664
797
|
confirmed_baseline_ref = (
|
|
665
798
|
dict(quest_yaml.get("confirmed_baseline_ref") or {})
|
|
@@ -675,22 +808,23 @@ class ArtifactService:
|
|
|
675
808
|
],
|
|
676
809
|
"updated_at": utc_now(),
|
|
677
810
|
}
|
|
678
|
-
write_json(self._paper_baseline_inventory_path(quest_root), payload)
|
|
811
|
+
write_json(self._paper_baseline_inventory_path(quest_root, workspace_root=workspace_root), payload)
|
|
679
812
|
return payload
|
|
680
813
|
|
|
681
814
|
def _ensure_open_source_prep(
|
|
682
815
|
self,
|
|
683
816
|
quest_root: Path,
|
|
684
817
|
*,
|
|
818
|
+
workspace_root: Path | None,
|
|
685
819
|
source_branch: str | None,
|
|
686
820
|
source_bundle_manifest_path: str,
|
|
687
821
|
baseline_inventory_path: str,
|
|
688
822
|
) -> 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)
|
|
823
|
+
root = self._open_source_root(quest_root, workspace_root=workspace_root, create=True)
|
|
824
|
+
cleanup_plan_path = self._open_source_cleanup_plan_path(quest_root, workspace_root=workspace_root)
|
|
825
|
+
include_paths_path = self._open_source_include_paths_path(quest_root, workspace_root=workspace_root)
|
|
826
|
+
exclude_paths_path = self._open_source_exclude_paths_path(quest_root, workspace_root=workspace_root)
|
|
827
|
+
manifest_path = self._open_source_manifest_path(quest_root, workspace_root=workspace_root)
|
|
694
828
|
if not cleanup_plan_path.exists():
|
|
695
829
|
write_text(
|
|
696
830
|
cleanup_plan_path,
|
|
@@ -737,11 +871,17 @@ class ArtifactService:
|
|
|
737
871
|
or source_bundle_manifest_path,
|
|
738
872
|
"baseline_inventory_path": str(existing.get("baseline_inventory_path") or baseline_inventory_path or "").strip()
|
|
739
873
|
or baseline_inventory_path,
|
|
740
|
-
"cleanup_plan_path": str(
|
|
874
|
+
"cleanup_plan_path": str(
|
|
875
|
+
existing.get("cleanup_plan_path") or self._workspace_relative(quest_root, cleanup_plan_path) or ""
|
|
876
|
+
).strip()
|
|
741
877
|
or "release/open_source/cleanup_plan.md",
|
|
742
|
-
"include_paths_path": str(
|
|
878
|
+
"include_paths_path": str(
|
|
879
|
+
existing.get("include_paths_path") or self._workspace_relative(quest_root, include_paths_path) or ""
|
|
880
|
+
).strip()
|
|
743
881
|
or "release/open_source/include_paths.json",
|
|
744
|
-
"exclude_paths_path": str(
|
|
882
|
+
"exclude_paths_path": str(
|
|
883
|
+
existing.get("exclude_paths_path") or self._workspace_relative(quest_root, exclude_paths_path) or ""
|
|
884
|
+
).strip()
|
|
745
885
|
or "release/open_source/exclude_paths.json",
|
|
746
886
|
"created_at": existing.get("created_at") or utc_now(),
|
|
747
887
|
"updated_at": utc_now(),
|
|
@@ -1064,6 +1204,7 @@ class ArtifactService:
|
|
|
1064
1204
|
"metric_contract": metric_contract,
|
|
1065
1205
|
"primary_metric": entry.get("primary_metric"),
|
|
1066
1206
|
"metrics_summary": metrics_summary,
|
|
1207
|
+
"metric_details": entry.get("metric_details") or [],
|
|
1067
1208
|
}
|
|
1068
1209
|
json_path = ensure_dir(baseline_root / "json") / "metric_contract.json"
|
|
1069
1210
|
write_json(json_path, payload)
|
|
@@ -1169,9 +1310,43 @@ class ArtifactService:
|
|
|
1169
1310
|
"Use `artifact.confirm_baseline(...)` or `artifact.waive_baseline(...)` first."
|
|
1170
1311
|
)
|
|
1171
1312
|
|
|
1313
|
+
@staticmethod
|
|
1314
|
+
def _artifact_record_identity(path: Path, payload: dict[str, Any], *, kind: str | None = None) -> str:
|
|
1315
|
+
normalized_kind = str(kind or payload.get("kind") or path.parent.name or "artifact").strip() or "artifact"
|
|
1316
|
+
branch_name = str(payload.get("branch") or "").strip()
|
|
1317
|
+
run_id = str(payload.get("run_id") or "").strip()
|
|
1318
|
+
if normalized_kind == "run" and run_id and branch_name:
|
|
1319
|
+
return f"{normalized_kind}:branch_run:{branch_name}:{run_id}"
|
|
1320
|
+
artifact_id = str(payload.get("artifact_id") or payload.get("id") or "").strip()
|
|
1321
|
+
if artifact_id:
|
|
1322
|
+
return f"{normalized_kind}:artifact:{artifact_id}"
|
|
1323
|
+
if normalized_kind == "run" and run_id:
|
|
1324
|
+
return f"{normalized_kind}:run:{run_id}"
|
|
1325
|
+
idea_id = str(payload.get("idea_id") or "").strip()
|
|
1326
|
+
if normalized_kind == "idea" and idea_id and branch_name:
|
|
1327
|
+
return f"{normalized_kind}:branch_idea:{branch_name}:{idea_id}"
|
|
1328
|
+
if normalized_kind == "idea" and idea_id:
|
|
1329
|
+
return f"{normalized_kind}:idea:{idea_id}"
|
|
1330
|
+
baseline_id = str(payload.get("baseline_id") or payload.get("entry_id") or "").strip()
|
|
1331
|
+
if baseline_id:
|
|
1332
|
+
return f"{normalized_kind}:baseline:{baseline_id}"
|
|
1333
|
+
interaction_id = str(payload.get("interaction_id") or "").strip()
|
|
1334
|
+
if interaction_id:
|
|
1335
|
+
return f"{normalized_kind}:interaction:{interaction_id}"
|
|
1336
|
+
return f"path:{path.resolve()}"
|
|
1337
|
+
|
|
1338
|
+
@staticmethod
|
|
1339
|
+
def _artifact_record_rank(payload: dict[str, Any], *, path: Path, mtime_ns: int) -> tuple[str, str, int, int, str]:
|
|
1340
|
+
return (
|
|
1341
|
+
str(payload.get("updated_at") or ""),
|
|
1342
|
+
str(payload.get("created_at") or ""),
|
|
1343
|
+
len(payload),
|
|
1344
|
+
mtime_ns,
|
|
1345
|
+
str(path),
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1172
1348
|
def _main_run_artifacts(self, quest_root: Path) -> list[dict[str, Any]]:
|
|
1173
|
-
|
|
1174
|
-
seen_paths: set[str] = set()
|
|
1349
|
+
records_by_identity: dict[str, dict[str, Any]] = {}
|
|
1175
1350
|
for root in self.quest_service.workspace_roots(quest_root):
|
|
1176
1351
|
artifacts_root = root / "artifacts" / "runs"
|
|
1177
1352
|
if not artifacts_root.exists():
|
|
@@ -1179,10 +1354,6 @@ class ArtifactService:
|
|
|
1179
1354
|
for path in sorted(artifacts_root.glob("*.json")):
|
|
1180
1355
|
if not path.is_file():
|
|
1181
1356
|
continue
|
|
1182
|
-
key = str(path.resolve())
|
|
1183
|
-
if key in seen_paths:
|
|
1184
|
-
continue
|
|
1185
|
-
seen_paths.add(key)
|
|
1186
1357
|
payload = read_json(path, {})
|
|
1187
1358
|
if not isinstance(payload, dict) or not payload:
|
|
1188
1359
|
continue
|
|
@@ -1194,7 +1365,19 @@ class ArtifactService:
|
|
|
1194
1365
|
enriched["_artifact_mtime_ns"] = path.stat().st_mtime_ns
|
|
1195
1366
|
except OSError:
|
|
1196
1367
|
enriched["_artifact_mtime_ns"] = 0
|
|
1197
|
-
|
|
1368
|
+
identity = self._artifact_record_identity(path, enriched, kind="run")
|
|
1369
|
+
existing = records_by_identity.get(identity)
|
|
1370
|
+
if existing is None or self._artifact_record_rank(
|
|
1371
|
+
enriched,
|
|
1372
|
+
path=path,
|
|
1373
|
+
mtime_ns=int(enriched.get("_artifact_mtime_ns") or 0),
|
|
1374
|
+
) >= self._artifact_record_rank(
|
|
1375
|
+
existing,
|
|
1376
|
+
path=Path(str(existing.get("_artifact_path") or path)),
|
|
1377
|
+
mtime_ns=int(existing.get("_artifact_mtime_ns") or 0),
|
|
1378
|
+
):
|
|
1379
|
+
records_by_identity[identity] = enriched
|
|
1380
|
+
records = list(records_by_identity.values())
|
|
1198
1381
|
records.sort(
|
|
1199
1382
|
key=lambda item: (
|
|
1200
1383
|
str(item.get("updated_at") or item.get("created_at") or ""),
|
|
@@ -1277,6 +1460,173 @@ class ArtifactService:
|
|
|
1277
1460
|
continue
|
|
1278
1461
|
return None
|
|
1279
1462
|
|
|
1463
|
+
def _branch_activation_worktree_root(
|
|
1464
|
+
self,
|
|
1465
|
+
quest_root: Path,
|
|
1466
|
+
*,
|
|
1467
|
+
branch_name: str,
|
|
1468
|
+
idea_id: str | None = None,
|
|
1469
|
+
run_id: str | None = None,
|
|
1470
|
+
) -> Path:
|
|
1471
|
+
normalized_branch = str(branch_name or "").strip()
|
|
1472
|
+
branch_kind = self._branch_kind_from_name(normalized_branch)
|
|
1473
|
+
normalized_idea_id = str(idea_id or "").strip() or None
|
|
1474
|
+
if branch_kind == "paper":
|
|
1475
|
+
normalized_run_id = str(run_id or "").strip() or None
|
|
1476
|
+
return canonical_worktree_root(
|
|
1477
|
+
quest_root,
|
|
1478
|
+
f"paper-{normalized_run_id or slugify(normalized_branch, 'paper')}",
|
|
1479
|
+
)
|
|
1480
|
+
if normalized_idea_id and branch_kind == "idea":
|
|
1481
|
+
return canonical_worktree_root(quest_root, f"idea-{normalized_idea_id}")
|
|
1482
|
+
normalized_run_id = str(run_id or "").strip() or None
|
|
1483
|
+
if normalized_run_id and branch_kind == "run":
|
|
1484
|
+
return canonical_worktree_root(quest_root, normalized_run_id)
|
|
1485
|
+
return canonical_worktree_root(quest_root, f"branch-{slugify(normalized_branch, 'branch')}")
|
|
1486
|
+
|
|
1487
|
+
@staticmethod
|
|
1488
|
+
def _resolve_activate_branch_anchor(
|
|
1489
|
+
*,
|
|
1490
|
+
anchor: str | None,
|
|
1491
|
+
has_idea: bool,
|
|
1492
|
+
has_main_result: bool,
|
|
1493
|
+
) -> str:
|
|
1494
|
+
normalized_anchor = str(anchor or "auto").strip().lower() or "auto"
|
|
1495
|
+
if normalized_anchor == "auto":
|
|
1496
|
+
if has_main_result:
|
|
1497
|
+
return "decision"
|
|
1498
|
+
if has_idea:
|
|
1499
|
+
return "experiment"
|
|
1500
|
+
return "idea"
|
|
1501
|
+
aliases = {
|
|
1502
|
+
"analysis": "analysis-campaign",
|
|
1503
|
+
}
|
|
1504
|
+
resolved_anchor = aliases.get(normalized_anchor, normalized_anchor)
|
|
1505
|
+
allowed = {
|
|
1506
|
+
"scout",
|
|
1507
|
+
"baseline",
|
|
1508
|
+
"idea",
|
|
1509
|
+
"experiment",
|
|
1510
|
+
"analysis-campaign",
|
|
1511
|
+
"write",
|
|
1512
|
+
"finalize",
|
|
1513
|
+
"decision",
|
|
1514
|
+
}
|
|
1515
|
+
if resolved_anchor not in allowed:
|
|
1516
|
+
allowed_text = ", ".join(sorted(allowed | {"auto"}))
|
|
1517
|
+
raise ValueError(f"Unsupported activate_branch anchor `{anchor}`. Allowed values: {allowed_text}.")
|
|
1518
|
+
return resolved_anchor
|
|
1519
|
+
|
|
1520
|
+
def _resolve_branch_activation_target(
|
|
1521
|
+
self,
|
|
1522
|
+
quest_root: Path,
|
|
1523
|
+
*,
|
|
1524
|
+
branch: str | None = None,
|
|
1525
|
+
idea_id: str | None = None,
|
|
1526
|
+
run_id: str | None = None,
|
|
1527
|
+
) -> dict[str, Any]:
|
|
1528
|
+
provided = sum(
|
|
1529
|
+
1
|
|
1530
|
+
for value in (
|
|
1531
|
+
str(branch or "").strip(),
|
|
1532
|
+
str(idea_id or "").strip(),
|
|
1533
|
+
str(run_id or "").strip(),
|
|
1534
|
+
)
|
|
1535
|
+
if value
|
|
1536
|
+
)
|
|
1537
|
+
if provided != 1:
|
|
1538
|
+
raise ValueError("activate_branch requires exactly one of `branch`, `idea_id`, or `run_id`.")
|
|
1539
|
+
|
|
1540
|
+
latest_idea: dict[str, Any] | None = None
|
|
1541
|
+
latest_run: dict[str, Any] | None = None
|
|
1542
|
+
normalized_branch = str(branch or "").strip()
|
|
1543
|
+
normalized_idea_id = str(idea_id or "").strip()
|
|
1544
|
+
normalized_run_id = str(run_id or "").strip()
|
|
1545
|
+
|
|
1546
|
+
if normalized_idea_id:
|
|
1547
|
+
candidates = [
|
|
1548
|
+
item for item in self._idea_artifacts(quest_root) if str(item.get("idea_id") or "").strip() == normalized_idea_id
|
|
1549
|
+
]
|
|
1550
|
+
if not candidates:
|
|
1551
|
+
raise FileNotFoundError(f"Unknown idea `{normalized_idea_id}`.")
|
|
1552
|
+
latest_idea = candidates[-1]
|
|
1553
|
+
normalized_branch = str(latest_idea.get("branch") or "").strip()
|
|
1554
|
+
elif normalized_run_id:
|
|
1555
|
+
candidates = [
|
|
1556
|
+
item for item in self._main_run_artifacts(quest_root) if str(item.get("run_id") or "").strip() == normalized_run_id
|
|
1557
|
+
]
|
|
1558
|
+
if not candidates:
|
|
1559
|
+
raise FileNotFoundError(f"Unknown main run `{normalized_run_id}`.")
|
|
1560
|
+
latest_run = candidates[-1]
|
|
1561
|
+
normalized_branch = str(latest_run.get("branch") or "").strip()
|
|
1562
|
+
else:
|
|
1563
|
+
if normalized_branch.startswith("analysis/"):
|
|
1564
|
+
raise ValueError(
|
|
1565
|
+
"activate_branch only supports durable idea/main branches. "
|
|
1566
|
+
"Analysis slice branches remain managed by analysis campaigns."
|
|
1567
|
+
)
|
|
1568
|
+
if not branch_exists(quest_root, normalized_branch):
|
|
1569
|
+
raise FileNotFoundError(f"Unknown branch `{normalized_branch}`.")
|
|
1570
|
+
|
|
1571
|
+
if not normalized_branch:
|
|
1572
|
+
raise ValueError("Unable to resolve a durable branch to activate.")
|
|
1573
|
+
|
|
1574
|
+
prepare_record = self._latest_prepare_branch_record(quest_root, normalized_branch)
|
|
1575
|
+
prepare_details = dict(prepare_record.get("details") or {}) if isinstance(prepare_record.get("details"), dict) else {}
|
|
1576
|
+
recorded_parent_branch = (
|
|
1577
|
+
str(prepare_record.get("parent_branch") or prepare_details.get("parent_branch") or "").strip() or None
|
|
1578
|
+
)
|
|
1579
|
+
recorded_branch_kind = (
|
|
1580
|
+
str(prepare_record.get("branch_kind") or prepare_details.get("branch_kind") or "").strip().lower()
|
|
1581
|
+
or self._branch_kind_from_name(normalized_branch)
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
latest_idea = latest_idea or self._latest_idea_for_branch(quest_root, normalized_branch)
|
|
1585
|
+
latest_run = latest_run or self._latest_main_run_for_branch(quest_root, normalized_branch)
|
|
1586
|
+
if not latest_run and recorded_branch_kind == "idea":
|
|
1587
|
+
latest_run = self._latest_child_main_run_for_branch(quest_root, normalized_branch)
|
|
1588
|
+
if not latest_run and recorded_parent_branch:
|
|
1589
|
+
latest_run = self._latest_main_run_for_branch(quest_root, recorded_parent_branch)
|
|
1590
|
+
resolved_idea_id = (
|
|
1591
|
+
normalized_idea_id
|
|
1592
|
+
or str((latest_run or {}).get("idea_id") or "").strip()
|
|
1593
|
+
or str((latest_idea or {}).get("idea_id") or "").strip()
|
|
1594
|
+
or str(prepare_record.get("idea_id") or "").strip()
|
|
1595
|
+
or self._latest_branch_idea_id(quest_root, normalized_branch)
|
|
1596
|
+
or None
|
|
1597
|
+
)
|
|
1598
|
+
idea_paths = dict((latest_idea or {}).get("paths") or {}) if isinstance((latest_idea or {}).get("paths"), dict) else {}
|
|
1599
|
+
recorded_root = (
|
|
1600
|
+
str((latest_idea or {}).get("worktree_root") or "").strip()
|
|
1601
|
+
or str((latest_run or {}).get("worktree_root") or "").strip()
|
|
1602
|
+
or str(prepare_record.get("worktree_root") or "").strip()
|
|
1603
|
+
or None
|
|
1604
|
+
)
|
|
1605
|
+
return {
|
|
1606
|
+
"branch": normalized_branch,
|
|
1607
|
+
"idea_id": resolved_idea_id,
|
|
1608
|
+
"run_id": normalized_run_id or str((latest_run or {}).get("run_id") or "").strip() or None,
|
|
1609
|
+
"has_main_result": bool((latest_run or {}).get("run_id")),
|
|
1610
|
+
"latest_idea": latest_idea,
|
|
1611
|
+
"latest_main_run": latest_run,
|
|
1612
|
+
"branch_kind": recorded_branch_kind,
|
|
1613
|
+
"parent_branch": recorded_parent_branch,
|
|
1614
|
+
"recorded_worktree_root": recorded_root,
|
|
1615
|
+
"idea_md_path": str(idea_paths.get("idea_md") or "").strip() or None,
|
|
1616
|
+
"idea_draft_path": str(idea_paths.get("idea_draft_md") or "").strip() or None,
|
|
1617
|
+
"suggested_worktree_root": self._branch_activation_worktree_root(
|
|
1618
|
+
quest_root,
|
|
1619
|
+
branch_name=normalized_branch,
|
|
1620
|
+
idea_id=resolved_idea_id,
|
|
1621
|
+
run_id=(
|
|
1622
|
+
normalized_run_id
|
|
1623
|
+
or str(prepare_record.get("run_id") or "").strip()
|
|
1624
|
+
or str((latest_run or {}).get("run_id") or "").strip()
|
|
1625
|
+
or None
|
|
1626
|
+
),
|
|
1627
|
+
),
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1280
1630
|
def _normalize_foundation_ref(self, foundation_ref: dict[str, Any] | str | None) -> dict[str, Any]:
|
|
1281
1631
|
if foundation_ref is None:
|
|
1282
1632
|
return {"kind": "current_head", "ref": None}
|
|
@@ -1445,6 +1795,17 @@ class ArtifactService:
|
|
|
1445
1795
|
]
|
|
1446
1796
|
return candidates[-1] if candidates else None
|
|
1447
1797
|
|
|
1798
|
+
def _latest_child_main_run_for_branch(self, quest_root: Path, branch_name: str) -> dict[str, Any] | None:
|
|
1799
|
+
normalized_branch = str(branch_name or "").strip()
|
|
1800
|
+
if not normalized_branch:
|
|
1801
|
+
return None
|
|
1802
|
+
candidates = [
|
|
1803
|
+
item
|
|
1804
|
+
for item in self._main_run_artifacts(quest_root)
|
|
1805
|
+
if str(item.get("parent_branch") or "").strip() == normalized_branch
|
|
1806
|
+
]
|
|
1807
|
+
return candidates[-1] if candidates else None
|
|
1808
|
+
|
|
1448
1809
|
def _latest_idea_for_branch(self, quest_root: Path, branch_name: str) -> dict[str, Any] | None:
|
|
1449
1810
|
normalized_branch = str(branch_name or "").strip()
|
|
1450
1811
|
if not normalized_branch:
|
|
@@ -1527,6 +1888,16 @@ class ArtifactService:
|
|
|
1527
1888
|
if not parent_branch:
|
|
1528
1889
|
raise ValueError("Unable to resolve a parent branch for the analysis campaign.")
|
|
1529
1890
|
|
|
1891
|
+
if self._branch_kind_from_name(parent_branch) == "idea":
|
|
1892
|
+
latest_child_run = self._latest_child_main_run_for_branch(quest_root, parent_branch)
|
|
1893
|
+
if isinstance(latest_child_run, dict) and str(latest_child_run.get("branch") or "").strip():
|
|
1894
|
+
parent_branch = str(latest_child_run.get("branch") or "").strip()
|
|
1895
|
+
recorded_worktree_root = str(latest_child_run.get("worktree_root") or "").strip()
|
|
1896
|
+
if recorded_worktree_root:
|
|
1897
|
+
candidate = Path(recorded_worktree_root)
|
|
1898
|
+
if candidate.exists():
|
|
1899
|
+
parent_worktree_root = candidate
|
|
1900
|
+
|
|
1530
1901
|
idea_id = self._latest_branch_idea_id(quest_root, parent_branch) or str(state.get("active_idea_id") or "").strip() or None
|
|
1531
1902
|
return parent_branch, parent_worktree_root, idea_id
|
|
1532
1903
|
|
|
@@ -1568,15 +1939,22 @@ class ArtifactService:
|
|
|
1568
1939
|
state=state,
|
|
1569
1940
|
foundation_ref={"kind": "idea", "ref": str(latest_idea.get("idea_id") or "").strip()},
|
|
1570
1941
|
)
|
|
1942
|
+
current_workspace_branch = str(state.get("current_workspace_branch") or "").strip()
|
|
1943
|
+
research_head_branch = str(state.get("research_head_branch") or "").strip()
|
|
1571
1944
|
active_branch = (
|
|
1572
|
-
|
|
1573
|
-
or
|
|
1945
|
+
current_workspace_branch
|
|
1946
|
+
or research_head_branch
|
|
1947
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
1574
1948
|
)
|
|
1575
1949
|
if normalized_branch and active_branch and normalized_branch == active_branch:
|
|
1576
1950
|
return self._resolve_idea_foundation(
|
|
1577
1951
|
quest_root,
|
|
1578
1952
|
state=state,
|
|
1579
|
-
foundation_ref=
|
|
1953
|
+
foundation_ref=(
|
|
1954
|
+
{"kind": "branch", "ref": normalized_branch}
|
|
1955
|
+
if current_workspace_branch and research_head_branch and current_workspace_branch != research_head_branch
|
|
1956
|
+
else None
|
|
1957
|
+
),
|
|
1580
1958
|
)
|
|
1581
1959
|
return self._resolve_idea_foundation(
|
|
1582
1960
|
quest_root,
|
|
@@ -1614,8 +1992,8 @@ class ArtifactService:
|
|
|
1614
1992
|
) -> tuple[str, str, dict[str, Any]]:
|
|
1615
1993
|
normalized_intent = self._normalize_lineage_intent(lineage_intent) or "continue_line"
|
|
1616
1994
|
active_branch = (
|
|
1617
|
-
str(state.get("
|
|
1618
|
-
or str(state.get("
|
|
1995
|
+
str(state.get("current_workspace_branch") or "").strip()
|
|
1996
|
+
or str(state.get("research_head_branch") or "").strip()
|
|
1619
1997
|
)
|
|
1620
1998
|
if not active_branch:
|
|
1621
1999
|
active_branch = current_branch(self._workspace_root_for(quest_root))
|
|
@@ -1643,6 +2021,7 @@ class ArtifactService:
|
|
|
1643
2021
|
def list_research_branches(self, quest_root: Path) -> dict[str, Any]:
|
|
1644
2022
|
state = self.quest_service.read_research_state(quest_root)
|
|
1645
2023
|
active_head_branch = str(state.get("research_head_branch") or "").strip() or None
|
|
2024
|
+
active_workspace_branch = str(state.get("current_workspace_branch") or "").strip() or None
|
|
1646
2025
|
idea_records = self._idea_artifacts(quest_root)
|
|
1647
2026
|
main_runs = self._main_run_artifacts(quest_root)
|
|
1648
2027
|
|
|
@@ -1709,6 +2088,7 @@ class ArtifactService:
|
|
|
1709
2088
|
"verdict": record.get("verdict"),
|
|
1710
2089
|
"status": record.get("status"),
|
|
1711
2090
|
"idea_id": record.get("idea_id"),
|
|
2091
|
+
"parent_branch": record.get("parent_branch"),
|
|
1712
2092
|
"primary_metric_id": details.get("primary_metric_id"),
|
|
1713
2093
|
"primary_value": details.get("primary_value"),
|
|
1714
2094
|
"delta_vs_baseline": details.get("delta_vs_baseline"),
|
|
@@ -1721,6 +2101,8 @@ class ArtifactService:
|
|
|
1721
2101
|
|
|
1722
2102
|
if active_head_branch:
|
|
1723
2103
|
ensure_branch_entry(active_head_branch)
|
|
2104
|
+
if active_workspace_branch:
|
|
2105
|
+
ensure_branch_entry(active_workspace_branch)
|
|
1724
2106
|
|
|
1725
2107
|
ordered_branches = sorted(
|
|
1726
2108
|
grouped.values(),
|
|
@@ -1756,10 +2138,15 @@ class ArtifactService:
|
|
|
1756
2138
|
else {}
|
|
1757
2139
|
)
|
|
1758
2140
|
parent_branch = str(latest_idea.get("parent_branch") or "").strip() or None
|
|
2141
|
+
experiment_parent_branch = (
|
|
2142
|
+
str((latest_experiment or {}).get("parent_branch") or "").strip()
|
|
2143
|
+
if isinstance(latest_experiment, dict)
|
|
2144
|
+
else None
|
|
2145
|
+
) or None
|
|
1759
2146
|
foundation_branch = (
|
|
1760
2147
|
str(latest_foundation.get("branch") or latest_foundation.get("ref") or "").strip() or None
|
|
1761
2148
|
)
|
|
1762
|
-
resolved_parent_branch = parent_branch or foundation_branch
|
|
2149
|
+
resolved_parent_branch = parent_branch or experiment_parent_branch or foundation_branch
|
|
1763
2150
|
has_main_result = isinstance(latest_experiment, dict) and bool(latest_experiment.get("run_id"))
|
|
1764
2151
|
numeric_branch_no = recorded_branch_numbers.get(branch_name)
|
|
1765
2152
|
if numeric_branch_no is None:
|
|
@@ -1774,7 +2161,8 @@ class ArtifactService:
|
|
|
1774
2161
|
"branch_name": branch_name,
|
|
1775
2162
|
"worktree_root": item.get("worktree_root"),
|
|
1776
2163
|
"is_active_head": branch_name == active_head_branch,
|
|
1777
|
-
"
|
|
2164
|
+
"is_active_workspace": branch_name == active_workspace_branch,
|
|
2165
|
+
"idea_id": latest_idea.get("idea_id") or (latest_experiment.get("idea_id") if isinstance(latest_experiment, dict) else None),
|
|
1778
2166
|
"idea_title": latest_idea.get("title"),
|
|
1779
2167
|
"idea_problem": latest_idea.get("problem"),
|
|
1780
2168
|
"next_target": latest_idea.get("next_target"),
|
|
@@ -1810,6 +2198,7 @@ class ArtifactService:
|
|
|
1810
2198
|
return {
|
|
1811
2199
|
"ok": True,
|
|
1812
2200
|
"active_head_branch": active_head_branch,
|
|
2201
|
+
"active_workspace_branch": active_workspace_branch,
|
|
1813
2202
|
"count": len(branches),
|
|
1814
2203
|
"branches": branches,
|
|
1815
2204
|
}
|
|
@@ -1819,9 +2208,10 @@ class ArtifactService:
|
|
|
1819
2208
|
snapshot = self.quest_service.snapshot(self._quest_id(quest_root))
|
|
1820
2209
|
active_campaign_id = str(state.get("active_analysis_campaign_id") or "").strip() or None
|
|
1821
2210
|
analysis_parent_branch = str(state.get("analysis_parent_branch") or "").strip() or None
|
|
2211
|
+
paper_parent_branch = str(state.get("paper_parent_branch") or "").strip() or None
|
|
1822
2212
|
current_workspace_branch = str(state.get("current_workspace_branch") or "").strip() or None
|
|
1823
2213
|
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
|
|
2214
|
+
canonical_branch = analysis_parent_branch or paper_parent_branch or current_workspace_branch or research_head_branch
|
|
1825
2215
|
latest_main_run = self._latest_main_run_for_branch(quest_root, canonical_branch or "")
|
|
1826
2216
|
selected_outline = read_json(self._paper_selected_outline_path(quest_root), {})
|
|
1827
2217
|
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
@@ -2155,14 +2545,27 @@ class ArtifactService:
|
|
|
2155
2545
|
create_worktree_flag: bool = True,
|
|
2156
2546
|
start_point: str | None = None,
|
|
2157
2547
|
) -> dict:
|
|
2158
|
-
|
|
2548
|
+
state = self.quest_service.read_research_state(quest_root)
|
|
2549
|
+
parent_branch = (
|
|
2550
|
+
str(start_point or "").strip()
|
|
2551
|
+
or str(state.get("current_workspace_branch") or "").strip()
|
|
2552
|
+
or str(state.get("research_head_branch") or "").strip()
|
|
2553
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
2554
|
+
or current_branch(quest_root)
|
|
2555
|
+
)
|
|
2159
2556
|
start_ref = start_point or parent_branch
|
|
2160
2557
|
branch_name = branch or self._default_branch_name(quest_root, run_id=run_id, idea_id=idea_id, branch_kind=branch_kind)
|
|
2161
2558
|
branch_result = ensure_branch(quest_root, branch_name, start_point=start_ref, checkout=False)
|
|
2162
2559
|
worktree_result = None
|
|
2163
2560
|
worktree_root = None
|
|
2164
2561
|
if create_worktree_flag:
|
|
2165
|
-
worktree_root =
|
|
2562
|
+
worktree_root = self._prepare_branch_worktree_root(
|
|
2563
|
+
quest_root,
|
|
2564
|
+
branch_name=branch_name,
|
|
2565
|
+
branch_kind=branch_kind,
|
|
2566
|
+
run_id=run_id,
|
|
2567
|
+
idea_id=idea_id,
|
|
2568
|
+
)
|
|
2166
2569
|
worktree_result = create_worktree(
|
|
2167
2570
|
quest_root,
|
|
2168
2571
|
branch=branch_name,
|
|
@@ -2184,9 +2587,11 @@ class ArtifactService:
|
|
|
2184
2587
|
"parent_branch": parent_branch,
|
|
2185
2588
|
"start_point": start_ref,
|
|
2186
2589
|
"worktree_root": str(worktree_root) if worktree_root else None,
|
|
2590
|
+
"workspace_mode": self._workspace_mode_for_branch(branch_name, has_idea=bool(idea_id)),
|
|
2187
2591
|
"source": {"kind": "system", "role": "artifact"},
|
|
2188
2592
|
},
|
|
2189
2593
|
checkpoint=False,
|
|
2594
|
+
workspace_root=worktree_root if worktree_root else None,
|
|
2190
2595
|
)
|
|
2191
2596
|
return {
|
|
2192
2597
|
"ok": True,
|
|
@@ -2200,6 +2605,364 @@ class ArtifactService:
|
|
|
2200
2605
|
"artifact": artifact_result,
|
|
2201
2606
|
}
|
|
2202
2607
|
|
|
2608
|
+
def activate_branch(
|
|
2609
|
+
self,
|
|
2610
|
+
quest_root: Path,
|
|
2611
|
+
*,
|
|
2612
|
+
branch: str | None = None,
|
|
2613
|
+
idea_id: str | None = None,
|
|
2614
|
+
run_id: str | None = None,
|
|
2615
|
+
anchor: str | None = "auto",
|
|
2616
|
+
promote_to_head: bool = False,
|
|
2617
|
+
create_worktree_if_missing: bool = True,
|
|
2618
|
+
) -> dict[str, Any]:
|
|
2619
|
+
state = self.quest_service.read_research_state(quest_root)
|
|
2620
|
+
active_campaign_id = str(state.get("active_analysis_campaign_id") or "").strip() or None
|
|
2621
|
+
if active_campaign_id:
|
|
2622
|
+
raise ValueError(
|
|
2623
|
+
"activate_branch cannot run while an analysis campaign is active. "
|
|
2624
|
+
"Finish or close the campaign first."
|
|
2625
|
+
)
|
|
2626
|
+
|
|
2627
|
+
target = self._resolve_branch_activation_target(
|
|
2628
|
+
quest_root,
|
|
2629
|
+
branch=branch,
|
|
2630
|
+
idea_id=idea_id,
|
|
2631
|
+
run_id=run_id,
|
|
2632
|
+
)
|
|
2633
|
+
branch_name = str(target.get("branch") or "").strip()
|
|
2634
|
+
if str(target.get("branch_kind") or self._branch_kind_from_name(branch_name)).strip().lower() != "paper":
|
|
2635
|
+
self._require_baseline_gate_open(quest_root, action="activate_branch")
|
|
2636
|
+
resolved_idea_id = str(target.get("idea_id") or "").strip() or None
|
|
2637
|
+
latest_main_run = (
|
|
2638
|
+
dict(target.get("latest_main_run") or {})
|
|
2639
|
+
if isinstance(target.get("latest_main_run"), dict)
|
|
2640
|
+
else {}
|
|
2641
|
+
)
|
|
2642
|
+
latest_idea = (
|
|
2643
|
+
dict(target.get("latest_idea") or {})
|
|
2644
|
+
if isinstance(target.get("latest_idea"), dict)
|
|
2645
|
+
else {}
|
|
2646
|
+
)
|
|
2647
|
+
branch_kind = str(target.get("branch_kind") or self._branch_kind_from_name(branch_name)).strip().lower() or "branch"
|
|
2648
|
+
source_parent_branch = str(target.get("parent_branch") or "").strip() or None
|
|
2649
|
+
|
|
2650
|
+
workspace_root = self._branch_workspace_root(quest_root, branch_name)
|
|
2651
|
+
worktree_result = None
|
|
2652
|
+
worktree_created = False
|
|
2653
|
+
if workspace_root is None:
|
|
2654
|
+
recorded_root = str(target.get("recorded_worktree_root") or "").strip()
|
|
2655
|
+
if recorded_root:
|
|
2656
|
+
candidate = Path(recorded_root)
|
|
2657
|
+
if candidate.exists():
|
|
2658
|
+
workspace_root = candidate
|
|
2659
|
+
if workspace_root is None:
|
|
2660
|
+
if not create_worktree_if_missing:
|
|
2661
|
+
raise FileNotFoundError(
|
|
2662
|
+
f"No existing worktree is available for branch `{branch_name}` and create_worktree_if_missing=False."
|
|
2663
|
+
)
|
|
2664
|
+
workspace_root = Path(target.get("suggested_worktree_root") or "")
|
|
2665
|
+
worktree_result = create_worktree(
|
|
2666
|
+
quest_root,
|
|
2667
|
+
branch=branch_name,
|
|
2668
|
+
worktree_root=workspace_root,
|
|
2669
|
+
start_point=branch_name,
|
|
2670
|
+
)
|
|
2671
|
+
if not bool(worktree_result.get("ok")):
|
|
2672
|
+
raise RuntimeError(
|
|
2673
|
+
f"Failed to activate branch `{branch_name}`: {worktree_result.get('stderr') or 'worktree creation failed.'}"
|
|
2674
|
+
)
|
|
2675
|
+
worktree_created = True
|
|
2676
|
+
|
|
2677
|
+
resolved_workspace_root = workspace_root or quest_root
|
|
2678
|
+
idea_md_path = (
|
|
2679
|
+
str(target.get("idea_md_path") or "").strip()
|
|
2680
|
+
or str((dict(latest_idea.get("paths") or {}) if isinstance(latest_idea.get("paths"), dict) else {}).get("idea_md") or "").strip()
|
|
2681
|
+
or (str(resolved_workspace_root / "memory" / "ideas" / resolved_idea_id / "idea.md") if resolved_idea_id else "")
|
|
2682
|
+
)
|
|
2683
|
+
idea_draft_path = (
|
|
2684
|
+
str(target.get("idea_draft_path") or "").strip()
|
|
2685
|
+
or str((dict(latest_idea.get("paths") or {}) if isinstance(latest_idea.get("paths"), dict) else {}).get("idea_draft_md") or "").strip()
|
|
2686
|
+
or (str(resolved_workspace_root / "memory" / "ideas" / resolved_idea_id / "draft.md") if resolved_idea_id else "")
|
|
2687
|
+
)
|
|
2688
|
+
resolved_idea_md_path = idea_md_path if resolved_idea_id else None
|
|
2689
|
+
resolved_idea_draft_path = idea_draft_path if resolved_idea_id else None
|
|
2690
|
+
has_main_result = bool(latest_main_run.get("run_id"))
|
|
2691
|
+
if branch_kind == "paper":
|
|
2692
|
+
next_anchor = "write" if str(anchor or "auto").strip().lower() == "auto" else self._resolve_activate_branch_anchor(
|
|
2693
|
+
anchor=anchor,
|
|
2694
|
+
has_idea=bool(resolved_idea_id),
|
|
2695
|
+
has_main_result=has_main_result,
|
|
2696
|
+
)
|
|
2697
|
+
else:
|
|
2698
|
+
next_anchor = self._resolve_activate_branch_anchor(
|
|
2699
|
+
anchor=anchor,
|
|
2700
|
+
has_idea=bool(resolved_idea_id),
|
|
2701
|
+
has_main_result=has_main_result,
|
|
2702
|
+
)
|
|
2703
|
+
workspace_mode = self._workspace_mode_for_branch(branch_name, has_idea=bool(resolved_idea_id))
|
|
2704
|
+
source_run_id = (
|
|
2705
|
+
str(target.get("run_id") or "").strip()
|
|
2706
|
+
or str(latest_main_run.get("run_id") or "").strip()
|
|
2707
|
+
or None
|
|
2708
|
+
)
|
|
2709
|
+
|
|
2710
|
+
artifact = self.record(
|
|
2711
|
+
quest_root,
|
|
2712
|
+
{
|
|
2713
|
+
"kind": "decision",
|
|
2714
|
+
"status": "completed",
|
|
2715
|
+
"verdict": "continue",
|
|
2716
|
+
"action": "activate_branch",
|
|
2717
|
+
"summary": f"Activated durable branch `{branch_name}` as the current workspace.",
|
|
2718
|
+
"reason": (
|
|
2719
|
+
"Return to an existing research branch without creating a new lineage node, "
|
|
2720
|
+
"so follow-up experiments or decisions continue from the correct historical context."
|
|
2721
|
+
),
|
|
2722
|
+
"idea_id": resolved_idea_id,
|
|
2723
|
+
"run_id": str(latest_main_run.get("run_id") or "").strip() or None,
|
|
2724
|
+
"branch": branch_name,
|
|
2725
|
+
"worktree_root": str(resolved_workspace_root),
|
|
2726
|
+
"worktree_rel_path": self._workspace_relative(quest_root, resolved_workspace_root),
|
|
2727
|
+
"flow_type": "branch_activation",
|
|
2728
|
+
"protocol_step": "activate",
|
|
2729
|
+
"details": {
|
|
2730
|
+
"activate_branch_by": (
|
|
2731
|
+
"idea_id"
|
|
2732
|
+
if str(idea_id or "").strip()
|
|
2733
|
+
else "run_id"
|
|
2734
|
+
if str(run_id or "").strip()
|
|
2735
|
+
else "branch"
|
|
2736
|
+
),
|
|
2737
|
+
"promote_to_head": bool(promote_to_head),
|
|
2738
|
+
"worktree_created": worktree_created,
|
|
2739
|
+
"next_anchor": next_anchor,
|
|
2740
|
+
"workspace_mode": workspace_mode,
|
|
2741
|
+
"latest_main_run_id": str(latest_main_run.get("run_id") or "").strip() or None,
|
|
2742
|
+
"branch_kind": branch_kind,
|
|
2743
|
+
"paper_parent_branch": source_parent_branch if branch_kind == "paper" else None,
|
|
2744
|
+
},
|
|
2745
|
+
},
|
|
2746
|
+
checkpoint=False,
|
|
2747
|
+
workspace_root=resolved_workspace_root,
|
|
2748
|
+
)
|
|
2749
|
+
|
|
2750
|
+
research_state_updates: dict[str, Any] = {
|
|
2751
|
+
"active_idea_id": resolved_idea_id,
|
|
2752
|
+
"current_workspace_branch": branch_name,
|
|
2753
|
+
"current_workspace_root": str(resolved_workspace_root),
|
|
2754
|
+
"active_idea_md_path": resolved_idea_md_path,
|
|
2755
|
+
"active_idea_draft_path": resolved_idea_draft_path,
|
|
2756
|
+
"active_analysis_campaign_id": None,
|
|
2757
|
+
"analysis_parent_branch": None,
|
|
2758
|
+
"analysis_parent_worktree_root": None,
|
|
2759
|
+
"paper_parent_branch": source_parent_branch if branch_kind == "paper" else None,
|
|
2760
|
+
"paper_parent_worktree_root": (
|
|
2761
|
+
str(self._branch_workspace_root(quest_root, source_parent_branch))
|
|
2762
|
+
if branch_kind == "paper" and source_parent_branch and self._branch_workspace_root(quest_root, source_parent_branch)
|
|
2763
|
+
else None
|
|
2764
|
+
),
|
|
2765
|
+
"paper_parent_run_id": source_run_id if branch_kind == "paper" else None,
|
|
2766
|
+
"next_pending_slice_id": None,
|
|
2767
|
+
"workspace_mode": workspace_mode,
|
|
2768
|
+
"last_flow_type": "branch_activation",
|
|
2769
|
+
}
|
|
2770
|
+
if promote_to_head:
|
|
2771
|
+
research_state_updates["research_head_branch"] = branch_name
|
|
2772
|
+
research_state_updates["research_head_worktree_root"] = str(resolved_workspace_root)
|
|
2773
|
+
research_state = self.quest_service.update_research_state(quest_root, **research_state_updates)
|
|
2774
|
+
self.quest_service.update_settings(self._quest_id(quest_root), active_anchor=next_anchor)
|
|
2775
|
+
|
|
2776
|
+
interaction = self.interact(
|
|
2777
|
+
quest_root,
|
|
2778
|
+
kind="milestone",
|
|
2779
|
+
message=(
|
|
2780
|
+
f"Activated branch `{branch_name}`.\n"
|
|
2781
|
+
f"- Worktree: `{resolved_workspace_root}`\n"
|
|
2782
|
+
f"- Active idea: `{resolved_idea_id or 'none'}`\n"
|
|
2783
|
+
f"- Latest main run: `{str(latest_main_run.get('run_id') or '').strip() or 'none'}`\n"
|
|
2784
|
+
f"- Promoted to head: `{bool(promote_to_head)}`\n"
|
|
2785
|
+
f"- Next anchor: `{next_anchor}`"
|
|
2786
|
+
),
|
|
2787
|
+
deliver_to_bound_conversations=True,
|
|
2788
|
+
include_recent_inbound_messages=False,
|
|
2789
|
+
attachments=[
|
|
2790
|
+
{
|
|
2791
|
+
"kind": "branch_activation",
|
|
2792
|
+
"branch": branch_name,
|
|
2793
|
+
"worktree_root": str(resolved_workspace_root),
|
|
2794
|
+
"idea_id": resolved_idea_id,
|
|
2795
|
+
"latest_main_run_id": str(latest_main_run.get("run_id") or "").strip() or None,
|
|
2796
|
+
"next_anchor": next_anchor,
|
|
2797
|
+
"promote_to_head": bool(promote_to_head),
|
|
2798
|
+
}
|
|
2799
|
+
],
|
|
2800
|
+
)
|
|
2801
|
+
return {
|
|
2802
|
+
"ok": True,
|
|
2803
|
+
"branch": branch_name,
|
|
2804
|
+
"worktree_root": str(resolved_workspace_root),
|
|
2805
|
+
"idea_id": resolved_idea_id,
|
|
2806
|
+
"latest_main_run_id": str(latest_main_run.get("run_id") or "").strip() or None,
|
|
2807
|
+
"branch_kind": branch_kind,
|
|
2808
|
+
"source_parent_branch": source_parent_branch,
|
|
2809
|
+
"idea_md_path": resolved_idea_md_path,
|
|
2810
|
+
"idea_draft_path": resolved_idea_draft_path,
|
|
2811
|
+
"workspace_mode": workspace_mode,
|
|
2812
|
+
"next_anchor": next_anchor,
|
|
2813
|
+
"promote_to_head": bool(promote_to_head),
|
|
2814
|
+
"worktree_created": worktree_created,
|
|
2815
|
+
"worktree": worktree_result,
|
|
2816
|
+
"artifact": artifact,
|
|
2817
|
+
"interaction": interaction,
|
|
2818
|
+
"research_state": research_state,
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
def _promote_workspace_to_run_branch(
|
|
2822
|
+
self,
|
|
2823
|
+
quest_root: Path,
|
|
2824
|
+
*,
|
|
2825
|
+
run_id: str,
|
|
2826
|
+
idea_id: str | None,
|
|
2827
|
+
workspace_root: Path,
|
|
2828
|
+
current_branch_name: str,
|
|
2829
|
+
) -> tuple[str, str | None, bool]:
|
|
2830
|
+
branch_kind = self._branch_kind_from_name(current_branch_name)
|
|
2831
|
+
if branch_kind == "paper":
|
|
2832
|
+
raise ValueError(
|
|
2833
|
+
"record_main_experiment cannot run while the active workspace is a paper branch. "
|
|
2834
|
+
"Return to the evidence branch or create a new run branch first."
|
|
2835
|
+
)
|
|
2836
|
+
if branch_kind == "run":
|
|
2837
|
+
prepare_record = self._latest_prepare_branch_record(quest_root, current_branch_name)
|
|
2838
|
+
parent_branch = str(prepare_record.get("parent_branch") or "").strip() or None
|
|
2839
|
+
return current_branch_name, parent_branch, False
|
|
2840
|
+
|
|
2841
|
+
target_branch = self._default_branch_name(quest_root, run_id=run_id, idea_id=idea_id, branch_kind="run")
|
|
2842
|
+
if branch_exists(quest_root, target_branch):
|
|
2843
|
+
raise ValueError(
|
|
2844
|
+
f"Run branch `{target_branch}` already exists. Reuse that run branch or choose a new `run_id`."
|
|
2845
|
+
)
|
|
2846
|
+
|
|
2847
|
+
ensure_branch(quest_root, target_branch, start_point=current_branch_name, checkout=False)
|
|
2848
|
+
run_command(["git", "switch", target_branch], cwd=workspace_root, check=True)
|
|
2849
|
+
self.record(
|
|
2850
|
+
quest_root,
|
|
2851
|
+
{
|
|
2852
|
+
"kind": "decision",
|
|
2853
|
+
"status": "prepared",
|
|
2854
|
+
"verdict": "prepared",
|
|
2855
|
+
"action": "prepare_branch",
|
|
2856
|
+
"reason": f"Materialized a dedicated main-experiment branch `{target_branch}` before durable recording.",
|
|
2857
|
+
"branch": target_branch,
|
|
2858
|
+
"run_id": run_id,
|
|
2859
|
+
"idea_id": idea_id,
|
|
2860
|
+
"branch_kind": "run",
|
|
2861
|
+
"parent_branch": current_branch_name,
|
|
2862
|
+
"start_point": current_branch_name,
|
|
2863
|
+
"worktree_root": str(workspace_root),
|
|
2864
|
+
"workspace_mode": "run",
|
|
2865
|
+
"source": {"kind": "system", "role": "artifact"},
|
|
2866
|
+
},
|
|
2867
|
+
checkpoint=False,
|
|
2868
|
+
workspace_root=workspace_root,
|
|
2869
|
+
)
|
|
2870
|
+
self.quest_service.update_research_state(
|
|
2871
|
+
quest_root,
|
|
2872
|
+
active_idea_id=idea_id,
|
|
2873
|
+
current_workspace_branch=target_branch,
|
|
2874
|
+
current_workspace_root=str(workspace_root),
|
|
2875
|
+
research_head_branch=target_branch,
|
|
2876
|
+
research_head_worktree_root=str(workspace_root),
|
|
2877
|
+
active_analysis_campaign_id=None,
|
|
2878
|
+
analysis_parent_branch=None,
|
|
2879
|
+
analysis_parent_worktree_root=None,
|
|
2880
|
+
paper_parent_branch=None,
|
|
2881
|
+
paper_parent_worktree_root=None,
|
|
2882
|
+
paper_parent_run_id=None,
|
|
2883
|
+
workspace_mode="run",
|
|
2884
|
+
last_flow_type="main_experiment_branch",
|
|
2885
|
+
)
|
|
2886
|
+
return target_branch, current_branch_name, True
|
|
2887
|
+
|
|
2888
|
+
def _ensure_active_paper_workspace(
|
|
2889
|
+
self,
|
|
2890
|
+
quest_root: Path,
|
|
2891
|
+
*,
|
|
2892
|
+
source_branch: str | None = None,
|
|
2893
|
+
source_run_id: str | None = None,
|
|
2894
|
+
source_idea_id: str | None = None,
|
|
2895
|
+
) -> dict[str, Any]:
|
|
2896
|
+
state = self.quest_service.read_research_state(quest_root)
|
|
2897
|
+
current_branch_name = (
|
|
2898
|
+
str(state.get("current_workspace_branch") or "").strip()
|
|
2899
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
2900
|
+
)
|
|
2901
|
+
current_workspace_root = self._workspace_root_for(quest_root)
|
|
2902
|
+
if (
|
|
2903
|
+
str(state.get("workspace_mode") or "").strip() == "paper"
|
|
2904
|
+
and self._branch_kind_from_name(current_branch_name) == "paper"
|
|
2905
|
+
):
|
|
2906
|
+
return {
|
|
2907
|
+
"ok": True,
|
|
2908
|
+
"branch": current_branch_name,
|
|
2909
|
+
"worktree_root": str(current_workspace_root),
|
|
2910
|
+
"source_branch": str(state.get("paper_parent_branch") or "").strip() or None,
|
|
2911
|
+
"source_run_id": str(state.get("paper_parent_run_id") or "").strip() or None,
|
|
2912
|
+
"source_idea_id": str(state.get("active_idea_id") or "").strip() or None,
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
resolved_source_branch = (
|
|
2916
|
+
str(source_branch or "").strip()
|
|
2917
|
+
or str(state.get("paper_parent_branch") or "").strip()
|
|
2918
|
+
or str(state.get("current_workspace_branch") or "").strip()
|
|
2919
|
+
or str(state.get("research_head_branch") or "").strip()
|
|
2920
|
+
or current_branch(current_workspace_root)
|
|
2921
|
+
)
|
|
2922
|
+
if not resolved_source_branch:
|
|
2923
|
+
raise ValueError("Unable to resolve the source branch for the paper workspace.")
|
|
2924
|
+
|
|
2925
|
+
latest_main_run = self._latest_main_run_for_branch(quest_root, resolved_source_branch)
|
|
2926
|
+
resolved_run_id = (
|
|
2927
|
+
str(source_run_id or "").strip()
|
|
2928
|
+
or str((latest_main_run or {}).get("run_id") or "").strip()
|
|
2929
|
+
or None
|
|
2930
|
+
)
|
|
2931
|
+
resolved_idea_id = (
|
|
2932
|
+
str(source_idea_id or "").strip()
|
|
2933
|
+
or str((latest_main_run or {}).get("idea_id") or "").strip()
|
|
2934
|
+
or str(state.get("active_idea_id") or "").strip()
|
|
2935
|
+
or None
|
|
2936
|
+
)
|
|
2937
|
+
paper_branch = (
|
|
2938
|
+
self._default_branch_name(quest_root, run_id=resolved_run_id, idea_id=resolved_idea_id, branch_kind="paper")
|
|
2939
|
+
if resolved_run_id
|
|
2940
|
+
else f"paper/{slugify(resolved_source_branch, 'paper')}"
|
|
2941
|
+
)
|
|
2942
|
+
if not branch_exists(quest_root, paper_branch):
|
|
2943
|
+
self.prepare_branch(
|
|
2944
|
+
quest_root,
|
|
2945
|
+
run_id=resolved_run_id,
|
|
2946
|
+
idea_id=resolved_idea_id,
|
|
2947
|
+
branch=paper_branch,
|
|
2948
|
+
branch_kind="paper",
|
|
2949
|
+
create_worktree_flag=True,
|
|
2950
|
+
start_point=resolved_source_branch,
|
|
2951
|
+
)
|
|
2952
|
+
activated = self.activate_branch(
|
|
2953
|
+
quest_root,
|
|
2954
|
+
branch=paper_branch,
|
|
2955
|
+
anchor="write",
|
|
2956
|
+
promote_to_head=False,
|
|
2957
|
+
create_worktree_if_missing=True,
|
|
2958
|
+
)
|
|
2959
|
+
return {
|
|
2960
|
+
**activated,
|
|
2961
|
+
"source_branch": resolved_source_branch,
|
|
2962
|
+
"source_run_id": resolved_run_id,
|
|
2963
|
+
"source_idea_id": resolved_idea_id,
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2203
2966
|
def submit_idea(
|
|
2204
2967
|
self,
|
|
2205
2968
|
quest_root: Path,
|
|
@@ -2235,8 +2998,8 @@ class ArtifactService:
|
|
|
2235
2998
|
if normalized_mode == "create":
|
|
2236
2999
|
resolved_idea_id = str(idea_id or generate_id("idea")).strip()
|
|
2237
3000
|
active_branch = (
|
|
2238
|
-
str(state.get("
|
|
2239
|
-
or str(state.get("
|
|
3001
|
+
str(state.get("current_workspace_branch") or "").strip()
|
|
3002
|
+
or str(state.get("research_head_branch") or "").strip()
|
|
2240
3003
|
or current_branch(self._workspace_root_for(quest_root))
|
|
2241
3004
|
)
|
|
2242
3005
|
active_parent_branch = self._idea_parent_branch(self._latest_idea_for_branch(quest_root, active_branch))
|
|
@@ -2441,9 +3204,17 @@ class ArtifactService:
|
|
|
2441
3204
|
raise ValueError("submit_idea(mode='revise') requires an existing active `idea_id`.")
|
|
2442
3205
|
if normalized_lineage_intent:
|
|
2443
3206
|
raise ValueError("submit_idea(mode='revise') does not accept `lineage_intent`; use mode='create' for new branch lineage.")
|
|
2444
|
-
branch_name = str(
|
|
3207
|
+
branch_name = str(
|
|
3208
|
+
state.get("current_workspace_branch")
|
|
3209
|
+
or state.get("research_head_branch")
|
|
3210
|
+
or f"idea/{quest_id}-{resolved_idea_id}"
|
|
3211
|
+
).strip()
|
|
2445
3212
|
worktree_root = Path(
|
|
2446
|
-
str(
|
|
3213
|
+
str(
|
|
3214
|
+
state.get("current_workspace_root")
|
|
3215
|
+
or state.get("research_head_worktree_root")
|
|
3216
|
+
or canonical_worktree_root(quest_root, f"idea-{resolved_idea_id}")
|
|
3217
|
+
)
|
|
2447
3218
|
)
|
|
2448
3219
|
ensure_dir(worktree_root / "memory" / "ideas" / resolved_idea_id)
|
|
2449
3220
|
idea_md_path = worktree_root / "memory" / "ideas" / resolved_idea_id / "idea.md"
|
|
@@ -2545,17 +3316,22 @@ class ArtifactService:
|
|
|
2545
3316
|
checkpoint=False,
|
|
2546
3317
|
workspace_root=worktree_root,
|
|
2547
3318
|
)
|
|
3319
|
+
research_state_updates: dict[str, Any] = {
|
|
3320
|
+
"active_idea_id": resolved_idea_id,
|
|
3321
|
+
"current_workspace_branch": branch_name,
|
|
3322
|
+
"current_workspace_root": str(worktree_root),
|
|
3323
|
+
"active_idea_md_path": str(idea_md_path),
|
|
3324
|
+
"active_idea_draft_path": str(idea_draft_path),
|
|
3325
|
+
"workspace_mode": "idea",
|
|
3326
|
+
"last_flow_type": "idea_revision",
|
|
3327
|
+
}
|
|
3328
|
+
current_head_branch = str(state.get("research_head_branch") or "").strip()
|
|
3329
|
+
if not current_head_branch or current_head_branch == branch_name:
|
|
3330
|
+
research_state_updates["research_head_branch"] = branch_name
|
|
3331
|
+
research_state_updates["research_head_worktree_root"] = str(worktree_root)
|
|
2548
3332
|
research_state = self.quest_service.update_research_state(
|
|
2549
3333
|
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",
|
|
3334
|
+
**research_state_updates,
|
|
2559
3335
|
)
|
|
2560
3336
|
self.quest_service.update_settings(quest_id, active_anchor="experiment")
|
|
2561
3337
|
checkpoint_result = self._checkpoint_with_optional_push(
|
|
@@ -2712,14 +3488,21 @@ class ArtifactService:
|
|
|
2712
3488
|
baseline_id: str | None = None,
|
|
2713
3489
|
baseline_variant_id: str | None = None,
|
|
2714
3490
|
evaluation_summary: dict[str, Any] | None = None,
|
|
3491
|
+
strict_metric_contract: bool = False,
|
|
2715
3492
|
) -> dict[str, Any]:
|
|
2716
3493
|
self._require_baseline_gate_open(quest_root, action="record_main_experiment")
|
|
2717
3494
|
state = self.quest_service.read_research_state(quest_root)
|
|
2718
|
-
|
|
3495
|
+
workspace_mode = str(state.get("workspace_mode") or "").strip()
|
|
3496
|
+
if workspace_mode == "analysis":
|
|
2719
3497
|
raise ValueError(
|
|
2720
3498
|
"record_main_experiment cannot run while the active workspace is an analysis slice. "
|
|
2721
3499
|
"Finish or close the analysis campaign first."
|
|
2722
3500
|
)
|
|
3501
|
+
if workspace_mode == "paper":
|
|
3502
|
+
raise ValueError(
|
|
3503
|
+
"record_main_experiment cannot run while the active workspace is a paper branch. "
|
|
3504
|
+
"Return to the source evidence branch or create a new run branch first."
|
|
3505
|
+
)
|
|
2723
3506
|
|
|
2724
3507
|
run_identifier = str(run_id or "").strip()
|
|
2725
3508
|
if not run_identifier:
|
|
@@ -2727,7 +3510,18 @@ class ArtifactService:
|
|
|
2727
3510
|
|
|
2728
3511
|
active_idea_id = str(state.get("active_idea_id") or "").strip() or None
|
|
2729
3512
|
workspace_root = self._workspace_root_for(quest_root)
|
|
2730
|
-
|
|
3513
|
+
current_branch_name = str(
|
|
3514
|
+
state.get("current_workspace_branch")
|
|
3515
|
+
or state.get("research_head_branch")
|
|
3516
|
+
or current_branch(workspace_root)
|
|
3517
|
+
).strip()
|
|
3518
|
+
branch_name, parent_branch, auto_promoted_run_branch = self._promote_workspace_to_run_branch(
|
|
3519
|
+
quest_root,
|
|
3520
|
+
run_id=run_identifier,
|
|
3521
|
+
idea_id=active_idea_id,
|
|
3522
|
+
workspace_root=workspace_root,
|
|
3523
|
+
current_branch_name=current_branch_name,
|
|
3524
|
+
)
|
|
2731
3525
|
attachment = self._active_baseline_attachment(quest_root, workspace_root=workspace_root)
|
|
2732
3526
|
baseline_entry = dict(attachment.get("entry") or {}) if isinstance(attachment, dict) else {}
|
|
2733
3527
|
selected_variant = dict(attachment.get("selected_variant") or {}) if isinstance(attachment, dict) else {}
|
|
@@ -2761,12 +3555,24 @@ class ArtifactService:
|
|
|
2761
3555
|
metric_contract or baseline_entry.get("metric_contract"),
|
|
2762
3556
|
baseline_id=resolved_baseline_id,
|
|
2763
3557
|
metrics_summary=normalized_metrics_summary,
|
|
3558
|
+
metric_rows=normalized_metric_rows,
|
|
2764
3559
|
primary_metric=baseline_entry.get("primary_metric"),
|
|
2765
3560
|
baseline_variants=baseline_entry.get("baseline_variants"),
|
|
2766
3561
|
)
|
|
3562
|
+
baseline_contract_payload = self._load_metric_contract_payload(quest_root, metric_contract_json_rel_path)
|
|
3563
|
+
metric_validation: dict[str, Any] | None = None
|
|
3564
|
+
if strict_metric_contract:
|
|
3565
|
+
metric_validation = validate_main_experiment_against_baseline_contract(
|
|
3566
|
+
baseline_contract_payload=baseline_contract_payload,
|
|
3567
|
+
run_metric_contract=effective_metric_contract,
|
|
3568
|
+
metric_rows=normalized_metric_rows,
|
|
3569
|
+
metrics_summary=normalized_metrics_summary,
|
|
3570
|
+
dataset_scope=dataset_scope,
|
|
3571
|
+
)
|
|
2767
3572
|
baseline_metrics = selected_baseline_metrics(baseline_entry, resolved_variant_id)
|
|
2768
3573
|
comparisons = compare_with_baseline(
|
|
2769
3574
|
metrics_summary=normalized_metrics_summary,
|
|
3575
|
+
metric_rows=normalized_metric_rows,
|
|
2770
3576
|
metric_contract=effective_metric_contract,
|
|
2771
3577
|
baseline_metrics=baseline_metrics,
|
|
2772
3578
|
)
|
|
@@ -2827,6 +3633,7 @@ class ArtifactService:
|
|
|
2827
3633
|
"",
|
|
2828
3634
|
f"- Run id: `{run_identifier}`",
|
|
2829
3635
|
f"- Branch: `{branch_name}`",
|
|
3636
|
+
f"- Parent branch: `{parent_branch or 'none'}`",
|
|
2830
3637
|
f"- Worktree: `{workspace_root}`",
|
|
2831
3638
|
f"- Idea: `{active_idea_id or 'none'}`",
|
|
2832
3639
|
f"- Baseline: `{resolved_baseline_id or 'none'}`",
|
|
@@ -2924,6 +3731,7 @@ class ArtifactService:
|
|
|
2924
3731
|
"verdict": verdict,
|
|
2925
3732
|
"idea_id": active_idea_id,
|
|
2926
3733
|
"branch": branch_name,
|
|
3734
|
+
"parent_branch": parent_branch,
|
|
2927
3735
|
"worktree_root": str(workspace_root),
|
|
2928
3736
|
"head_commit": head_commit(workspace_root),
|
|
2929
3737
|
"baseline_ref": {
|
|
@@ -2956,6 +3764,7 @@ class ArtifactService:
|
|
|
2956
3764
|
"evidence_paths": resolved_evidence_paths,
|
|
2957
3765
|
"files_changed": resolved_changed_files,
|
|
2958
3766
|
"run_md_path": str(run_md_path),
|
|
3767
|
+
"metric_validation": metric_validation,
|
|
2959
3768
|
}
|
|
2960
3769
|
write_json(result_json_path, result_payload)
|
|
2961
3770
|
|
|
@@ -2970,6 +3779,7 @@ class ArtifactService:
|
|
|
2970
3779
|
"reason": conclusion.strip() or progress_eval.get("reason") or "Main experiment result recorded.",
|
|
2971
3780
|
"idea_id": active_idea_id,
|
|
2972
3781
|
"branch": branch_name,
|
|
3782
|
+
"parent_branch": parent_branch,
|
|
2973
3783
|
"worktree_root": str(workspace_root),
|
|
2974
3784
|
"worktree_rel_path": self._workspace_relative(quest_root, workspace_root),
|
|
2975
3785
|
"flow_type": "main_experiment",
|
|
@@ -2989,6 +3799,7 @@ class ArtifactService:
|
|
|
2989
3799
|
"breakthrough_level": progress_eval.get("breakthrough_level"),
|
|
2990
3800
|
"need_research_paper": delivery_policy.get("need_research_paper"),
|
|
2991
3801
|
"recommended_next_route": delivery_policy.get("recommended_next_route"),
|
|
3802
|
+
"auto_promoted_run_branch": auto_promoted_run_branch,
|
|
2992
3803
|
"changed_file_count": len(resolved_changed_files),
|
|
2993
3804
|
"evidence_count": len(resolved_evidence_paths),
|
|
2994
3805
|
"evaluation_summary": normalized_evaluation_summary,
|
|
@@ -3008,6 +3819,7 @@ class ArtifactService:
|
|
|
3008
3819
|
},
|
|
3009
3820
|
"progress_eval": progress_eval,
|
|
3010
3821
|
"evaluation_summary": normalized_evaluation_summary,
|
|
3822
|
+
"metric_validation": metric_validation,
|
|
3011
3823
|
"files_changed": resolved_changed_files,
|
|
3012
3824
|
"evidence_paths": resolved_evidence_paths,
|
|
3013
3825
|
"verdict": verdict,
|
|
@@ -3049,6 +3861,22 @@ class ArtifactService:
|
|
|
3049
3861
|
],
|
|
3050
3862
|
)
|
|
3051
3863
|
self.quest_service.update_settings(self._quest_id(quest_root), active_anchor="decision")
|
|
3864
|
+
research_state = self.quest_service.update_research_state(
|
|
3865
|
+
quest_root,
|
|
3866
|
+
active_idea_id=active_idea_id,
|
|
3867
|
+
current_workspace_branch=branch_name,
|
|
3868
|
+
current_workspace_root=str(workspace_root),
|
|
3869
|
+
research_head_branch=branch_name,
|
|
3870
|
+
research_head_worktree_root=str(workspace_root),
|
|
3871
|
+
active_analysis_campaign_id=None,
|
|
3872
|
+
analysis_parent_branch=None,
|
|
3873
|
+
analysis_parent_worktree_root=None,
|
|
3874
|
+
paper_parent_branch=None,
|
|
3875
|
+
paper_parent_worktree_root=None,
|
|
3876
|
+
paper_parent_run_id=None,
|
|
3877
|
+
workspace_mode="run",
|
|
3878
|
+
last_flow_type="main_experiment_recorded",
|
|
3879
|
+
)
|
|
3052
3880
|
return {
|
|
3053
3881
|
"ok": True,
|
|
3054
3882
|
"guidance": artifact.get("guidance"),
|
|
@@ -3058,10 +3886,14 @@ class ArtifactService:
|
|
|
3058
3886
|
"suggested_artifact_calls": artifact.get("suggested_artifact_calls"),
|
|
3059
3887
|
"next_instruction": artifact.get("next_instruction"),
|
|
3060
3888
|
"run_id": run_identifier,
|
|
3889
|
+
"branch": branch_name,
|
|
3890
|
+
"parent_branch": parent_branch,
|
|
3891
|
+
"auto_promoted_run_branch": auto_promoted_run_branch,
|
|
3061
3892
|
"run_md_path": str(run_md_path),
|
|
3062
3893
|
"result_json_path": str(result_json_path),
|
|
3063
3894
|
"artifact": artifact,
|
|
3064
3895
|
"interaction": interaction,
|
|
3896
|
+
"research_state": research_state,
|
|
3065
3897
|
"metrics_summary": normalized_metrics_summary,
|
|
3066
3898
|
"baseline_comparisons": {
|
|
3067
3899
|
key: value for key, value in comparisons.items() if key != "primary"
|
|
@@ -3069,6 +3901,7 @@ class ArtifactService:
|
|
|
3069
3901
|
"progress_eval": progress_eval,
|
|
3070
3902
|
"evaluation_summary": normalized_evaluation_summary,
|
|
3071
3903
|
"delivery_policy": delivery_policy,
|
|
3904
|
+
"metric_validation": metric_validation,
|
|
3072
3905
|
}
|
|
3073
3906
|
|
|
3074
3907
|
def create_analysis_campaign(
|
|
@@ -3560,11 +4393,31 @@ class ArtifactService:
|
|
|
3560
4393
|
if normalized_mode not in {"candidate", "select", "revise"}:
|
|
3561
4394
|
raise ValueError("submit_paper_outline mode must be `candidate`, `select`, or `revise`.")
|
|
3562
4395
|
|
|
3563
|
-
|
|
4396
|
+
paper_context = (
|
|
4397
|
+
self._ensure_active_paper_workspace(quest_root)
|
|
4398
|
+
if normalized_mode in {"select", "revise"}
|
|
4399
|
+
else {
|
|
4400
|
+
"worktree_root": str(self._workspace_root_for(quest_root)),
|
|
4401
|
+
"branch": str(self.quest_service.read_research_state(quest_root).get("current_workspace_branch") or "").strip() or None,
|
|
4402
|
+
}
|
|
4403
|
+
)
|
|
4404
|
+
workspace_root = Path(str(paper_context.get("worktree_root") or self._workspace_root_for(quest_root)))
|
|
4405
|
+
paper_root = (
|
|
4406
|
+
ensure_dir(workspace_root / "paper")
|
|
4407
|
+
if normalized_mode in {"select", "revise"}
|
|
4408
|
+
else self._paper_root(quest_root, workspace_root=workspace_root, create=True)
|
|
4409
|
+
)
|
|
4410
|
+
if normalized_mode in {"select", "revise"}:
|
|
4411
|
+
selected_outline_path = paper_root / "selected_outline.json"
|
|
4412
|
+
else:
|
|
4413
|
+
selected_outline_path = self._paper_selected_outline_path(quest_root, workspace_root=workspace_root)
|
|
4414
|
+
existing_selected = read_json(selected_outline_path, {})
|
|
4415
|
+
if not isinstance(existing_selected, dict) or not existing_selected:
|
|
4416
|
+
existing_selected = read_json(quest_root / "paper" / "selected_outline.json", {})
|
|
3564
4417
|
existing_selected = existing_selected if isinstance(existing_selected, dict) else {}
|
|
3565
4418
|
if normalized_mode == "candidate":
|
|
3566
4419
|
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"
|
|
4420
|
+
candidate_path = self._paper_outline_candidates_root(quest_root, workspace_root=workspace_root) / f"{resolved_outline_id}.json"
|
|
3568
4421
|
existing = read_json(candidate_path, {})
|
|
3569
4422
|
existing = existing if isinstance(existing, dict) else {}
|
|
3570
4423
|
record = self._normalize_paper_outline_record(
|
|
@@ -3599,7 +4452,7 @@ class ArtifactService:
|
|
|
3599
4452
|
},
|
|
3600
4453
|
},
|
|
3601
4454
|
checkpoint=False,
|
|
3602
|
-
workspace_root=
|
|
4455
|
+
workspace_root=workspace_root,
|
|
3603
4456
|
)
|
|
3604
4457
|
return {
|
|
3605
4458
|
"ok": True,
|
|
@@ -3613,8 +4466,13 @@ class ArtifactService:
|
|
|
3613
4466
|
source_outline_id = str(outline_id or existing_selected.get("outline_id") or "").strip()
|
|
3614
4467
|
if not source_outline_id:
|
|
3615
4468
|
raise ValueError("submit_paper_outline(select/revise) requires an existing `outline_id` or selected outline.")
|
|
3616
|
-
source_candidate_path =
|
|
4469
|
+
source_candidate_path = paper_root / "outlines" / "candidates" / f"{source_outline_id}.json"
|
|
3617
4470
|
source_record = read_json(source_candidate_path, {})
|
|
4471
|
+
if not isinstance(source_record, dict) or not source_record:
|
|
4472
|
+
fallback_candidate_path = quest_root / "paper" / "outlines" / "candidates" / f"{source_outline_id}.json"
|
|
4473
|
+
source_record = read_json(fallback_candidate_path, {})
|
|
4474
|
+
if isinstance(source_record, dict) and source_record:
|
|
4475
|
+
source_candidate_path = fallback_candidate_path
|
|
3618
4476
|
if not isinstance(source_record, dict) or not source_record:
|
|
3619
4477
|
source_record = existing_selected if str(existing_selected.get("outline_id") or "").strip() == source_outline_id else {}
|
|
3620
4478
|
if not source_record:
|
|
@@ -3632,7 +4490,6 @@ class ArtifactService:
|
|
|
3632
4490
|
created_at=str(source_record.get("created_at") or "") or None,
|
|
3633
4491
|
)
|
|
3634
4492
|
|
|
3635
|
-
selected_outline_path = self._paper_selected_outline_path(quest_root)
|
|
3636
4493
|
write_json(selected_outline_path, resolved_record)
|
|
3637
4494
|
if source_candidate_path.exists():
|
|
3638
4495
|
source_record["status"] = "selected" if normalized_mode == "select" else "revised"
|
|
@@ -3640,10 +4497,10 @@ class ArtifactService:
|
|
|
3640
4497
|
write_json(source_candidate_path, source_record)
|
|
3641
4498
|
revised_outline_path = None
|
|
3642
4499
|
if normalized_mode == "revise":
|
|
3643
|
-
revised_outline_path =
|
|
4500
|
+
revised_outline_path = ensure_dir(paper_root / "outlines" / "revisions") / f"{source_outline_id}.json"
|
|
3644
4501
|
write_json(revised_outline_path, resolved_record)
|
|
3645
4502
|
|
|
3646
|
-
outline_selection_path =
|
|
4503
|
+
outline_selection_path = paper_root / "outline_selection.md"
|
|
3647
4504
|
action_label = "selected" if normalized_mode == "select" else "revised"
|
|
3648
4505
|
selection_lines = [
|
|
3649
4506
|
f"# Outline {normalized_mode.capitalize()}",
|
|
@@ -3682,7 +4539,7 @@ class ArtifactService:
|
|
|
3682
4539
|
},
|
|
3683
4540
|
},
|
|
3684
4541
|
checkpoint=False,
|
|
3685
|
-
workspace_root=
|
|
4542
|
+
workspace_root=workspace_root,
|
|
3686
4543
|
)
|
|
3687
4544
|
return {
|
|
3688
4545
|
"ok": True,
|
|
@@ -3710,24 +4567,44 @@ class ArtifactService:
|
|
|
3710
4567
|
pdf_path: str | None = None,
|
|
3711
4568
|
latex_root_path: str | None = None,
|
|
3712
4569
|
) -> dict[str, Any]:
|
|
3713
|
-
|
|
4570
|
+
paper_context = self._ensure_active_paper_workspace(quest_root)
|
|
4571
|
+
workspace_root = Path(str(paper_context.get("worktree_root") or self._workspace_root_for(quest_root)))
|
|
4572
|
+
paper_root = self._paper_root(quest_root, workspace_root=workspace_root, create=True)
|
|
4573
|
+
selected_outline_path = self._paper_selected_outline_path(quest_root, workspace_root=workspace_root)
|
|
3714
4574
|
selected_outline = read_json(selected_outline_path, {})
|
|
4575
|
+
if not isinstance(selected_outline, dict) or not selected_outline:
|
|
4576
|
+
fallback_selected_outline_path = quest_root / "paper" / "selected_outline.json"
|
|
4577
|
+
selected_outline = read_json(fallback_selected_outline_path, {})
|
|
4578
|
+
if isinstance(selected_outline, dict) and selected_outline:
|
|
4579
|
+
selected_outline_path = fallback_selected_outline_path
|
|
3715
4580
|
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
3716
4581
|
if not selected_outline and not str(outline_path or "").strip():
|
|
3717
4582
|
raise ValueError("submit_paper_bundle requires a selected outline or explicit `outline_path`.")
|
|
3718
4583
|
|
|
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
|
-
)
|
|
4584
|
+
manifest_path = self._paper_bundle_manifest_path(quest_root, workspace_root=workspace_root)
|
|
4585
|
+
baseline_inventory = self._write_paper_baseline_inventory(quest_root, workspace_root=workspace_root)
|
|
4586
|
+
baseline_inventory_path = self._paper_baseline_inventory_path(quest_root, workspace_root=workspace_root)
|
|
4587
|
+
source_branch = str(paper_context.get("source_branch") or "").strip() or None
|
|
4588
|
+
paper_branch = str(paper_context.get("branch") or "").strip() or current_branch(workspace_root)
|
|
4589
|
+
source_run_id = str(paper_context.get("source_run_id") or "").strip() or None
|
|
4590
|
+
source_idea_id = str(paper_context.get("source_idea_id") or "").strip() or None
|
|
4591
|
+
paper_manifest_rel = self._workspace_relative(quest_root, manifest_path) or "paper/paper_bundle_manifest.json"
|
|
4592
|
+
paper_inventory_rel = self._workspace_relative(quest_root, baseline_inventory_path) or "paper/baseline_inventory.json"
|
|
3726
4593
|
open_source_manifest = self._ensure_open_source_prep(
|
|
3727
4594
|
quest_root,
|
|
4595
|
+
workspace_root=workspace_root,
|
|
3728
4596
|
source_branch=source_branch,
|
|
3729
|
-
source_bundle_manifest_path=
|
|
3730
|
-
baseline_inventory_path=
|
|
4597
|
+
source_bundle_manifest_path=paper_manifest_rel,
|
|
4598
|
+
baseline_inventory_path=paper_inventory_rel,
|
|
4599
|
+
)
|
|
4600
|
+
default_draft_path = self._workspace_relative(quest_root, paper_root / "draft.md") or "paper/draft.md"
|
|
4601
|
+
default_writing_plan_path = self._workspace_relative(quest_root, paper_root / "writing_plan.md") or "paper/writing_plan.md"
|
|
4602
|
+
default_references_path = self._workspace_relative(quest_root, paper_root / "references.bib") or "paper/references.bib"
|
|
4603
|
+
default_claim_map_path = (
|
|
4604
|
+
self._workspace_relative(quest_root, paper_root / "claim_evidence_map.json") or "paper/claim_evidence_map.json"
|
|
4605
|
+
)
|
|
4606
|
+
default_compile_report_path = (
|
|
4607
|
+
self._workspace_relative(quest_root, paper_root / "build" / "compile_report.json") or "paper/build/compile_report.json"
|
|
3731
4608
|
)
|
|
3732
4609
|
manifest = {
|
|
3733
4610
|
"schema_version": 1,
|
|
@@ -3740,15 +4617,23 @@ class ArtifactService:
|
|
|
3740
4617
|
or "paper",
|
|
3741
4618
|
"summary": str(summary or "").strip() or None,
|
|
3742
4619
|
"outline_path": str(outline_path or selected_outline_path).strip() or None,
|
|
3743
|
-
"
|
|
3744
|
-
"
|
|
3745
|
-
"
|
|
3746
|
-
"
|
|
3747
|
-
"
|
|
4620
|
+
"paper_branch": paper_branch,
|
|
4621
|
+
"source_branch": source_branch,
|
|
4622
|
+
"source_run_id": source_run_id,
|
|
4623
|
+
"source_idea_id": source_idea_id,
|
|
4624
|
+
"draft_path": str(draft_path or default_draft_path).strip() or None,
|
|
4625
|
+
"writing_plan_path": str(writing_plan_path or default_writing_plan_path).strip() or None,
|
|
4626
|
+
"references_path": str(references_path or default_references_path).strip() or None,
|
|
4627
|
+
"claim_evidence_map_path": str(claim_evidence_map_path or default_claim_map_path).strip() or None,
|
|
4628
|
+
"compile_report_path": str(compile_report_path or default_compile_report_path).strip() or None,
|
|
3748
4629
|
"pdf_path": str(pdf_path or "").strip() or None,
|
|
3749
4630
|
"latex_root_path": str(latex_root_path or "").strip() or None,
|
|
3750
|
-
"baseline_inventory_path":
|
|
3751
|
-
"open_source_manifest_path":
|
|
4631
|
+
"baseline_inventory_path": paper_inventory_rel,
|
|
4632
|
+
"open_source_manifest_path": self._workspace_relative(
|
|
4633
|
+
quest_root,
|
|
4634
|
+
self._open_source_manifest_path(quest_root, workspace_root=workspace_root),
|
|
4635
|
+
)
|
|
4636
|
+
or "release/open_source/manifest.json",
|
|
3752
4637
|
"open_source_cleanup_plan_path": str(open_source_manifest.get("cleanup_plan_path") or "").strip()
|
|
3753
4638
|
or "release/open_source/cleanup_plan.md",
|
|
3754
4639
|
"selected_outline_ref": str(selected_outline.get("outline_id") or "").strip() or None,
|
|
@@ -3773,24 +4658,27 @@ class ArtifactService:
|
|
|
3773
4658
|
"draft_path": manifest.get("draft_path"),
|
|
3774
4659
|
"pdf_path": manifest.get("pdf_path"),
|
|
3775
4660
|
"baseline_inventory_path": str(baseline_inventory_path),
|
|
3776
|
-
"open_source_manifest_path": str(self._open_source_manifest_path(quest_root)),
|
|
4661
|
+
"open_source_manifest_path": str(self._open_source_manifest_path(quest_root, workspace_root=workspace_root)),
|
|
3777
4662
|
},
|
|
3778
4663
|
"details": {
|
|
3779
4664
|
"title": manifest.get("title"),
|
|
3780
4665
|
"selected_outline_ref": manifest.get("selected_outline_ref"),
|
|
3781
4666
|
"baseline_inventory_count": len(baseline_inventory.get("supplementary_baselines") or []),
|
|
3782
4667
|
"open_source_status": open_source_manifest.get("status"),
|
|
4668
|
+
"paper_branch": paper_branch,
|
|
4669
|
+
"source_branch": source_branch,
|
|
4670
|
+
"source_run_id": source_run_id,
|
|
3783
4671
|
},
|
|
3784
4672
|
},
|
|
3785
4673
|
checkpoint=False,
|
|
3786
|
-
workspace_root=
|
|
4674
|
+
workspace_root=workspace_root,
|
|
3787
4675
|
)
|
|
3788
4676
|
return {
|
|
3789
4677
|
"ok": True,
|
|
3790
4678
|
"manifest_path": str(manifest_path),
|
|
3791
4679
|
"manifest": manifest,
|
|
3792
4680
|
"baseline_inventory_path": str(baseline_inventory_path),
|
|
3793
|
-
"open_source_manifest_path": str(self._open_source_manifest_path(quest_root)),
|
|
4681
|
+
"open_source_manifest_path": str(self._open_source_manifest_path(quest_root, workspace_root=workspace_root)),
|
|
3794
4682
|
"artifact": artifact,
|
|
3795
4683
|
}
|
|
3796
4684
|
|
|
@@ -4229,17 +5117,42 @@ class ArtifactService:
|
|
|
4229
5117
|
message=f"analysis: summarize {campaign_id}",
|
|
4230
5118
|
)
|
|
4231
5119
|
restored_idea_id = self._latest_branch_idea_id(quest_root, parent_branch) or str(manifest.get("active_idea_id") or "").strip() or None
|
|
4232
|
-
|
|
5120
|
+
startup_contract = self._startup_contract(quest_root)
|
|
5121
|
+
raw_need_research_paper = startup_contract.get("need_research_paper")
|
|
5122
|
+
need_research_paper = raw_need_research_paper if isinstance(raw_need_research_paper, bool) else True
|
|
5123
|
+
base_research_state = self.quest_service.update_research_state(
|
|
4233
5124
|
quest_root,
|
|
4234
5125
|
active_idea_id=restored_idea_id,
|
|
4235
5126
|
active_analysis_campaign_id=None,
|
|
5127
|
+
analysis_parent_branch=None,
|
|
5128
|
+
analysis_parent_worktree_root=None,
|
|
5129
|
+
paper_parent_branch=None,
|
|
5130
|
+
paper_parent_worktree_root=None,
|
|
5131
|
+
paper_parent_run_id=None,
|
|
4236
5132
|
next_pending_slice_id=None,
|
|
4237
5133
|
current_workspace_branch=parent_branch,
|
|
4238
5134
|
current_workspace_root=str(parent_worktree_root),
|
|
4239
|
-
workspace_mode="idea",
|
|
5135
|
+
workspace_mode="run" if self._branch_kind_from_name(parent_branch) == "run" else "idea",
|
|
4240
5136
|
last_flow_type="analysis_campaign_complete",
|
|
4241
5137
|
)
|
|
4242
|
-
|
|
5138
|
+
writing_workspace: dict[str, Any] | None = None
|
|
5139
|
+
if need_research_paper:
|
|
5140
|
+
try:
|
|
5141
|
+
writing_workspace = self._ensure_active_paper_workspace(
|
|
5142
|
+
quest_root,
|
|
5143
|
+
source_branch=parent_branch,
|
|
5144
|
+
source_run_id=str(manifest.get("parent_run_id") or "").strip() or None,
|
|
5145
|
+
source_idea_id=restored_idea_id,
|
|
5146
|
+
)
|
|
5147
|
+
except Exception:
|
|
5148
|
+
writing_workspace = None
|
|
5149
|
+
|
|
5150
|
+
if writing_workspace:
|
|
5151
|
+
research_state = self.quest_service.read_research_state(quest_root)
|
|
5152
|
+
self.quest_service.update_settings(self._quest_id(quest_root), active_anchor="write")
|
|
5153
|
+
else:
|
|
5154
|
+
research_state = base_research_state
|
|
5155
|
+
self.quest_service.update_settings(self._quest_id(quest_root), active_anchor="decision")
|
|
4243
5156
|
interaction = self.interact(
|
|
4244
5157
|
quest_root,
|
|
4245
5158
|
kind="milestone",
|
|
@@ -4248,7 +5161,15 @@ class ArtifactService:
|
|
|
4248
5161
|
f"- Returned to parent branch: `{parent_branch}`\n"
|
|
4249
5162
|
f"- Parent worktree: `{parent_worktree_root}`\n"
|
|
4250
5163
|
f"- Analysis summary: `{summary_path}`\n"
|
|
4251
|
-
|
|
5164
|
+
+ (
|
|
5165
|
+
(
|
|
5166
|
+
f"- Writing branch: `{writing_workspace.get('branch')}`\n"
|
|
5167
|
+
f"- Writing worktree: `{writing_workspace.get('worktree_root')}`\n"
|
|
5168
|
+
"Writing is now active on the dedicated paper branch."
|
|
5169
|
+
)
|
|
5170
|
+
if writing_workspace
|
|
5171
|
+
else "Use the completed analysis evidence to make the next durable route decision."
|
|
5172
|
+
)
|
|
4252
5173
|
),
|
|
4253
5174
|
deliver_to_bound_conversations=True,
|
|
4254
5175
|
include_recent_inbound_messages=False,
|
|
@@ -4259,6 +5180,8 @@ class ArtifactService:
|
|
|
4259
5180
|
"parent_branch": parent_branch,
|
|
4260
5181
|
"parent_worktree_root": str(parent_worktree_root),
|
|
4261
5182
|
"summary_path": str(summary_path),
|
|
5183
|
+
"writing_branch": writing_workspace.get("branch") if writing_workspace else None,
|
|
5184
|
+
"writing_worktree_root": writing_workspace.get("worktree_root") if writing_workspace else None,
|
|
4262
5185
|
}
|
|
4263
5186
|
],
|
|
4264
5187
|
)
|
|
@@ -4284,6 +5207,8 @@ class ArtifactService:
|
|
|
4284
5207
|
"completed": True,
|
|
4285
5208
|
"returned_to_branch": parent_branch,
|
|
4286
5209
|
"returned_to_worktree_root": str(parent_worktree_root),
|
|
5210
|
+
"writing_branch": writing_workspace.get("branch") if writing_workspace else None,
|
|
5211
|
+
"writing_worktree_root": writing_workspace.get("worktree_root") if writing_workspace else None,
|
|
4287
5212
|
}
|
|
4288
5213
|
|
|
4289
5214
|
def publish_baseline(self, quest_root: Path, payload: dict) -> dict:
|
|
@@ -4348,6 +5273,7 @@ class ArtifactService:
|
|
|
4348
5273
|
metrics_summary: dict[str, Any] | None = None,
|
|
4349
5274
|
primary_metric: dict[str, Any] | None = None,
|
|
4350
5275
|
auto_advance: bool = True,
|
|
5276
|
+
strict_metric_contract: bool = False,
|
|
4351
5277
|
) -> dict[str, Any]:
|
|
4352
5278
|
resolved = self._resolve_baseline_path(quest_root, baseline_path, baseline_id=baseline_id)
|
|
4353
5279
|
resolved_baseline_id = str(resolved["baseline_id"] or "").strip()
|
|
@@ -4439,6 +5365,72 @@ class ArtifactService:
|
|
|
4439
5365
|
or ""
|
|
4440
5366
|
).strip() or None
|
|
4441
5367
|
|
|
5368
|
+
source_metrics_summary = (
|
|
5369
|
+
selected_variant.get("metrics_summary")
|
|
5370
|
+
if isinstance(selected_variant, dict) and selected_variant.get("metrics_summary") is not None
|
|
5371
|
+
else entry.get("metrics_summary")
|
|
5372
|
+
)
|
|
5373
|
+
canonical_baseline = (
|
|
5374
|
+
validate_baseline_metric_contract_submission(
|
|
5375
|
+
metric_contract=entry.get("metric_contract"),
|
|
5376
|
+
metrics_summary=source_metrics_summary,
|
|
5377
|
+
primary_metric=entry.get("primary_metric"),
|
|
5378
|
+
)
|
|
5379
|
+
if strict_metric_contract
|
|
5380
|
+
else canonicalize_baseline_submission(
|
|
5381
|
+
metric_contract=entry.get("metric_contract"),
|
|
5382
|
+
metrics_summary=source_metrics_summary,
|
|
5383
|
+
primary_metric=entry.get("primary_metric"),
|
|
5384
|
+
)
|
|
5385
|
+
)
|
|
5386
|
+
entry = {
|
|
5387
|
+
**entry,
|
|
5388
|
+
"metrics_summary": canonical_baseline["metrics_summary"],
|
|
5389
|
+
"metric_contract": canonical_baseline["metric_contract"],
|
|
5390
|
+
"metric_details": canonical_baseline["metric_details"],
|
|
5391
|
+
}
|
|
5392
|
+
if isinstance(selected_variant, dict):
|
|
5393
|
+
selected_variant = {
|
|
5394
|
+
**selected_variant,
|
|
5395
|
+
"metrics_summary": canonical_baseline["metrics_summary"],
|
|
5396
|
+
}
|
|
5397
|
+
if isinstance(entry.get("baseline_variants"), list):
|
|
5398
|
+
entry["baseline_variants"] = [
|
|
5399
|
+
(
|
|
5400
|
+
{
|
|
5401
|
+
**variant,
|
|
5402
|
+
"metrics_summary": canonical_baseline["metrics_summary"],
|
|
5403
|
+
}
|
|
5404
|
+
if isinstance(variant, dict)
|
|
5405
|
+
and str(variant.get("variant_id") or "").strip() == str(resolved_variant_id or "").strip()
|
|
5406
|
+
else variant
|
|
5407
|
+
)
|
|
5408
|
+
for variant in entry.get("baseline_variants", [])
|
|
5409
|
+
]
|
|
5410
|
+
primary_metric_id = str(
|
|
5411
|
+
(entry.get("primary_metric") or {}).get("metric_id")
|
|
5412
|
+
or (entry.get("primary_metric") or {}).get("name")
|
|
5413
|
+
or (entry.get("primary_metric") or {}).get("id")
|
|
5414
|
+
or (canonical_baseline["metric_contract"] or {}).get("primary_metric_id")
|
|
5415
|
+
or ""
|
|
5416
|
+
).strip()
|
|
5417
|
+
if primary_metric_id and primary_metric_id in canonical_baseline["metrics_summary"]:
|
|
5418
|
+
primary_metric_meta = next(
|
|
5419
|
+
(
|
|
5420
|
+
item
|
|
5421
|
+
for item in (canonical_baseline["metric_contract"] or {}).get("metrics", [])
|
|
5422
|
+
if isinstance(item, dict) and str(item.get("metric_id") or "").strip() == primary_metric_id
|
|
5423
|
+
),
|
|
5424
|
+
{},
|
|
5425
|
+
)
|
|
5426
|
+
entry["primary_metric"] = {
|
|
5427
|
+
**(dict(entry.get("primary_metric") or {}) if isinstance(entry.get("primary_metric"), dict) else {}),
|
|
5428
|
+
"metric_id": primary_metric_id,
|
|
5429
|
+
"value": canonical_baseline["metrics_summary"][primary_metric_id],
|
|
5430
|
+
"direction": primary_metric_meta.get("direction")
|
|
5431
|
+
or (entry.get("primary_metric") or {}).get("direction"),
|
|
5432
|
+
}
|
|
5433
|
+
|
|
4442
5434
|
metric_contract_json = self._write_baseline_metric_contract_json(
|
|
4443
5435
|
quest_root,
|
|
4444
5436
|
baseline_root=resolved_root,
|
|
@@ -4540,6 +5532,7 @@ class ArtifactService:
|
|
|
4540
5532
|
"artifact": artifact,
|
|
4541
5533
|
"baseline_registry_entry": registry_entry,
|
|
4542
5534
|
"snapshot": self.quest_service.snapshot(self._quest_id(quest_root)),
|
|
5535
|
+
"metric_details": canonical_baseline["metric_details"],
|
|
4543
5536
|
"legacy_guidance": "Baseline gate confirmed. Idea selection is now the default next anchor.",
|
|
4544
5537
|
}
|
|
4545
5538
|
|
|
@@ -5159,6 +6152,8 @@ class ArtifactService:
|
|
|
5159
6152
|
return f"idea/{quest_id}-{idea_id}"
|
|
5160
6153
|
if branch_kind == "quest":
|
|
5161
6154
|
return f"quest/{quest_id}"
|
|
6155
|
+
if branch_kind == "paper":
|
|
6156
|
+
return f"paper/{run_id or generate_id('paper')}"
|
|
5162
6157
|
return f"run/{run_id or generate_id('run')}"
|
|
5163
6158
|
|
|
5164
6159
|
def _bound_conversations(self, quest_root: Path) -> list[str]:
|
|
@@ -5187,7 +6182,14 @@ class ArtifactService:
|
|
|
5187
6182
|
return targets
|
|
5188
6183
|
|
|
5189
6184
|
def _connectors_config(self) -> dict[str, Any]:
|
|
5190
|
-
|
|
6185
|
+
manager = ConfigManager(self.home)
|
|
6186
|
+
connectors = manager.load_named_normalized("connectors")
|
|
6187
|
+
for name, config in list(connectors.items()):
|
|
6188
|
+
if str(name).startswith("_") or not isinstance(config, dict):
|
|
6189
|
+
continue
|
|
6190
|
+
if not manager.is_connector_system_enabled(str(name)):
|
|
6191
|
+
config["enabled"] = False
|
|
6192
|
+
return connectors
|
|
5191
6193
|
|
|
5192
6194
|
@staticmethod
|
|
5193
6195
|
def _delivery_policy(connectors: dict[str, Any]) -> str:
|