@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.
- package/bin/ds.js +20 -3
- package/docs/en/00_QUICK_START.md +24 -5
- package/docs/en/01_SETTINGS_REFERENCE.md +4 -0
- package/docs/en/05_TUI_GUIDE.md +466 -96
- package/docs/en/09_DOCTOR.md +24 -5
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +113 -15
- package/docs/en/README.md +2 -0
- package/docs/zh/00_QUICK_START.md +24 -5
- package/docs/zh/01_SETTINGS_REFERENCE.md +4 -0
- package/docs/zh/05_TUI_GUIDE.md +465 -82
- package/docs/zh/09_DOCTOR.md +24 -5
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +113 -15
- package/docs/zh/README.md +2 -0
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/artifact/service.py +125 -2
- package/src/deepscientist/cli.py +3 -0
- package/src/deepscientist/codex_cli_compat.py +117 -0
- package/src/deepscientist/config/service.py +53 -6
- package/src/deepscientist/connector/lingzhu_support.py +23 -4
- package/src/deepscientist/daemon/app.py +111 -30
- package/src/deepscientist/mcp/server.py +161 -19
- package/src/deepscientist/prompts/builder.py +13 -54
- package/src/deepscientist/quest/service.py +99 -0
- package/src/deepscientist/quest/stage_views.py +134 -29
- package/src/deepscientist/runners/codex.py +11 -2
- package/src/deepscientist/runners/runtime_overrides.py +3 -0
- package/src/deepscientist/shared.py +6 -1
- package/src/prompts/system.md +220 -2065
- package/src/skills/baseline/SKILL.md +265 -994
- package/src/skills/baseline/references/artifact-payload-examples.md +39 -0
- package/src/skills/baseline/references/baseline-checklist-template.md +21 -32
- package/src/skills/baseline/references/baseline-plan-template.md +41 -57
- package/src/tui/dist/app/AppContainer.js +1442 -52
- package/src/tui/dist/components/Composer.js +1 -1
- package/src/tui/dist/components/ConfigScreen.js +190 -36
- package/src/tui/dist/components/GradientStatusText.js +1 -20
- package/src/tui/dist/components/InputPrompt.js +41 -32
- package/src/tui/dist/components/LoadingIndicator.js +1 -1
- package/src/tui/dist/components/Logo.js +61 -38
- package/src/tui/dist/components/MainContent.js +10 -3
- package/src/tui/dist/components/WelcomePanel.js +4 -12
- package/src/tui/dist/components/messages/AssistantMessage.js +1 -1
- package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -3
- package/src/tui/dist/components/messages/OperationMessage.js +1 -1
- package/src/tui/dist/index.js +28 -1
- package/src/tui/dist/layouts/DefaultAppLayout.js +3 -3
- package/src/tui/dist/lib/api.js +17 -0
- package/src/tui/dist/lib/connectorConfig.js +90 -0
- package/src/tui/dist/lib/connectors.js +261 -0
- package/src/tui/dist/lib/qr.js +21 -0
- package/src/tui/dist/semantic-colors.js +29 -19
- package/src/tui/package.json +2 -1
- package/src/ui/dist/assets/{AiManusChatView-CnJcXynW.js → AiManusChatView-DaF9Nge_.js} +12 -12
- package/src/ui/dist/assets/{AnalysisPlugin-DeyzPEhV.js → AnalysisPlugin-BSVx6dXE.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-CB1YODQn.js → CliPlugin-C9gzJX41.js} +9 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-B-xicq1e.js → CodeEditorPlugin-DU9G0Tox.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DT54ysXa.js → CodeViewerPlugin-DoX_fI9l.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-DQtKT-VD.js → DocViewerPlugin-C4FWIXuU.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-hqHbCfnv.js → GitDiffViewerPlugin-BgfFMgtf.js} +20 -20
- package/src/ui/dist/assets/{ImageViewerPlugin-OcVo33jV.js → ImageViewerPlugin-tcPkfY_x.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-DdGwhEUV.js → LabCopilotPanel-_dKV60Bf.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-Ciz1gDaX.js → LabPlugin-Bje0ayoC.js} +2 -2
- package/src/ui/dist/assets/{LatexPlugin-BhmjNQRC.js → LatexPlugin-CVsBzAln.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-BzdVH9Bx.js → MarkdownViewerPlugin-xjmrqv_8.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DmyHspXt.js → MarketplacePlugin-mMM2A8wP.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-BTVYRGkm.js → NotebookEditor-3kVDSOBo.js} +11 -11
- package/src/ui/dist/assets/{NotebookEditor-BMXKrDRk.js → NotebookEditor-SoJ8X-MO.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-CvcjJHXv.js → PdfLoader-DElVuHl9.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DW2ej8Vk.js → PdfMarkdownPlugin-Bq88XT4G.js} +2 -2
- package/src/ui/dist/assets/{PdfViewerPlugin-CmlDxbhU.js → PdfViewerPlugin-CsCXMo9S.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-DAjQZPSv.js → SearchPlugin-oUPvy19k.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-C-nVAZb_.js → TextViewerPlugin-CRkT9yNy.js} +5 -5
- package/src/ui/dist/assets/{VNCViewer-D7-dIYon.js → VNCViewer-BgbuvWhR.js} +10 -10
- package/src/ui/dist/assets/{bot-C_G4WtNI.js → bot-v_RASACv.js} +1 -1
- package/src/ui/dist/assets/{code-Cd7WfiWq.js → code-5hC9d0VH.js} +1 -1
- package/src/ui/dist/assets/{file-content-B57zsL9y.js → file-content-D1PxfOrp.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DVoheLFq.js → file-diff-panel-DG1oT_Hj.js} +1 -1
- package/src/ui/dist/assets/{file-socket-B5kXFxZP.js → file-socket-BmdFYQlk.js} +1 -1
- package/src/ui/dist/assets/{image-LLOjkMHF.js → image-Dqe2X2tW.js} +1 -1
- package/src/ui/dist/assets/{index-Dxa2eYMY.js → index-DVsMKK_y.js} +1 -1
- package/src/ui/dist/assets/{index-C3r2iGrp.js → index-Duvz8Ip0.js} +12 -12
- package/src/ui/dist/assets/{index-CLQauncb.js → index-Nt9hS4ck.js} +470 -165
- package/src/ui/dist/assets/{index-hOUOWbW2.js → index-RDlNXXx1.js} +2 -2
- package/src/ui/dist/assets/{monaco-BGGAEii3.js → monaco-DIXge1CP.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DlEr1_y5.js → pdf-effect-queue-BBTTQaO-.js} +1 -1
- package/src/ui/dist/assets/{popover-CWJbJuYY.js → popover-BWlolyxo.js} +1 -1
- package/src/ui/dist/assets/{project-sync-CRJiucYO.js → project-sync-BM5PkFH4.js} +1 -1
- package/src/ui/dist/assets/{select-CoHB7pvH.js → select-D4dAtrA8.js} +2 -2
- package/src/ui/dist/assets/{sigma-D5aJWR8J.js → sigma-CKbE5jJT.js} +1 -1
- package/src/ui/dist/assets/{square-check-big-DUK_mnkS.js → square-check-big-CZNGMgiB.js} +1 -1
- package/src/ui/dist/assets/{trash-ChU3SEE3.js → trash-DaB37xAz.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-BrJBV3tY.js → useCliAccess-C2OmAcWe.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-C2OQaVWc.js → useFileDiffOverlay-Dowd1Ij4.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-C7Qqh-om.js → wrap-text-BGjAhAUq.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-rtX0FKya.js → zoom-out-dMZQMXzc.js} +1 -1
- package/src/ui/dist/index.html +1 -1
- 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
|
|
843
|
-
"-
|
|
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
|
-
"-
|
|
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
|
-
"-
|
|
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
|
-
"-
|
|
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
|
-
"-
|
|
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
|
|
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
|
-
|
|
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
|
|
361
|
+
candidates.append(path.resolve())
|
|
357
362
|
except OSError:
|
|
358
363
|
return None
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
except ValueError:
|
|
364
|
+
|
|
365
|
+
if not candidates:
|
|
362
366
|
return None
|
|
363
|
-
|
|
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"
|
|
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"
|
|
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(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1466
|
-
|
|
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 =
|
|
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():
|