@researai/deepscientist 1.5.13 → 1.5.15

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.
Files changed (142) hide show
  1. package/README.md +8 -0
  2. package/assets/branding/logo-raster.png +0 -0
  3. package/bin/ds.js +134 -49
  4. package/docs/en/00_QUICK_START.md +2 -2
  5. package/docs/en/01_SETTINGS_REFERENCE.md +20 -4
  6. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
  7. package/docs/en/05_TUI_GUIDE.md +466 -96
  8. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  9. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
  10. package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  11. package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  12. package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  13. package/docs/en/README.md +8 -0
  14. package/docs/zh/00_QUICK_START.md +2 -2
  15. package/docs/zh/01_SETTINGS_REFERENCE.md +20 -4
  16. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
  17. package/docs/zh/05_TUI_GUIDE.md +465 -82
  18. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  19. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
  20. package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  21. package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  22. package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  23. package/docs/zh/README.md +8 -0
  24. package/install.sh +2 -0
  25. package/package.json +1 -1
  26. package/pyproject.toml +1 -1
  27. package/src/deepscientist/__init__.py +1 -1
  28. package/src/deepscientist/artifact/charts.py +567 -0
  29. package/src/deepscientist/artifact/guidance.py +50 -10
  30. package/src/deepscientist/artifact/metrics.py +228 -5
  31. package/src/deepscientist/artifact/schemas.py +3 -0
  32. package/src/deepscientist/artifact/service.py +4004 -538
  33. package/src/deepscientist/bash_exec/models.py +23 -0
  34. package/src/deepscientist/bash_exec/monitor.py +147 -67
  35. package/src/deepscientist/bash_exec/runtime.py +218 -156
  36. package/src/deepscientist/bash_exec/service.py +79 -64
  37. package/src/deepscientist/bash_exec/shells.py +87 -0
  38. package/src/deepscientist/bridges/connectors.py +51 -2
  39. package/src/deepscientist/config/models.py +6 -3
  40. package/src/deepscientist/config/service.py +7 -2
  41. package/src/deepscientist/connector/lingzhu_support.py +23 -4
  42. package/src/deepscientist/connector/weixin_support.py +122 -1
  43. package/src/deepscientist/daemon/api/handlers.py +75 -4
  44. package/src/deepscientist/daemon/api/router.py +1 -0
  45. package/src/deepscientist/daemon/app.py +869 -236
  46. package/src/deepscientist/doctor.py +51 -0
  47. package/src/deepscientist/file_lock.py +48 -0
  48. package/src/deepscientist/gitops/diff.py +167 -1
  49. package/src/deepscientist/mcp/server.py +331 -21
  50. package/src/deepscientist/process_control.py +161 -0
  51. package/src/deepscientist/prompts/builder.py +275 -491
  52. package/src/deepscientist/quest/service.py +2336 -145
  53. package/src/deepscientist/quest/stage_views.py +305 -29
  54. package/src/deepscientist/runners/base.py +2 -0
  55. package/src/deepscientist/runners/codex.py +88 -5
  56. package/src/deepscientist/runners/runtime_overrides.py +17 -1
  57. package/src/deepscientist/shared.py +6 -1
  58. package/src/prompts/contracts/shared_interaction.md +13 -4
  59. package/src/prompts/system.md +984 -1985
  60. package/src/skills/analysis-campaign/SKILL.md +31 -2
  61. package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
  62. package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
  63. package/src/skills/baseline/SKILL.md +267 -994
  64. package/src/skills/baseline/references/baseline-checklist-template.md +21 -32
  65. package/src/skills/baseline/references/baseline-plan-template.md +41 -57
  66. package/src/skills/decision/SKILL.md +19 -2
  67. package/src/skills/experiment/SKILL.md +8 -2
  68. package/src/skills/finalize/SKILL.md +18 -0
  69. package/src/skills/idea/SKILL.md +78 -0
  70. package/src/skills/idea/references/idea-generation-playbook.md +100 -0
  71. package/src/skills/idea/references/outline-seeding-example.md +60 -0
  72. package/src/skills/intake-audit/SKILL.md +1 -1
  73. package/src/skills/optimize/SKILL.md +1644 -0
  74. package/src/skills/rebuttal/SKILL.md +2 -1
  75. package/src/skills/review/SKILL.md +2 -1
  76. package/src/skills/write/SKILL.md +80 -12
  77. package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
  78. package/src/tui/dist/app/AppContainer.js +1445 -52
  79. package/src/tui/dist/components/Composer.js +1 -1
  80. package/src/tui/dist/components/ConfigScreen.js +190 -36
  81. package/src/tui/dist/components/GradientStatusText.js +1 -20
  82. package/src/tui/dist/components/InputPrompt.js +41 -32
  83. package/src/tui/dist/components/LoadingIndicator.js +1 -1
  84. package/src/tui/dist/components/Logo.js +61 -38
  85. package/src/tui/dist/components/MainContent.js +10 -3
  86. package/src/tui/dist/components/WelcomePanel.js +4 -12
  87. package/src/tui/dist/components/messages/AssistantMessage.js +1 -1
  88. package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -3
  89. package/src/tui/dist/components/messages/OperationMessage.js +1 -1
  90. package/src/tui/dist/index.js +28 -1
  91. package/src/tui/dist/layouts/DefaultAppLayout.js +3 -3
  92. package/src/tui/dist/lib/api.js +17 -0
  93. package/src/tui/dist/lib/connectors.js +261 -0
  94. package/src/tui/dist/semantic-colors.js +29 -19
  95. package/src/tui/package.json +1 -1
  96. package/src/ui/dist/assets/{AiManusChatView-CnJcXynW.js → AiManusChatView-DDjbFnbt.js} +12 -12
  97. package/src/ui/dist/assets/{AnalysisPlugin-DeyzPEhV.js → AnalysisPlugin-Yb5IdmaU.js} +1 -1
  98. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +31037 -0
  99. package/src/ui/dist/assets/{CodeEditorPlugin-B-xicq1e.js → CodeEditorPlugin-C4D2TIkU.js} +8 -8
  100. package/src/ui/dist/assets/{CodeViewerPlugin-DT54ysXa.js → CodeViewerPlugin-BVoNZIvC.js} +5 -5
  101. package/src/ui/dist/assets/{DocViewerPlugin-DQtKT-VD.js → DocViewerPlugin-CLChbllo.js} +3 -3
  102. package/src/ui/dist/assets/{GitDiffViewerPlugin-hqHbCfnv.js → GitDiffViewerPlugin-C4xeFyFQ.js} +20 -20
  103. package/src/ui/dist/assets/{ImageViewerPlugin-OcVo33jV.js → ImageViewerPlugin-OiMUAcLi.js} +5 -5
  104. package/src/ui/dist/assets/{LabCopilotPanel-DdGwhEUV.js → LabCopilotPanel-BjD2ThQF.js} +11 -11
  105. package/src/ui/dist/assets/{LabPlugin-Ciz1gDaX.js → LabPlugin-DQPg-NrB.js} +2 -2
  106. package/src/ui/dist/assets/{LatexPlugin-BhmjNQRC.js → LatexPlugin-CI05XAV9.js} +7 -7
  107. package/src/ui/dist/assets/{MarkdownViewerPlugin-BzdVH9Bx.js → MarkdownViewerPlugin-DpeBLYZf.js} +4 -4
  108. package/src/ui/dist/assets/{MarketplacePlugin-DmyHspXt.js → MarketplacePlugin-DolE58Q2.js} +3 -3
  109. package/src/ui/dist/assets/{NotebookEditor-BTVYRGkm.js → NotebookEditor-7Qm2rSWD.js} +11 -11
  110. package/src/ui/dist/assets/{NotebookEditor-BMXKrDRk.js → NotebookEditor-C1kWaxKi.js} +1 -1
  111. package/src/ui/dist/assets/{PdfLoader-CvcjJHXv.js → PdfLoader-BfOHw8Zw.js} +1 -1
  112. package/src/ui/dist/assets/{PdfMarkdownPlugin-DW2ej8Vk.js → PdfMarkdownPlugin-BulDREv1.js} +2 -2
  113. package/src/ui/dist/assets/{PdfViewerPlugin-CmlDxbhU.js → PdfViewerPlugin-C-daaOaL.js} +10 -10
  114. package/src/ui/dist/assets/{SearchPlugin-DAjQZPSv.js → SearchPlugin-CjpaiJ3A.js} +1 -1
  115. package/src/ui/dist/assets/{TextViewerPlugin-C-nVAZb_.js → TextViewerPlugin-BxIyqPQC.js} +5 -5
  116. package/src/ui/dist/assets/{VNCViewer-D7-dIYon.js → VNCViewer-HAg9mF7M.js} +10 -10
  117. package/src/ui/dist/assets/{bot-C_G4WtNI.js → bot-0DYntytV.js} +1 -1
  118. package/src/ui/dist/assets/{code-Cd7WfiWq.js → code-B20Slj_w.js} +1 -1
  119. package/src/ui/dist/assets/{file-content-B57zsL9y.js → file-content-DT24KFma.js} +1 -1
  120. package/src/ui/dist/assets/{file-diff-panel-DVoheLFq.js → file-diff-panel-DK13YPql.js} +1 -1
  121. package/src/ui/dist/assets/{file-socket-B5kXFxZP.js → file-socket-B4T2o4nR.js} +1 -1
  122. package/src/ui/dist/assets/{image-LLOjkMHF.js → image-DSeR_sDS.js} +1 -1
  123. package/src/ui/dist/assets/{index-hOUOWbW2.js → index-BrFje2Uk.js} +2 -2
  124. package/src/ui/dist/assets/{index-Dxa2eYMY.js → index-BwRJaoTl.js} +1 -1
  125. package/src/ui/dist/assets/{index-CLQauncb.js → index-D_E4281X.js} +5418 -28620
  126. package/src/ui/dist/assets/{index-C3r2iGrp.js → index-DnYB3xb1.js} +12 -12
  127. package/src/ui/dist/assets/{index-BQG-1s2o.css → index-G7AcWcMu.css} +43 -2
  128. package/src/ui/dist/assets/{monaco-BGGAEii3.js → monaco-LExaAN3Y.js} +1 -1
  129. package/src/ui/dist/assets/{pdf-effect-queue-DlEr1_y5.js → pdf-effect-queue-BJk5okWJ.js} +1 -1
  130. package/src/ui/dist/assets/{popover-CWJbJuYY.js → popover-D3Gg_FoV.js} +1 -1
  131. package/src/ui/dist/assets/{project-sync-CRJiucYO.js → project-sync-C_ygLlVU.js} +1 -1
  132. package/src/ui/dist/assets/{select-CoHB7pvH.js → select-CpAK6uWm.js} +2 -2
  133. package/src/ui/dist/assets/{sigma-D5aJWR8J.js → sigma-DEccaSgk.js} +1 -1
  134. package/src/ui/dist/assets/{square-check-big-DUK_mnkS.js → square-check-big-uUfyVsbD.js} +1 -1
  135. package/src/ui/dist/assets/{trash-ChU3SEE3.js → trash-CXvwwSe8.js} +1 -1
  136. package/src/ui/dist/assets/{useCliAccess-BrJBV3tY.js → useCliAccess-Bnop4mgR.js} +1 -1
  137. package/src/ui/dist/assets/{useFileDiffOverlay-C2OQaVWc.js → useFileDiffOverlay-B8eUAX0I.js} +1 -1
  138. package/src/ui/dist/assets/{wrap-text-C7Qqh-om.js → wrap-text-9vbOBpkW.js} +1 -1
  139. package/src/ui/dist/assets/{zoom-out-rtX0FKya.js → zoom-out-BgVMmOW4.js} +1 -1
  140. package/src/ui/dist/index.html +2 -2
  141. package/uv.lock +1 -1
  142. package/src/ui/dist/assets/CliPlugin-CB1YODQn.js +0 -5905
@@ -62,6 +62,26 @@ def _field(label: str, value: object, *, tone: str = "default") -> dict[str, Any
62
62
  }
63
63
 
64
64
 
65
+ def _selection_score_summary(value: object) -> str | None:
66
+ if not isinstance(value, dict):
67
+ return None
68
+ parts: list[str] = []
69
+ for key, raw in value.items():
70
+ name = str(key or "").strip()
71
+ if not name:
72
+ continue
73
+ if isinstance(raw, float):
74
+ rendered = f"{raw:.4f}".rstrip("0").rstrip(".")
75
+ else:
76
+ rendered = str(raw).strip()
77
+ if not rendered:
78
+ continue
79
+ parts.append(f"{name}={rendered}")
80
+ if len(parts) >= 4:
81
+ break
82
+ return " · ".join(parts) or None
83
+
84
+
65
85
  def _evaluation_summary(value: object) -> dict[str, Any]:
66
86
  if not isinstance(value, dict):
67
87
  return {}
@@ -215,6 +235,8 @@ class QuestStageViewBuilder:
215
235
  def build(self) -> dict[str, Any]:
216
236
  selection_type = str(self.selection.get("selection_type") or "").strip()
217
237
  self.stage_key = self._resolve_effective_stage_key()
238
+ if selection_type == "idea_candidate":
239
+ return self._build_idea_candidate()
218
240
  if selection_type == "branch_node" and self.stage_key not in {"experiment", "analysis", "paper"}:
219
241
  return self._build_branch()
220
242
  if self.stage_key == "baseline":
@@ -280,6 +302,8 @@ class QuestStageViewBuilder:
280
302
  normalized = [str(item).strip() for item in raw if str(item).strip()]
281
303
  if normalized:
282
304
  return normalized
305
+ if str(self.selection.get("selection_type") or "").strip() == "idea_candidate":
306
+ return self._idea_candidate_scope_paths()
283
307
  if str(self.selection.get("selection_type") or "").strip() == "branch_node":
284
308
  return self._branch_scope_paths()
285
309
  defaults = {
@@ -344,37 +368,83 @@ class QuestStageViewBuilder:
344
368
  return True
345
369
  return False
346
370
 
347
- def _path_in_quest(self, raw_path: object) -> tuple[Path, str] | None:
371
+ def _path_in_quest(self, raw_path: object) -> tuple[Path, str, str] | None:
348
372
  text = str(raw_path or "").strip()
349
373
  if not text:
350
374
  return None
351
375
  path = Path(text)
376
+ candidates: list[Path] = []
352
377
  if not path.is_absolute():
353
- path = (self.quest_root / text).resolve()
378
+ for base in (self.workspace_root, self.quest_root):
379
+ try:
380
+ candidates.append((base / text).resolve())
381
+ except OSError:
382
+ continue
354
383
  else:
355
384
  try:
356
- path = path.resolve()
385
+ candidates.append(path.resolve())
357
386
  except OSError:
358
387
  return None
359
- try:
360
- relative = path.relative_to(self.quest_root.resolve()).as_posix()
361
- except ValueError:
388
+
389
+ if not candidates:
362
390
  return None
363
- return path, relative
391
+
392
+ seen: set[str] = set()
393
+ unique_candidates: list[Path] = []
394
+ for candidate in candidates:
395
+ key = str(candidate)
396
+ if key in seen:
397
+ continue
398
+ seen.add(key)
399
+ unique_candidates.append(candidate)
400
+
401
+ existing_candidates: list[Path] = []
402
+ missing_candidates: list[Path] = []
403
+ for candidate in unique_candidates:
404
+ try:
405
+ if candidate.exists():
406
+ existing_candidates.append(candidate)
407
+ else:
408
+ missing_candidates.append(candidate)
409
+ except OSError:
410
+ missing_candidates.append(candidate)
411
+
412
+ ordered = [*existing_candidates, *missing_candidates]
413
+
414
+ workspace_root = self.workspace_root.resolve()
415
+ quest_root = self.quest_root.resolve()
416
+ for candidate in ordered:
417
+ if candidate.exists():
418
+ try:
419
+ relative = candidate.relative_to(workspace_root).as_posix()
420
+ return candidate, relative, "path"
421
+ except ValueError:
422
+ pass
423
+ try:
424
+ relative = candidate.relative_to(quest_root).as_posix()
425
+ return candidate, relative, "questpath"
426
+ except ValueError:
427
+ pass
428
+ try:
429
+ relative = candidate.relative_to(workspace_root).as_posix()
430
+ return candidate, relative, "path"
431
+ except ValueError:
432
+ continue
433
+ return None
364
434
 
365
435
  def _document_id_for_path(self, raw_path: object) -> str | None:
366
436
  resolved = self._path_in_quest(raw_path)
367
437
  if resolved is None:
368
438
  return None
369
- path, relative = resolved
439
+ path, relative, document_scope = resolved
370
440
  if path.exists() and path.is_file():
371
- return f"questpath::{relative}"
441
+ return f"{document_scope}::{relative}"
372
442
  return None
373
443
 
374
444
  def _relative_path_or_raw(self, raw_path: object) -> str | None:
375
445
  resolved = self._path_in_quest(raw_path)
376
446
  if resolved is not None:
377
- _path, relative = resolved
447
+ _path, relative, _document_scope = resolved
378
448
  return relative
379
449
  text = str(raw_path or "").strip()
380
450
  return text or None
@@ -383,7 +453,7 @@ class QuestStageViewBuilder:
383
453
  resolved = self._path_in_quest(raw_path)
384
454
  if resolved is None:
385
455
  return None
386
- path, _relative = resolved
456
+ path, _relative, _document_scope = resolved
387
457
  if not path.exists() or not path.is_file():
388
458
  return None
389
459
  try:
@@ -464,7 +534,7 @@ class QuestStageViewBuilder:
464
534
  "exists": path.exists(),
465
535
  "scope": "external",
466
536
  }
467
- path, relative = resolved
537
+ path, relative, document_scope = resolved
468
538
  exists = path.exists()
469
539
  kind = "directory" if (exists and path.is_dir()) or expected_kind == "directory" else "file"
470
540
  scope = self.quest_service._classify_relative_scope(relative)[0]
@@ -474,7 +544,7 @@ class QuestStageViewBuilder:
474
544
  "description": description,
475
545
  "path": relative,
476
546
  "absolute_path": str(path),
477
- "document_id": f"questpath::{relative}" if exists and path.is_file() else None,
547
+ "document_id": f"{document_scope}::{relative}" if exists and path.is_file() else None,
478
548
  "kind": kind,
479
549
  "exists": exists,
480
550
  "scope": scope,
@@ -508,30 +578,88 @@ class QuestStageViewBuilder:
508
578
  resolved = self._path_in_quest(raw_path)
509
579
  if resolved is None:
510
580
  return None
511
- _path, relative = resolved
581
+ _path, relative, _document_scope = resolved
512
582
  return relative
513
583
 
514
- def _paper_latex_root(self, bundle_manifest: dict[str, Any]) -> str | None:
515
- preferred = self._paper_relative_path(bundle_manifest.get("latex_root_path"))
516
- if preferred:
517
- return preferred
584
+ def _paper_latex_root(
585
+ self,
586
+ bundle_manifest: dict[str, Any],
587
+ *,
588
+ compile_report: dict[str, Any] | None = None,
589
+ ) -> str | None:
590
+ for candidate in (
591
+ bundle_manifest.get("latex_root_path"),
592
+ (compile_report or {}).get("latex_root_path"),
593
+ (compile_report or {}).get("main_file_path"),
594
+ ):
595
+ resolved = self._path_in_quest(candidate)
596
+ if resolved is None:
597
+ continue
598
+ path, relative, _document_scope = resolved
599
+ if path.is_dir():
600
+ return relative
601
+ if path.suffix.lower() == ".tex":
602
+ return PurePosixPath(relative).parent.as_posix()
518
603
  paper_root = self._paper_root()
519
604
  for candidate in (paper_root / "latex", paper_root / "tex"):
520
605
  if candidate.exists():
521
- return candidate.relative_to(self.quest_root).as_posix()
606
+ try:
607
+ return candidate.relative_to(self.workspace_root.resolve()).as_posix()
608
+ except ValueError:
609
+ return candidate.relative_to(self.quest_root).as_posix()
522
610
  return None
523
611
 
524
- def _paper_main_tex(self, latex_root_rel: str | None) -> str | None:
612
+ def _paper_main_tex(
613
+ self,
614
+ latex_root_rel: str | None,
615
+ *,
616
+ bundle_manifest: dict[str, Any] | None = None,
617
+ compile_report: dict[str, Any] | None = None,
618
+ ) -> str | None:
619
+ for candidate in (
620
+ (compile_report or {}).get("main_file_path"),
621
+ bundle_manifest.get("main_tex_path") if isinstance(bundle_manifest, dict) else None,
622
+ bundle_manifest.get("latex_root_path") if isinstance(bundle_manifest, dict) else None,
623
+ (compile_report or {}).get("latex_root_path"),
624
+ ):
625
+ resolved = self._path_in_quest(candidate)
626
+ if resolved is None:
627
+ continue
628
+ path, relative, _document_scope = resolved
629
+ if path.suffix.lower() == ".tex":
630
+ return relative
631
+ if path.is_dir():
632
+ preferred = path / "main.tex"
633
+ if preferred.exists():
634
+ nested = self._path_in_quest(preferred)
635
+ if nested is not None:
636
+ _resolved_path, nested_relative, _nested_scope = nested
637
+ return nested_relative
525
638
  if not latex_root_rel:
526
639
  return None
527
- latex_root = self.quest_root / latex_root_rel
640
+ latex_root = (self.workspace_root / latex_root_rel).resolve()
641
+ if not latex_root.exists():
642
+ latex_root = (self.quest_root / latex_root_rel).resolve()
643
+ if latex_root.is_file() and latex_root.suffix.lower() == ".tex":
644
+ nested = self._path_in_quest(latex_root)
645
+ if nested is not None:
646
+ _resolved_path, nested_relative, _nested_scope = nested
647
+ return nested_relative
648
+ return None
528
649
  preferred = latex_root / "main.tex"
529
650
  if preferred.exists():
530
- return preferred.relative_to(self.quest_root).as_posix()
651
+ nested = self._path_in_quest(preferred)
652
+ if nested is not None:
653
+ _resolved_path, nested_relative, _nested_scope = nested
654
+ return nested_relative
531
655
  candidates = sorted(latex_root.glob("*.tex"))
532
656
  if not candidates:
533
657
  return None
534
- return candidates[0].relative_to(self.quest_root).as_posix()
658
+ nested = self._path_in_quest(candidates[0])
659
+ if nested is None:
660
+ return None
661
+ _resolved_path, nested_relative, _nested_scope = nested
662
+ return nested_relative
535
663
 
536
664
  def _paper_pdf_candidates(
537
665
  self,
@@ -589,6 +717,13 @@ class QuestStageViewBuilder:
589
717
  "artifacts/reports",
590
718
  ]
591
719
 
720
+ def _idea_candidate_scope_paths(self) -> list[str]:
721
+ candidate_id = str(self.selection.get("selection_ref") or self.selection.get("idea_id") or "").strip()
722
+ return [
723
+ *( [f"memory/ideas/_candidates/{candidate_id}"] if candidate_id else []),
724
+ "artifacts/reports",
725
+ ]
726
+
592
727
  def _experiment_scope_paths(self, run_id: str | None) -> list[str]:
593
728
  return [
594
729
  *( [f"experiments/main/{run_id}"] if run_id else []),
@@ -813,6 +948,25 @@ class QuestStageViewBuilder:
813
948
  items.append(item)
814
949
  return items
815
950
 
951
+ def _idea_candidate_stage_items(self) -> list[dict[str, Any]]:
952
+ candidate_id = str(self.selection.get("selection_ref") or self.selection.get("idea_id") or "").strip()
953
+ if not candidate_id:
954
+ return []
955
+ items: list[dict[str, Any]] = []
956
+ for item in self.artifacts:
957
+ payload = self._payload(item)
958
+ if str(payload.get("kind") or "").strip() != "idea":
959
+ continue
960
+ if str(payload.get("idea_id") or "").strip() != candidate_id:
961
+ continue
962
+ flow_type = str(payload.get("flow_type") or "").strip()
963
+ protocol_step = str(payload.get("protocol_step") or "").strip()
964
+ details = dict(payload.get("details") or {}) if isinstance(payload.get("details"), dict) else {}
965
+ submission_mode = str(details.get("submission_mode") or payload.get("submission_mode") or "").strip().lower()
966
+ if flow_type == "idea_submission" and (protocol_step == "candidate" or submission_mode == "candidate"):
967
+ items.append(item)
968
+ return items
969
+
816
970
  def _build_idea(self) -> dict[str, Any]:
817
971
  idea_items = self._idea_stage_items()
818
972
  latest = idea_items[-1] if idea_items else None
@@ -840,6 +994,8 @@ class QuestStageViewBuilder:
840
994
  draft_md_rel_path = self._relative_path_or_raw(draft_md_path)
841
995
  draft_markdown = self._markdown_body_for_path(draft_md_path)
842
996
  lineage_intent = str(payload.get("lineage_intent") or details.get("lineage_intent") or "").strip() or None
997
+ selection_scores = details.get("selection_scores")
998
+ selection_score_summary = _selection_score_summary(selection_scores)
843
999
  note = (
844
1000
  str(payload.get("summary") or payload.get("reason") or "").strip()
845
1001
  or "No durable idea submission has been recorded yet."
@@ -883,6 +1039,11 @@ class QuestStageViewBuilder:
883
1039
  _field("Problem", details.get("problem") or "Not recorded"),
884
1040
  _field("Hypothesis", details.get("hypothesis") or "Not recorded"),
885
1041
  _field("Mechanism", details.get("mechanism") or "Not recorded"),
1042
+ _field("Method Brief", details.get("method_brief") or "Not recorded"),
1043
+ _field("Selection Scores", selection_score_summary or "Not recorded"),
1044
+ _field("Mechanism Family", details.get("mechanism_family") or "Not recorded"),
1045
+ _field("Change Layer", details.get("change_layer") or "Not recorded"),
1046
+ _field("Source Lens", details.get("source_lens") or "Not recorded"),
886
1047
  _field("Expected Gain", details.get("expected_gain") or "Not recorded"),
887
1048
  _field("Risks", details.get("risks") or "Not recorded"),
888
1049
  _field("Evidence Paths", details.get("evidence_paths") or "Not recorded"),
@@ -905,6 +1066,11 @@ class QuestStageViewBuilder:
905
1066
  "problem": details.get("problem"),
906
1067
  "hypothesis": details.get("hypothesis"),
907
1068
  "mechanism": details.get("mechanism"),
1069
+ "method_brief": details.get("method_brief"),
1070
+ "selection_scores": selection_scores or None,
1071
+ "mechanism_family": details.get("mechanism_family"),
1072
+ "change_layer": details.get("change_layer"),
1073
+ "source_lens": details.get("source_lens"),
908
1074
  "expected_gain": details.get("expected_gain"),
909
1075
  "risks": details.get("risks") or [],
910
1076
  "evidence_paths": details.get("evidence_paths") or [],
@@ -924,6 +1090,101 @@ class QuestStageViewBuilder:
924
1090
  subviews=["overview", "details", "draft"] if draft_markdown else ["overview", "details"],
925
1091
  )
926
1092
 
1093
+ def _build_idea_candidate(self) -> dict[str, Any]:
1094
+ candidate_items = self._idea_candidate_stage_items()
1095
+ latest = candidate_items[-1] if candidate_items else None
1096
+ payload = self._payload(latest or {})
1097
+ details = dict(payload.get("details") or {}) if isinstance(payload.get("details"), dict) else {}
1098
+ candidate_id = str(self.selection.get("selection_ref") or payload.get("idea_id") or "candidate").strip() or "candidate"
1099
+ title_text = (
1100
+ str(details.get("title") or self.selection.get("label") or candidate_id).strip() or candidate_id
1101
+ )
1102
+ paths = dict(payload.get("paths") or {}) if isinstance(payload.get("paths"), dict) else {}
1103
+ candidate_root = paths.get("candidate_root") or str(self.quest_root / "memory" / "ideas" / "_candidates" / candidate_id)
1104
+ idea_md_path = paths.get("idea_md") or str(Path(candidate_root) / "idea.md")
1105
+ draft_md_path = paths.get("idea_draft_md") or details.get("idea_draft_path") or str(Path(candidate_root) / "draft.md")
1106
+ idea_markdown = self._markdown_body_for_path(idea_md_path)
1107
+ draft_markdown = self._markdown_body_for_path(draft_md_path)
1108
+ idea_md_rel_path = self._relative_path_or_raw(idea_md_path)
1109
+ draft_md_rel_path = self._relative_path_or_raw(draft_md_path)
1110
+ candidate_root_rel_path = self._relative_path_or_raw(candidate_root)
1111
+ selection_scores = details.get("selection_scores")
1112
+ selection_score_summary = _selection_score_summary(selection_scores)
1113
+ note = (
1114
+ str(payload.get("summary") or payload.get("reason") or self.selection.get("summary") or "").strip()
1115
+ or "No durable candidate brief summary has been recorded yet."
1116
+ )
1117
+ lineage_intent = str(payload.get("lineage_intent") or details.get("lineage_intent") or "").strip() or None
1118
+ parent_branch = str(payload.get("parent_branch") or details.get("parent_branch") or self.selection.get("branch_name") or "").strip() or None
1119
+ foundation_reason = str(payload.get("foundation_reason") or details.get("foundation_reason") or "").strip() or None
1120
+ return self._base_payload(
1121
+ title=f"Candidate Brief · {title_text}",
1122
+ note=note,
1123
+ status=str(payload.get("status") or "candidate").strip() or "candidate",
1124
+ tags=[
1125
+ "candidate-brief",
1126
+ details.get("mechanism_family") or "",
1127
+ details.get("change_layer") or "",
1128
+ details.get("source_lens") or "",
1129
+ lineage_intent or "",
1130
+ ],
1131
+ overview=[
1132
+ _field("Candidate ID", candidate_id),
1133
+ _field("Parent Branch", parent_branch or "Not recorded"),
1134
+ _field("Next Target", details.get("next_target") or "optimize"),
1135
+ _field("Candidate Root", candidate_root_rel_path or candidate_root),
1136
+ ],
1137
+ key_facts=[
1138
+ _field("Problem", details.get("problem") or "Not recorded"),
1139
+ _field("Hypothesis", details.get("hypothesis") or "Not recorded"),
1140
+ _field("Mechanism", details.get("mechanism") or "Not recorded"),
1141
+ _field("Method Brief", details.get("method_brief") or "Not recorded"),
1142
+ _field("Selection Scores", selection_score_summary or "Not recorded"),
1143
+ _field("Mechanism Family", details.get("mechanism_family") or "Not recorded"),
1144
+ _field("Change Layer", details.get("change_layer") or "Not recorded"),
1145
+ _field("Source Lens", details.get("source_lens") or "Not recorded"),
1146
+ _field("Expected Gain", details.get("expected_gain") or "Not recorded"),
1147
+ _field("Foundation Reason", foundation_reason or "Not recorded"),
1148
+ ],
1149
+ key_files=self._dedupe_files(
1150
+ [
1151
+ self._file_entry(candidate_root, label="Candidate Root", description="Branchless candidate brief workspace.", expected_kind="directory"),
1152
+ self._file_entry(idea_md_path, label="Candidate Markdown", description="Durable candidate brief document."),
1153
+ self._file_entry(draft_md_path, label="Candidate Draft", description="Long-form candidate brief draft."),
1154
+ ]
1155
+ ),
1156
+ history=self._artifact_history(candidate_items),
1157
+ details={
1158
+ "idea": {
1159
+ "idea_id": candidate_id,
1160
+ "title": title_text,
1161
+ "problem": details.get("problem"),
1162
+ "hypothesis": details.get("hypothesis"),
1163
+ "mechanism": details.get("mechanism"),
1164
+ "method_brief": details.get("method_brief"),
1165
+ "selection_scores": selection_scores or None,
1166
+ "mechanism_family": details.get("mechanism_family"),
1167
+ "change_layer": details.get("change_layer"),
1168
+ "source_lens": details.get("source_lens"),
1169
+ "expected_gain": details.get("expected_gain"),
1170
+ "next_target": details.get("next_target") or "optimize",
1171
+ "lineage_intent": lineage_intent,
1172
+ "parent_branch": parent_branch,
1173
+ "candidate_root": candidate_root_rel_path or candidate_root,
1174
+ "idea_path": idea_md_rel_path,
1175
+ "idea_markdown": idea_markdown,
1176
+ "draft_path": draft_md_rel_path,
1177
+ "draft_markdown": draft_markdown,
1178
+ "decision_reason": payload.get("reason"),
1179
+ },
1180
+ "latest_artifact": self._artifact_detail(latest, payload),
1181
+ },
1182
+ lineage_intent=lineage_intent,
1183
+ idea_draft_path=draft_md_rel_path,
1184
+ draft_available=bool(draft_markdown),
1185
+ subviews=["overview", "details", "draft"] if draft_markdown else ["overview", "details"],
1186
+ )
1187
+
927
1188
  def _build_branch(self) -> dict[str, Any]:
928
1189
  idea_items = [
929
1190
  item
@@ -946,6 +1207,8 @@ class QuestStageViewBuilder:
946
1207
  idea_title = str(latest_idea_details.get("title") or "").strip() or None
947
1208
  idea_problem = str(latest_idea_details.get("problem") or "").strip() or None
948
1209
  next_target = str(latest_idea_details.get("next_target") or "").strip() or None
1210
+ selection_scores = latest_idea_details.get("selection_scores")
1211
+ selection_score_summary = _selection_score_summary(selection_scores)
949
1212
  lineage_intent = str(
950
1213
  latest_idea_payload.get("lineage_intent")
951
1214
  or latest_idea_details.get("lineage_intent")
@@ -1065,6 +1328,11 @@ class QuestStageViewBuilder:
1065
1328
  key_facts=[
1066
1329
  _field("Idea Title", idea_title or "Not recorded"),
1067
1330
  _field("Idea Problem", idea_problem or "Not recorded"),
1331
+ _field("Method Brief", latest_idea_details.get("method_brief") or "Not recorded"),
1332
+ _field("Selection Scores", selection_score_summary or "Not recorded"),
1333
+ _field("Mechanism Family", latest_idea_details.get("mechanism_family") or "Not recorded"),
1334
+ _field("Change Layer", latest_idea_details.get("change_layer") or "Not recorded"),
1335
+ _field("Source Lens", latest_idea_details.get("source_lens") or "Not recorded"),
1068
1336
  _field("Foundation", foundation_label or "Current head"),
1069
1337
  _field("Foundation Reason", foundation_reason or "Not recorded"),
1070
1338
  _field("Next Target", next_target or "Not recorded"),
@@ -1120,6 +1388,11 @@ class QuestStageViewBuilder:
1120
1388
  "lineage_intent": lineage_intent,
1121
1389
  "idea_title": idea_title,
1122
1390
  "idea_problem": idea_problem,
1391
+ "method_brief": latest_idea_details.get("method_brief"),
1392
+ "selection_scores": selection_scores or None,
1393
+ "mechanism_family": latest_idea_details.get("mechanism_family"),
1394
+ "change_layer": latest_idea_details.get("change_layer"),
1395
+ "source_lens": latest_idea_details.get("source_lens"),
1123
1396
  "next_target": next_target,
1124
1397
  "idea_draft_path": idea_draft_rel_path,
1125
1398
  "idea_draft_markdown": idea_draft_markdown,
@@ -1460,10 +1733,11 @@ class QuestStageViewBuilder:
1460
1733
  },
1461
1734
  )
1462
1735
 
1463
- def _paper_files(self) -> list[dict[str, Any]]:
1736
+ def _paper_files(self, *, compile_report: dict[str, Any] | None = None) -> list[dict[str, Any]]:
1464
1737
  bundle_manifest = self._paper_bundle_manifest()
1465
- latex_root_rel = self._paper_latex_root(bundle_manifest)
1466
- main_tex_rel = self._paper_main_tex(latex_root_rel)
1738
+ compile_report = compile_report if isinstance(compile_report, dict) else {}
1739
+ latex_root_rel = self._paper_latex_root(bundle_manifest, compile_report=compile_report)
1740
+ main_tex_rel = self._paper_main_tex(latex_root_rel, bundle_manifest=bundle_manifest, compile_report=compile_report)
1467
1741
  pdf_candidates = self._paper_pdf_candidates(bundle_manifest, main_tex_rel=main_tex_rel)
1468
1742
  paper_root = self._paper_root()
1469
1743
  open_source_root = self._open_source_root()
@@ -1474,11 +1748,13 @@ class QuestStageViewBuilder:
1474
1748
  for path in candidates
1475
1749
  ],
1476
1750
  self._file_entry(paper_root / "selected_outline.json", label="Selected Outline", description="Chosen paper outline."),
1751
+ self._file_entry(paper_root / "outline" / "manifest.json", label="Outline Manifest", description="Author-facing paper outline manifest."),
1477
1752
  self._file_entry(paper_root / "outline_selection.md", label="Outline Selection Note", description="Outline selection rationale."),
1478
1753
  self._file_entry(paper_root / "draft.md", label="Draft Markdown", description="Current paper draft."),
1479
1754
  self._file_entry(paper_root / "writing_plan.md", label="Writing Plan", description="Paper writing plan."),
1480
1755
  self._file_entry(paper_root / "references.bib", label="References", description="Bibliography file."),
1481
1756
  self._file_entry(paper_root / "claim_evidence_map.json", label="Claim-Evidence Map", description="Claim to evidence mapping."),
1757
+ self._file_entry(paper_root / "paper_line_state.json", label="Paper Line State", description="Derived summary state for the active paper line."),
1482
1758
  self._file_entry(paper_root / "baseline_inventory.json", label="Baseline Inventory", description="Canonical and supplementary baseline inventory for writing."),
1483
1759
  self._file_entry(paper_root / "build" / "compile_report.json", label="Compile Report", description="Paper build/compile report."),
1484
1760
  self._file_entry(paper_root / "paper_bundle_manifest.json", label="Bundle Manifest", description="Final paper bundle manifest."),
@@ -1537,8 +1813,8 @@ class QuestStageViewBuilder:
1537
1813
  if not isinstance(compile_report, dict):
1538
1814
  compile_report = {}
1539
1815
  bundle_manifest = self._paper_bundle_manifest()
1540
- latex_root_rel = self._paper_latex_root(bundle_manifest)
1541
- main_tex_rel = self._paper_main_tex(latex_root_rel)
1816
+ latex_root_rel = self._paper_latex_root(bundle_manifest, compile_report=compile_report)
1817
+ main_tex_rel = self._paper_main_tex(latex_root_rel, bundle_manifest=bundle_manifest, compile_report=compile_report)
1542
1818
  references_bib = read_text(paper_root / "references.bib", "")
1543
1819
  references_count = sum(1 for line in references_bib.splitlines() if line.lstrip().startswith("@"))
1544
1820
  pdf_paths = self._paper_pdf_candidates(bundle_manifest, main_tex_rel=main_tex_rel)
@@ -1577,7 +1853,7 @@ class QuestStageViewBuilder:
1577
1853
  _field("LaTeX Root", latex_root_rel or "Not recorded"),
1578
1854
  _field("Main TeX", main_tex_rel or "Not recorded"),
1579
1855
  ],
1580
- key_files=self._paper_files(),
1856
+ key_files=self._paper_files(compile_report=compile_report),
1581
1857
  history=self._artifact_history(paper_items),
1582
1858
  details={
1583
1859
  "paper": {
@@ -17,6 +17,8 @@ class RunRequest:
17
17
  approval_policy: str
18
18
  sandbox_mode: str
19
19
  turn_reason: str = "user_message"
20
+ turn_intent: str = "continue_stage"
21
+ turn_mode: str = "stage_execution"
20
22
  reasoning_effort: str | None = None
21
23
  turn_id: str | None = None
22
24
  attempt_index: int = 1