@researai/deepscientist 1.5.12 → 1.5.14

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 (99) hide show
  1. package/bin/ds.js +20 -3
  2. package/docs/en/00_QUICK_START.md +24 -5
  3. package/docs/en/01_SETTINGS_REFERENCE.md +4 -0
  4. package/docs/en/05_TUI_GUIDE.md +466 -96
  5. package/docs/en/09_DOCTOR.md +24 -5
  6. package/docs/en/15_CODEX_PROVIDER_SETUP.md +113 -15
  7. package/docs/en/README.md +2 -0
  8. package/docs/zh/00_QUICK_START.md +24 -5
  9. package/docs/zh/01_SETTINGS_REFERENCE.md +4 -0
  10. package/docs/zh/05_TUI_GUIDE.md +465 -82
  11. package/docs/zh/09_DOCTOR.md +24 -5
  12. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +113 -15
  13. package/docs/zh/README.md +2 -0
  14. package/package.json +2 -1
  15. package/pyproject.toml +1 -1
  16. package/src/deepscientist/__init__.py +1 -1
  17. package/src/deepscientist/artifact/service.py +125 -2
  18. package/src/deepscientist/cli.py +3 -0
  19. package/src/deepscientist/codex_cli_compat.py +117 -0
  20. package/src/deepscientist/config/service.py +53 -6
  21. package/src/deepscientist/connector/lingzhu_support.py +23 -4
  22. package/src/deepscientist/daemon/app.py +111 -30
  23. package/src/deepscientist/mcp/server.py +161 -19
  24. package/src/deepscientist/prompts/builder.py +13 -54
  25. package/src/deepscientist/quest/service.py +99 -0
  26. package/src/deepscientist/quest/stage_views.py +134 -29
  27. package/src/deepscientist/runners/codex.py +11 -2
  28. package/src/deepscientist/runners/runtime_overrides.py +3 -0
  29. package/src/deepscientist/shared.py +6 -1
  30. package/src/prompts/system.md +220 -2065
  31. package/src/skills/baseline/SKILL.md +265 -994
  32. package/src/skills/baseline/references/artifact-payload-examples.md +39 -0
  33. package/src/skills/baseline/references/baseline-checklist-template.md +21 -32
  34. package/src/skills/baseline/references/baseline-plan-template.md +41 -57
  35. package/src/tui/dist/app/AppContainer.js +1442 -52
  36. package/src/tui/dist/components/Composer.js +1 -1
  37. package/src/tui/dist/components/ConfigScreen.js +190 -36
  38. package/src/tui/dist/components/GradientStatusText.js +1 -20
  39. package/src/tui/dist/components/InputPrompt.js +41 -32
  40. package/src/tui/dist/components/LoadingIndicator.js +1 -1
  41. package/src/tui/dist/components/Logo.js +61 -38
  42. package/src/tui/dist/components/MainContent.js +10 -3
  43. package/src/tui/dist/components/WelcomePanel.js +4 -12
  44. package/src/tui/dist/components/messages/AssistantMessage.js +1 -1
  45. package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -3
  46. package/src/tui/dist/components/messages/OperationMessage.js +1 -1
  47. package/src/tui/dist/index.js +28 -1
  48. package/src/tui/dist/layouts/DefaultAppLayout.js +3 -3
  49. package/src/tui/dist/lib/api.js +17 -0
  50. package/src/tui/dist/lib/connectorConfig.js +90 -0
  51. package/src/tui/dist/lib/connectors.js +261 -0
  52. package/src/tui/dist/lib/qr.js +21 -0
  53. package/src/tui/dist/semantic-colors.js +29 -19
  54. package/src/tui/package.json +2 -1
  55. package/src/ui/dist/assets/{AiManusChatView-CnJcXynW.js → AiManusChatView-DaF9Nge_.js} +12 -12
  56. package/src/ui/dist/assets/{AnalysisPlugin-DeyzPEhV.js → AnalysisPlugin-BSVx6dXE.js} +1 -1
  57. package/src/ui/dist/assets/{CliPlugin-CB1YODQn.js → CliPlugin-C9gzJX41.js} +9 -9
  58. package/src/ui/dist/assets/{CodeEditorPlugin-B-xicq1e.js → CodeEditorPlugin-DU9G0Tox.js} +8 -8
  59. package/src/ui/dist/assets/{CodeViewerPlugin-DT54ysXa.js → CodeViewerPlugin-DoX_fI9l.js} +5 -5
  60. package/src/ui/dist/assets/{DocViewerPlugin-DQtKT-VD.js → DocViewerPlugin-C4FWIXuU.js} +3 -3
  61. package/src/ui/dist/assets/{GitDiffViewerPlugin-hqHbCfnv.js → GitDiffViewerPlugin-BgfFMgtf.js} +20 -20
  62. package/src/ui/dist/assets/{ImageViewerPlugin-OcVo33jV.js → ImageViewerPlugin-tcPkfY_x.js} +5 -5
  63. package/src/ui/dist/assets/{LabCopilotPanel-DdGwhEUV.js → LabCopilotPanel-_dKV60Bf.js} +11 -11
  64. package/src/ui/dist/assets/{LabPlugin-Ciz1gDaX.js → LabPlugin-Bje0ayoC.js} +2 -2
  65. package/src/ui/dist/assets/{LatexPlugin-BhmjNQRC.js → LatexPlugin-CVsBzAln.js} +7 -7
  66. package/src/ui/dist/assets/{MarkdownViewerPlugin-BzdVH9Bx.js → MarkdownViewerPlugin-xjmrqv_8.js} +4 -4
  67. package/src/ui/dist/assets/{MarketplacePlugin-DmyHspXt.js → MarketplacePlugin-mMM2A8wP.js} +3 -3
  68. package/src/ui/dist/assets/{NotebookEditor-BTVYRGkm.js → NotebookEditor-3kVDSOBo.js} +11 -11
  69. package/src/ui/dist/assets/{NotebookEditor-BMXKrDRk.js → NotebookEditor-SoJ8X-MO.js} +1 -1
  70. package/src/ui/dist/assets/{PdfLoader-CvcjJHXv.js → PdfLoader-DElVuHl9.js} +1 -1
  71. package/src/ui/dist/assets/{PdfMarkdownPlugin-DW2ej8Vk.js → PdfMarkdownPlugin-Bq88XT4G.js} +2 -2
  72. package/src/ui/dist/assets/{PdfViewerPlugin-CmlDxbhU.js → PdfViewerPlugin-CsCXMo9S.js} +10 -10
  73. package/src/ui/dist/assets/{SearchPlugin-DAjQZPSv.js → SearchPlugin-oUPvy19k.js} +1 -1
  74. package/src/ui/dist/assets/{TextViewerPlugin-C-nVAZb_.js → TextViewerPlugin-CRkT9yNy.js} +5 -5
  75. package/src/ui/dist/assets/{VNCViewer-D7-dIYon.js → VNCViewer-BgbuvWhR.js} +10 -10
  76. package/src/ui/dist/assets/{bot-C_G4WtNI.js → bot-v_RASACv.js} +1 -1
  77. package/src/ui/dist/assets/{code-Cd7WfiWq.js → code-5hC9d0VH.js} +1 -1
  78. package/src/ui/dist/assets/{file-content-B57zsL9y.js → file-content-D1PxfOrp.js} +1 -1
  79. package/src/ui/dist/assets/{file-diff-panel-DVoheLFq.js → file-diff-panel-DG1oT_Hj.js} +1 -1
  80. package/src/ui/dist/assets/{file-socket-B5kXFxZP.js → file-socket-BmdFYQlk.js} +1 -1
  81. package/src/ui/dist/assets/{image-LLOjkMHF.js → image-Dqe2X2tW.js} +1 -1
  82. package/src/ui/dist/assets/{index-Dxa2eYMY.js → index-DVsMKK_y.js} +1 -1
  83. package/src/ui/dist/assets/{index-C3r2iGrp.js → index-Duvz8Ip0.js} +12 -12
  84. package/src/ui/dist/assets/{index-CLQauncb.js → index-Nt9hS4ck.js} +470 -165
  85. package/src/ui/dist/assets/{index-hOUOWbW2.js → index-RDlNXXx1.js} +2 -2
  86. package/src/ui/dist/assets/{monaco-BGGAEii3.js → monaco-DIXge1CP.js} +1 -1
  87. package/src/ui/dist/assets/{pdf-effect-queue-DlEr1_y5.js → pdf-effect-queue-BBTTQaO-.js} +1 -1
  88. package/src/ui/dist/assets/{popover-CWJbJuYY.js → popover-BWlolyxo.js} +1 -1
  89. package/src/ui/dist/assets/{project-sync-CRJiucYO.js → project-sync-BM5PkFH4.js} +1 -1
  90. package/src/ui/dist/assets/{select-CoHB7pvH.js → select-D4dAtrA8.js} +2 -2
  91. package/src/ui/dist/assets/{sigma-D5aJWR8J.js → sigma-CKbE5jJT.js} +1 -1
  92. package/src/ui/dist/assets/{square-check-big-DUK_mnkS.js → square-check-big-CZNGMgiB.js} +1 -1
  93. package/src/ui/dist/assets/{trash-ChU3SEE3.js → trash-DaB37xAz.js} +1 -1
  94. package/src/ui/dist/assets/{useCliAccess-BrJBV3tY.js → useCliAccess-C2OmAcWe.js} +1 -1
  95. package/src/ui/dist/assets/{useFileDiffOverlay-C2OQaVWc.js → useFileDiffOverlay-Dowd1Ij4.js} +1 -1
  96. package/src/ui/dist/assets/{wrap-text-C7Qqh-om.js → wrap-text-BGjAhAUq.js} +1 -1
  97. package/src/ui/dist/assets/{zoom-out-rtX0FKya.js → zoom-out-dMZQMXzc.js} +1 -1
  98. package/src/ui/dist/index.html +1 -1
  99. package/uv.lock +1 -1
@@ -744,10 +744,8 @@ class PromptBuilder:
744
744
  f"- baseline_execution_policy: {baseline_execution_policy if launch_mode == 'custom' else 'n/a'}",
745
745
  f"- manuscript_edit_mode: {manuscript_edit_mode if custom_profile in {'review_audit', 'revision_rebuttal'} else 'n/a'}",
746
746
  f"- delivery_mode: {'paper_required' if need_research_paper else 'algorithm_first'}",
747
+ "- requested_skill_rule: stage-specific execution detail lives in the requested skill; this block only adds runtime launch policy.",
747
748
  "- idea_stage_rule: every accepted idea submission should normally create a new branch/worktree and a new user-visible research node.",
748
- "- idea_draft_rule: before `artifact.submit_idea(...)`, first finish a concise durable Markdown draft for the chosen route; keep `idea.md` compact and `draft.md` richer.",
749
- "- idea_literature_floor_rule: before writing or submitting a final selected idea, durably survey at least 5 and usually 5 to 10 related and usable papers; prioritize direct task-modeling or mechanism-neighbor work and only backfill with the closest adjacent translatable papers when necessary.",
750
- "- idea_reference_rule: the final selected-idea draft should use one consistent standard citation format and include a `References` or `Bibliography` section for the survey-stage papers that actually shaped the motivation, mechanism, or claim boundary.",
751
749
  "- lineage_rule: normal idea routing uses exactly two lineage intents: `continue_line` creates a child of the current active branch; `branch_alternative` creates a sibling-like branch from the current branch's parent foundation.",
752
750
  "- revise_rule: `artifact.submit_idea(mode='revise', ...)` is maintenance-only compatibility for the same branch and should not be the default research-route mechanism.",
753
751
  "- post_main_result_rule: after every `artifact.record_main_experiment(...)`, first interpret the measured result and only then choose the next route.",
@@ -839,13 +837,8 @@ class PromptBuilder:
839
837
  lines.extend(
840
838
  [
841
839
  "- delivery_goal: the quest should normally continue until at least one paper-like deliverable exists.",
842
- "- main_result_rule: a strong main experiment is evidence, not the endpoint; usually continue into the necessary analysis, writing, or further strengthening work.",
843
- "- main_run_branch_rule: every durable main experiment should live on its own dedicated `run/*` branch/worktree so the result becomes a stable Canvas node instead of mutating the idea branch in place.",
844
- "- main_run_branch_rule_2: if the current workspace is still an idea branch when `artifact.record_main_experiment(...)` runs, the runtime will materialize a child `run/*` branch before durable recording; still prefer planning and implementation with that dedicated run branch in mind from the start.",
845
- "- paper_branch_rule: after the required analysis for a strong main result is complete, writing should continue on a dedicated `paper/*` branch/worktree derived from that run branch rather than on the quest root or on the evidence branch itself.",
846
- "- paper_branch_rule_2: treat the paper branch as the writing surface and the parent run branch as the evidence source; do not record new main experiments from the paper branch.",
847
- "- paper_template_rule: once paper writing starts, choose a real venue template from the `write` skill's `templates/` folder, copy it into `paper/latex/`, and default to `templates/iclr2026/` for general ML unless the user or venue contract clearly points elsewhere.",
848
- "- writing_rule: when the evidence becomes strong enough, analysis and paper writing remain in scope by default.",
840
+ "- main_result_rule: a strong main experiment is evidence, not the endpoint; usually continue into analysis, writing, or strengthening work.",
841
+ "- paper_branch_rule: writing should normally continue on a dedicated `paper/*` branch/worktree derived from the evidence line rather than mutating the evidence branch itself.",
849
842
  "- review_gate_rule: before declaring a substantial paper/draft task done, open `review` for an independent skeptical audit; if that audit finds serious gaps, route to `analysis-campaign`, `baseline`, `scout`, or `write` instead of stopping.",
850
843
  "- stop_rule: do not stop with only an improved algorithm or isolated run logs unless the user explicitly narrows scope.",
851
844
  ]
@@ -880,24 +873,15 @@ class PromptBuilder:
880
873
  "- collaboration_mode: long-horizon, continuity-first, artifact-aware",
881
874
  "- response_pattern: say what changed -> say what it means -> say what happens next",
882
875
  "- interaction_protocol: first message may be plain conversation; after that, treat artifact.interact threads and mailbox polls as the main continuity spine across TUI, web, and connectors",
876
+ "- shared_interaction_contract_precedence: use the shared interaction contract as the default user-facing cadence; the rules below add runtime-specific execution behavior instead of restating the same chat cadence",
883
877
  "- mailbox_protocol: artifact.interact(include_recent_inbound_messages=True) is the queued human-message mailbox; when it returns user text, treat that input as higher priority than background subtasks until it has been acknowledged",
884
878
  "- acknowledgment_protocol: after artifact.interact returns any human message, immediately send one substantive artifact.interact(...) follow-up; if the active connector runtime already emitted a transport-level receipt acknowledgement, do not send a redundant receipt-only message; if answerable, answer directly, otherwise state the short plan, nearest checkpoint, and that the current background subtask is paused",
885
- "- progress_protocol: emit artifact.interact(kind='progress', reply_mode='threaded', ...) at real human-meaningful checkpoints; if no natural checkpoint appears during active user-relevant work, prefer a concise keepalive once work has crossed roughly 6 tool calls with a human-meaningful delta, and do not drift beyond roughly 12 tool calls or about 8 minutes without a user-visible update",
886
- "- stage_kickoff_protocol: after entering any stage or companion skill, send one user-visible artifact.interact progress update within the first 3 tool calls of substantial work",
887
- "- read_plan_keepalive_protocol: if work is still mostly reading, searching, comparison, or planning, do not wait too long for a 'big result'; send one concise user-visible checkpoint after about 5 consecutive tool calls if the user would otherwise see silence",
888
879
  "- subtask_boundary_protocol: send a user-visible update whenever the active subtask changes materially, especially across intake -> audit, audit -> experiment planning, experiment planning -> run launch, run result -> drafting, or drafting -> review/rebuttal",
889
880
  "- smoke_then_detach_protocol: for baseline reproduction, main experiments, and analysis experiments, first validate the command path with a bounded smoke test; once the smoke test passes, launch the real long run with bash_exec(mode='detach', ...) and usually leave timeout_seconds unset rather than guessing a fake deadline",
890
881
  "- progress_first_monitoring_protocol: when supervising a long-running bash_exec session, judge health by forward progress rather than by whether the final artifact has already appeared within a short window",
891
- "- delta_monitoring_protocol: compare deltas such as new sample counters, new task counters, new saved files, new last_output_seq values, or changed last_progress payloads; if any of these move forward, treat the run as alive and keep observing",
892
- "- long_run_reporting_protocol: for long-running bash_exec monitoring loops, inspect real logs or status after each completed sleep/await cycle and at least once every 30 minutes at worst, but only send a user-visible update when there is a human-meaningful delta or when the 30-minute visibility bound would otherwise be exceeded; those updates should report the current status, the latest concrete evidence of progress or failure, and the next checkpoint",
893
- "- long_run_watchdog_protocol: for baseline reproduction, baseline-running stages, main experiments, and other important detached runs, do not let more than 30 minutes pass without a real progress inspection and, if the run is still active, a user-visible artifact.interact progress update",
882
+ "- long_run_reporting_protocol: inspect real logs/status after each meaningful await cycle and at least once every 30 minutes at worst, but only send a user-visible update when there is a human-meaningful delta, blocker, recovery, route change, or the visibility bound would otherwise be exceeded",
894
883
  "- intervention_threshold_protocol: do not kill or restart a run merely because a short watch window passed without final completion; intervene only on explicit failure, clear invalidity, process exit, or no meaningful delta across a sufficiently long observation window",
895
- "- slow_model_patience_protocol: if the user says the model, endpoint, or workload is expected to be slow, widen the observation window before intervention and avoid repeated no-change updates",
896
- "- saved_log_read_protocol: bash_exec(mode='read', id=...) returns the full saved rendered log when it is 2000 lines or fewer; for longer logs it returns a preview with the first 500 lines plus the last 1500 lines and tells you to use start/tail for omitted middle windows",
897
- "- log_window_protocol: when you need a specific omitted middle region from a long saved log, use bash_exec(mode='read', id=..., start=..., tail=...) to read a forward rendered-line window",
898
- "- tail_monitoring_protocol: when monitoring a detached run, prefer bash_exec(mode='read', id=..., tail_limit=..., order='desc') so you inspect the newest seq-based evidence first instead of re-reading full logs every time",
899
- "- managed_recovery_protocol: if a detached baseline, main-experiment, or analysis run is clearly invalid, wedged, or superseded, stop it with bash_exec(mode='kill', id=...), document the reason, fix the issue, and relaunch cleanly instead of letting a bad run linger",
900
- "- timeout_protocol: before using bash_exec(mode='await', ...), estimate whether the command can finish within the selected wait window; if runtime is uncertain or likely longer, use bash_exec(mode='detach', ...) and monitor, or set timeout_seconds intentionally",
884
+ "- timeout_protocol: before using bash_exec(mode='await', ...), estimate whether the command can finish within the selected wait window; if runtime is uncertain or likely longer, use bash_exec(mode='detach', ...) and monitor instead of guessing a fake deadline",
901
885
  "- blocking_protocol: use reply_mode='blocking' only for true unresolved user decisions; ordinary progress updates should stay threaded and non-blocking",
902
886
  "- credential_blocking_protocol: if continuation requires user-supplied external credentials or secrets such as an API key, GitHub key/token, or Hugging Face key/token, emit one structured blocking decision request that asks the user to provide the credential or choose an alternative route; do not invent placeholders or silently skip the blocked step",
903
887
  "- credential_wait_protocol: if that credential request remains unanswered, keep the quest waiting rather than self-resolving; if you are resumed without new credentials and no other work is possible, a long low-frequency park such as `bash_exec(command='sleep 3600', mode='await', timeout_seconds=3700)` is acceptable to avoid busy-looping",
@@ -906,42 +890,13 @@ class PromptBuilder:
906
890
  "- respect_protocol: write user-facing updates as natural, respectful, easy-to-follow chat; do not sound like a formal status report or internal tool log",
907
891
  "- omission_protocol: for ordinary user-facing updates, omit file paths, artifact ids, branch/worktree ids, session ids, raw commands, raw logs, and internal tool names unless the user asked for them or needs them to act",
908
892
  "- compaction_protocol: ordinary artifact.interact progress updates should usually fit in 2 to 4 short sentences and should not read like a monitoring transcript or execution diary",
909
- "- tool_call_keepalive_protocol: for active multi-step work outside long detached experiment waits, prefer sending one concise artifact.interact progress update after roughly 6 tool calls when there is already a human-meaningful delta, and do not exceed roughly 12 tool calls or about 8 minutes without a user-visible checkpoint",
893
+ "- watchdog_payload_protocol: if a tool result includes `watchdog_notes`, `progress_watchdog_note`, `visibility_watchdog_note`, or `state_change_watchdog_note`, treat that as an action item and send the required artifact.interact(...) update before doing more background work",
910
894
  "- human_progress_shape_protocol: ordinary progress updates should usually make three things explicit in human language: the current task, the main difficulty or latest real progress, and the concrete next measure you will take",
911
- "- milestone_graduation_protocol: keep ordinary subtask completions concise; upgrade to a richer milestone report only when a stage-significant deliverable or route-changing checkpoint becomes durably true",
912
- "- eta_visibility_protocol: for baseline reproduction, main experiments, analysis experiments, and other important long-running phases, progress updates should also make the expected time to the next meaningful result, next milestone, or next user-visible update explicit; use roughly 10 to 30 minutes as the normal update window, and if the ETA is unreliable, say that and give a realistic next check-in window instead",
913
- "- stage_plan_protocol: for `baseline`, `experiment`, and `analysis-campaign`, do not jump straight into substantial setup, code changes, or real runs; first create or update quest-visible `PLAN.md` and `CHECKLIST.md`, then keep them aligned with the actual route",
914
- "- baseline_plan_protocol: in `baseline`, read the source paper and source repo first when they exist, then make `PLAN.md` cover the route, source package, code touchpoints, smoke path, real-run path, fallback options, monitoring rules, and verification targets before substantial work continues",
915
- "- experiment_plan_protocol: in `experiment`, make `PLAN.md` start with the selected idea summarized in 1 to 2 sentences and then map the idea into code touchpoints, comparability rules, smoke / pilot path, full-run path, fallback options, monitoring rules, and revision notes",
916
- "- analysis_plan_protocol: in `analysis-campaign`, treat `PLAN.md` as the campaign charter and make it cover the slice list, comparability boundary, asset and comparator plan, smoke / full-run policy, reporting plan, and revision log before real slices launch",
917
- "- checklist_maintenance_protocol: for those same stages, treat `CHECKLIST.md` as the living execution surface and update it during reading, setup, coding, smoke tests, real runs, validation, aggregation, and route changes instead of letting progress live only in chat",
918
- "- plan_revision_protocol: if the route, comparability contract, source package, execution strategy, slice ordering, or campaign interpretation changes materially, revise `PLAN.md` before continuing",
919
- "- plan_execution_stability_protocol: once `baseline` or `experiment` has a concrete `PLAN.md` route, implement that plan cleanly instead of repeatedly reshaping code and commands mid-flight; the normal default is one bounded smoke or pilot validation and then one real run, and extra retries should happen only after concrete failure, invalidity, or genuinely new evidence justifies them",
920
- "- stage_milestone_summary_protocol: for accepted baseline, selected idea, completed main experiment, and completed analysis-campaign milestones, usually open with 1 to 2 sentences that say what happened, what it means, and the exact next step before expanding into more detail",
921
- "- idea_milestone_protocol: immediately after a successful accepted artifact.submit_idea(...), send a threaded milestone that explains the idea in plain language and explicitly states whether it currently looks valid, research-worthy, and insight-bearing, plus the main risk and exact next experiment",
922
- "- idea_divergence_protocol: in the idea stage, separate divergence from convergence; unless strong durable evidence already narrows the route to one obvious serious option, do not collapse onto the first plausible route before generating a small but meaningfully diverse candidate slate",
923
- "- idea_lens_protocol: when idea candidates cluster around one mechanism family, deliberately switch ideation lenses such as problem-first vs solution-first, tension hunting, analogy transfer, inversion, or adjacent-possible reasoning before final selection",
924
- "- idea_frontier_protocol: a temporary raw ideation slate may be larger, but after convergence the serious frontier should usually shrink back to 2 to 3 candidates and at most 5",
925
- "- idea_why_now_protocol: every serious idea candidate should answer why now or what changed, not just what the mechanism is",
926
- "- idea_balance_protocol: when the search space is not tiny, carry at least one conservative route and one higher-upside route into the final comparison",
927
- "- idea_pitch_protocol: before artifact.submit_idea(...), make the winner pass a two-sentence pitch, a strongest-objection check, and a concrete why-now statement",
928
- "- idea_literature_floor_protocol: do not write or submit the final selected idea until the durable survey covers at least 5 and usually 5 to 10 related and usable papers; if fewer than 5 direct papers exist, document the shortage and use the closest adjacent translatable work instead of skipping the gate",
929
- "- idea_reference_protocol: the final selected-idea draft should cite the survey-stage papers it actually uses and end with a standard-format `References` or `Bibliography` section",
930
- "- experiment_milestone_protocol: immediately after artifact.record_main_experiment(...) writes the durable result, send a threaded milestone that explains what was run, the main result, whether primary performance improved / worsened / stayed mixed versus the active baseline or best prior anchor, whether the route still looks promising, and the exact next step",
931
- "- analysis_milestone_protocol: immediately after a meaningful completed analysis-campaign synthesis or route-significant campaign checkpoint, send a threaded milestone that explains which campaign question or slice set just closed, whether the claim boundary became stronger / weaker / mixed, the main caveat, and the exact next route",
932
- "- paper_milestone_protocol: immediately after a meaningful paper or draft milestone such as selected outline, evidence-complete draft, major revision package, or bundle-ready paper, send a threaded milestone that explains what document milestone is now complete, which claims are now supportable, what still needs strengthening, and the exact next revision or execution route",
933
- "- asset_grounded_analysis_protocol: before artifact.create_analysis_campaign(...), reuse current quest and user-provided assets first and only plan slices that are executable with the current assets, runtime/tooling, and available credentials",
934
- "- infeasible_slice_protocol: if an analysis slice cannot actually be executed after bounded recovery, do not fake completion; record the slice with a non-success status, report the blocker explicitly, and do not pretend the system can do it",
935
- "- explicit_improvement_protocol: never make the user infer performance improvement only from raw metrics; say plainly whether performance improved, worsened, or stayed mixed",
936
- "- verified_reference_breadth_protocol: for paper-like writing, run broad literature search and reading, aim for roughly 30 to 50 verified references unless scope clearly justifies fewer, use one consistent citation workflow SEARCH -> VERIFY -> RETRIEVE -> VALIDATE -> ADD, use Semantic Scholar by default or Google Scholar manual search/export for discovery, use DOI/Crossref or other real metadata backfills for BibTeX and verification, Every final citation must correspond to a real paper from an actual source, store actual bibliography entries in paper/references.bib as valid BibTeX, do one explicit reference audit before bundling, and never invent citations from memory or hand-write BibTeX from scratch",
937
- "- narrative_focus_protocol: for paper-like writing, organize the paper around one cohesive contribution, make What / Why / So What clear early, assume many readers judge in the order title -> abstract -> introduction -> figures, front-load value in those surfaces, use a five-part abstract formula, keep the introduction concise with 2 to 4 specific contribution bullets, and if the first sentence could be pasted into many unrelated ML papers then rewrite it until it becomes specific",
938
- "- writing_reasoning_externalization_protocol: for paper-like writing, externalize major reasoning into durable notes such as paper/outline_selection.md, paper/claim_evidence_map.json, paper/related_work_map.md, paper/figure_storyboard.md, and paper/reviewer_first_pass.md; those notes should summarize current judgment, alternatives considered, evidence used, risks, and next revision action rather than hidden chain-of-thought",
939
- "- outline_intro_value_protocol: for outlines and introductions, make research value explicit early and use a standard introduction arc: problem and stakes -> concrete gap/bottleneck -> remedy/core idea -> evidence preview -> contributions",
895
+ "- stage_contract_protocol: stage-specific plan/checklist rules, milestone rules, literature rules, and writing rules belong in the requested skill; do not expect this runtime block to restate them",
940
896
  "- teammate_voice_protocol: write like a calm capable teammate using natural first-person phrasing when helpful, for example 'I'm working on ...', 'The main issue right now is ...', 'Next I'll ...'; do not sound like a dashboard or incident log",
941
- "- tqdm_progress_protocol: when you control the experiment code for baseline reproduction, main experiments, or analysis experiments, instrument long loops with a throttled tqdm-style progress reporter when feasible and also prefer periodic __DS_PROGRESS__ JSON markers so monitoring stays both human-readable and machine-usable",
942
897
  "- translation_protocol: convert internal actions into user-facing meaning; describe what was finished and why it matters instead of naming every touched file, counter, timestamp, or subprocess",
943
898
  "- detail_gate_protocol: include exact counters, worker labels, timestamps, retry counts, or file names only when the user explicitly asked for them, when they change the recommended action, or when they are the only honest way to explain a real blocker",
944
- "- monitoring_summary_protocol: for long-running monitoring loops, summarize the frontier state in plain language such as still progressing, temporarily stalled, recovered, or needs intervention; do not narrate each watch window and do not send a no-change update merely because a sleep finished unless the user-visible timing bound requires it",
899
+ "- monitoring_summary_protocol: for long-running monitoring loops, summarize the frontier state in plain language such as still progressing, temporarily stalled, recovered, or needs intervention; do not narrate each watch window",
945
900
  "- preflight_rewrite_protocol: before sending artifact.interact, quickly self-check whether the draft reads like a monitoring log, file inventory, or internal diary; if it mentions watch windows, heartbeats, retry counters, raw counts, timestamps, or multiple file names without being necessary for user action, rewrite it into conclusion -> meaning -> next step first",
946
901
  "- non_research_mode_protocol: if the user message looks like a non-research request, ask for a second confirmation before engaging stage skills or research workflow; after completion, leave one blocking standby interaction instead of repeatedly pinging",
947
902
  "- workspace_discipline: read and modify code inside current_workspace_root; treat quest_root as the canonical repo identity and durable runtime root",
@@ -1057,6 +1012,10 @@ class PromptBuilder:
1057
1012
  f"- latest_thread_interaction_id: {snapshot.get('latest_thread_interaction_id') or 'none'}",
1058
1013
  f"- default_reply_interaction_id: {snapshot.get('default_reply_interaction_id') or 'none'}",
1059
1014
  f"- last_artifact_interact_at: {snapshot.get('last_artifact_interact_at') or 'none'}",
1015
+ f"- seconds_since_last_artifact_interact: {snapshot.get('seconds_since_last_artifact_interact') if snapshot.get('seconds_since_last_artifact_interact') is not None else 'none'}",
1016
+ f"- tool_calls_since_last_artifact_interact: {snapshot.get('tool_calls_since_last_artifact_interact') or 0}",
1017
+ f"- last_tool_activity_at: {snapshot.get('last_tool_activity_at') or 'none'}",
1018
+ f"- last_tool_activity_name: {snapshot.get('last_tool_activity_name') or 'none'}",
1060
1019
  f"- last_delivered_batch_id: {snapshot.get('last_delivered_batch_id') or 'none'}",
1061
1020
  f"- bound_conversations: {', '.join(snapshot.get('bound_conversations') or []) or 'none'}",
1062
1021
  f"- cloud_linked: {snapshot.get('cloud', {}).get('linked', False)}",
@@ -864,6 +864,7 @@ class QuestService:
864
864
  from ..bash_exec import BashExecService
865
865
 
866
866
  bash_summary = BashExecService(self.home).summary(quest_root)
867
+ interaction_watchdog = self.artifact_interaction_watchdog_status(quest_root)
867
868
  payload = {
868
869
  "quest_id": quest_yaml.get("quest_id", quest_id),
869
870
  "title": quest_yaml.get("title", quest_id),
@@ -887,6 +888,10 @@ class QuestService:
887
888
  "stop_reason": runtime_state.get("stop_reason"),
888
889
  "active_interaction_id": runtime_state.get("active_interaction_id"),
889
890
  "last_artifact_interact_at": runtime_state.get("last_artifact_interact_at"),
891
+ "last_tool_activity_at": runtime_state.get("last_tool_activity_at"),
892
+ "last_tool_activity_name": runtime_state.get("last_tool_activity_name"),
893
+ "tool_calls_since_last_artifact_interact": int(runtime_state.get("tool_calls_since_last_artifact_interact") or 0),
894
+ "seconds_since_last_artifact_interact": interaction_watchdog.get("seconds_since_last_artifact_interact"),
890
895
  "last_delivered_batch_id": runtime_state.get("last_delivered_batch_id"),
891
896
  "last_delivered_at": runtime_state.get("last_delivered_at"),
892
897
  "bound_conversations": self._binding_sources_payload(quest_root).get("sources") or ["local:default"],
@@ -907,6 +912,7 @@ class QuestService:
907
912
  "bash_session_count": int(bash_summary.get("session_count") or 0),
908
913
  "bash_running_count": int(bash_summary.get("running_count") or 0),
909
914
  },
915
+ "interaction_watchdog": interaction_watchdog,
910
916
  "recent_artifacts": [],
911
917
  "recent_runs": [],
912
918
  }
@@ -1228,6 +1234,7 @@ class QuestService:
1228
1234
  "bash_session_count": int(bash_summary.get("session_count") or 0),
1229
1235
  "bash_running_count": int(bash_summary.get("running_count") or 0),
1230
1236
  }
1237
+ interaction_watchdog = self.artifact_interaction_watchdog_status(quest_root)
1231
1238
  guidance = None
1232
1239
  try:
1233
1240
  from ..artifact.guidance import build_guidance_for_snapshot
@@ -1287,6 +1294,10 @@ class QuestService:
1287
1294
  "retry_state": runtime_state.get("retry_state"),
1288
1295
  "last_transition_at": runtime_state.get("last_transition_at"),
1289
1296
  "last_artifact_interact_at": runtime_state.get("last_artifact_interact_at"),
1297
+ "last_tool_activity_at": runtime_state.get("last_tool_activity_at"),
1298
+ "last_tool_activity_name": runtime_state.get("last_tool_activity_name"),
1299
+ "tool_calls_since_last_artifact_interact": int(runtime_state.get("tool_calls_since_last_artifact_interact") or 0),
1300
+ "seconds_since_last_artifact_interact": interaction_watchdog.get("seconds_since_last_artifact_interact"),
1290
1301
  "last_delivered_batch_id": runtime_state.get("last_delivered_batch_id"),
1291
1302
  "last_delivered_at": runtime_state.get("last_delivered_at"),
1292
1303
  "bound_conversations": self._binding_sources_payload(quest_root).get("sources") or ["local:default"],
@@ -1302,6 +1313,7 @@ class QuestService:
1302
1313
  },
1303
1314
  "paths": paths,
1304
1315
  "counts": counts,
1316
+ "interaction_watchdog": interaction_watchdog,
1305
1317
  "team": {"mode": "single", "active_workers": []},
1306
1318
  "cloud": {"linked": False, "base_url": "https://deepscientist.cc"},
1307
1319
  "history_count": len(history),
@@ -2674,6 +2686,9 @@ class QuestService:
2674
2686
  "stop_reason": None,
2675
2687
  "last_transition_at": timestamp,
2676
2688
  "last_artifact_interact_at": None,
2689
+ "last_tool_activity_at": None,
2690
+ "last_tool_activity_name": None,
2691
+ "tool_calls_since_last_artifact_interact": 0,
2677
2692
  "pending_user_message_count": pending_count,
2678
2693
  "last_delivered_batch_id": None,
2679
2694
  "last_delivered_at": None,
@@ -2738,6 +2753,7 @@ class QuestService:
2738
2753
  payload = defaults
2739
2754
  merged = {**defaults, **payload}
2740
2755
  merged["pending_user_message_count"] = int(merged.get("pending_user_message_count") or 0)
2756
+ merged["tool_calls_since_last_artifact_interact"] = int(merged.get("tool_calls_since_last_artifact_interact") or 0)
2741
2757
  merged["retry_state"] = dict(merged.get("retry_state") or {}) if isinstance(merged.get("retry_state"), dict) else None
2742
2758
  return merged
2743
2759
 
@@ -2754,6 +2770,9 @@ class QuestService:
2754
2770
  active_interaction_id: str | None | object = _UNSET,
2755
2771
  last_transition_at: str | None | object = _UNSET,
2756
2772
  last_artifact_interact_at: str | None | object = _UNSET,
2773
+ last_tool_activity_at: str | None | object = _UNSET,
2774
+ last_tool_activity_name: str | None | object = _UNSET,
2775
+ tool_calls_since_last_artifact_interact: int | object = _UNSET,
2757
2776
  pending_user_message_count: int | object = _UNSET,
2758
2777
  last_delivered_batch_id: str | None | object = _UNSET,
2759
2778
  last_delivered_at: str | None | object = _UNSET,
@@ -2785,6 +2804,12 @@ class QuestService:
2785
2804
  state["active_interaction_id"] = str(active_interaction_id).strip() if active_interaction_id else None
2786
2805
  if last_artifact_interact_at is not _UNSET:
2787
2806
  state["last_artifact_interact_at"] = last_artifact_interact_at
2807
+ if last_tool_activity_at is not _UNSET:
2808
+ state["last_tool_activity_at"] = last_tool_activity_at
2809
+ if last_tool_activity_name is not _UNSET:
2810
+ state["last_tool_activity_name"] = str(last_tool_activity_name).strip() if last_tool_activity_name else None
2811
+ if tool_calls_since_last_artifact_interact is not _UNSET:
2812
+ state["tool_calls_since_last_artifact_interact"] = max(0, int(tool_calls_since_last_artifact_interact))
2788
2813
  if pending_user_message_count is not _UNSET:
2789
2814
  state["pending_user_message_count"] = max(0, int(pending_user_message_count))
2790
2815
  if last_delivered_batch_id is not _UNSET:
@@ -3056,10 +3081,67 @@ class QuestService:
3056
3081
  quest_root=quest_root,
3057
3082
  active_interaction_id=interaction_id or artifact_id,
3058
3083
  last_artifact_interact_at=timestamp,
3084
+ last_tool_activity_at=timestamp,
3085
+ last_tool_activity_name="artifact.interact",
3086
+ tool_calls_since_last_artifact_interact=0,
3059
3087
  pending_user_message_count=len((self._read_message_queue(quest_root).get("pending") or [])),
3060
3088
  )
3061
3089
  return payload
3062
3090
 
3091
+ def record_tool_activity(
3092
+ self,
3093
+ quest_root: Path,
3094
+ *,
3095
+ tool_name: str,
3096
+ created_at: str | None = None,
3097
+ ) -> dict[str, Any]:
3098
+ timestamp = created_at or utc_now()
3099
+ current_state = self._read_runtime_state(quest_root)
3100
+ next_count = int(current_state.get("tool_calls_since_last_artifact_interact") or 0) + 1
3101
+ payload = {
3102
+ "event_id": generate_id("evt"),
3103
+ "type": "tool_activity",
3104
+ "quest_id": quest_root.name,
3105
+ "tool_name": str(tool_name or "").strip() or "tool",
3106
+ "tool_calls_since_last_artifact_interact": next_count,
3107
+ "created_at": timestamp,
3108
+ }
3109
+ append_jsonl(self._interaction_journal_path(quest_root), payload)
3110
+ self.update_runtime_state(
3111
+ quest_root=quest_root,
3112
+ last_tool_activity_at=timestamp,
3113
+ last_tool_activity_name=payload["tool_name"],
3114
+ tool_calls_since_last_artifact_interact=next_count,
3115
+ )
3116
+ return payload
3117
+
3118
+ @staticmethod
3119
+ def _seconds_since_iso_timestamp(value: str | None) -> int | None:
3120
+ normalized = str(value or "").strip()
3121
+ if not normalized:
3122
+ return None
3123
+ candidate = normalized.replace("Z", "+00:00")
3124
+ try:
3125
+ parsed = datetime.fromisoformat(candidate)
3126
+ except ValueError:
3127
+ return None
3128
+ if parsed.tzinfo is None:
3129
+ parsed = parsed.replace(tzinfo=UTC)
3130
+ return max(int((datetime.now(UTC) - parsed.astimezone(UTC)).total_seconds()), 0)
3131
+
3132
+ def artifact_interaction_watchdog_status(self, quest_root: Path) -> dict[str, Any]:
3133
+ runtime_state = self._read_runtime_state(quest_root)
3134
+ last_artifact_interact_at = str(runtime_state.get("last_artifact_interact_at") or "").strip() or None
3135
+ last_tool_activity_at = str(runtime_state.get("last_tool_activity_at") or "").strip() or None
3136
+ return {
3137
+ "last_artifact_interact_at": last_artifact_interact_at,
3138
+ "seconds_since_last_artifact_interact": self._seconds_since_iso_timestamp(last_artifact_interact_at),
3139
+ "tool_calls_since_last_artifact_interact": int(runtime_state.get("tool_calls_since_last_artifact_interact") or 0),
3140
+ "last_tool_activity_at": last_tool_activity_at,
3141
+ "seconds_since_last_tool_activity": self._seconds_since_iso_timestamp(last_tool_activity_at),
3142
+ "last_tool_activity_name": str(runtime_state.get("last_tool_activity_name") or "").strip() or None,
3143
+ }
3144
+
3063
3145
  def latest_artifact_interaction_records(self, quest_root: Path, limit: int = 10) -> list[dict[str, Any]]:
3064
3146
  items = [
3065
3147
  item
@@ -3320,6 +3402,7 @@ class QuestService:
3320
3402
  "path": relative,
3321
3403
  "kind": "directory",
3322
3404
  "scope": self._classify_relative_scope(relative)[0],
3405
+ "folder_kind": self._snapshot_folder_kind(child, relative),
3323
3406
  "children": self._snapshot_tree_nodes(child, revision=revision, prefix=relative),
3324
3407
  "git_status": None,
3325
3408
  "recently_changed": False,
@@ -3361,6 +3444,22 @@ class QuestService:
3361
3444
  cursor[parts[-1]] = None
3362
3445
  return tree
3363
3446
 
3447
+ @staticmethod
3448
+ def _snapshot_folder_kind(tree: dict[str, dict | None], relative: str) -> str | None:
3449
+ normalized = str(relative or "").strip().replace("\\", "/")
3450
+ if not normalized or normalized.startswith(".ds/"):
3451
+ return None
3452
+ if not isinstance(tree, dict):
3453
+ return None
3454
+ if tree.get("main.tex") is None and "main.tex" in tree:
3455
+ return "latex"
3456
+ for name, child in tree.items():
3457
+ if child is not None:
3458
+ continue
3459
+ if Path(name).suffix.lower() == ".tex":
3460
+ return "latex"
3461
+ return None
3462
+
3364
3463
  def _git_snapshot_paths(self, quest_root: Path, revision: str) -> list[str]:
3365
3464
  result = run_command(
3366
3465
  ["git", "ls-tree", "-r", "--full-tree", "--name-only", revision],
@@ -344,37 +344,83 @@ class QuestStageViewBuilder:
344
344
  return True
345
345
  return False
346
346
 
347
- def _path_in_quest(self, raw_path: object) -> tuple[Path, str] | None:
347
+ def _path_in_quest(self, raw_path: object) -> tuple[Path, str, str] | None:
348
348
  text = str(raw_path or "").strip()
349
349
  if not text:
350
350
  return None
351
351
  path = Path(text)
352
+ candidates: list[Path] = []
352
353
  if not path.is_absolute():
353
- path = (self.quest_root / text).resolve()
354
+ for base in (self.workspace_root, self.quest_root):
355
+ try:
356
+ candidates.append((base / text).resolve())
357
+ except OSError:
358
+ continue
354
359
  else:
355
360
  try:
356
- path = path.resolve()
361
+ candidates.append(path.resolve())
357
362
  except OSError:
358
363
  return None
359
- try:
360
- relative = path.relative_to(self.quest_root.resolve()).as_posix()
361
- except ValueError:
364
+
365
+ if not candidates:
362
366
  return None
363
- return path, relative
367
+
368
+ seen: set[str] = set()
369
+ unique_candidates: list[Path] = []
370
+ for candidate in candidates:
371
+ key = str(candidate)
372
+ if key in seen:
373
+ continue
374
+ seen.add(key)
375
+ unique_candidates.append(candidate)
376
+
377
+ existing_candidates: list[Path] = []
378
+ missing_candidates: list[Path] = []
379
+ for candidate in unique_candidates:
380
+ try:
381
+ if candidate.exists():
382
+ existing_candidates.append(candidate)
383
+ else:
384
+ missing_candidates.append(candidate)
385
+ except OSError:
386
+ missing_candidates.append(candidate)
387
+
388
+ ordered = [*existing_candidates, *missing_candidates]
389
+
390
+ workspace_root = self.workspace_root.resolve()
391
+ quest_root = self.quest_root.resolve()
392
+ for candidate in ordered:
393
+ if candidate.exists():
394
+ try:
395
+ relative = candidate.relative_to(workspace_root).as_posix()
396
+ return candidate, relative, "path"
397
+ except ValueError:
398
+ pass
399
+ try:
400
+ relative = candidate.relative_to(quest_root).as_posix()
401
+ return candidate, relative, "questpath"
402
+ except ValueError:
403
+ pass
404
+ try:
405
+ relative = candidate.relative_to(workspace_root).as_posix()
406
+ return candidate, relative, "path"
407
+ except ValueError:
408
+ continue
409
+ return None
364
410
 
365
411
  def _document_id_for_path(self, raw_path: object) -> str | None:
366
412
  resolved = self._path_in_quest(raw_path)
367
413
  if resolved is None:
368
414
  return None
369
- path, relative = resolved
415
+ path, relative, document_scope = resolved
370
416
  if path.exists() and path.is_file():
371
- return f"questpath::{relative}"
417
+ return f"{document_scope}::{relative}"
372
418
  return None
373
419
 
374
420
  def _relative_path_or_raw(self, raw_path: object) -> str | None:
375
421
  resolved = self._path_in_quest(raw_path)
376
422
  if resolved is not None:
377
- _path, relative = resolved
423
+ _path, relative, _document_scope = resolved
378
424
  return relative
379
425
  text = str(raw_path or "").strip()
380
426
  return text or None
@@ -383,7 +429,7 @@ class QuestStageViewBuilder:
383
429
  resolved = self._path_in_quest(raw_path)
384
430
  if resolved is None:
385
431
  return None
386
- path, _relative = resolved
432
+ path, _relative, _document_scope = resolved
387
433
  if not path.exists() or not path.is_file():
388
434
  return None
389
435
  try:
@@ -464,7 +510,7 @@ class QuestStageViewBuilder:
464
510
  "exists": path.exists(),
465
511
  "scope": "external",
466
512
  }
467
- path, relative = resolved
513
+ path, relative, document_scope = resolved
468
514
  exists = path.exists()
469
515
  kind = "directory" if (exists and path.is_dir()) or expected_kind == "directory" else "file"
470
516
  scope = self.quest_service._classify_relative_scope(relative)[0]
@@ -474,7 +520,7 @@ class QuestStageViewBuilder:
474
520
  "description": description,
475
521
  "path": relative,
476
522
  "absolute_path": str(path),
477
- "document_id": f"questpath::{relative}" if exists and path.is_file() else None,
523
+ "document_id": f"{document_scope}::{relative}" if exists and path.is_file() else None,
478
524
  "kind": kind,
479
525
  "exists": exists,
480
526
  "scope": scope,
@@ -508,30 +554,88 @@ class QuestStageViewBuilder:
508
554
  resolved = self._path_in_quest(raw_path)
509
555
  if resolved is None:
510
556
  return None
511
- _path, relative = resolved
557
+ _path, relative, _document_scope = resolved
512
558
  return relative
513
559
 
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
560
+ def _paper_latex_root(
561
+ self,
562
+ bundle_manifest: dict[str, Any],
563
+ *,
564
+ compile_report: dict[str, Any] | None = None,
565
+ ) -> str | None:
566
+ for candidate in (
567
+ bundle_manifest.get("latex_root_path"),
568
+ (compile_report or {}).get("latex_root_path"),
569
+ (compile_report or {}).get("main_file_path"),
570
+ ):
571
+ resolved = self._path_in_quest(candidate)
572
+ if resolved is None:
573
+ continue
574
+ path, relative, _document_scope = resolved
575
+ if path.is_dir():
576
+ return relative
577
+ if path.suffix.lower() == ".tex":
578
+ return PurePosixPath(relative).parent.as_posix()
518
579
  paper_root = self._paper_root()
519
580
  for candidate in (paper_root / "latex", paper_root / "tex"):
520
581
  if candidate.exists():
521
- return candidate.relative_to(self.quest_root).as_posix()
582
+ try:
583
+ return candidate.relative_to(self.workspace_root.resolve()).as_posix()
584
+ except ValueError:
585
+ return candidate.relative_to(self.quest_root).as_posix()
522
586
  return None
523
587
 
524
- def _paper_main_tex(self, latex_root_rel: str | None) -> str | None:
588
+ def _paper_main_tex(
589
+ self,
590
+ latex_root_rel: str | None,
591
+ *,
592
+ bundle_manifest: dict[str, Any] | None = None,
593
+ compile_report: dict[str, Any] | None = None,
594
+ ) -> str | None:
595
+ for candidate in (
596
+ (compile_report or {}).get("main_file_path"),
597
+ bundle_manifest.get("main_tex_path") if isinstance(bundle_manifest, dict) else None,
598
+ bundle_manifest.get("latex_root_path") if isinstance(bundle_manifest, dict) else None,
599
+ (compile_report or {}).get("latex_root_path"),
600
+ ):
601
+ resolved = self._path_in_quest(candidate)
602
+ if resolved is None:
603
+ continue
604
+ path, relative, _document_scope = resolved
605
+ if path.suffix.lower() == ".tex":
606
+ return relative
607
+ if path.is_dir():
608
+ preferred = path / "main.tex"
609
+ if preferred.exists():
610
+ nested = self._path_in_quest(preferred)
611
+ if nested is not None:
612
+ _resolved_path, nested_relative, _nested_scope = nested
613
+ return nested_relative
525
614
  if not latex_root_rel:
526
615
  return None
527
- latex_root = self.quest_root / latex_root_rel
616
+ latex_root = (self.workspace_root / latex_root_rel).resolve()
617
+ if not latex_root.exists():
618
+ latex_root = (self.quest_root / latex_root_rel).resolve()
619
+ if latex_root.is_file() and latex_root.suffix.lower() == ".tex":
620
+ nested = self._path_in_quest(latex_root)
621
+ if nested is not None:
622
+ _resolved_path, nested_relative, _nested_scope = nested
623
+ return nested_relative
624
+ return None
528
625
  preferred = latex_root / "main.tex"
529
626
  if preferred.exists():
530
- return preferred.relative_to(self.quest_root).as_posix()
627
+ nested = self._path_in_quest(preferred)
628
+ if nested is not None:
629
+ _resolved_path, nested_relative, _nested_scope = nested
630
+ return nested_relative
531
631
  candidates = sorted(latex_root.glob("*.tex"))
532
632
  if not candidates:
533
633
  return None
534
- return candidates[0].relative_to(self.quest_root).as_posix()
634
+ nested = self._path_in_quest(candidates[0])
635
+ if nested is None:
636
+ return None
637
+ _resolved_path, nested_relative, _nested_scope = nested
638
+ return nested_relative
535
639
 
536
640
  def _paper_pdf_candidates(
537
641
  self,
@@ -1460,10 +1564,11 @@ class QuestStageViewBuilder:
1460
1564
  },
1461
1565
  )
1462
1566
 
1463
- def _paper_files(self) -> list[dict[str, Any]]:
1567
+ def _paper_files(self, *, compile_report: dict[str, Any] | None = None) -> list[dict[str, Any]]:
1464
1568
  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)
1569
+ compile_report = compile_report if isinstance(compile_report, dict) else {}
1570
+ latex_root_rel = self._paper_latex_root(bundle_manifest, compile_report=compile_report)
1571
+ main_tex_rel = self._paper_main_tex(latex_root_rel, bundle_manifest=bundle_manifest, compile_report=compile_report)
1467
1572
  pdf_candidates = self._paper_pdf_candidates(bundle_manifest, main_tex_rel=main_tex_rel)
1468
1573
  paper_root = self._paper_root()
1469
1574
  open_source_root = self._open_source_root()
@@ -1537,8 +1642,8 @@ class QuestStageViewBuilder:
1537
1642
  if not isinstance(compile_report, dict):
1538
1643
  compile_report = {}
1539
1644
  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)
1645
+ latex_root_rel = self._paper_latex_root(bundle_manifest, compile_report=compile_report)
1646
+ main_tex_rel = self._paper_main_tex(latex_root_rel, bundle_manifest=bundle_manifest, compile_report=compile_report)
1542
1647
  references_bib = read_text(paper_root / "references.bib", "")
1543
1648
  references_count = sum(1 for line in references_bib.splitlines() if line.lstrip().startswith("@"))
1544
1649
  pdf_paths = self._paper_pdf_candidates(bundle_manifest, main_tex_rel=main_tex_rel)
@@ -1577,7 +1682,7 @@ class QuestStageViewBuilder:
1577
1682
  _field("LaTeX Root", latex_root_rel or "Not recorded"),
1578
1683
  _field("Main TeX", main_tex_rel or "Not recorded"),
1579
1684
  ],
1580
- key_files=self._paper_files(),
1685
+ key_files=self._paper_files(compile_report=compile_report),
1581
1686
  history=self._artifact_history(paper_items),
1582
1687
  details={
1583
1688
  "paper": {
@@ -11,11 +11,12 @@ from pathlib import Path
11
11
  from typing import Any
12
12
 
13
13
  from ..artifact import ArtifactService
14
+ from ..codex_cli_compat import adapt_profile_only_provider_config, normalize_codex_reasoning_effort
14
15
  from ..config import ConfigManager
15
16
  from ..gitops import export_git_graph
16
17
  from ..prompts import PromptBuilder
17
18
  from ..runtime_logs import JsonlLogger
18
- from ..shared import append_jsonl, ensure_dir, generate_id, read_yaml, resolve_runner_binary, utc_now, write_json, write_text
19
+ from ..shared import append_jsonl, ensure_dir, generate_id, read_text, read_yaml, resolve_runner_binary, utc_now, write_json, write_text
19
20
  from ..web_search import extract_web_search_payload
20
21
  from .base import RunRequest, RunResult
21
22
 
@@ -920,7 +921,10 @@ class CodexRunner:
920
921
  command.extend(["--model", normalized_model])
921
922
  if request.approval_policy:
922
923
  command.extend(["-c", f'approval_policy="{request.approval_policy}"'])
923
- reasoning_effort = request.reasoning_effort
924
+ reasoning_effort, _ = normalize_codex_reasoning_effort(
925
+ request.reasoning_effort,
926
+ resolved_binary=resolved_binary or self.binary,
927
+ )
924
928
  if reasoning_effort:
925
929
  command.extend(["-c", f'model_reasoning_effort="{reasoning_effort}"'])
926
930
  tool_timeout_sec = self._positive_timeout_seconds(resolved_runner_config.get("mcp_tool_timeout_sec"))
@@ -945,6 +949,7 @@ class CodexRunner:
945
949
  target = ensure_dir(workspace_root / ".codex")
946
950
  resolved_runner_config = runner_config if isinstance(runner_config, dict) else self._load_runner_config()
947
951
  configured_home = str(resolved_runner_config.get("config_dir") or os.environ.get("CODEX_HOME") or str(Path.home() / ".codex"))
952
+ profile = str(resolved_runner_config.get("profile") or "").strip()
948
953
  source = Path(configured_home).expanduser()
949
954
  for filename in ("config.toml", "auth.json"):
950
955
  source_path = source / filename
@@ -953,6 +958,10 @@ class CodexRunner:
953
958
  if source_path.resolve() == target_path.resolve():
954
959
  continue
955
960
  shutil.copy2(source_path, target_path)
961
+ config_path = target / "config.toml"
962
+ if profile and config_path.exists():
963
+ adapted_text, _ = adapt_profile_only_provider_config(read_text(config_path), profile=profile)
964
+ write_text(config_path, adapted_text)
956
965
  ensure_dir(target / "skills")
957
966
  quest_skills_root = quest_root / ".codex" / "skills"
958
967
  if quest_skills_root.exists():