@researai/deepscientist 1.5.17 → 1.6.0

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 (894) hide show
  1. package/AGENTS.md +309 -130
  2. package/AISB/catalog/aisb.b1.agentic_coding.yaml +244 -0
  3. package/AISB/catalog/aisb.b10.climate_earth.yaml +235 -0
  4. package/AISB/catalog/aisb.b11.model_efficiency.yaml +231 -0
  5. package/AISB/catalog/aisb.b12.embodied_ai.yaml +238 -0
  6. package/AISB/catalog/aisb.b2.agent_systems.yaml +229 -0
  7. package/AISB/catalog/aisb.b3.self_evolving_rl.yaml +237 -0
  8. package/AISB/catalog/aisb.b4.lm_reasoning.yaml +240 -0
  9. package/AISB/catalog/aisb.b5.math_proof.yaml +235 -0
  10. package/AISB/catalog/aisb.b6.research_process.yaml +243 -0
  11. package/AISB/catalog/aisb.b7.multimodal_fusion.yaml +232 -0
  12. package/AISB/catalog/aisb.b8.lifesci_drug.yaml +275 -0
  13. package/AISB/catalog/aisb.b9.material_science.yaml +237 -0
  14. package/AISB/catalog/aisb.t3.001_savvy.yaml +159 -0
  15. package/AISB/catalog/aisb.t3.001_savvy.zh.yaml +121 -0
  16. package/AISB/catalog/aisb.t3.002_pinet.yaml +189 -0
  17. package/AISB/catalog/aisb.t3.002_pinet.zh.yaml +130 -0
  18. package/AISB/catalog/aisb.t3.004_decentralattn.yaml +184 -0
  19. package/AISB/catalog/aisb.t3.004_decentralattn.zh.yaml +153 -0
  20. package/AISB/catalog/aisb.t3.005_tsae.yaml +193 -0
  21. package/AISB/catalog/aisb.t3.005_tsae.zh.yaml +139 -0
  22. package/AISB/catalog/aisb.t3.006_physense.yaml +194 -0
  23. package/AISB/catalog/aisb.t3.006_physense.zh.yaml +118 -0
  24. package/AISB/catalog/aisb.t3.007_reasoningiqa.yaml +169 -0
  25. package/AISB/catalog/aisb.t3.007_reasoningiqa.zh.yaml +133 -0
  26. package/AISB/catalog/aisb.t3.008_meanflows.yaml +188 -0
  27. package/AISB/catalog/aisb.t3.008_meanflows.zh.yaml +140 -0
  28. package/AISB/catalog/aisb.t3.009_scoremissing.yaml +179 -0
  29. package/AISB/catalog/aisb.t3.009_scoremissing.zh.yaml +119 -0
  30. package/AISB/catalog/aisb.t3.010_suitabilityfilter.yaml +221 -0
  31. package/AISB/catalog/aisb.t3.010_suitabilityfilter.zh.yaml +141 -0
  32. package/AISB/catalog/aisb.t3.011_osd.yaml +206 -0
  33. package/AISB/catalog/aisb.t3.011_osd.zh.yaml +163 -0
  34. package/AISB/catalog/aisb.t3.012_efficientqat.yaml +206 -0
  35. package/AISB/catalog/aisb.t3.012_efficientqat.zh.yaml +159 -0
  36. package/AISB/catalog/aisb.t3.013_appl.yaml +152 -0
  37. package/AISB/catalog/aisb.t3.013_appl.zh.yaml +126 -0
  38. package/AISB/catalog/aisb.t3.014_piguard.yaml +207 -0
  39. package/AISB/catalog/aisb.t3.014_piguard.zh.yaml +164 -0
  40. package/AISB/catalog/aisb.t3.015_frspec.yaml +209 -0
  41. package/AISB/catalog/aisb.t3.015_frspec.zh.yaml +163 -0
  42. package/AISB/catalog/aisb.t3.016_mathfusion.yaml +166 -0
  43. package/AISB/catalog/aisb.t3.016_mathfusion.zh.yaml +145 -0
  44. package/AISB/catalog/aisb.t3.017_multimodalglp.yaml +171 -0
  45. package/AISB/catalog/aisb.t3.017_multimodalglp.zh.yaml +122 -0
  46. package/AISB/catalog/aisb.t3.018_cotsynth.yaml +206 -0
  47. package/AISB/catalog/aisb.t3.018_cotsynth.zh.yaml +162 -0
  48. package/AISB/catalog/aisb.t3.019_dyscaleut.yaml +211 -0
  49. package/AISB/catalog/aisb.t3.019_dyscaleut.zh.yaml +148 -0
  50. package/AISB/catalog/aisb.t3.020_aristotle.yaml +173 -0
  51. package/AISB/catalog/aisb.t3.020_aristotle.zh.yaml +119 -0
  52. package/AISB/catalog/aisb.t3.021_tokenrecycling.yaml +160 -0
  53. package/AISB/catalog/aisb.t3.021_tokenrecycling.zh.yaml +129 -0
  54. package/AISB/catalog/aisb.t3.022_chainofreasoning.yaml +204 -0
  55. package/AISB/catalog/aisb.t3.022_chainofreasoning.zh.yaml +161 -0
  56. package/AISB/catalog/aisb.t3.023_guidedembed.yaml +211 -0
  57. package/AISB/catalog/aisb.t3.023_guidedembed.zh.yaml +189 -0
  58. package/AISB/catalog/aisb.t3.024_outputcentric.yaml +148 -0
  59. package/AISB/catalog/aisb.t3.024_outputcentric.zh.yaml +131 -0
  60. package/AISB/catalog/aisb.t3.025_deeper.yaml +143 -0
  61. package/AISB/catalog/aisb.t3.025_deeper.zh.yaml +116 -0
  62. package/AISB/catalog/aisb.t3.026_gartkg.yaml +195 -0
  63. package/AISB/catalog/aisb.t3.026_gartkg.zh.yaml +127 -0
  64. package/AISB/catalog/aisb.t3.027_citeeval.yaml +182 -0
  65. package/AISB/catalog/aisb.t3.027_citeeval.zh.yaml +135 -0
  66. package/AISB/catalog/aisb.t3.028_sbam.yaml +206 -0
  67. package/AISB/catalog/aisb.t3.028_sbam.zh.yaml +166 -0
  68. package/AISB/catalog/aisb.t3.029_cdqgeoembed.yaml +224 -0
  69. package/AISB/catalog/aisb.t3.029_cdqgeoembed.zh.yaml +142 -0
  70. package/AISB/catalog/aisb.t3.030_processrm.yaml +211 -0
  71. package/AISB/catalog/aisb.t3.030_processrm.zh.yaml +166 -0
  72. package/AISB/catalog/aisb.t3.031_circuitstability.yaml +172 -0
  73. package/AISB/catalog/aisb.t3.031_circuitstability.zh.yaml +134 -0
  74. package/AISB/catalog/aisb.t3.032_ptsolver.yaml +169 -0
  75. package/AISB/catalog/aisb.t3.032_ptsolver.zh.yaml +135 -0
  76. package/AISB/catalog/aisb.t3.033_gcse.yaml +144 -0
  77. package/AISB/catalog/aisb.t3.033_gcse.zh.yaml +126 -0
  78. package/AISB/catalog/aisb.t3.034_ensemblewm.yaml +183 -0
  79. package/AISB/catalog/aisb.t3.034_ensemblewm.zh.yaml +146 -0
  80. package/AISB/catalog/aisb.t3.035_moralvalueswa.yaml +207 -0
  81. package/AISB/catalog/aisb.t3.035_moralvalueswa.zh.yaml +165 -0
  82. package/AISB/catalog/aisb.t3.036_weakstrongpref.yaml +210 -0
  83. package/AISB/catalog/aisb.t3.036_weakstrongpref.zh.yaml +194 -0
  84. package/AISB/catalog/aisb.t3.037_dementiamask.yaml +172 -0
  85. package/AISB/catalog/aisb.t3.037_dementiamask.zh.yaml +132 -0
  86. package/AISB/catalog/aisb.t3.038_tinysam.yaml +284 -0
  87. package/AISB/catalog/aisb.t3.038_tinysam.zh.yaml +240 -0
  88. package/AISB/catalog/aisb.t3.039_calf.yaml +224 -0
  89. package/AISB/catalog/aisb.t3.039_calf.zh.yaml +194 -0
  90. package/AISB/catalog/aisb.t3.040_graniteguardian.yaml +199 -0
  91. package/AISB/catalog/aisb.t3.040_graniteguardian.zh.yaml +174 -0
  92. package/AISB/catalog/aisb.t3.041_amdm.yaml +149 -0
  93. package/AISB/catalog/aisb.t3.041_amdm.zh.yaml +137 -0
  94. package/AISB/catalog/aisb.t3.042_xpatch.yaml +216 -0
  95. package/AISB/catalog/aisb.t3.042_xpatch.zh.yaml +182 -0
  96. package/AISB/catalog/aisb.t3.043_vhm.yaml +268 -0
  97. package/AISB/catalog/aisb.t3.043_vhm.zh.yaml +193 -0
  98. package/AISB/catalog/aisb.t3.044_rgvi.yaml +224 -0
  99. package/AISB/catalog/aisb.t3.044_rgvi.zh.yaml +176 -0
  100. package/AISB/catalog/aisb.t3.045_pslstm.yaml +203 -0
  101. package/AISB/catalog/aisb.t3.045_pslstm.zh.yaml +179 -0
  102. package/AISB/catalog/aisb.t3.046_nonstatts.yaml +208 -0
  103. package/AISB/catalog/aisb.t3.046_nonstatts.zh.yaml +194 -0
  104. package/AISB/catalog/aisb.t3.047_timepfn.yaml +156 -0
  105. package/AISB/catalog/aisb.t3.047_timepfn.zh.yaml +124 -0
  106. package/AISB/catalog/aisb.t3.048_proxyspex.yaml +148 -0
  107. package/AISB/catalog/aisb.t3.048_proxyspex.zh.yaml +125 -0
  108. package/AISB/catalog/aisb.t3.049_hogwildinference.yaml +183 -0
  109. package/AISB/catalog/aisb.t3.049_hogwildinference.zh.yaml +138 -0
  110. package/AISB/catalog/aisb.t3.050_causalpfn.yaml +214 -0
  111. package/AISB/catalog/aisb.t3.050_causalpfn.zh.yaml +190 -0
  112. package/AISB/catalog/aisb.t3.051_flashtp.yaml +169 -0
  113. package/AISB/catalog/aisb.t3.051_flashtp.zh.yaml +124 -0
  114. package/AISB/catalog/aisb.t3.052_nsdiff.yaml +155 -0
  115. package/AISB/catalog/aisb.t3.052_nsdiff.zh.yaml +138 -0
  116. package/AISB/catalog/aisb.t3.053_k2vae.yaml +158 -0
  117. package/AISB/catalog/aisb.t3.053_k2vae.zh.yaml +132 -0
  118. package/AISB/catalog/aisb.t3.054_timebase.yaml +178 -0
  119. package/AISB/catalog/aisb.t3.054_timebase.zh.yaml +158 -0
  120. package/AISB/catalog/aisb.t3.055_csbrain.yaml +238 -0
  121. package/AISB/catalog/aisb.t3.055_csbrain.zh.yaml +184 -0
  122. package/AISB/catalog/aisb.t3.056_infosam.yaml +224 -0
  123. package/AISB/catalog/aisb.t3.056_infosam.zh.yaml +189 -0
  124. package/AISB/catalog/aisb.t3.057_mdreid.yaml +129 -0
  125. package/AISB/catalog/aisb.t3.057_mdreid.zh.yaml +117 -0
  126. package/AISB/catalog/aisb.t3.058_mindglitch.yaml +171 -0
  127. package/AISB/catalog/aisb.t3.058_mindglitch.zh.yaml +145 -0
  128. package/AISB/catalog/aisb.t3.059_selfsupervised.yaml +154 -0
  129. package/AISB/catalog/aisb.t3.059_selfsupervised.zh.yaml +125 -0
  130. package/AISB/catalog/aisb.t3.060_iaggad.yaml +121 -0
  131. package/AISB/catalog/aisb.t3.060_iaggad.zh.yaml +100 -0
  132. package/AISB/catalog/aisb.t3.061_hsgkn.yaml +136 -0
  133. package/AISB/catalog/aisb.t3.061_hsgkn.zh.yaml +113 -0
  134. package/AISB/catalog/aisb.t3.062_visionts.yaml +237 -0
  135. package/AISB/catalog/aisb.t3.062_visionts.zh.yaml +216 -0
  136. package/AISB/catalog/aisb.t3.063_tsrag.yaml +162 -0
  137. package/AISB/catalog/aisb.t3.063_tsrag.zh.yaml +138 -0
  138. package/AISB/catalog/aisb.t3.064_pir.yaml +221 -0
  139. package/AISB/catalog/aisb.t3.064_pir.zh.yaml +197 -0
  140. package/AISB/catalog/aisb.t3.065_proteinbinding.yaml +234 -0
  141. package/AISB/catalog/aisb.t3.065_proteinbinding.zh.yaml +167 -0
  142. package/AISB/catalog/aisb.t3.066_tropicalattention.yaml +267 -0
  143. package/AISB/catalog/aisb.t3.066_tropicalattention.zh.yaml +229 -0
  144. package/AISB/catalog/aisb.t3.067_kanad.yaml +193 -0
  145. package/AISB/catalog/aisb.t3.067_kanad.zh.yaml +167 -0
  146. package/AISB/catalog/aisb.t3.068_sempo.yaml +187 -0
  147. package/AISB/catalog/aisb.t3.068_sempo.zh.yaml +148 -0
  148. package/AISB/catalog/aisb.t3.069_treehfd.yaml +129 -0
  149. package/AISB/catalog/aisb.t3.069_treehfd.zh.yaml +111 -0
  150. package/AISB/catalog/aisb.t3.070_certifiedunlearning.yaml +224 -0
  151. package/AISB/catalog/aisb.t3.070_certifiedunlearning.zh.yaml +171 -0
  152. package/AISB/catalog/aisb.t3.071_neuralmjd.yaml +142 -0
  153. package/AISB/catalog/aisb.t3.071_neuralmjd.zh.yaml +120 -0
  154. package/AISB/catalog/aisb.t3.072_fedgmt.yaml +181 -0
  155. package/AISB/catalog/aisb.t3.072_fedgmt.zh.yaml +158 -0
  156. package/AISB/catalog/aisb.t3.073_rld.yaml +161 -0
  157. package/AISB/catalog/aisb.t3.073_rld.zh.yaml +129 -0
  158. package/AISB/catalog/aisb.t3.074_lsvi.yaml +163 -0
  159. package/AISB/catalog/aisb.t3.074_lsvi.zh.yaml +129 -0
  160. package/AISB/catalog/aisb.t3.075_treeslicedentropy.yaml +201 -0
  161. package/AISB/catalog/aisb.t3.075_treeslicedentropy.zh.yaml +148 -0
  162. package/AISB/catalog/aisb.t3.076_aanet.yaml +169 -0
  163. package/AISB/catalog/aisb.t3.076_aanet.zh.yaml +129 -0
  164. package/AISB/catalog/aisb.t3.077_cmnn.yaml +199 -0
  165. package/AISB/catalog/aisb.t3.077_cmnn.zh.yaml +165 -0
  166. package/AISB/catalog/aisb.t3.078_conformalanomaly.yaml +146 -0
  167. package/AISB/catalog/aisb.t3.078_conformalanomaly.zh.yaml +117 -0
  168. package/AISB/catalog/aisb.t3.079_dpfkmeans.yaml +131 -0
  169. package/AISB/catalog/aisb.t3.079_dpfkmeans.zh.yaml +104 -0
  170. package/AISB/catalog/aisb.t3.080_latentscorereweight.yaml +169 -0
  171. package/AISB/catalog/aisb.t3.080_latentscorereweight.zh.yaml +123 -0
  172. package/AISB/catalog/aisb.t3.081_qmamba.yaml +150 -0
  173. package/AISB/catalog/aisb.t3.081_qmamba.zh.yaml +117 -0
  174. package/AISB/catalog/aisb.t3.082_onlinellmrouting.yaml +160 -0
  175. package/AISB/catalog/aisb.t3.082_onlinellmrouting.zh.yaml +133 -0
  176. package/AISB/catalog/aisb.t3.083_starformer.yaml +178 -0
  177. package/AISB/catalog/aisb.t3.083_starformer.zh.yaml +140 -0
  178. package/AISB/catalog/aisb.t3.084_ift.yaml +139 -0
  179. package/AISB/catalog/aisb.t3.084_ift.zh.yaml +111 -0
  180. package/AISB/catalog/aisb.t3.085_neuralsurv.yaml +183 -0
  181. package/AISB/catalog/aisb.t3.085_neuralsurv.zh.yaml +143 -0
  182. package/AISB/catalog/aisb.t3.086_stella.yaml +197 -0
  183. package/AISB/catalog/aisb.t3.086_stella.zh.yaml +142 -0
  184. package/AISB/catalog/aisb.t3.087_moses.yaml +167 -0
  185. package/AISB/catalog/aisb.t3.087_moses.zh.yaml +132 -0
  186. package/AISB/catalog/aisb.t3.088_channelnorm.yaml +140 -0
  187. package/AISB/catalog/aisb.t3.088_channelnorm.zh.yaml +109 -0
  188. package/AISB/catalog/aisb.t3.089_causalvelocity.yaml +730 -0
  189. package/AISB/catalog/aisb.t3.089_causalvelocity.zh.yaml +668 -0
  190. package/AISB/catalog/aisb.t3.090_rstib.yaml +144 -0
  191. package/AISB/catalog/aisb.t3.090_rstib.zh.yaml +109 -0
  192. package/AISB/catalog/aisb.t3.091_timeawarecausal.yaml +132 -0
  193. package/AISB/catalog/aisb.t3.091_timeawarecausal.zh.yaml +107 -0
  194. package/AISB/catalog/aisb.t3.092_kmeanslocalopt.yaml +138 -0
  195. package/AISB/catalog/aisb.t3.092_kmeanslocalopt.zh.yaml +110 -0
  196. package/AISB/catalog/aisb.t3.093_fedwmsam.yaml +134 -0
  197. package/AISB/catalog/aisb.t3.093_fedwmsam.zh.yaml +106 -0
  198. package/AISB/catalog/aisb.t3.094_boundre.yaml +147 -0
  199. package/AISB/catalog/aisb.t3.094_boundre.zh.yaml +114 -0
  200. package/AISB/catalog/aisb.t3.095_fastfeaturecp.yaml +153 -0
  201. package/AISB/catalog/aisb.t3.095_fastfeaturecp.zh.yaml +118 -0
  202. package/AISB/catalog/aisb.t3.096_m3svm.yaml +189 -0
  203. package/AISB/catalog/aisb.t3.096_m3svm.zh.yaml +149 -0
  204. package/AISB/catalog/aisb.t3.097_wassersteintl.yaml +212 -0
  205. package/AISB/catalog/aisb.t3.097_wassersteintl.zh.yaml +169 -0
  206. package/AISB/catalog/aisb.t3.098_xmahalanobis.yaml +171 -0
  207. package/AISB/catalog/aisb.t3.098_xmahalanobis.zh.yaml +127 -0
  208. package/AISB/catalog/aisb.t3.099_ollalanding.yaml +248 -0
  209. package/AISB/catalog/aisb.t3.099_ollalanding.zh.yaml +182 -0
  210. package/AISB/catalog/aisb.t3.100_invmissingdata.yaml +179 -0
  211. package/AISB/catalog/aisb.t3.100_invmissingdata.zh.yaml +150 -0
  212. package/AISB/catalog/aisb.t3.101_acia.yaml +164 -0
  213. package/AISB/catalog/aisb.t3.101_acia.zh.yaml +109 -0
  214. package/AISB/catalog/aisb.t3.102_stochasticff.yaml +178 -0
  215. package/AISB/catalog/aisb.t3.102_stochasticff.zh.yaml +130 -0
  216. package/AISB/catalog/aisb.t3.103_qdcp.yaml +150 -0
  217. package/AISB/catalog/aisb.t3.103_qdcp.zh.yaml +116 -0
  218. package/AISB/catalog/aisb.t3.104_balancedactiveinf.yaml +137 -0
  219. package/AISB/catalog/aisb.t3.104_balancedactiveinf.zh.yaml +104 -0
  220. package/AISB/catalog/aisb.t3.105_binaryclasseval.yaml +161 -0
  221. package/AISB/catalog/aisb.t3.105_binaryclasseval.zh.yaml +130 -0
  222. package/AISB/image/001_aisb.t3.001_savvy.jpg +0 -0
  223. package/AISB/image/002_aisb.t3.002_pinet.jpg +0 -0
  224. package/AISB/image/003_aisb.t3.003_dmsqd.jpg +0 -0
  225. package/AISB/image/004_aisb.t3.004_decentralattn.jpg +0 -0
  226. package/AISB/image/005_aisb.t3.005_tsae.jpg +0 -0
  227. package/AISB/image/006_aisb.t3.006_physense.jpg +0 -0
  228. package/AISB/image/007_aisb.t3.007_reasoningiqa.jpg +0 -0
  229. package/AISB/image/008_aisb.t3.008_meanflows.jpg +0 -0
  230. package/AISB/image/009_aisb.t3.009_scoremissing.jpg +0 -0
  231. package/AISB/image/010_aisb.t3.010_suitabilityfilter.jpg +0 -0
  232. package/AISB/image/011_aisb.t3.011_osd.jpg +0 -0
  233. package/AISB/image/012_aisb.t3.012_efficientqat.jpg +0 -0
  234. package/AISB/image/013_aisb.t3.013_appl.jpg +0 -0
  235. package/AISB/image/014_aisb.t3.014_piguard.jpg +0 -0
  236. package/AISB/image/015_aisb.t3.015_frspec.jpg +0 -0
  237. package/AISB/image/016_aisb.t3.016_mathfusion.jpg +0 -0
  238. package/AISB/image/017_aisb.t3.017_multimodalglp.jpg +0 -0
  239. package/AISB/image/018_aisb.t3.018_cotsynth.jpg +0 -0
  240. package/AISB/image/019_aisb.t3.019_dyscaleut.jpg +0 -0
  241. package/AISB/image/020_aisb.t3.020_aristotle.jpg +0 -0
  242. package/AISB/image/021_aisb.t3.021_tokenrecycling.jpg +0 -0
  243. package/AISB/image/022_aisb.t3.022_chainofreasoning.jpg +0 -0
  244. package/AISB/image/023_aisb.t3.023_guidedembed.jpg +0 -0
  245. package/AISB/image/024_aisb.t3.024_outputcentric.jpg +0 -0
  246. package/AISB/image/025_aisb.t3.025_deeper.jpg +0 -0
  247. package/AISB/image/026_aisb.t3.026_gartkg.jpg +0 -0
  248. package/AISB/image/027_aisb.t3.027_citeeval.jpg +0 -0
  249. package/AISB/image/028_aisb.t3.028_sbam.jpg +0 -0
  250. package/AISB/image/029_aisb.t3.029_cdqgeoembed.jpg +0 -0
  251. package/AISB/image/030_aisb.t3.030_processrm.jpg +0 -0
  252. package/AISB/image/031_aisb.t3.031_circuitstability.jpg +0 -0
  253. package/AISB/image/032_aisb.t3.032_ptsolver.jpg +0 -0
  254. package/AISB/image/033_aisb.t3.033_gcse.jpg +0 -0
  255. package/AISB/image/034_aisb.t3.034_ensemblewm.jpg +0 -0
  256. package/AISB/image/035_aisb.t3.035_moralvalueswa.jpg +0 -0
  257. package/AISB/image/036_aisb.t3.036_weakstrongpref.jpg +0 -0
  258. package/AISB/image/037_aisb.t3.037_dementiamask.jpg +0 -0
  259. package/AISB/image/038_aisb.t3.038_tinysam.jpg +0 -0
  260. package/AISB/image/039_aisb.t3.039_calf.jpg +0 -0
  261. package/AISB/image/040_aisb.t3.040_graniteguardian.jpg +0 -0
  262. package/AISB/image/041_aisb.t3.041_amdm.jpg +0 -0
  263. package/AISB/image/042_aisb.t3.042_xpatch.jpg +0 -0
  264. package/AISB/image/043_aisb.t3.043_vhm.jpg +0 -0
  265. package/AISB/image/044_aisb.t3.044_rgvi.jpg +0 -0
  266. package/AISB/image/045_aisb.t3.045_pslstm.jpg +0 -0
  267. package/AISB/image/046_aisb.t3.046_nonstatts.jpg +0 -0
  268. package/AISB/image/047_aisb.t3.047_timepfn.jpg +0 -0
  269. package/AISB/image/048_aisb.t3.048_proxyspex.jpg +0 -0
  270. package/AISB/image/049_aisb.t3.049_hogwildinference.jpg +0 -0
  271. package/AISB/image/050_aisb.t3.050_causalpfn.jpg +0 -0
  272. package/AISB/image/051_aisb.t3.051_flashtp.jpg +0 -0
  273. package/AISB/image/052_aisb.t3.052_nsdiff.jpg +0 -0
  274. package/AISB/image/053_aisb.t3.053_k2vae.jpg +0 -0
  275. package/AISB/image/054_aisb.t3.054_timebase.jpg +0 -0
  276. package/AISB/image/055_aisb.t3.055_csbrain.jpg +0 -0
  277. package/AISB/image/056_aisb.t3.056_infosam.jpg +0 -0
  278. package/AISB/image/057_aisb.t3.057_mdreid.jpg +0 -0
  279. package/AISB/image/058_aisb.t3.058_mindglitch.jpg +0 -0
  280. package/AISB/image/059_aisb.t3.059_selfsupervised.jpg +0 -0
  281. package/AISB/image/060_aisb.t3.060_iaggad.jpg +0 -0
  282. package/AISB/image/061_aisb.t3.061_hsgkn.jpg +0 -0
  283. package/AISB/image/062_aisb.t3.062_visionts.jpg +0 -0
  284. package/AISB/image/063_aisb.t3.063_tsrag.jpg +0 -0
  285. package/AISB/image/064_aisb.t3.064_pir.jpg +0 -0
  286. package/AISB/image/065_aisb.t3.065_proteinbinding.jpg +0 -0
  287. package/AISB/image/066_aisb.t3.066_tropicalattention.jpg +0 -0
  288. package/AISB/image/067_aisb.t3.067_kanad.jpg +0 -0
  289. package/AISB/image/068_aisb.t3.068_sempo.jpg +0 -0
  290. package/AISB/image/069_aisb.t3.069_treehfd.jpg +0 -0
  291. package/AISB/image/070_aisb.t3.070_certifiedunlearning.jpg +0 -0
  292. package/AISB/image/071_aisb.t3.071_neuralmjd.jpg +0 -0
  293. package/AISB/image/072_aisb.t3.072_fedgmt.jpg +0 -0
  294. package/AISB/image/073_aisb.t3.073_rld.jpg +0 -0
  295. package/AISB/image/074_aisb.t3.074_lsvi.jpg +0 -0
  296. package/AISB/image/075_aisb.t3.075_treeslicedentropy.jpg +0 -0
  297. package/AISB/image/076_aisb.t3.076_aanet.jpg +0 -0
  298. package/AISB/image/077_aisb.t3.077_cmnn.jpg +0 -0
  299. package/AISB/image/078_aisb.t3.078_conformalanomaly.jpg +0 -0
  300. package/AISB/image/079_aisb.t3.079_dpfkmeans.jpg +0 -0
  301. package/AISB/image/080_aisb.t3.080_latentscorereweight.jpg +0 -0
  302. package/AISB/image/081_aisb.t3.081_qmamba.jpg +0 -0
  303. package/AISB/image/082_aisb.t3.082_onlinellmrouting.jpg +0 -0
  304. package/AISB/image/083_aisb.t3.083_starformer.jpg +0 -0
  305. package/AISB/image/084_aisb.t3.084_ift.jpg +0 -0
  306. package/AISB/image/085_aisb.t3.085_neuralsurv.jpg +0 -0
  307. package/AISB/image/086_aisb.t3.086_stella.jpg +0 -0
  308. package/AISB/image/087_aisb.t3.087_moses.jpg +0 -0
  309. package/AISB/image/088_aisb.t3.088_channelnorm.jpg +0 -0
  310. package/AISB/image/089_aisb.t3.089_causalvelocity.jpg +0 -0
  311. package/AISB/image/090_aisb.t3.090_rstib.jpg +0 -0
  312. package/AISB/image/091_aisb.t3.091_timeawarecausal.jpg +0 -0
  313. package/AISB/image/092_aisb.t3.092_kmeanslocalopt.jpg +0 -0
  314. package/AISB/image/093_aisb.t3.093_fedwmsam.jpg +0 -0
  315. package/AISB/image/094_aisb.t3.094_boundre.jpg +0 -0
  316. package/AISB/image/095_aisb.t3.095_fastfeaturecp.jpg +0 -0
  317. package/AISB/image/096_aisb.t3.096_m3svm.jpg +0 -0
  318. package/AISB/image/097_aisb.t3.097_wassersteintl.jpg +0 -0
  319. package/AISB/image/098_aisb.t3.098_xmahalanobis.jpg +0 -0
  320. package/AISB/image/099_aisb.t3.099_ollalanding.jpg +0 -0
  321. package/AISB/image/100_aisb.t3.100_invmissingdata.jpg +0 -0
  322. package/AISB/image/101_aisb.t3.101_acia.jpg +0 -0
  323. package/AISB/image/102_aisb.t3.102_stochasticff.jpg +0 -0
  324. package/AISB/image/103_aisb.t3.103_qdcp.jpg +0 -0
  325. package/AISB/image/104_aisb.t3.104_balancedactiveinf.jpg +0 -0
  326. package/AISB/image/105_aisb.t3.105_binaryclasseval.jpg +0 -0
  327. package/AISB/image/106_aisb.t1.reasoning_lite.jpg +0 -0
  328. package/AISB/image/107_aisb.t2.paper_audit.jpg +0 -0
  329. package/AISB/image/108_aisb.t3.multi_gpu_search.jpg +0 -0
  330. package/AISB/image/109_aisb.t3.tdc_admet.jpg +0 -0
  331. package/AISB/image/aisb.b1.agentic_coding.svg +16 -0
  332. package/AISB/image/aisb.b10.climate_earth.svg +16 -0
  333. package/AISB/image/aisb.b11.model_efficiency.svg +16 -0
  334. package/AISB/image/aisb.b12.embodied_ai.svg +16 -0
  335. package/AISB/image/aisb.b2.agent_systems.svg +16 -0
  336. package/AISB/image/aisb.b3.self_evolving_rl.svg +16 -0
  337. package/AISB/image/aisb.b4.lm_reasoning.svg +16 -0
  338. package/AISB/image/aisb.b5.math_proof.svg +16 -0
  339. package/AISB/image/aisb.b6.research_process.svg +16 -0
  340. package/AISB/image/aisb.b7.multimodal_fusion.svg +16 -0
  341. package/AISB/image/aisb.b8.lifesci_drug.svg +16 -0
  342. package/AISB/image/aisb.b9.material_science.svg +16 -0
  343. package/README.md +132 -11
  344. package/bin/ds.js +376 -49
  345. package/docs/en/00_QUICK_START.md +135 -18
  346. package/docs/en/01_SETTINGS_REFERENCE.md +468 -96
  347. package/docs/en/02_START_RESEARCH_GUIDE.md +26 -5
  348. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +14 -3
  349. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +2 -0
  350. package/docs/en/05_TUI_GUIDE.md +171 -2
  351. package/docs/en/07_MEMORY_AND_MCP.md +38 -2
  352. package/docs/en/09_DOCTOR.md +64 -4
  353. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +38 -1
  354. package/docs/en/11_LICENSE_AND_RISK.md +4 -0
  355. package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +15 -0
  356. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +9 -0
  357. package/docs/en/15_CODEX_PROVIDER_SETUP.md +622 -187
  358. package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +14 -0
  359. package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +14 -0
  360. package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +14 -0
  361. package/docs/en/21_LOCAL_MODEL_BACKENDS_GUIDE.md +105 -2
  362. package/docs/en/22_BENCHSTORE_YAML_REFERENCE.md +469 -0
  363. package/docs/en/23_BENCHSTORE_GITHUB_RELEASES_SPEC.md +316 -0
  364. package/docs/en/24_CLAUDE_CODE_PROVIDER_SETUP.md +469 -0
  365. package/docs/en/25_OPENCODE_PROVIDER_SETUP.md +653 -0
  366. package/docs/en/26_CITATION_AND_ATTRIBUTION.md +119 -0
  367. package/docs/en/27_KIMI_CODE_PROVIDER_SETUP.md +180 -0
  368. package/docs/en/28_DISCORD_CONNECTOR_GUIDE.md +61 -0
  369. package/docs/en/29_SLACK_CONNECTOR_GUIDE.md +60 -0
  370. package/docs/en/30_SETTINGS_CONTROL_CENTER_GUIDE.md +371 -0
  371. package/docs/en/{19_LOCAL_BROWSER_AUTH.md → 31_LOCAL_BROWSER_AUTH.md} +1 -1
  372. package/docs/en/32_WINDOWS_WSL2_DEPLOYMENT_GUIDE.md +273 -0
  373. package/docs/en/33_WORKSPACE_EXPLORER_QA.md +121 -0
  374. package/docs/en/91_DEVELOPMENT.md +29 -0
  375. package/docs/en/99_ACKNOWLEDGEMENTS.md +24 -19
  376. package/docs/en/README.md +44 -7
  377. package/docs/images/admin/admin-connectors-health-en.png +0 -0
  378. package/docs/images/admin/admin-controllers-en.png +0 -0
  379. package/docs/images/admin/admin-diagnostics-en.png +0 -0
  380. package/docs/images/admin/admin-errors-en.png +0 -0
  381. package/docs/images/admin/admin-issues-en.png +0 -0
  382. package/docs/images/admin/admin-logs-en.png +0 -0
  383. package/docs/images/admin/admin-quest-detail-en.png +0 -0
  384. package/docs/images/admin/admin-quests-en.png +0 -0
  385. package/docs/images/admin/admin-repairs-en.png +0 -0
  386. package/docs/images/admin/admin-runtime-en.png +0 -0
  387. package/docs/images/admin/admin-search-en.png +0 -0
  388. package/docs/images/admin/admin-stats-en.png +0 -0
  389. package/docs/images/admin/admin-summary-en.png +0 -0
  390. package/docs/images/connectors/connector-discord-en.png +0 -0
  391. package/docs/images/connectors/connector-feishu-en.png +0 -0
  392. package/docs/images/connectors/connector-lingzhu-en.png +0 -0
  393. package/docs/images/connectors/connector-qq-en.png +0 -0
  394. package/docs/images/connectors/connector-slack-en.png +0 -0
  395. package/docs/images/connectors/connector-telegram-en.png +0 -0
  396. package/docs/images/connectors/connector-weixin-en.png +0 -0
  397. package/docs/images/connectors/connector-whatsapp-en.png +0 -0
  398. package/docs/images/settings/settings-baselines-en.png +0 -0
  399. package/docs/images/settings/settings-config-en.png +0 -0
  400. package/docs/images/settings/settings-connectors-overview-en.png +0 -0
  401. package/docs/images/settings/settings-deepxiv-en.png +0 -0
  402. package/docs/images/settings/settings-mcp-servers-en.png +0 -0
  403. package/docs/images/settings/settings-plugins-en.png +0 -0
  404. package/docs/images/settings/settings-runners-en.png +0 -0
  405. package/docs/zh/00_QUICK_START.md +92 -17
  406. package/docs/zh/01_SETTINGS_REFERENCE.md +219 -98
  407. package/docs/zh/02_START_RESEARCH_GUIDE.md +26 -5
  408. package/docs/zh/05_TUI_GUIDE.md +171 -2
  409. package/docs/zh/07_MEMORY_AND_MCP.md +29 -2
  410. package/docs/zh/09_DOCTOR.md +39 -4
  411. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +24 -1
  412. package/docs/zh/11_LICENSE_AND_RISK.md +4 -0
  413. package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +15 -0
  414. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +9 -0
  415. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +550 -188
  416. package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +105 -2
  417. package/docs/zh/22_BENCHSTORE_YAML_REFERENCE.md +459 -0
  418. package/docs/zh/23_BENCHSTORE_GITHUB_RELEASES_SPEC.md +287 -0
  419. package/docs/zh/23_CLAUDE_RUNNER_GUIDE.md +103 -0
  420. package/docs/zh/24_CLAUDE_CODE_PROVIDER_SETUP.md +460 -0
  421. package/docs/zh/25_OPENCODE_PROVIDER_SETUP.md +660 -0
  422. package/docs/zh/26_CITATION_AND_ATTRIBUTION.md +102 -0
  423. package/docs/zh/27_KIMI_CODE_PROVIDER_SETUP.md +51 -0
  424. package/docs/zh/{19_LOCAL_BROWSER_AUTH.md → 31_LOCAL_BROWSER_AUTH.md} +1 -1
  425. package/docs/zh/32_WINDOWS_WSL2_DEPLOYMENT_GUIDE.md +264 -0
  426. package/docs/zh/33_WORKSPACE_EXPLORER_QA.md +127 -0
  427. package/docs/zh/99_ACKNOWLEDGEMENTS.md +23 -19
  428. package/docs/zh/README.md +29 -7
  429. package/install.sh +122 -16
  430. package/package.json +4 -1
  431. package/pyproject.toml +2 -1
  432. package/src/deepscientist/__init__.py +1 -1
  433. package/src/deepscientist/acp/envelope.py +13 -0
  434. package/src/deepscientist/admin/__init__.py +3 -0
  435. package/src/deepscientist/admin/charts.py +681 -0
  436. package/src/deepscientist/admin/logs.py +119 -0
  437. package/src/deepscientist/admin/repairs.py +217 -0
  438. package/src/deepscientist/admin/service.py +1310 -0
  439. package/src/deepscientist/admin/system_info.py +700 -0
  440. package/src/deepscientist/admin/tasks.py +465 -0
  441. package/src/deepscientist/admin/tool_metrics.py +600 -0
  442. package/src/deepscientist/artifact/guidance.py +8 -4
  443. package/src/deepscientist/artifact/schemas.py +115 -0
  444. package/src/deepscientist/artifact/service.py +4268 -260
  445. package/src/deepscientist/bash_exec/monitor.py +30 -3
  446. package/src/deepscientist/bash_exec/service.py +134 -1
  447. package/src/deepscientist/benchstore/__init__.py +4 -0
  448. package/src/deepscientist/benchstore/prompt_builder.py +224 -0
  449. package/src/deepscientist/benchstore/service.py +1716 -0
  450. package/src/deepscientist/channels/weixin_ilink.py +8 -1
  451. package/src/deepscientist/cli.py +92 -17
  452. package/src/deepscientist/codex_cli_compat.py +2 -2
  453. package/src/deepscientist/config/models.py +82 -11
  454. package/src/deepscientist/config/service.py +927 -91
  455. package/src/deepscientist/connector/weixin_support.py +48 -17
  456. package/src/deepscientist/daemon/api/handlers.py +697 -210
  457. package/src/deepscientist/daemon/api/router.py +76 -1
  458. package/src/deepscientist/daemon/app.py +1054 -51
  459. package/src/deepscientist/diagnostics/runner_failures.py +147 -0
  460. package/src/deepscientist/doctor.py +212 -65
  461. package/src/deepscientist/evidence_packets.py +590 -0
  462. package/src/deepscientist/home.py +52 -4
  463. package/src/deepscientist/kimi_cli_compat.py +50 -0
  464. package/src/deepscientist/latex_runtime.py +2 -2
  465. package/src/deepscientist/mcp/context.py +2 -0
  466. package/src/deepscientist/mcp/schemas.py +114 -0
  467. package/src/deepscientist/mcp/server.py +1566 -126
  468. package/src/deepscientist/memory/service.py +203 -16
  469. package/src/deepscientist/process_control.py +8 -1
  470. package/src/deepscientist/prompts/builder.py +836 -92
  471. package/src/deepscientist/quest/__init__.py +2 -2
  472. package/src/deepscientist/quest/layout.py +12 -1
  473. package/src/deepscientist/quest/node_traces.py +10 -0
  474. package/src/deepscientist/quest/service.py +1430 -139
  475. package/src/deepscientist/quest/stage_views.py +1 -1
  476. package/src/deepscientist/runners/__init__.py +18 -0
  477. package/src/deepscientist/runners/base.py +89 -1
  478. package/src/deepscientist/runners/builtins.py +13 -1
  479. package/src/deepscientist/runners/claude.py +391 -0
  480. package/src/deepscientist/runners/codex.py +421 -21
  481. package/src/deepscientist/runners/codex_telemetry.py +127 -0
  482. package/src/deepscientist/runners/kimi.py +334 -0
  483. package/src/deepscientist/runners/metadata.py +68 -0
  484. package/src/deepscientist/runners/opencode.py +414 -0
  485. package/src/deepscientist/runners/runtime_overrides.py +100 -0
  486. package/src/deepscientist/runners/simple_cli.py +538 -0
  487. package/src/deepscientist/runtime_storage.py +303 -0
  488. package/src/deepscientist/shared.py +61 -16
  489. package/src/deepscientist/skills/installer.py +37 -0
  490. package/src/deepscientist/skills/registry.py +2 -0
  491. package/src/deepscientist/tinytex.py +2 -2
  492. package/src/deepscientist/tui.py +10 -3
  493. package/src/prompts/benchstore/system.md +77 -0
  494. package/src/prompts/connectors/qq.md +33 -2
  495. package/src/prompts/connectors/weixin.md +208 -23
  496. package/src/prompts/contracts/admin_ops.md +74 -0
  497. package/src/prompts/contracts/admin_ops_knowledge.md +138 -0
  498. package/src/prompts/contracts/shared_interaction.md +5 -11
  499. package/src/prompts/start_setup/system.md +422 -0
  500. package/src/prompts/system.md +409 -315
  501. package/src/prompts/system_copilot.md +88 -12
  502. package/src/skills/analysis-campaign/SKILL.md +239 -578
  503. package/src/skills/analysis-campaign/references/artifact-flow-examples.md +102 -0
  504. package/src/skills/analysis-campaign/references/boundary-cases.md +98 -0
  505. package/src/skills/analysis-campaign/references/campaign-checklist-template.md +39 -24
  506. package/src/skills/analysis-campaign/references/campaign-design.md +26 -10
  507. package/src/skills/analysis-campaign/references/campaign-plan-template.md +53 -54
  508. package/src/skills/analysis-campaign/references/operational-guidance.md +97 -0
  509. package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +10 -20
  510. package/src/skills/baseline/SKILL.md +183 -461
  511. package/src/skills/baseline/references/artifact-flow-examples.md +106 -0
  512. package/src/skills/baseline/references/artifact-payload-examples.md +1 -1
  513. package/src/skills/baseline/references/baseline-checklist-template.md +27 -35
  514. package/src/skills/baseline/references/baseline-plan-template.md +37 -76
  515. package/src/skills/baseline/references/boundary-cases.md +86 -0
  516. package/src/skills/baseline/references/codebase-audit-checklist.md +2 -6
  517. package/src/skills/baseline/references/comparability-contract.md +7 -12
  518. package/src/skills/baseline/references/operational-guidance.md +56 -0
  519. package/src/skills/baseline/references/route-selection.md +5 -25
  520. package/src/skills/decision/SKILL.md +113 -306
  521. package/src/skills/decision/references/checkpoint-memory-template.md +47 -0
  522. package/src/skills/decision/references/operational-guidance.md +94 -0
  523. package/src/skills/decision/references/research-route-criteria.md +7 -8
  524. package/src/skills/decision/references/strategic-decision-template.md +13 -26
  525. package/src/skills/experiment/SKILL.md +132 -670
  526. package/src/skills/experiment/references/execution-playbook.md +374 -0
  527. package/src/skills/experiment/references/main-experiment-checklist-template.md +26 -2
  528. package/src/skills/experiment/references/main-experiment-plan-template.md +28 -17
  529. package/src/skills/experiment/references/operational-guidance.md +108 -0
  530. package/src/skills/finalize/SKILL.md +62 -0
  531. package/src/skills/finalize/references/checkpoint-memory-template.md +49 -0
  532. package/src/skills/finalize/references/resume-packet-template.md +7 -0
  533. package/src/skills/idea/SKILL.md +228 -15
  534. package/src/skills/idea/references/controlled-brainstorming-playbook.md +78 -0
  535. package/src/skills/idea/references/current-board-packet-template.md +61 -0
  536. package/src/skills/idea/references/high-value-idea-sourcing.md +119 -0
  537. package/src/skills/idea/references/idea-generation-playbook.md +21 -0
  538. package/src/skills/idea/references/idea-thinking-flow.md +6 -0
  539. package/src/skills/idea/references/literature-survey-template.md +3 -0
  540. package/src/skills/idea/references/objective-contract-template.md +54 -0
  541. package/src/skills/idea/references/outline-seeding-example.md +56 -0
  542. package/src/skills/idea/references/pre-idea-draft-template.md +105 -0
  543. package/src/skills/idea/references/related-work-playbook.md +75 -2
  544. package/src/skills/idea/references/research-history-playbook.md +114 -0
  545. package/src/skills/idea/references/selection-gate.md +58 -6
  546. package/src/skills/intake-audit/SKILL.md +43 -2
  547. package/src/skills/intake-audit/references/state-audit-template.md +10 -0
  548. package/src/skills/nature-data/SKILL.md +128 -0
  549. package/src/skills/nature-data/UPSTREAM_LICENSE.txt +21 -0
  550. package/src/skills/nature-data/agents/openai.yaml +4 -0
  551. package/src/skills/nature-data/references/chinese-author-alignment.md +84 -0
  552. package/src/skills/nature-data/references/fair-metadata-checklist.md +105 -0
  553. package/src/skills/nature-data/references/policy-principles.md +103 -0
  554. package/src/skills/nature-data/references/repository-and-identifiers.md +96 -0
  555. package/src/skills/nature-data/references/source-basis.md +54 -0
  556. package/src/skills/nature-data/references/statement-patterns.md +153 -0
  557. package/src/skills/nature-figure/SKILL.md +197 -0
  558. package/src/skills/nature-figure/UPSTREAM_LICENSE.txt +21 -0
  559. package/src/skills/nature-figure/agents/openai.yaml +4 -0
  560. package/src/skills/nature-figure/evals/evals.json +37 -0
  561. package/src/skills/nature-figure/references/api.md +428 -0
  562. package/src/skills/nature-figure/references/backend-selection.md +100 -0
  563. package/src/skills/nature-figure/references/chart-types.md +281 -0
  564. package/src/skills/nature-figure/references/common-patterns.md +349 -0
  565. package/src/skills/nature-figure/references/design-theory.md +436 -0
  566. package/src/skills/nature-figure/references/figure-contract.md +93 -0
  567. package/src/skills/nature-figure/references/nature-2026-observations.md +112 -0
  568. package/src/skills/nature-figure/references/qa-contract.md +119 -0
  569. package/src/skills/nature-figure/references/r-template-index.md +66 -0
  570. package/src/skills/nature-figure/references/r-workflow.md +161 -0
  571. package/src/skills/nature-figure/references/tutorials.md +250 -0
  572. package/src/skills/nature-paper2ppt/SKILL.md +507 -0
  573. package/src/skills/nature-paper2ppt/UPSTREAM_LICENSE.txt +21 -0
  574. package/src/skills/nature-paper2ppt/agents/openai.yaml +4 -0
  575. package/src/skills/nature-polishing/SKILL.md +385 -0
  576. package/src/skills/nature-polishing/UPSTREAM_LICENSE.txt +21 -0
  577. package/src/skills/nature-polishing/agents/openai.yaml +4 -0
  578. package/src/skills/nature-polishing/references/phrasebank-playbook.md +162 -0
  579. package/src/skills/nature-polishing/references/section-moves.md +240 -0
  580. package/src/skills/nature-polishing/references/style-guardrails.md +94 -0
  581. package/src/skills/nature-polishing/references/writing-strategy.md +148 -0
  582. package/src/skills/optimize/SKILL.md +177 -1568
  583. package/src/skills/optimize/references/brief-shaping-playbook.md +95 -0
  584. package/src/skills/optimize/references/candidate-board-template.md +13 -0
  585. package/src/skills/optimize/references/candidate-ranking-template.md +51 -0
  586. package/src/skills/optimize/references/codegen-route-playbook.md +50 -0
  587. package/src/skills/optimize/references/debug-response-template.md +29 -0
  588. package/src/skills/optimize/references/frontier-review-template.md +32 -0
  589. package/src/skills/optimize/references/fusion-playbook.md +36 -0
  590. package/src/skills/optimize/references/method-brief-template.md +73 -0
  591. package/src/skills/optimize/references/operational-guidance.md +621 -0
  592. package/src/skills/optimize/references/optimization-memory-template.md +30 -0
  593. package/src/skills/optimize/references/optimize-checklist-template.md +18 -0
  594. package/src/skills/optimize/references/plateau-response-playbook.md +28 -0
  595. package/src/skills/optimize/references/prompt-patterns.md +49 -0
  596. package/src/skills/paper-outline/SKILL.md +227 -0
  597. package/src/skills/paper-outline/references/outline-patterns.md +87 -0
  598. package/src/skills/paper-plot/SKILL.md +79 -0
  599. package/src/skills/paper-plot/agents/openai.yaml +4 -0
  600. package/src/skills/paper-plot/references/bar_grouped_hatch.md +96 -0
  601. package/src/skills/paper-plot/references/bar_paired_delta.md +72 -0
  602. package/src/skills/paper-plot/references/line_confidence_band.md +75 -0
  603. package/src/skills/paper-plot/references/line_loss_with_inset.md +65 -0
  604. package/src/skills/paper-plot/references/line_training_curve.md +44 -0
  605. package/src/skills/paper-plot/references/radar_dual_series.md +59 -0
  606. package/src/skills/paper-plot/references/scatter_broken_axis.md +59 -0
  607. package/src/skills/paper-plot/references/scatter_tsne_cluster.md +72 -0
  608. package/src/skills/paper-plot/scripts/bar_memevolve.py +109 -0
  609. package/src/skills/paper-plot/scripts/bar_spice.py +166 -0
  610. package/src/skills/paper-plot/scripts/line_aime.py +94 -0
  611. package/src/skills/paper-plot/scripts/line_loss_inset.py +157 -0
  612. package/src/skills/paper-plot/scripts/line_selfdistill.py +168 -0
  613. package/src/skills/paper-plot/scripts/radar_dora.py +151 -0
  614. package/src/skills/paper-plot/scripts/scatter_break.py +169 -0
  615. package/src/skills/paper-plot/scripts/scatter_tsne.py +133 -0
  616. package/src/skills/rebuttal/SKILL.md +9 -0
  617. package/src/skills/references/tool-usage-by-stage.md +438 -0
  618. package/src/skills/review/SKILL.md +105 -7
  619. package/src/skills/science/PROVENANCE.md +44 -0
  620. package/src/skills/science/SKILL.md +137 -0
  621. package/src/skills/science/references/artifact-science-tool.md +110 -0
  622. package/src/skills/science/references/claim-type-discipline.md +56 -0
  623. package/src/skills/science/references/domain-index.md +422 -0
  624. package/src/skills/science/references/hpc-via-bash-exec.md +42 -0
  625. package/src/skills/science/references/package-check-playbook.md +64 -0
  626. package/src/skills/science/references/package-index.min.json +3616 -0
  627. package/src/skills/science/references/packages/abinit.md +80 -0
  628. package/src/skills/science/references/packages/acts.md +73 -0
  629. package/src/skills/science/references/packages/aiida-core.md +80 -0
  630. package/src/skills/science/references/packages/alamode.md +80 -0
  631. package/src/skills/science/references/packages/amuse.md +88 -0
  632. package/src/skills/science/references/packages/anndata.md +88 -0
  633. package/src/skills/science/references/packages/arbor.md +80 -0
  634. package/src/skills/science/references/packages/arc.md +73 -0
  635. package/src/skills/science/references/packages/astropy.md +88 -0
  636. package/src/skills/science/references/packages/astroquery.md +88 -0
  637. package/src/skills/science/references/packages/atomate2.md +80 -0
  638. package/src/skills/science/references/packages/atomsmltr.md +73 -0
  639. package/src/skills/science/references/packages/awkward.md +73 -0
  640. package/src/skills/science/references/packages/batman.md +88 -0
  641. package/src/skills/science/references/packages/biopython.md +88 -0
  642. package/src/skills/science/references/packages/bloqade.md +73 -0
  643. package/src/skills/science/references/packages/brian2.md +73 -0
  644. package/src/skills/science/references/packages/bullet3.md +73 -0
  645. package/src/skills/science/references/packages/calculix.md +80 -0
  646. package/src/skills/science/references/packages/cantera.md +73 -0
  647. package/src/skills/science/references/packages/cavity-md-ipi.md +80 -0
  648. package/src/skills/science/references/packages/ccdproc.md +88 -0
  649. package/src/skills/science/references/packages/celerite2.md +88 -0
  650. package/src/skills/science/references/packages/cellrank.md +73 -0
  651. package/src/skills/science/references/packages/cesm.md +80 -0
  652. package/src/skills/science/references/packages/chemicals.md +73 -0
  653. package/src/skills/science/references/packages/chempy.md +73 -0
  654. package/src/skills/science/references/packages/cirq.md +73 -0
  655. package/src/skills/science/references/packages/coffea.md +73 -0
  656. package/src/skills/science/references/packages/cp2k.md +88 -0
  657. package/src/skills/science/references/packages/custodian.md +80 -0
  658. package/src/skills/science/references/packages/dart.md +73 -0
  659. package/src/skills/science/references/packages/datamol.md +88 -0
  660. package/src/skills/science/references/packages/dd4hep.md +73 -0
  661. package/src/skills/science/references/packages/dealii.md +80 -0
  662. package/src/skills/science/references/packages/deepchem.md +88 -0
  663. package/src/skills/science/references/packages/delphes.md +73 -0
  664. package/src/skills/science/references/packages/devito.md +80 -0
  665. package/src/skills/science/references/packages/dftb.md +88 -0
  666. package/src/skills/science/references/packages/dftd4.md +88 -0
  667. package/src/skills/science/references/packages/dftk-jl.md +80 -0
  668. package/src/skills/science/references/packages/dolfinx.md +80 -0
  669. package/src/skills/science/references/packages/drake.md +73 -0
  670. package/src/skills/science/references/packages/dumux.md +73 -0
  671. package/src/skills/science/references/packages/elk.md +80 -0
  672. package/src/skills/science/references/packages/elmerfem.md +80 -0
  673. package/src/skills/science/references/packages/enzo-e.md +88 -0
  674. package/src/skills/science/references/packages/espresso.md +80 -0
  675. package/src/skills/science/references/packages/exoplanet.md +88 -0
  676. package/src/skills/science/references/packages/fairroot.md +73 -0
  677. package/src/skills/science/references/packages/fbpic.md +80 -0
  678. package/src/skills/science/references/packages/fdtdbath-meep.md +80 -0
  679. package/src/skills/science/references/packages/geant4.md +73 -0
  680. package/src/skills/science/references/packages/geosx.md +80 -0
  681. package/src/skills/science/references/packages/gprmax.md +80 -0
  682. package/src/skills/science/references/packages/gromacs.md +80 -0
  683. package/src/skills/science/references/packages/gwaslab.md +73 -0
  684. package/src/skills/science/references/packages/gz-sim.md +73 -0
  685. package/src/skills/science/references/packages/hail.md +88 -0
  686. package/src/skills/science/references/packages/hiphive.md +80 -0
  687. package/src/skills/science/references/packages/hoomd-blue.md +80 -0
  688. package/src/skills/science/references/packages/itensor.md +73 -0
  689. package/src/skills/science/references/packages/itensors-jl.md +73 -0
  690. package/src/skills/science/references/packages/jdftx.md +73 -0
  691. package/src/skills/science/references/packages/jobflow.md +80 -0
  692. package/src/skills/science/references/packages/kadanoffbaym-jl.md +73 -0
  693. package/src/skills/science/references/packages/kite.md +80 -0
  694. package/src/skills/science/references/packages/kratos.md +80 -0
  695. package/src/skills/science/references/packages/kwant.md +73 -0
  696. package/src/skills/science/references/packages/lammps.md +80 -0
  697. package/src/skills/science/references/packages/lightkurve.md +88 -0
  698. package/src/skills/science/references/packages/limix.md +73 -0
  699. package/src/skills/science/references/packages/maxwelllink.md +80 -0
  700. package/src/skills/science/references/packages/mcdc.md +73 -0
  701. package/src/skills/science/references/packages/meep.md +80 -0
  702. package/src/skills/science/references/packages/mfem.md +80 -0
  703. package/src/skills/science/references/packages/mitgcm.md +73 -0
  704. package/src/skills/science/references/packages/modflow6.md +73 -0
  705. package/src/skills/science/references/packages/molecool.md +73 -0
  706. package/src/skills/science/references/packages/mom6.md +73 -0
  707. package/src/skills/science/references/packages/moose.md +80 -0
  708. package/src/skills/science/references/packages/mpas-model.md +73 -0
  709. package/src/skills/science/references/packages/mujoco.md +73 -0
  710. package/src/skills/science/references/packages/mumax3.md +73 -0
  711. package/src/skills/science/references/packages/nekrs.md +80 -0
  712. package/src/skills/science/references/packages/nessi.md +73 -0
  713. package/src/skills/science/references/packages/nest-simulator.md +73 -0
  714. package/src/skills/science/references/packages/netket.md +73 -0
  715. package/src/skills/science/references/packages/neuron.md +73 -0
  716. package/src/skills/science/references/packages/nextflow.md +88 -0
  717. package/src/skills/science/references/packages/nwchem.md +88 -0
  718. package/src/skills/science/references/packages/openbabel.md +88 -0
  719. package/src/skills/science/references/packages/openems.md +80 -0
  720. package/src/skills/science/references/packages/openff-toolkit.md +88 -0
  721. package/src/skills/science/references/packages/openfoam-dev.md +80 -0
  722. package/src/skills/science/references/packages/openmc.md +73 -0
  723. package/src/skills/science/references/packages/openmm.md +80 -0
  724. package/src/skills/science/references/packages/openmoc.md +73 -0
  725. package/src/skills/science/references/packages/openmx.md +80 -0
  726. package/src/skills/science/references/packages/opensees.md +80 -0
  727. package/src/skills/science/references/packages/opensn.md +80 -0
  728. package/src/skills/science/references/packages/opm-simulators.md +73 -0
  729. package/src/skills/science/references/packages/oqupy.md +73 -0
  730. package/src/skills/science/references/packages/packmol.md +80 -0
  731. package/src/skills/science/references/packages/palabos.md +80 -0
  732. package/src/skills/science/references/packages/parflow.md +80 -0
  733. package/src/skills/science/references/packages/pennylane.md +88 -0
  734. package/src/skills/science/references/packages/perceval.md +73 -0
  735. package/src/skills/science/references/packages/phono3py.md +73 -0
  736. package/src/skills/science/references/packages/phonopy.md +73 -0
  737. package/src/skills/science/references/packages/photutils.md +88 -0
  738. package/src/skills/science/references/packages/picongpu.md +80 -0
  739. package/src/skills/science/references/packages/plink-ng.md +88 -0
  740. package/src/skills/science/references/packages/precice.md +73 -0
  741. package/src/skills/science/references/packages/psc.md +80 -0
  742. package/src/skills/science/references/packages/psi4.md +88 -0
  743. package/src/skills/science/references/packages/pybinding.md +73 -0
  744. package/src/skills/science/references/packages/pyfr.md +80 -0
  745. package/src/skills/science/references/packages/pyhf.md +73 -0
  746. package/src/skills/science/references/packages/pyiron_base.md +80 -0
  747. package/src/skills/science/references/packages/pylcp.md +73 -0
  748. package/src/skills/science/references/packages/pylith.md +80 -0
  749. package/src/skills/science/references/packages/pynbody.md +88 -0
  750. package/src/skills/science/references/packages/pysam.md +88 -0
  751. package/src/skills/science/references/packages/pyscf.md +88 -0
  752. package/src/skills/science/references/packages/q-e.md +73 -0
  753. package/src/skills/science/references/packages/qibo.md +73 -0
  754. package/src/skills/science/references/packages/qiskit.md +73 -0
  755. package/src/skills/science/references/packages/quantica-jl.md +73 -0
  756. package/src/skills/science/references/packages/quantumoptics-jl.md +73 -0
  757. package/src/skills/science/references/packages/quimb.md +73 -0
  758. package/src/skills/science/references/packages/qulacs.md +73 -0
  759. package/src/skills/science/references/packages/qutip.md +73 -0
  760. package/src/skills/science/references/packages/rdkit.md +88 -0
  761. package/src/skills/science/references/packages/rmg-py.md +73 -0
  762. package/src/skills/science/references/packages/root.md +73 -0
  763. package/src/skills/science/references/packages/scanpy.md +88 -0
  764. package/src/skills/science/references/packages/scikit-allel.md +88 -0
  765. package/src/skills/science/references/packages/scikit-bio.md +88 -0
  766. package/src/skills/science/references/packages/scqubits.md +73 -0
  767. package/src/skills/science/references/packages/scuff-em.md +80 -0
  768. package/src/skills/science/references/packages/scvi-tools.md +73 -0
  769. package/src/skills/science/references/packages/seissol.md +73 -0
  770. package/src/skills/science/references/packages/sfepy.md +80 -0
  771. package/src/skills/science/references/packages/sisl.md +73 -0
  772. package/src/skills/science/references/packages/smilei.md +80 -0
  773. package/src/skills/science/references/packages/snakemake.md +88 -0
  774. package/src/skills/science/references/packages/specfem3d-globe.md +80 -0
  775. package/src/skills/science/references/packages/specutils.md +88 -0
  776. package/src/skills/science/references/packages/spglib.md +80 -0
  777. package/src/skills/science/references/packages/squidpy.md +88 -0
  778. package/src/skills/science/references/packages/starry.md +88 -0
  779. package/src/skills/science/references/packages/strawberryfields.md +73 -0
  780. package/src/skills/science/references/packages/su2.md +80 -0
  781. package/src/skills/science/references/packages/sunny-jl.md +73 -0
  782. package/src/skills/science/references/packages/sw4.md +73 -0
  783. package/src/skills/science/references/packages/swift.md +88 -0
  784. package/src/skills/science/references/packages/tdnegf.md +73 -0
  785. package/src/skills/science/references/packages/tenpy.md +73 -0
  786. package/src/skills/science/references/packages/thermo.md +73 -0
  787. package/src/skills/science/references/packages/tkwant.md +73 -0
  788. package/src/skills/science/references/packages/tvb-root.md +73 -0
  789. package/src/skills/science/references/packages/uproot5.md +73 -0
  790. package/src/skills/science/references/packages/vampire.md +80 -0
  791. package/src/skills/science/references/packages/wannier_tools.md +73 -0
  792. package/src/skills/science/references/packages/warpx.md +80 -0
  793. package/src/skills/science/references/packages/wrf.md +73 -0
  794. package/src/skills/science/references/packages/xtb.md +88 -0
  795. package/src/skills/science/references/packages/yt.md +73 -0
  796. package/src/skills/science/references/science-task-brief-template.md +71 -0
  797. package/src/skills/scout/SKILL.md +83 -425
  798. package/src/skills/scout/references/literature-scout-template.md +5 -24
  799. package/src/skills/scout/references/operational-guidance.md +191 -0
  800. package/src/skills/scout/references/paper-triage-playbook.md +11 -35
  801. package/src/skills/write/SKILL.md +744 -1246
  802. package/src/skills/write/references/experiments_analysis_patterns.md +129 -0
  803. package/src/skills/write/references/oral_package_patterns.md +252 -0
  804. package/src/skills/write/references/oral_writing_principles.md +291 -0
  805. package/src/skills/write/references/section_rewrite_checklist.md +234 -0
  806. package/src/tui/dist/app/AppContainer.js +1314 -27
  807. package/src/tui/dist/components/Composer.js +26 -1
  808. package/src/tui/dist/components/ConfigScreen.js +2 -1
  809. package/src/tui/dist/components/InputPrompt.js +25 -9
  810. package/src/tui/dist/components/MainContent.js +18 -3
  811. package/src/tui/dist/components/QuestScreen.js +3 -2
  812. package/src/tui/dist/components/UtilityScreen.js +37 -0
  813. package/src/tui/dist/hooks/useSafeInput.js +10 -0
  814. package/src/tui/dist/index.js +13 -1
  815. package/src/tui/dist/layouts/DefaultAppLayout.js +11 -8
  816. package/src/tui/dist/lib/api.js +89 -1
  817. package/src/tui/package.json +1 -1
  818. package/src/ui/dist/assets/{AnalysisPlugin-BCKAfjba.js → AnalysisPlugin-CA94NGmI.js} +1 -1
  819. package/src/ui/dist/assets/CliPlugin-DHBzphZU.js +79 -0
  820. package/src/ui/dist/assets/CodeEditorPlugin-BOFwD2rn.js +2 -0
  821. package/src/ui/dist/assets/{CodeViewerPlugin-CbaFRrUU.js → CodeViewerPlugin-CqDpgjik.js} +4 -4
  822. package/src/ui/dist/assets/{DocViewerPlugin-DAjLVeQD.js → DocViewerPlugin-UDBgt8-4.js} +3 -3
  823. package/src/ui/dist/assets/GitCommitViewerPlugin-BmHtZ0bZ.js +6 -0
  824. package/src/ui/dist/assets/{GitDiffViewerPlugin-CQACjoAA.js → GitDiffViewerPlugin-CAxjNorQ.js} +2 -2
  825. package/src/ui/dist/assets/{GitSnapshotViewer-0r4nLPke.js → GitSnapshotViewer-CweA6VON.js} +2 -2
  826. package/src/ui/dist/assets/{ImageViewerPlugin-nBOmI2v_.js → ImageViewerPlugin-C8wHGvGN.js} +5 -5
  827. package/src/ui/dist/assets/LabPlugin-COyyLUol.js +32 -0
  828. package/src/ui/dist/assets/{LatexPlugin-ZwtV8pIp.js → LatexPlugin-BQjAaA5J.js} +4 -4
  829. package/src/ui/dist/assets/{MarkdownViewerPlugin-DKqVfKyW.js → MarkdownViewerPlugin-Dy1NE2dI.js} +3 -3
  830. package/src/ui/dist/assets/{MarketplacePlugin-BwxStZ9D.js → MarketplacePlugin-DMIZtEJ2.js} +2 -2
  831. package/src/ui/dist/assets/NotebookEditor-CFHMq_Qt.js +91 -0
  832. package/src/ui/dist/assets/{NotebookEditor-DB9N_T9q.js → NotebookEditor-WFyd8Ybt.js} +3 -3
  833. package/src/ui/dist/assets/{PdfLoader-eWBONbQP.js → PdfLoader-CLE5u5TS.js} +3 -3
  834. package/src/ui/dist/assets/{PdfMarkdownPlugin-D22YOZL3.js → PdfMarkdownPlugin-_iNK_H83.js} +1 -1
  835. package/src/ui/dist/assets/PdfViewerPlugin-DgWsbInT.js +22 -0
  836. package/src/ui/dist/assets/SearchPlugin-DrZmn5iw.js +11 -0
  837. package/src/ui/dist/assets/{TextViewerPlugin-C5xqeeUH.js → TextViewerPlugin-D1-T3aC7.js} +4 -4
  838. package/src/ui/dist/assets/branding/runner-claude.svg +107 -0
  839. package/src/ui/dist/assets/branding/runner-codex.svg +10 -0
  840. package/src/ui/dist/assets/branding/runner-kimi.svg +14 -0
  841. package/src/ui/dist/assets/branding/runner-opencode.svg +7 -0
  842. package/src/ui/dist/assets/cli-store-CoZ-x5Ip.js +1 -0
  843. package/src/ui/dist/assets/{code-WlFHE7z_.js → code-DbsmSd3Y.js} +1 -1
  844. package/src/ui/dist/assets/file-diff-panel-DsvyRz47.js +1 -0
  845. package/src/ui/dist/assets/{wrap-text-BC-Hltpd.js → file-jump-queue-DeQBikaw.js} +3 -3
  846. package/src/ui/dist/assets/{file-socket-CfQPKQKj.js → file-socket-DA5XIx88.js} +1 -1
  847. package/src/ui/dist/assets/fonts/ds-fonts.css +50 -4
  848. package/src/ui/dist/assets/images/deepxiv/register-guide.png +0 -0
  849. package/src/ui/dist/assets/index-39vY9LmZ.js +1 -0
  850. package/src/ui/dist/assets/{index-CwNu1aH4.js → index-BsO46tJA.js} +1 -1
  851. package/src/ui/dist/assets/index-CHzJ2xtB.js +3530 -0
  852. package/src/ui/dist/assets/index-DH-zxoZ3.css +33 -0
  853. package/src/ui/dist/assets/{plugin-notebook-HbW2K-1c.js → plugin-notebook-JRhysCqj.js} +2 -2
  854. package/src/ui/dist/assets/{project-sync-C9IdzdZW.js → project-sync-DPmWKmKD.js} +1 -1
  855. package/src/ui/dist/assets/{zoom-out-E_gaeAxL.js → zoom-out-DAukFWen.js} +3 -3
  856. package/src/ui/dist/index.html +3 -3
  857. package/src/skills/analysis-campaign/references/artifact-orchestration.md +0 -58
  858. package/src/skills/baseline/references/memory-playbook.md +0 -40
  859. package/src/skills/baseline/references/publishable-baseline-package.md +0 -30
  860. package/src/skills/write/references/outline-evidence-contract-example.md +0 -107
  861. package/src/skills/write/references/paper-experiment-matrix-template.md +0 -131
  862. package/src/skills/write/references/paper-section-playbook.md +0 -64
  863. package/src/skills/write/references/reviewer-first-writing.md +0 -64
  864. package/src/skills/write/references/revision-checklist.md +0 -70
  865. package/src/skills/write/references/section-contracts.md +0 -82
  866. package/src/skills/write/references/sentence-level-proofing.md +0 -49
  867. package/src/ui/dist/assets/AiManusChatView-Bv-Z8YpU.js +0 -204
  868. package/src/ui/dist/assets/CliPlugin-BCKcpc35.js +0 -109
  869. package/src/ui/dist/assets/CodeEditorPlugin-DbOfSJ8K.js +0 -2
  870. package/src/ui/dist/assets/GitCommitViewerPlugin-CIUqbUDO.js +0 -1
  871. package/src/ui/dist/assets/LabCopilotPanel-BHxOxF4z.js +0 -14
  872. package/src/ui/dist/assets/LabPlugin-BKoZGs95.js +0 -22
  873. package/src/ui/dist/assets/NotebookEditor-BEQhaQbt.js +0 -81
  874. package/src/ui/dist/assets/PdfViewerPlugin-c-RK9DLM.js +0 -17
  875. package/src/ui/dist/assets/SearchPlugin-CxF9ytAx.js +0 -16
  876. package/src/ui/dist/assets/VNCViewer-BoLGLnHz.js +0 -11
  877. package/src/ui/dist/assets/bot-DREQOxzP.js +0 -6
  878. package/src/ui/dist/assets/chevron-up-C9Qpx4DE.js +0 -6
  879. package/src/ui/dist/assets/file-content-BZMz3RYp.js +0 -1
  880. package/src/ui/dist/assets/file-diff-panel-CQhw0jS2.js +0 -1
  881. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +0 -1
  882. package/src/ui/dist/assets/git-commit-horizontal-DxZ8DCZh.js +0 -6
  883. package/src/ui/dist/assets/image-Bgl4VIyx.js +0 -6
  884. package/src/ui/dist/assets/index-BpV6lusQ.css +0 -33
  885. package/src/ui/dist/assets/index-CBNVuWcP.js +0 -2496
  886. package/src/ui/dist/assets/index-DrUnlf6K.js +0 -1
  887. package/src/ui/dist/assets/index-NW-h8VzN.js +0 -1
  888. package/src/ui/dist/assets/pdf-effect-queue-J8OnM0jE.js +0 -6
  889. package/src/ui/dist/assets/popover-CLc0pPP8.js +0 -1
  890. package/src/ui/dist/assets/select-Cs2PmzwL.js +0 -11
  891. package/src/ui/dist/assets/sigma-ClKcHAXm.js +0 -6
  892. package/src/ui/dist/assets/trash-DwpbFr3w.js +0 -11
  893. package/src/ui/dist/assets/useCliAccess-NQ8m0Let.js +0 -1
  894. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +0 -1
@@ -32,6 +32,7 @@ from ..skills import SkillInstaller
32
32
  from ..web_search import extract_web_search_payload
33
33
  from .layout import (
34
34
  QUEST_DIRECTORIES,
35
+ default_active_anchor,
35
36
  gitignore,
36
37
  initial_brief,
37
38
  initial_plan,
@@ -44,21 +45,34 @@ from .stage_views import QuestStageViewBuilder
44
45
 
45
46
  _UNSET = object()
46
47
  _NUMERIC_QUEST_ID_PATTERN = re.compile(r"^\d{1,10}$")
48
+ _SYSTEM_NUMERIC_QUEST_ID_PATTERN = re.compile(r"^(?P<prefix>[SB])-(?P<number>\d{1,10})$", re.IGNORECASE)
47
49
  _MAX_NUMERIC_QUEST_ID_VALUE = 9_999_999_999
48
50
  _NUMERIC_QUEST_ID_PAD_WIDTH = 3
49
51
  _CRASH_AUTO_RESUME_WINDOW = timedelta(hours=24)
50
52
  _JSONL_CACHE_MAX_BYTES = 4 * 1024 * 1024
51
53
  _CODEX_HISTORY_TAIL_LIMIT = 400
52
54
  _JSONL_STREAM_CHUNK_BYTES = 64 * 1024
55
+ _JSONL_LINE_COUNT_CACHE_SUFFIX = ".linecount.json"
53
56
  _EVENTS_OVERSIZED_LINE_BYTES = 8 * 1024 * 1024
54
57
  _OVERSIZED_EVENT_PREFIX_BYTES = 4096
55
58
  _PROJECTION_SCHEMA_VERSION = 1
56
59
  _PROJECTION_BUILD_TOTAL_STEPS = 3
57
60
  _PROJECTION_REFRESH_THROTTLE_SECONDS = 1.0
61
+ _SHARED_MEMORY_DOCUMENT_PREFIX = "sharedmemory::"
58
62
  _EVENT_TYPE_BYTES_RE = re.compile(rb'"(?:type|event_type)"\s*:\s*"([^"]+)"')
59
63
  _EVENT_TOOL_NAME_BYTES_RE = re.compile(rb'"tool_name"\s*:\s*"([^"]+)"')
60
64
  _EVENT_RUN_ID_BYTES_RE = re.compile(rb'"run_id"\s*:\s*"([^"]+)"')
61
65
  CONTINUATION_POLICIES = {"auto", "when_external_progress", "wait_for_user_or_resume", "none"}
66
+ AUTONOMOUS_BLOCKING_WAIT_REASONS = {
67
+ "completion_approval",
68
+ "credential_required",
69
+ "privacy_or_data_export_boundary",
70
+ "large_cost_or_external_paid_api",
71
+ "user_gated_decision_request",
72
+ }
73
+ _CHAT_ATTACHMENT_TEXT_EXTENSIONS = {".txt", ".md", ".markdown", ".mdx", ".json", ".csv", ".log", ".yaml", ".yml"}
74
+ _CHAT_ATTACHMENT_TEXT_MIME_PREFIXES = ("text/",)
75
+ _CHAT_ATTACHMENT_TEXT_MIME_TYPES = {"application/json", "application/x-yaml", "text/csv"}
62
76
 
63
77
 
64
78
  def _oversized_event_placeholder(*, prefix: bytes, line_bytes: int) -> dict[str, Any]:
@@ -101,6 +115,7 @@ def _iter_jsonl_records_safely(
101
115
  prefix = bytearray()
102
116
  current_bytes = 0
103
117
  oversized = False
118
+ cursor = 0
104
119
  while True:
105
120
  chunk = handle.read(_JSONL_STREAM_CHUNK_BYTES)
106
121
  if not chunk:
@@ -114,7 +129,8 @@ def _iter_jsonl_records_safely(
114
129
  if oversized:
115
130
  current_bytes += len(segment)
116
131
  if has_newline:
117
- yield _oversized_event_placeholder(prefix=bytes(prefix), line_bytes=current_bytes)
132
+ cursor += 1
133
+ yield cursor, _oversized_event_placeholder(prefix=bytes(prefix), line_bytes=current_bytes)
118
134
  prefix = bytearray()
119
135
  current_bytes = 0
120
136
  oversized = False
@@ -133,7 +149,8 @@ def _iter_jsonl_records_safely(
133
149
  current_bytes = next_bytes
134
150
  oversized = True
135
151
  if has_newline:
136
- yield _oversized_event_placeholder(prefix=bytes(prefix), line_bytes=current_bytes)
152
+ cursor += 1
153
+ yield cursor, _oversized_event_placeholder(prefix=bytes(prefix), line_bytes=current_bytes)
137
154
  prefix = bytearray()
138
155
  current_bytes = 0
139
156
  oversized = False
@@ -148,28 +165,27 @@ def _iter_jsonl_records_safely(
148
165
  buffer.clear()
149
166
  line_bytes = current_bytes
150
167
  current_bytes = 0
151
- if raw:
152
- try:
153
- payload = json.loads(raw)
154
- except json.JSONDecodeError:
155
- payload = None
156
- if isinstance(payload, dict):
157
- yield payload
168
+ cursor += 1
169
+ payload = _parse_jsonl_record_line_safely(
170
+ raw,
171
+ oversized_line_bytes=oversized_line_bytes,
172
+ )
173
+ yield cursor, payload
158
174
  start = newline_index + 1
159
175
  continue
160
176
  break
161
177
 
162
178
  if oversized:
163
- yield _oversized_event_placeholder(prefix=bytes(prefix), line_bytes=current_bytes)
179
+ cursor += 1
180
+ yield cursor, _oversized_event_placeholder(prefix=bytes(prefix), line_bytes=current_bytes)
164
181
  elif buffer:
165
182
  raw = bytes(buffer).strip()
166
- if raw:
167
- try:
168
- payload = json.loads(raw)
169
- except json.JSONDecodeError:
170
- payload = None
171
- if isinstance(payload, dict):
172
- yield payload
183
+ cursor += 1
184
+ payload = _parse_jsonl_record_line_safely(
185
+ raw,
186
+ oversized_line_bytes=oversized_line_bytes,
187
+ )
188
+ yield cursor, payload
173
189
 
174
190
 
175
191
  def _parse_jsonl_record_line_safely(
@@ -188,11 +204,69 @@ def _parse_jsonl_record_line_safely(
188
204
  )
189
205
  try:
190
206
  payload = json.loads(raw)
191
- except json.JSONDecodeError:
207
+ except (UnicodeDecodeError, json.JSONDecodeError):
192
208
  return None
193
209
  return payload if isinstance(payload, dict) else None
194
210
 
195
211
 
212
+ def _jsonl_line_count_cache_path(path: Path) -> Path:
213
+ return path.with_name(f".{path.name}{_JSONL_LINE_COUNT_CACHE_SUFFIX}")
214
+
215
+
216
+ def _jsonl_path_state(path: Path) -> tuple[int, int, int] | None:
217
+ if not path.exists():
218
+ return None
219
+ stat = path.stat()
220
+ return (
221
+ stat.st_ino,
222
+ getattr(stat, "st_mtime_ns", int(stat.st_mtime * 1_000_000_000)),
223
+ stat.st_size,
224
+ )
225
+
226
+
227
+ def _jsonl_line_count_from_cache(path: Path) -> int | None:
228
+ current_state = _jsonl_path_state(path)
229
+ if current_state is None:
230
+ return 0
231
+
232
+ cache_path = _jsonl_line_count_cache_path(path)
233
+ payload = read_json(cache_path, {})
234
+ if not isinstance(payload, dict):
235
+ payload = {}
236
+ raw_state = payload.get("state")
237
+ raw_total = payload.get("total")
238
+ if not isinstance(raw_state, (list, tuple)) or len(raw_state) != 3:
239
+ return None
240
+ try:
241
+ cached_state = tuple(int(item) for item in raw_state)
242
+ cached_total = int(raw_total)
243
+ except (TypeError, ValueError):
244
+ return None
245
+ if cached_total < 0:
246
+ return None
247
+ if cached_state == current_state:
248
+ return cached_total
249
+
250
+ # If the file only grew through append-only writes, count just the delta.
251
+ if cached_state[0] == current_state[0] and current_state[2] > cached_state[2]:
252
+ appended_count = 0
253
+ for relative_cursor, _payload in _iter_jsonl_records_from_offset_safely(
254
+ path,
255
+ start_offset=cached_state[2],
256
+ ):
257
+ appended_count = relative_cursor
258
+ total = cached_total + appended_count
259
+ write_json(
260
+ cache_path,
261
+ {
262
+ "state": list(current_state),
263
+ "total": total,
264
+ },
265
+ )
266
+ return total
267
+ return None
268
+
269
+
196
270
  def _tail_jsonl_records_safely(
197
271
  path: Path,
198
272
  *,
@@ -225,6 +299,9 @@ def _tail_jsonl_records_safely(
225
299
  def _count_jsonl_lines_fast(path: Path, *, chunk_size: int = 1024 * 1024) -> int:
226
300
  if not path.exists():
227
301
  return 0
302
+ cached_total = _jsonl_line_count_from_cache(path)
303
+ if cached_total is not None:
304
+ return cached_total
228
305
  total = 0
229
306
  last_byte = b""
230
307
  with path.open("rb") as handle:
@@ -235,9 +312,18 @@ def _count_jsonl_lines_fast(path: Path, *, chunk_size: int = 1024 * 1024) -> int
235
312
  total += chunk.count(b"\n")
236
313
  last_byte = chunk[-1:]
237
314
  if total == 0 and last_byte:
238
- return 1
239
- if last_byte not in {b"", b"\n"}:
315
+ total = 1
316
+ elif last_byte not in {b"", b"\n"}:
240
317
  total += 1
318
+ state = _jsonl_path_state(path)
319
+ if state is not None:
320
+ write_json(
321
+ _jsonl_line_count_cache_path(path),
322
+ {
323
+ "state": list(state),
324
+ "total": total,
325
+ },
326
+ )
241
327
  return total
242
328
 
243
329
 
@@ -284,13 +370,12 @@ def _iter_jsonl_records_from_offset_safely(
284
370
  return
285
371
  with path.open("rb") as handle:
286
372
  handle.seek(max(int(start_offset or 0), 0))
287
- for raw_line in handle:
373
+ for relative_cursor, raw_line in enumerate(handle, start=1):
288
374
  payload = _parse_jsonl_record_line_safely(
289
375
  raw_line,
290
376
  oversized_line_bytes=oversized_line_bytes,
291
377
  )
292
- if isinstance(payload, dict):
293
- yield payload
378
+ yield relative_cursor, payload
294
379
 
295
380
 
296
381
  class QuestService:
@@ -319,6 +404,33 @@ class QuestService:
319
404
  self._quest_projection_refresh_lock = threading.Lock()
320
405
  self._quest_projection_refresh_at: dict[str, float] = {}
321
406
 
407
+ def _configured_default_runner(self) -> str:
408
+ config = ConfigManager(self.home).load_named("config")
409
+ return self._resolve_enabled_runner_name(config.get("default_runner"))
410
+
411
+ def _resolve_enabled_runner_name(self, *candidates: Any) -> str:
412
+ runners = ConfigManager(self.home).load_runners_config()
413
+ seen: set[str] = set()
414
+ enabled: list[str] = []
415
+ for name, cfg in runners.items():
416
+ normalized = str(name or "").strip().lower()
417
+ if not normalized or normalized in seen:
418
+ continue
419
+ seen.add(normalized)
420
+ if isinstance(cfg, dict) and cfg.get("enabled") is not False:
421
+ enabled.append(normalized)
422
+
423
+ checked: set[str] = set()
424
+ for raw in [*candidates, "codex"]:
425
+ normalized = str(raw or "").strip().lower()
426
+ if not normalized or normalized in checked:
427
+ continue
428
+ checked.add(normalized)
429
+ cfg = runners.get(normalized)
430
+ if isinstance(cfg, dict) and cfg.get("enabled") is not False:
431
+ return normalized
432
+ return enabled[0] if enabled else "codex"
433
+
322
434
  def _quest_root(self, quest_id: str) -> Path:
323
435
  return self.quests_root / quest_id
324
436
 
@@ -330,31 +442,33 @@ class QuestService:
330
442
 
331
443
  def _normalized_binding_sources(self, sources: list[Any] | None) -> list[str]:
332
444
  local_present = False
333
- external_source: str | None = None
445
+ external_sources: list[str] = []
446
+ external_index: dict[str, int] = {}
334
447
  for raw in sources or []:
335
448
  normalized = self._normalize_binding_source(raw)
336
449
  if not normalized:
337
450
  continue
338
- if normalized == "local:default":
339
- local_present = True
340
- continue
341
451
  parsed = parse_conversation_id(normalized)
342
452
  connector = str((parsed or {}).get("connector") or "").strip().lower()
343
- if connector == "local":
453
+ if normalized == "local:default" or connector == "local":
344
454
  local_present = True
345
455
  continue
346
- external_source = normalized
347
- if external_source:
348
- return ["local:default", external_source]
456
+ identity = conversation_identity_key(normalized)
457
+ existing_index = external_index.get(identity)
458
+ if existing_index is None:
459
+ external_index[identity] = len(external_sources)
460
+ external_sources.append(normalized)
461
+ else:
462
+ external_sources[existing_index] = normalized
349
463
  if local_present:
350
- return ["local:default"]
351
- return ["local:default"]
464
+ return ["local:default", *external_sources]
465
+ return external_sources
352
466
 
353
467
  def _binding_sources_payload(self, quest_root: Path) -> dict[str, list[str]]:
354
468
  bindings_path = quest_root / ".ds" / "bindings.json"
355
- payload = read_json(bindings_path, {"sources": ["local:default"]})
356
- raw_sources = payload.get("sources") if isinstance(payload, dict) else ["local:default"]
357
- sources = self._normalized_binding_sources(raw_sources if isinstance(raw_sources, list) else ["local:default"])
469
+ payload = read_json(bindings_path, {"sources": []})
470
+ raw_sources = payload.get("sources") if isinstance(payload, dict) else []
471
+ sources = self._normalized_binding_sources(raw_sources if isinstance(raw_sources, list) else [])
358
472
  return {"sources": sources}
359
473
 
360
474
  def preferred_locale(self, quest_root: Path | None = None) -> str:
@@ -400,11 +514,21 @@ class QuestService:
400
514
  if not isinstance(payload, dict):
401
515
  payload = {}
402
516
  normalized = dict(payload)
403
- normalized.setdefault("active_anchor", "baseline")
517
+ startup_contract = dict(normalized.get("startup_contract") or {}) if isinstance(normalized.get("startup_contract"), dict) else None
518
+ normalized.setdefault("startup_contract", startup_contract)
404
519
  normalized.setdefault("baseline_gate", "pending")
405
520
  normalized.setdefault("confirmed_baseline_ref", None)
406
521
  normalized.setdefault("requested_baseline_ref", None)
407
- normalized.setdefault("startup_contract", None)
522
+ active_anchor = str(normalized.get("active_anchor") or "").strip()
523
+ if not active_anchor:
524
+ normalized["active_anchor"] = default_active_anchor(startup_contract)
525
+ elif (
526
+ active_anchor == "baseline"
527
+ and str((startup_contract or {}).get("workspace_mode") or "").strip().lower() == "copilot"
528
+ and not isinstance(normalized.get("confirmed_baseline_ref"), dict)
529
+ and not isinstance(normalized.get("requested_baseline_ref"), dict)
530
+ ):
531
+ normalized["active_anchor"] = "scout"
408
532
  return normalized
409
533
 
410
534
  @staticmethod
@@ -882,7 +1006,7 @@ class QuestService:
882
1006
  if not artifacts_root.exists():
883
1007
  continue
884
1008
  for folder in sorted(artifacts_root.iterdir()):
885
- if not folder.is_dir():
1009
+ if not folder.is_dir() or folder.name == "graphs":
886
1010
  continue
887
1011
  for path in sorted(folder.glob("*.json")):
888
1012
  item = self._read_cached_json(path, {})
@@ -1515,16 +1639,18 @@ class QuestService:
1515
1639
  for artifact in recent_artifacts:
1516
1640
  payload = artifact.get("payload") if isinstance(artifact.get("payload"), dict) else {}
1517
1641
  artifact_path = artifact.get("path")
1642
+ artifact_kind = str(payload.get("kind") or artifact.get("kind") or "").strip()
1518
1643
  entries.append(
1519
1644
  {
1520
1645
  "id": f"artifact:{payload.get('artifact_id') or artifact_path}",
1521
1646
  "kind": "artifact",
1522
- "title": str(payload.get("artifact_id") or artifact.get("kind") or "artifact"),
1647
+ "title": str(payload.get("title") or payload.get("artifact_id") or artifact.get("kind") or "artifact"),
1523
1648
  "summary": payload.get("summary") or payload.get("message") or payload.get("reason") or "Artifact updated.",
1524
1649
  "status": payload.get("status"),
1525
1650
  "reason": payload.get("reason"),
1526
1651
  "created_at": payload.get("updated_at") or payload.get("created_at"),
1527
1652
  "paths": list((payload.get("paths") or {}).values()) + ([str(artifact_path)] if artifact_path else []),
1653
+ "stage_key": "science" if artifact_kind.startswith("science.") else payload.get("stage_key"),
1528
1654
  }
1529
1655
  )
1530
1656
  add_file(str(artifact_path) if artifact_path else None, source="artifact")
@@ -1832,6 +1958,37 @@ class QuestService:
1832
1958
  best_root = paper_root
1833
1959
  return best_root
1834
1960
 
1961
+ @staticmethod
1962
+ def _paper_line_state_from_root(paper_root: Path) -> dict[str, Any]:
1963
+ path = paper_root / "paper_line_state.json"
1964
+ payload = read_json(path, {})
1965
+ return payload if isinstance(payload, dict) else {}
1966
+
1967
+ @staticmethod
1968
+ def _filter_paper_evidence_items(
1969
+ items: list[dict[str, Any]],
1970
+ *,
1971
+ selected_outline_ref: str | None = None,
1972
+ paper_line_id: str | None = None,
1973
+ ) -> list[dict[str, Any]]:
1974
+ normalized_outline = str(selected_outline_ref or "").strip() or None
1975
+ normalized_line = str(paper_line_id or "").strip() or None
1976
+ filtered: list[dict[str, Any]] = []
1977
+ for item in items:
1978
+ if not isinstance(item, dict):
1979
+ continue
1980
+ item_outline = str(item.get("selected_outline_ref") or "").strip() or None
1981
+ item_line = str(item.get("paper_line_id") or "").strip() or None
1982
+ if normalized_line:
1983
+ if item_line and item_line != normalized_line:
1984
+ continue
1985
+ if not item_line and normalized_outline and item_outline and item_outline != normalized_outline:
1986
+ continue
1987
+ elif normalized_outline and item_outline and item_outline != normalized_outline:
1988
+ continue
1989
+ filtered.append(dict(item))
1990
+ return filtered
1991
+
1835
1992
  def _outline_record_from_paper_root(self, paper_root: Path) -> dict[str, Any]:
1836
1993
  outline_root = paper_root / "outline"
1837
1994
  manifest_path = outline_root / "manifest.json"
@@ -1893,52 +2050,49 @@ class QuestService:
1893
2050
  return payload if isinstance(payload, dict) else {}
1894
2051
 
1895
2052
  def _paper_evidence_payload(self, quest_root: Path, workspace_root: Path) -> dict[str, Any] | None:
1896
- best_payload: dict[str, Any] | None = None
1897
- best_rank: tuple[str, float] = ("", -1.0)
1898
- for candidate in self._snapshot_workspace_candidates(quest_root, workspace_root):
1899
- paper_root = candidate / "paper"
1900
- ledger_json_path = paper_root / "evidence_ledger.json"
1901
- if not ledger_json_path.exists():
1902
- continue
1903
- payload = read_json(ledger_json_path, {})
1904
- if not isinstance(payload, dict) or not payload:
1905
- continue
1906
- items = [dict(item) for item in (payload.get("items") or []) if isinstance(item, dict)]
1907
- latest = max(
1908
- self._path_mtime(ledger_json_path),
1909
- self._path_mtime(paper_root / "evidence_ledger.md"),
1910
- self._path_mtime(paper_root),
1911
- )
1912
- rank = (str(payload.get("updated_at") or payload.get("created_at") or ""), latest)
1913
- if rank < best_rank:
1914
- continue
1915
- best_rank = rank
1916
- best_payload = {
1917
- "paper_root": str(paper_root),
1918
- "workspace_root": str(paper_root.parent),
1919
- "selected_outline_ref": str(payload.get("selected_outline_ref") or "").strip() or None,
1920
- "item_count": len(items),
1921
- "main_text_ready_count": sum(
1922
- 1
1923
- for item in items
1924
- if str(item.get("paper_role") or "").strip() == "main_text"
1925
- and str(item.get("status") or "").strip().lower() in {"ready", "completed", "analyzed", "written", "recorded", "supported"}
1926
- ),
1927
- "appendix_item_count": sum(
1928
- 1 for item in items if str(item.get("paper_role") or "").strip() == "appendix"
1929
- ),
1930
- "unmapped_item_count": sum(
1931
- 1
1932
- for item in items
1933
- if not str(item.get("section_id") or "").strip() or not str(item.get("paper_role") or "").strip()
1934
- ),
1935
- "items": items[:40],
1936
- "paths": {
1937
- "ledger_json": str(ledger_json_path),
1938
- "ledger_md": str(paper_root / "evidence_ledger.md") if (paper_root / "evidence_ledger.md").exists() else None,
1939
- },
1940
- }
1941
- return best_payload
2053
+ paper_root = self._best_paper_root(quest_root, workspace_root)
2054
+ if paper_root is None:
2055
+ return None
2056
+ ledger_json_path = paper_root / "evidence_ledger.json"
2057
+ if not ledger_json_path.exists():
2058
+ return None
2059
+ payload = read_json(ledger_json_path, {})
2060
+ if not isinstance(payload, dict) or not payload:
2061
+ return None
2062
+ selected_outline_ref = str(payload.get("selected_outline_ref") or "").strip() or None
2063
+ line_state = self._paper_line_state_from_root(paper_root)
2064
+ paper_line_id = str(line_state.get("paper_line_id") or "").strip() or None
2065
+ items = self._filter_paper_evidence_items(
2066
+ [dict(item) for item in (payload.get("items") or []) if isinstance(item, dict)],
2067
+ selected_outline_ref=selected_outline_ref,
2068
+ paper_line_id=paper_line_id,
2069
+ )
2070
+ return {
2071
+ "paper_root": str(paper_root),
2072
+ "workspace_root": str(paper_root.parent),
2073
+ "selected_outline_ref": selected_outline_ref,
2074
+ "paper_line_id": paper_line_id,
2075
+ "item_count": len(items),
2076
+ "main_text_ready_count": sum(
2077
+ 1
2078
+ for item in items
2079
+ if str(item.get("paper_role") or "").strip() == "main_text"
2080
+ and str(item.get("status") or "").strip().lower() in {"ready", "completed", "analyzed", "written", "recorded", "supported"}
2081
+ ),
2082
+ "appendix_item_count": sum(
2083
+ 1 for item in items if str(item.get("paper_role") or "").strip() == "appendix"
2084
+ ),
2085
+ "unmapped_item_count": sum(
2086
+ 1
2087
+ for item in items
2088
+ if not str(item.get("section_id") or "").strip() or not str(item.get("paper_role") or "").strip()
2089
+ ),
2090
+ "items": items[:40],
2091
+ "paths": {
2092
+ "ledger_json": str(ledger_json_path),
2093
+ "ledger_md": str(paper_root / "evidence_ledger.md") if (paper_root / "evidence_ledger.md").exists() else None,
2094
+ },
2095
+ }
1942
2096
 
1943
2097
  def _paper_contract_payload(self, quest_root: Path, workspace_root: Path) -> dict[str, Any] | None:
1944
2098
  paper_root = self._best_paper_root(quest_root, workspace_root)
@@ -1958,6 +2112,7 @@ class QuestService:
1958
2112
  bundle_manifest = bundle_manifest if isinstance(bundle_manifest, dict) else {}
1959
2113
  experiment_matrix_path = paper_root / "paper_experiment_matrix.md"
1960
2114
  experiment_matrix_json_path = paper_root / "paper_experiment_matrix.json"
2115
+ manuscript_coverage_path = paper_root / "manuscript_coverage.json"
1961
2116
  claim_map_path = paper_root / "claim_evidence_map.json"
1962
2117
  paper_line_state_path = paper_root / "paper_line_state.json"
1963
2118
  evidence_ledger = self._paper_evidence_payload(quest_root, workspace_root)
@@ -2008,11 +2163,14 @@ class QuestService:
2008
2163
  return {
2009
2164
  "paper_root": str(paper_root),
2010
2165
  "workspace_root": str(paper_root.parent),
2166
+ "paper_line_id": str(self._paper_line_state_from_root(paper_root).get("paper_line_id") or "").strip() or None,
2011
2167
  "paper_branch": str(bundle_manifest.get("paper_branch") or "").strip() or current_branch(paper_root.parent),
2012
2168
  "source_branch": str(bundle_manifest.get("source_branch") or "").strip() or None,
2013
2169
  "selected_outline_ref": str(selected_outline.get("outline_id") or bundle_manifest.get("selected_outline_ref") or "").strip() or None,
2014
2170
  "title": str(selected_outline.get("title") or bundle_manifest.get("title") or "").strip() or None,
2015
2171
  "story": str(selected_outline.get("story") or "").strip() or None,
2172
+ "paper_view": selected_outline.get("paper_view") if isinstance(selected_outline.get("paper_view"), dict) else None,
2173
+ "evidence_view": selected_outline.get("evidence_view") if isinstance(selected_outline.get("evidence_view"), dict) else None,
2016
2174
  "research_questions": detailed_outline.get("research_questions") if isinstance(detailed_outline.get("research_questions"), list) else [],
2017
2175
  "experimental_designs": detailed_outline.get("experimental_designs") if isinstance(detailed_outline.get("experimental_designs"), list) else [],
2018
2176
  "contributions": detailed_outline.get("contributions") if isinstance(detailed_outline.get("contributions"), list) else [],
@@ -2030,6 +2188,7 @@ class QuestService:
2030
2188
  "outline_manifest": str(outline_manifest_path) if outline_manifest_path.exists() else None,
2031
2189
  "experiment_matrix": str(experiment_matrix_path) if experiment_matrix_path.exists() else None,
2032
2190
  "experiment_matrix_json": str(experiment_matrix_json_path) if experiment_matrix_json_path.exists() else None,
2191
+ "manuscript_coverage": str(manuscript_coverage_path) if manuscript_coverage_path.exists() else None,
2033
2192
  "bundle_manifest": str(bundle_manifest_path) if bundle_manifest_path.exists() else None,
2034
2193
  "claim_evidence_map": str(claim_map_path) if claim_map_path.exists() else None,
2035
2194
  "paper_line_state": str(paper_line_state_path) if paper_line_state_path.exists() else None,
@@ -2423,11 +2582,25 @@ class QuestService:
2423
2582
  evidence_items = [
2424
2583
  dict(item) for item in ((paper_evidence or {}).get("items") or []) if isinstance(item, dict)
2425
2584
  ]
2426
- ledger_by_item = {
2427
- str(item.get("item_id") or "").strip(): item
2428
- for item in evidence_items
2429
- if str(item.get("item_id") or "").strip()
2430
- }
2585
+ ledger_by_item: dict[str, list[dict[str, Any]]] = {}
2586
+ for item in evidence_items:
2587
+ item_id = str(item.get("item_id") or "").strip()
2588
+ if item_id:
2589
+ ledger_by_item.setdefault(item_id, []).append(item)
2590
+
2591
+ def ready_ledger_item(item_id: str) -> dict[str, Any] | None:
2592
+ candidates = ledger_by_item.get(item_id) or []
2593
+ ready_statuses = {"ready", "completed", "analyzed", "written", "recorded", "supported"}
2594
+ ready = [
2595
+ item
2596
+ for item in candidates
2597
+ if str(item.get("status") or "").strip().lower() in ready_statuses
2598
+ ]
2599
+ if ready:
2600
+ main = [item for item in ready if str(item.get("paper_role") or "").strip() == "main_text"]
2601
+ return main[0] if main else ready[0]
2602
+ return candidates[0] if candidates else None
2603
+
2431
2604
  unresolved_required_items: list[dict[str, Any]] = []
2432
2605
  ready_section_count = 0
2433
2606
  for section in paper_contract.get("sections") or []:
@@ -2436,7 +2609,7 @@ class QuestService:
2436
2609
  required_items = [str(item).strip() for item in (section.get("required_items") or []) if str(item).strip()]
2437
2610
  section_ready = True
2438
2611
  for item_id in required_items:
2439
- ledger_item = ledger_by_item.get(item_id)
2612
+ ledger_item = ready_ledger_item(item_id)
2440
2613
  status = str((ledger_item or {}).get("status") or "").strip().lower()
2441
2614
  if status not in {"ready", "completed", "analyzed", "written", "recorded", "supported"}:
2442
2615
  unresolved_required_items.append(
@@ -2523,6 +2696,18 @@ class QuestService:
2523
2696
  if isinstance(paper_contract.get("bundle_manifest"), dict)
2524
2697
  else {}
2525
2698
  )
2699
+ package_type = str(bundle_manifest.get("package_type") or "draft_checkpoint").strip().lower().replace("-", "_")
2700
+ if package_type in {"", "draft", "memo", "checkpoint", "paper_memo"}:
2701
+ package_type = "draft_checkpoint"
2702
+ elif package_type in {"review", "review_bundle"}:
2703
+ package_type = "review_package"
2704
+ elif package_type in {"final", "final_bundle", "submission", "submission_bundle"}:
2705
+ package_type = "submission_package"
2706
+ elif package_type not in {"draft_checkpoint", "review_package", "submission_package"}:
2707
+ package_type = "draft_checkpoint"
2708
+ coverage_path = str(((paper_contract.get("paths") or {}).get("manuscript_coverage") or "")).strip()
2709
+ manuscript_coverage = read_json(Path(coverage_path), {}) if coverage_path else {}
2710
+ manuscript_coverage = manuscript_coverage if isinstance(manuscript_coverage, dict) else {}
2526
2711
  submission_checklist_path = str(((paper_contract.get("paths") or {}).get("submission_checklist") or "")).strip()
2527
2712
  submission_checklist = read_json(Path(submission_checklist_path), {}) if submission_checklist_path else {}
2528
2713
  submission_checklist = submission_checklist if isinstance(submission_checklist, dict) else {}
@@ -2536,9 +2721,23 @@ class QuestService:
2536
2721
  closure_state = "bundle_not_ready"
2537
2722
  delivery_state = "not_ready"
2538
2723
  keep_bundle_fixed_by_default = False
2539
- if bundle_status == "present":
2724
+ evidence_ready = contract_ok
2725
+ analysis_ready = writing_ready
2726
+ academic_outline_ready = bool(manuscript_coverage.get("academic_outline_ready"))
2727
+ analysis_plan_ready = bool(manuscript_coverage.get("analysis_plan_ready"))
2728
+ language_firewall_ok = bool(manuscript_coverage.get("language_firewall_ok"))
2729
+ draft_checkpoint_ready = bool(active_line.get("draft_checkpoint_ready")) or draft_status == "present" or bundle_status == "present"
2730
+ manuscript_ready = bool(active_line.get("manuscript_ready")) or bool(manuscript_coverage.get("manuscript_ready"))
2731
+ submission_ready = bool(active_line.get("submission_ready")) or bool(manuscript_coverage.get("submission_ready"))
2732
+ if submission_ready:
2540
2733
  closure_state = "delivery_ready"
2541
- delivery_state = "bundle_ready"
2734
+ delivery_state = "submission_ready"
2735
+ elif manuscript_ready:
2736
+ closure_state = "review_before_submission"
2737
+ delivery_state = "manuscript_ready"
2738
+ elif draft_checkpoint_ready:
2739
+ closure_state = "draft_checkpoint_continue_writing"
2740
+ delivery_state = "draft_checkpoint_ready"
2542
2741
  if delivered_at or "delivered" in overall_status:
2543
2742
  delivery_state = "delivered"
2544
2743
  closure_state = "delivered_continue_research" if "continue" in overall_status else "delivered_parked"
@@ -2547,15 +2746,30 @@ class QuestService:
2547
2746
  if unmapped_completed_items:
2548
2747
  recommended_next_stage = "write"
2549
2748
  recommended_action = "sync_paper_contract"
2749
+ elif manuscript_coverage and not academic_outline_ready:
2750
+ recommended_next_stage = "write"
2751
+ recommended_action = "repair_academic_outline_with_paper_outline"
2752
+ elif manuscript_coverage and not analysis_plan_ready:
2753
+ recommended_next_stage = "write"
2754
+ recommended_action = "repair_analysis_plan_with_paper_outline"
2550
2755
  elif unresolved_required_items or blocking_pending_slices:
2551
2756
  recommended_next_stage = "analysis-campaign"
2552
2757
  recommended_action = "complete_required_supplementary"
2758
+ elif manuscript_coverage and not language_firewall_ok and draft_checkpoint_ready:
2759
+ recommended_next_stage = "write"
2760
+ recommended_action = "repair_manuscript_language"
2553
2761
  elif draft_status != "present":
2554
2762
  recommended_next_stage = "write"
2555
2763
  recommended_action = "draft_paper"
2556
2764
  elif bundle_status != "present":
2557
2765
  recommended_next_stage = "write"
2558
- recommended_action = "prepare_bundle"
2766
+ recommended_action = "submit_draft_checkpoint"
2767
+ elif not manuscript_ready:
2768
+ recommended_next_stage = "write"
2769
+ recommended_action = "expand_manuscript_and_figures"
2770
+ elif not submission_ready:
2771
+ recommended_next_stage = "review"
2772
+ recommended_action = "prepare_submission_package"
2559
2773
  else:
2560
2774
  recommended_next_stage = "finalize"
2561
2775
  recommended_action = "finalize_paper_line"
@@ -2574,7 +2788,16 @@ class QuestService:
2574
2788
  "selected_outline_ref": selected_outline_ref,
2575
2789
  "contract_ok": contract_ok,
2576
2790
  "writing_ready": writing_ready,
2577
- "finalize_ready": writing_ready and bundle_status == "present",
2791
+ "evidence_ready": evidence_ready,
2792
+ "analysis_ready": analysis_ready,
2793
+ "academic_outline_ready": academic_outline_ready,
2794
+ "analysis_plan_ready": analysis_plan_ready,
2795
+ "language_firewall_ok": language_firewall_ok,
2796
+ "draft_checkpoint_ready": draft_checkpoint_ready,
2797
+ "manuscript_ready": manuscript_ready,
2798
+ "submission_ready": submission_ready,
2799
+ "finalize_ready": submission_ready,
2800
+ "package_type": package_type,
2578
2801
  "closure_state": closure_state,
2579
2802
  "delivery_state": delivery_state,
2580
2803
  "delivered_at": delivered_at,
@@ -2603,6 +2826,27 @@ class QuestService:
2603
2826
  "draft_status": draft_status,
2604
2827
  "bundle_status": bundle_status,
2605
2828
  "blocking_reasons": blocking_reasons,
2829
+ "manuscript_blocking_reasons": list(
2830
+ active_line.get("manuscript_blocking_reasons")
2831
+ or manuscript_coverage.get("manuscript_blockers")
2832
+ or []
2833
+ ),
2834
+ "manuscript_warning_reasons": list(
2835
+ active_line.get("manuscript_warning_reasons")
2836
+ or manuscript_coverage.get("manuscript_warnings")
2837
+ or []
2838
+ ),
2839
+ "submission_blocking_reasons": list(
2840
+ active_line.get("submission_blocking_reasons")
2841
+ or manuscript_coverage.get("submission_blockers")
2842
+ or []
2843
+ ),
2844
+ "submission_warning_reasons": list(
2845
+ active_line.get("submission_warning_reasons")
2846
+ or manuscript_coverage.get("submission_warnings")
2847
+ or []
2848
+ ),
2849
+ "manuscript_coverage": manuscript_coverage or None,
2606
2850
  "recommended_next_stage": recommended_next_stage,
2607
2851
  "recommended_action": recommended_action,
2608
2852
  "unresolved_required_items": unresolved_required_items[:12],
@@ -2635,6 +2879,40 @@ class QuestService:
2635
2879
  return text
2636
2880
  return text.zfill(_NUMERIC_QUEST_ID_PAD_WIDTH)
2637
2881
 
2882
+ @staticmethod
2883
+ def _parse_reserved_numeric_quest_id(value: str | None) -> int | None:
2884
+ numeric_value = QuestService._parse_numeric_quest_id(value)
2885
+ if numeric_value is not None:
2886
+ return numeric_value
2887
+ raw = str(value or "").strip()
2888
+ match = _SYSTEM_NUMERIC_QUEST_ID_PATTERN.fullmatch(raw)
2889
+ if match is None:
2890
+ return None
2891
+ return QuestService._parse_numeric_quest_id(match.group("number"))
2892
+
2893
+ @staticmethod
2894
+ def _quest_class_for(
2895
+ *,
2896
+ quest_id: str | None,
2897
+ startup_contract: dict[str, Any] | None = None,
2898
+ ) -> str:
2899
+ normalized_quest_id = str(quest_id or "").strip()
2900
+ match = _SYSTEM_NUMERIC_QUEST_ID_PATTERN.fullmatch(normalized_quest_id)
2901
+ if match is not None:
2902
+ prefix = str(match.group("prefix") or "").strip().upper()
2903
+ if prefix == "S":
2904
+ return "settings"
2905
+ if prefix == "B":
2906
+ return "benchstore"
2907
+
2908
+ contract = startup_contract if isinstance(startup_contract, dict) else {}
2909
+ custom_profile = str(contract.get("custom_profile") or "").strip().lower()
2910
+ if custom_profile in {"admin_ops", "settings_issue"}:
2911
+ return "settings"
2912
+ if isinstance(contract.get("benchstore_context"), dict) or isinstance(contract.get("start_setup_session"), dict):
2913
+ return "benchstore"
2914
+ return "research"
2915
+
2638
2916
  @contextmanager
2639
2917
  def _quest_id_state_lock(self):
2640
2918
  lock_path = self._quest_id_lock_path()
@@ -2720,7 +2998,7 @@ class QuestService:
2720
2998
  return self._format_numeric_quest_id(next_numeric_id)
2721
2999
 
2722
3000
  def _reserve_numeric_quest_id(self, quest_id: str) -> None:
2723
- numeric_value = self._parse_numeric_quest_id(quest_id)
3001
+ numeric_value = self._parse_reserved_numeric_quest_id(quest_id)
2724
3002
  if numeric_value is None:
2725
3003
  return
2726
3004
  with self._quest_id_state_lock():
@@ -2730,24 +3008,28 @@ class QuestService:
2730
3008
  self._write_quest_id_state_locked(next_numeric_id)
2731
3009
 
2732
3010
  def _normalize_quest_id(self, quest_id: str | None) -> tuple[str, bool]:
2733
- raw = str(quest_id or "").strip().lower()
3011
+ raw = str(quest_id or "").strip()
2734
3012
  if not raw:
2735
3013
  return self._allocate_next_numeric_quest_id(), True
2736
- slug = re.sub(r"[^a-z0-9._-]+", "-", raw).strip("._-")
3014
+ slug = re.sub(r"[^A-Za-z0-9._-]+", "-", raw).strip("._-")
2737
3015
  if not slug:
2738
3016
  return self._allocate_next_numeric_quest_id(), True
3017
+ system_match = _SYSTEM_NUMERIC_QUEST_ID_PATTERN.fullmatch(slug)
3018
+ if system_match is not None:
3019
+ slug = f"{str(system_match.group('prefix') or '').upper()}-{system_match.group('number')}"
2739
3020
  return slug[:80], False
2740
3021
 
2741
3022
  def create(
2742
3023
  self,
2743
3024
  goal: str,
2744
3025
  quest_id: str | None = None,
2745
- runner: str = "codex",
3026
+ runner: str | None = None,
2746
3027
  title: str | None = None,
2747
3028
  *,
2748
3029
  requested_baseline_ref: dict[str, Any] | None = None,
2749
3030
  startup_contract: dict[str, Any] | None = None,
2750
3031
  ) -> dict:
3032
+ resolved_runner = str(runner or self._configured_default_runner()).strip().lower() or "codex"
2751
3033
  quest_id, auto_generated = self._normalize_quest_id(quest_id)
2752
3034
  quest_root = self._quest_root(quest_id)
2753
3035
  if quest_root.exists():
@@ -2763,7 +3045,7 @@ class QuestService:
2763
3045
  quest_id,
2764
3046
  goal,
2765
3047
  quest_root,
2766
- runner,
3048
+ resolved_runner,
2767
3049
  title=title,
2768
3050
  requested_baseline_ref=dict(requested_baseline_ref) if isinstance(requested_baseline_ref, dict) else None,
2769
3051
  startup_contract=dict(startup_contract) if isinstance(startup_contract, dict) else None,
@@ -2794,8 +3076,9 @@ class QuestService:
2794
3076
  *,
2795
3077
  title: str | None = None,
2796
3078
  goal: str | None = None,
2797
- runner: str = "codex",
3079
+ runner: str | None = None,
2798
3080
  ) -> dict[str, Any]:
3081
+ resolved_runner = str(runner or self._configured_default_runner()).strip().lower() or "codex"
2799
3082
  quest_root = self._quest_root(quest_id)
2800
3083
  if not quest_root.exists():
2801
3084
  raise FileNotFoundError(f"Unknown quest `{quest_id}`.")
@@ -2815,7 +3098,7 @@ class QuestService:
2815
3098
  quest_id,
2816
3099
  restored_goal,
2817
3100
  quest_root,
2818
- runner,
3101
+ resolved_runner,
2819
3102
  title=restored_title,
2820
3103
  ),
2821
3104
  )
@@ -3021,9 +3304,16 @@ class QuestService:
3021
3304
 
3022
3305
  bash_summary = BashExecService(self.home).summary(quest_root)
3023
3306
  interaction_watchdog = self.artifact_interaction_watchdog_status(quest_root)
3307
+ quest_class = self._quest_class_for(
3308
+ quest_id=str(quest_yaml.get("quest_id") or quest_id).strip(),
3309
+ startup_contract=quest_yaml.get("startup_contract") if isinstance(quest_yaml.get("startup_contract"), dict) else None,
3310
+ )
3311
+ workspace_mode = str(research_state.get("workspace_mode") or "quest").strip().lower() or "quest"
3312
+ listed_in_projects = quest_class == "research" and workspace_mode in {"copilot", "autonomous"}
3024
3313
  payload = {
3025
3314
  "quest_id": quest_yaml.get("quest_id", quest_id),
3026
3315
  "title": quest_yaml.get("title", quest_id),
3316
+ "goal": quest_yaml.get("goal"),
3027
3317
  "quest_root": str(quest_root.resolve()),
3028
3318
  "status": runtime_state.get("display_status") or runtime_state.get("status") or quest_yaml.get("status", "idle"),
3029
3319
  "runtime_status": runtime_state.get("status") or quest_yaml.get("status", "idle"),
@@ -3039,7 +3329,9 @@ class QuestService:
3039
3329
  "research_head_worktree_root": research_state.get("research_head_worktree_root"),
3040
3330
  "current_workspace_branch": research_state.get("current_workspace_branch"),
3041
3331
  "current_workspace_root": research_state.get("current_workspace_root"),
3042
- "workspace_mode": research_state.get("workspace_mode") or "quest",
3332
+ "workspace_mode": workspace_mode,
3333
+ "quest_class": quest_class,
3334
+ "listed_in_projects": listed_in_projects,
3043
3335
  "active_idea_id": research_state.get("active_idea_id"),
3044
3336
  "active_baseline_id": active_baseline_id,
3045
3337
  "active_baseline_variant_id": active_baseline_variant_id,
@@ -3048,6 +3340,7 @@ class QuestService:
3048
3340
  "continuation_anchor": runtime_state.get("continuation_anchor"),
3049
3341
  "continuation_reason": runtime_state.get("continuation_reason"),
3050
3342
  "continuation_updated_at": runtime_state.get("continuation_updated_at"),
3343
+ "waiting_notice": runtime_state.get("waiting_notice"),
3051
3344
  "last_resume_source": runtime_state.get("last_resume_source"),
3052
3345
  "last_resume_at": runtime_state.get("last_resume_at"),
3053
3346
  "last_recovery_abandoned_run_id": runtime_state.get("last_recovery_abandoned_run_id"),
@@ -3142,18 +3435,18 @@ class QuestService:
3142
3435
  self._jsonl_tail_cache.pop(cache_key, None)
3143
3436
  return [], 0, False
3144
3437
  if normalized_limit <= 0:
3145
- total = sum(1 for _ in _iter_jsonl_records_safely(path))
3438
+ total = _count_jsonl_lines_fast(path)
3146
3439
  return [], total, False
3147
3440
 
3148
3441
  if before is not None:
3149
- stop_cursor = max(int(before) - 1, 0)
3150
3442
  window: deque[tuple[int, dict[str, Any]]] = deque(maxlen=normalized_limit)
3151
3443
  total = 0
3152
- for payload in _iter_jsonl_records_safely(path):
3153
- total += 1
3154
- if total >= before:
3444
+ for cursor, payload in _iter_jsonl_records_safely(path):
3445
+ total = cursor
3446
+ if cursor >= before:
3155
3447
  break
3156
- window.append((total, payload))
3448
+ if isinstance(payload, dict):
3449
+ window.append((cursor, payload))
3157
3450
  has_more = bool(window and window[0][0] > 1)
3158
3451
  return list(window), total, has_more
3159
3452
 
@@ -3173,6 +3466,17 @@ class QuestService:
3173
3466
  window = cached_records[-normalized_limit:]
3174
3467
  has_more = cached_total > len(window)
3175
3468
  return window, cached_total, has_more
3469
+ if cached_limit < normalized_limit:
3470
+ window, total = _tail_jsonl_records_safely(path, limit=normalized_limit)
3471
+ with self._jsonl_cache_lock:
3472
+ self._jsonl_tail_cache[cache_key] = {
3473
+ "state": state,
3474
+ "limit": normalized_limit,
3475
+ "total": total,
3476
+ "records": list(window),
3477
+ }
3478
+ has_more = total > len(window)
3479
+ return list(window), total, has_more
3176
3480
 
3177
3481
  if (
3178
3482
  cached_tail
@@ -3196,11 +3500,11 @@ class QuestService:
3196
3500
  )
3197
3501
  )
3198
3502
  if appended_records:
3199
- next_cursor = cached_total + 1
3200
- for payload in appended_records:
3201
- window.append((next_cursor, payload))
3202
- next_cursor += 1
3203
- total = cached_total + len(appended_records)
3503
+ total = cached_total
3504
+ for relative_cursor, payload in appended_records:
3505
+ total = cached_total + relative_cursor
3506
+ if isinstance(payload, dict):
3507
+ window.append((total, payload))
3204
3508
  else:
3205
3509
  total = cached_total
3206
3510
  stored_records = list(window)
@@ -3211,6 +3515,13 @@ class QuestService:
3211
3515
  "total": total,
3212
3516
  "records": stored_records,
3213
3517
  }
3518
+ write_json(
3519
+ _jsonl_line_count_cache_path(path),
3520
+ {
3521
+ "state": list(state),
3522
+ "total": total,
3523
+ },
3524
+ )
3214
3525
  selected = stored_records[-normalized_limit:]
3215
3526
  has_more = total > len(selected)
3216
3527
  return selected, total, has_more
@@ -3230,12 +3541,14 @@ class QuestService:
3230
3541
  total = 0
3231
3542
  saw_more = False
3232
3543
  normalized_after = max(int(after or 0), 0)
3233
- for payload in _iter_jsonl_records_safely(path):
3234
- total += 1
3235
- if total <= normalized_after:
3544
+ for cursor, payload in _iter_jsonl_records_safely(path):
3545
+ total = cursor
3546
+ if cursor <= normalized_after:
3547
+ continue
3548
+ if not isinstance(payload, dict):
3236
3549
  continue
3237
3550
  if len(collected) < normalized_limit:
3238
- collected.append((total, payload))
3551
+ collected.append((cursor, payload))
3239
3552
  continue
3240
3553
  saw_more = True
3241
3554
  return collected, total, saw_more
@@ -3514,6 +3827,7 @@ class QuestService:
3514
3827
  payload = {
3515
3828
  "quest_id": quest_yaml.get("quest_id", quest_id),
3516
3829
  "title": quest_yaml.get("title", quest_id),
3830
+ "goal": quest_yaml.get("goal"),
3517
3831
  "quest_root": str(quest_root.resolve()),
3518
3832
  "status": runtime_state.get("display_status") or runtime_state.get("status") or quest_yaml.get("status", "idle"),
3519
3833
  "runtime_status": runtime_state.get("status") or quest_yaml.get("status", "idle"),
@@ -3552,6 +3866,7 @@ class QuestService:
3552
3866
  "continuation_anchor": runtime_state.get("continuation_anchor"),
3553
3867
  "continuation_reason": runtime_state.get("continuation_reason"),
3554
3868
  "continuation_updated_at": runtime_state.get("continuation_updated_at"),
3869
+ "waiting_notice": runtime_state.get("waiting_notice"),
3555
3870
  "last_resume_source": runtime_state.get("last_resume_source"),
3556
3871
  "last_resume_at": runtime_state.get("last_resume_at"),
3557
3872
  "last_recovery_abandoned_run_id": runtime_state.get("last_recovery_abandoned_run_id"),
@@ -3850,9 +4165,12 @@ class QuestService:
3850
4165
  def bind_source(self, quest_id: str, source: str) -> dict:
3851
4166
  quest_root = self._quest_root(quest_id)
3852
4167
  bindings_path = quest_root / ".ds" / "bindings.json"
4168
+ existing_payload = read_json(bindings_path, {"sources": []})
4169
+ existing_sources = existing_payload.get("sources") if isinstance(existing_payload, dict) else []
3853
4170
  bindings = self._binding_sources_payload(quest_root)
3854
4171
  normalized_source = self._normalize_binding_source(source)
3855
- next_sources = self._normalized_binding_sources([*(bindings.get("sources") or []), normalized_source])
4172
+ seed_sources = list(existing_sources) if isinstance(existing_sources, list) else list(bindings.get("sources") or [])
4173
+ next_sources = self._normalized_binding_sources([*seed_sources, normalized_source])
3856
4174
  changed = list(bindings.get("sources") or []) != next_sources
3857
4175
  if changed:
3858
4176
  bindings["sources"] = next_sources
@@ -3911,6 +4229,7 @@ class QuestService:
3911
4229
  active_anchor: str | None = None,
3912
4230
  default_runner: str | None = None,
3913
4231
  workspace_mode: str | None = None,
4232
+ decision_policy: str | None = None,
3914
4233
  ) -> dict:
3915
4234
  quest_root = self._quest_root(quest_id)
3916
4235
  quest_yaml_path = self._quest_yaml_path(quest_root)
@@ -3954,6 +4273,13 @@ class QuestService:
3954
4273
  if normalized_runner not in available_runners:
3955
4274
  allowed = ", ".join(sorted(available_runners))
3956
4275
  raise ValueError(f"Unsupported runner `{normalized_runner}`. Available runners: {allowed}.")
4276
+ runners = ConfigManager(self.home).load_runners_config()
4277
+ runner_cfg = runners.get(normalized_runner, {}) if isinstance(runners.get(normalized_runner), dict) else {}
4278
+ if runner_cfg.get("enabled") is False:
4279
+ fallback = self._resolve_enabled_runner_name(normalized_runner)
4280
+ if fallback == normalized_runner:
4281
+ raise ValueError(f"Runner `{normalized_runner}` is disabled and no enabled fallback runner is available.")
4282
+ normalized_runner = fallback
3957
4283
  if quest_data.get("default_runner") != normalized_runner:
3958
4284
  quest_data["default_runner"] = normalized_runner
3959
4285
  changed = True
@@ -3980,6 +4306,33 @@ class QuestService:
3980
4306
  "copilot_mode" if normalized_workspace_mode == "copilot" else "autonomous_mode"
3981
4307
  )
3982
4308
 
4309
+ if decision_policy is not None:
4310
+ normalized_decision_policy = str(decision_policy).strip().lower()
4311
+ if normalized_decision_policy not in {"autonomous", "user_gated"}:
4312
+ raise ValueError("Unsupported decision policy. Allowed values: autonomous, user_gated.")
4313
+ startup_contract = (
4314
+ dict(quest_data.get("startup_contract") or {})
4315
+ if isinstance(quest_data.get("startup_contract"), dict)
4316
+ else {}
4317
+ )
4318
+ if str(startup_contract.get("decision_policy") or "").strip().lower() != normalized_decision_policy:
4319
+ startup_contract["decision_policy"] = normalized_decision_policy
4320
+ quest_data["startup_contract"] = startup_contract
4321
+ changed = True
4322
+ effective_workspace_mode = str(
4323
+ research_state_updates.get("workspace_mode")
4324
+ or self.read_research_state(quest_root).get("workspace_mode")
4325
+ or startup_contract.get("workspace_mode")
4326
+ or ""
4327
+ ).strip().lower()
4328
+ if normalized_decision_policy == "autonomous" and effective_workspace_mode == "autonomous":
4329
+ runtime_state = self._read_runtime_state(quest_root)
4330
+ current_policy = str(runtime_state.get("continuation_policy") or "").strip().lower()
4331
+ current_reason = str(runtime_state.get("continuation_reason") or "").strip()
4332
+ if current_policy == "wait_for_user_or_resume" and current_reason not in AUTONOMOUS_BLOCKING_WAIT_REASONS:
4333
+ runtime_state_updates["continuation_policy"] = "auto"
4334
+ runtime_state_updates["continuation_reason"] = "autonomous_decision_policy"
4335
+
3983
4336
  if changed:
3984
4337
  quest_data["updated_at"] = utc_now()
3985
4338
  write_yaml(quest_yaml_path, quest_data)
@@ -4088,11 +4441,26 @@ class QuestService:
4088
4441
  active_run_id=active_run_id or None,
4089
4442
  last_transition_at=last_transition_at,
4090
4443
  )
4444
+ # Reconcile continuation_policy with current workspace_mode so that
4445
+ # a mode switch that happened before/during the crash is respected.
4446
+ research_state = self.read_research_state(quest_root)
4447
+ workspace_mode = str(research_state.get("workspace_mode") or "").strip().lower()
4448
+ current_policy = str(runtime_state.get("continuation_policy") or "").strip().lower()
4449
+ reconciled_policy_updates: dict[str, Any] = {}
4450
+ if workspace_mode == "autonomous" and current_policy == "wait_for_user_or_resume":
4451
+ reconciled_policy_updates["continuation_policy"] = "auto"
4452
+ reconciled_policy_updates["continuation_reason"] = "autonomous_mode_reconciled"
4453
+ reconciled_policy_updates["continuation_updated_at"] = utc_now()
4454
+ elif workspace_mode == "copilot" and current_policy == "auto":
4455
+ reconciled_policy_updates["continuation_policy"] = "wait_for_user_or_resume"
4456
+ reconciled_policy_updates["continuation_reason"] = "copilot_mode_reconciled"
4457
+ reconciled_policy_updates["continuation_updated_at"] = utc_now()
4091
4458
  self.update_runtime_state(
4092
4459
  quest_root=quest_root,
4093
4460
  status="stopped",
4094
4461
  active_run_id=None,
4095
4462
  stop_reason="crash_recovered",
4463
+ **reconciled_policy_updates,
4096
4464
  )
4097
4465
  summary = (
4098
4466
  f"Recovered quest from stale runtime state; previous status `{previous_status}`"
@@ -4232,8 +4600,15 @@ class QuestService:
4232
4600
 
4233
4601
  def node_traces(self, quest_id: str, *, selection_type: str | None = None) -> dict:
4234
4602
  quest_root = self._quest_root(quest_id)
4235
- workflow = self.workflow(quest_id)
4236
4603
  snapshot = self.snapshot(quest_id)
4604
+ try:
4605
+ workflow = self._build_details_projection_payload(
4606
+ quest_root,
4607
+ source_signature=self._projection_source_signature(quest_root, "details"),
4608
+ update_progress=lambda *_args, **_kwargs: None,
4609
+ )
4610
+ except Exception:
4611
+ workflow = self.workflow(quest_id)
4237
4612
  payload = QuestNodeTraceManager(quest_root).materialize(
4238
4613
  quest_id=quest_id,
4239
4614
  workflow=workflow,
@@ -4474,6 +4849,7 @@ class QuestService:
4474
4849
  mode: str | None = None,
4475
4850
  profile: str | None = None,
4476
4851
  ) -> dict:
4852
+ profile = str(profile or "").strip().lower() or None
4477
4853
  if revision:
4478
4854
  return self._revision_explorer(quest_id, revision=revision, mode=mode or "ref")
4479
4855
 
@@ -4504,7 +4880,7 @@ class QuestService:
4504
4880
  }
4505
4881
 
4506
4882
  def search_files(self, quest_id: str, term: str, limit: int = 50) -> dict[str, Any]:
4507
- query = term.strip()
4883
+ query = self._normalize_explorer_search_query(term)
4508
4884
  normalized_query = query.casefold()
4509
4885
  workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
4510
4886
  resolved_limit = max(1, min(limit, 200))
@@ -4530,6 +4906,41 @@ class QuestService:
4530
4906
  except OSError:
4531
4907
  continue
4532
4908
 
4909
+ relative = path.relative_to(workspace_root).as_posix()
4910
+ scope, writable = self._classify_path_scope(workspace_root, path)
4911
+ path_haystack = relative.casefold()
4912
+ name_haystack = path.name.casefold()
4913
+ if normalized_query in path_haystack or normalized_query in name_haystack:
4914
+ haystack = path_haystack
4915
+ match_spans: list[dict[str, int]] = []
4916
+ start = 0
4917
+ while True:
4918
+ found = haystack.find(normalized_query, start)
4919
+ if found < 0:
4920
+ break
4921
+ match_spans.append({"start": found, "end": found + len(query)})
4922
+ start = found + max(1, len(query))
4923
+ renderer_hint, mime_type = self._renderer_hint_for(path)
4924
+ items.append(
4925
+ {
4926
+ "id": f"{relative}:path",
4927
+ "document_id": f"path::{relative}",
4928
+ "title": path.name,
4929
+ "path": relative,
4930
+ "scope": scope,
4931
+ "writable": writable,
4932
+ "line_number": 0,
4933
+ "line_text": relative,
4934
+ "snippet": relative[:320],
4935
+ "match_spans": match_spans,
4936
+ "open_kind": renderer_hint,
4937
+ "mime_type": mime_type,
4938
+ }
4939
+ )
4940
+ if len(items) >= resolved_limit:
4941
+ truncated = True
4942
+ break
4943
+
4533
4944
  renderer_hint, mime_type = self._renderer_hint_for(path)
4534
4945
  if not self._is_text_document(path, mime_type, renderer_hint):
4535
4946
  continue
@@ -4552,8 +4963,6 @@ class QuestService:
4552
4963
  continue
4553
4964
 
4554
4965
  files_scanned += 1
4555
- relative = path.relative_to(workspace_root).as_posix()
4556
- scope, writable = self._classify_path_scope(workspace_root, path)
4557
4966
 
4558
4967
  for line_index, line in enumerate(content.splitlines(), start=1):
4559
4968
  haystack = line.casefold()
@@ -4600,6 +5009,15 @@ class QuestService:
4600
5009
  "files_scanned": files_scanned,
4601
5010
  }
4602
5011
 
5012
+ @staticmethod
5013
+ def _normalize_explorer_search_query(term: str) -> str:
5014
+ query = str(term or "").strip()
5015
+ if len(query) >= 2 and query.startswith("*") and query.endswith("*"):
5016
+ inner = query.strip("*").strip()
5017
+ if inner and not any(marker in inner for marker in "*?[]"):
5018
+ return inner
5019
+ return query
5020
+
4603
5021
  def open_document(self, quest_id: str, document_id: str) -> dict:
4604
5022
  quest_root = self._require_initialized_quest_root(quest_id)
4605
5023
  workspace_root = self.active_workspace_root(quest_root)
@@ -4637,12 +5055,16 @@ class QuestService:
4637
5055
  },
4638
5056
  }
4639
5057
 
5058
+ shared_source_quest_id = None
5059
+ parsed_shared_memory = self._parse_shared_memory_document_id(document_id)
5060
+ if parsed_shared_memory is not None:
5061
+ shared_source_quest_id = parsed_shared_memory[0]
4640
5062
  path, writable, scope, source_kind = self.resolve_document(quest_id, document_id)
4641
5063
  renderer_hint, mime_type = self._renderer_hint_for(path)
4642
5064
  is_text = self._is_text_document(path, mime_type, renderer_hint)
4643
5065
  content = read_text(path) if is_text else ""
4644
5066
  revision = f"sha256:{sha256_text(content)}" if is_text else f"sha256:{hashlib.sha256(path.read_bytes()).hexdigest()}"
4645
- return {
5067
+ payload = {
4646
5068
  "document_id": document_id,
4647
5069
  "quest_id": quest_id,
4648
5070
  "title": path.name if "::" in document_id else document_id,
@@ -4664,9 +5086,17 @@ class QuestService:
4664
5086
  "renderer_hint": renderer_hint,
4665
5087
  },
4666
5088
  }
5089
+ if shared_source_quest_id:
5090
+ payload["source_quest_id"] = shared_source_quest_id
5091
+ if isinstance(payload.get("meta"), dict):
5092
+ payload["meta"]["source_quest_id"] = shared_source_quest_id
5093
+ payload["meta"]["shared"] = True
5094
+ return payload
4667
5095
 
4668
5096
  def resolve_document(self, quest_id: str, document_id: str) -> tuple[Path, bool, str, str]:
4669
5097
  quest_root = self._require_initialized_quest_root(quest_id)
5098
+ if document_id.startswith(_SHARED_MEMORY_DOCUMENT_PREFIX):
5099
+ return self._resolve_shared_memory_document(document_id)
4670
5100
  workspace_root = self.active_workspace_root(quest_root)
4671
5101
  resolution_root = self._document_resolution_root(
4672
5102
  quest_root=quest_root,
@@ -4683,6 +5113,32 @@ class QuestService:
4683
5113
  return self._resolve_document(quest_root, f"questpath::{legacy_relative}")
4684
5114
  raise
4685
5115
 
5116
+ def _resolve_shared_memory_document(self, document_id: str) -> tuple[Path, bool, str, str]:
5117
+ parsed = self._parse_shared_memory_document_id(document_id)
5118
+ if parsed is None:
5119
+ raise FileNotFoundError(f"Unknown shared memory document `{document_id}`.")
5120
+ source_quest_id, relative = parsed
5121
+ source_quest_root = self._require_initialized_quest_root(source_quest_id)
5122
+ root = (source_quest_root / "memory").resolve()
5123
+ path = (root / relative).resolve()
5124
+ if path != root and root not in path.parents:
5125
+ raise ValueError("Document ID escapes shared quest memory.")
5126
+ if not path.exists() or not path.is_file():
5127
+ raise FileNotFoundError(f"Unknown shared quest memory `{source_quest_id}:{relative}`.")
5128
+ return path, False, "shared_quest_memory", "shared_quest_memory"
5129
+
5130
+ @staticmethod
5131
+ def _parse_shared_memory_document_id(document_id: str) -> tuple[str, str] | None:
5132
+ raw = str(document_id or "").strip()
5133
+ if not raw.startswith(_SHARED_MEMORY_DOCUMENT_PREFIX):
5134
+ return None
5135
+ _prefix, source_quest_id, relative = (raw.split("::", 2) + ["", "", ""])[:3]
5136
+ source_quest_id = source_quest_id.strip()
5137
+ relative = relative.lstrip("/")
5138
+ if not source_quest_id or not relative:
5139
+ return None
5140
+ return source_quest_id, relative
5141
+
4686
5142
  def save_document(self, quest_id: str, document_id: str, content: str, previous_revision: str | None = None) -> dict:
4687
5143
  current = self.open_document(quest_id, document_id)
4688
5144
  if not current.get("writable", False):
@@ -4859,6 +5315,246 @@ class QuestService:
4859
5315
  "saved_at": utc_now(),
4860
5316
  }
4861
5317
 
5318
+ def save_chat_attachment_draft(
5319
+ self,
5320
+ quest_id: str,
5321
+ *,
5322
+ file_name: str,
5323
+ mime_type: str | None,
5324
+ content: bytes,
5325
+ draft_id: str | None = None,
5326
+ ) -> dict[str, Any]:
5327
+ quest_root = self._quest_root(quest_id)
5328
+ normalized_draft_id = slugify(str(draft_id or generate_id("draft")), default=generate_id("draft"))
5329
+ draft_root = self._chat_attachment_draft_root(quest_root, normalized_draft_id)
5330
+ original_name = Path(file_name).name or "attachment.bin"
5331
+ suffix = Path(original_name).suffix.lower()
5332
+ if not suffix:
5333
+ guessed_suffix = mimetypes.guess_extension(mime_type or "", strict=False) or ""
5334
+ suffix = ".jpg" if guessed_suffix == ".jpe" else guessed_suffix
5335
+ safe_stem = slugify(Path(original_name).stem or "attachment", default="attachment")
5336
+ stored_name = f"{safe_stem}{suffix or '.bin'}"
5337
+ asset_path = resolve_within(draft_root, stored_name)
5338
+ if draft_root.exists():
5339
+ for child in draft_root.iterdir():
5340
+ if child.is_file():
5341
+ child.unlink(missing_ok=True)
5342
+ elif child.is_dir():
5343
+ shutil.rmtree(child, ignore_errors=True)
5344
+ ensure_dir(draft_root)
5345
+ asset_path.write_bytes(content)
5346
+ quest_relative_path = asset_path.relative_to(quest_root).as_posix()
5347
+ asset_document_id = f"path::{quest_relative_path}"
5348
+ attachment = self._chat_attachment_payload(
5349
+ quest_id=quest_id,
5350
+ name=original_name,
5351
+ stored_name=stored_name,
5352
+ mime_type=mime_type,
5353
+ asset_path=asset_path,
5354
+ draft_id=normalized_draft_id,
5355
+ )
5356
+ attachment["status"] = "success"
5357
+ write_json(
5358
+ draft_root / "manifest.json",
5359
+ {
5360
+ "draft_id": normalized_draft_id,
5361
+ "quest_id": quest_id,
5362
+ "created_at": utc_now(),
5363
+ "attachment": attachment,
5364
+ "asset_document_id": asset_document_id,
5365
+ },
5366
+ )
5367
+ return {
5368
+ "ok": True,
5369
+ "quest_id": quest_id,
5370
+ "draft_id": normalized_draft_id,
5371
+ **attachment,
5372
+ }
5373
+
5374
+ def delete_chat_attachment_draft(self, quest_id: str, *, draft_id: str) -> dict[str, Any]:
5375
+ self._quest_root(quest_id)
5376
+ normalized_draft_id = slugify(str(draft_id or "").strip(), default="")
5377
+ if not normalized_draft_id:
5378
+ return {"ok": False, "message": "`draft_id` is required."}
5379
+ draft_root = self._chat_attachment_draft_root(self._quest_root(quest_id), normalized_draft_id)
5380
+ if not draft_root.exists():
5381
+ return {
5382
+ "ok": True,
5383
+ "status": "already_deleted",
5384
+ "quest_id": quest_id,
5385
+ "draft_id": normalized_draft_id,
5386
+ }
5387
+ shutil.rmtree(draft_root, ignore_errors=True)
5388
+ return {
5389
+ "ok": True,
5390
+ "status": "deleted",
5391
+ "quest_id": quest_id,
5392
+ "draft_id": normalized_draft_id,
5393
+ }
5394
+
5395
+ def import_chat_attachment_drafts(
5396
+ self,
5397
+ target_quest_id: str,
5398
+ *,
5399
+ source_quest_id: str,
5400
+ attachments: list[dict[str, Any]],
5401
+ ) -> dict[str, Any]:
5402
+ target_quest_root = self._quest_root(target_quest_id)
5403
+ source_quest_root = self._quest_root(source_quest_id)
5404
+ imported: list[dict[str, Any]] = []
5405
+ for index, raw_attachment in enumerate(attachments, start=1):
5406
+ if not isinstance(raw_attachment, dict):
5407
+ continue
5408
+ quest_relative_path = str(raw_attachment.get("quest_relative_path") or "").strip()
5409
+ absolute_path = str(raw_attachment.get("path") or "").strip()
5410
+ source_path: Path | None = None
5411
+ if quest_relative_path:
5412
+ source_path = resolve_within(source_quest_root, quest_relative_path)
5413
+ elif absolute_path:
5414
+ candidate = Path(absolute_path).resolve()
5415
+ if candidate == source_quest_root or source_quest_root in candidate.parents:
5416
+ source_path = candidate
5417
+ if source_path is None or not source_path.exists() or not source_path.is_file():
5418
+ continue
5419
+ file_name = str(
5420
+ raw_attachment.get("name")
5421
+ or raw_attachment.get("file_name")
5422
+ or source_path.name
5423
+ or f"attachment-{index:03d}"
5424
+ ).strip() or f"attachment-{index:03d}"
5425
+ mime_type = str(raw_attachment.get("content_type") or raw_attachment.get("mime_type") or "").strip() or None
5426
+ created = self.save_chat_attachment_draft(
5427
+ target_quest_root.name,
5428
+ file_name=file_name,
5429
+ mime_type=mime_type,
5430
+ content=source_path.read_bytes(),
5431
+ )
5432
+ imported.append(created)
5433
+ return {
5434
+ "ok": True,
5435
+ "quest_id": target_quest_id,
5436
+ "source_quest_id": source_quest_id,
5437
+ "imported_count": len(imported),
5438
+ "attachments": imported,
5439
+ }
5440
+
5441
+ def finalize_chat_attachment_drafts(
5442
+ self,
5443
+ quest_id: str,
5444
+ *,
5445
+ draft_ids: list[str],
5446
+ client_message_id: str | None,
5447
+ ) -> list[dict[str, Any]]:
5448
+ quest_root = self._quest_root(quest_id)
5449
+ normalized_draft_ids = [
5450
+ slugify(str(item or "").strip(), default="")
5451
+ for item in draft_ids
5452
+ if str(item or "").strip()
5453
+ ]
5454
+ if not normalized_draft_ids:
5455
+ return []
5456
+ batch_slug = slugify(
5457
+ str(client_message_id or generate_id("userfile")).strip(),
5458
+ default=generate_id("userfile"),
5459
+ )
5460
+ batch_root = ensure_dir(quest_root / "userfiles" / "web" / batch_slug)
5461
+ materialized: list[dict[str, Any]] = []
5462
+ for index, normalized_draft_id in enumerate(normalized_draft_ids, start=1):
5463
+ draft_root = self._chat_attachment_draft_root(quest_root, normalized_draft_id)
5464
+ manifest = read_json(draft_root / "manifest.json", {})
5465
+ attachment = dict(manifest.get("attachment") or {})
5466
+ source_path = Path(str(attachment.get("path") or "").strip())
5467
+ if not draft_root.exists() or not source_path.exists():
5468
+ raise FileNotFoundError(f"Unknown chat attachment draft `{normalized_draft_id}`.")
5469
+ target_name = self._dedupe_attachment_filename(
5470
+ batch_root,
5471
+ str(attachment.get("stored_name") or source_path.name or f"attachment-{index:03d}.bin"),
5472
+ )
5473
+ target_path = resolve_within(batch_root, target_name)
5474
+ ensure_dir(target_path.parent)
5475
+ shutil.move(str(source_path), str(target_path))
5476
+ finalized = self._chat_attachment_payload(
5477
+ quest_id=quest_id,
5478
+ name=str(attachment.get("name") or target_name),
5479
+ stored_name=target_name,
5480
+ mime_type=str(attachment.get("content_type") or "").strip() or None,
5481
+ asset_path=target_path,
5482
+ draft_id=None,
5483
+ )
5484
+ finalized["source_path"] = str(source_path)
5485
+ finalized["materialized"] = True
5486
+ finalized["uploaded_at"] = str(attachment.get("uploaded_at") or utc_now())
5487
+ materialized.append(finalized)
5488
+ shutil.rmtree(draft_root, ignore_errors=True)
5489
+ write_json(
5490
+ batch_root / "manifest.json",
5491
+ {
5492
+ "quest_id": quest_id,
5493
+ "client_message_id": str(client_message_id or "").strip() or None,
5494
+ "materialized_at": utc_now(),
5495
+ "attachments": materialized,
5496
+ },
5497
+ )
5498
+ return materialized
5499
+
5500
+ @staticmethod
5501
+ def _chat_attachment_draft_root(quest_root: Path, draft_id: str) -> Path:
5502
+ return quest_root / "userfiles" / "web" / "_staging" / draft_id
5503
+
5504
+ @staticmethod
5505
+ def _dedupe_attachment_filename(batch_root: Path, file_name: str) -> str:
5506
+ base_name = Path(file_name).name or "attachment.bin"
5507
+ stem = Path(base_name).stem or "attachment"
5508
+ suffix = Path(base_name).suffix
5509
+ candidate = base_name
5510
+ counter = 2
5511
+ while (batch_root / candidate).exists():
5512
+ candidate = f"{stem}-{counter}{suffix}"
5513
+ counter += 1
5514
+ return candidate
5515
+
5516
+ @staticmethod
5517
+ def _is_readable_chat_attachment(path: Path, mime_type: str | None) -> bool:
5518
+ normalized_mime = str(mime_type or "").strip().lower()
5519
+ if any(normalized_mime.startswith(prefix) for prefix in _CHAT_ATTACHMENT_TEXT_MIME_PREFIXES):
5520
+ return True
5521
+ if normalized_mime in _CHAT_ATTACHMENT_TEXT_MIME_TYPES:
5522
+ return True
5523
+ return path.suffix.lower() in _CHAT_ATTACHMENT_TEXT_EXTENSIONS
5524
+
5525
+ def _chat_attachment_payload(
5526
+ self,
5527
+ *,
5528
+ quest_id: str,
5529
+ name: str,
5530
+ stored_name: str,
5531
+ mime_type: str | None,
5532
+ asset_path: Path,
5533
+ draft_id: str | None,
5534
+ ) -> dict[str, Any]:
5535
+ quest_root = self._quest_root(quest_id)
5536
+ resolved_path = asset_path.resolve()
5537
+ content_type = mimetypes.guess_type(resolved_path.name)[0] or mime_type or "application/octet-stream"
5538
+ quest_relative_path = resolved_path.relative_to(quest_root).as_posix()
5539
+ payload: dict[str, Any] = {
5540
+ "kind": "image" if str(content_type).startswith("image/") else "path",
5541
+ "name": name,
5542
+ "file_name": stored_name,
5543
+ "content_type": content_type,
5544
+ "path": str(resolved_path),
5545
+ "quest_relative_path": quest_relative_path,
5546
+ "asset_document_id": f"path::{quest_relative_path}",
5547
+ "asset_url": f"/api/quests/{quest_id}/documents/asset?document_id={quote(f'path::{quest_relative_path}', safe='')}",
5548
+ "size_bytes": resolved_path.stat().st_size if resolved_path.exists() else 0,
5549
+ "uploaded_at": utc_now(),
5550
+ "upload_origin": "web",
5551
+ }
5552
+ if draft_id:
5553
+ payload["draft_id"] = draft_id
5554
+ if self._is_readable_chat_attachment(resolved_path, content_type):
5555
+ payload["extracted_text_path"] = quest_relative_path
5556
+ return payload
5557
+
4862
5558
  @staticmethod
4863
5559
  def _normalize_workspace_relative_path(
4864
5560
  relative: str | None,
@@ -5274,9 +5970,10 @@ class QuestService:
5274
5970
  @staticmethod
5275
5971
  def _default_message_queue() -> dict[str, Any]:
5276
5972
  return {
5277
- "version": 1,
5973
+ "version": 2,
5278
5974
  "pending": [],
5279
5975
  "completed": [],
5976
+ "message_states": {},
5280
5977
  }
5281
5978
 
5282
5979
  def _default_runtime_state(
@@ -5307,6 +6004,7 @@ class QuestService:
5307
6004
  "continuation_anchor": None,
5308
6005
  "continuation_reason": None,
5309
6006
  "continuation_updated_at": None,
6007
+ "waiting_notice": None,
5310
6008
  "last_resume_source": None,
5311
6009
  "last_resume_at": None,
5312
6010
  "last_recovery_abandoned_run_id": None,
@@ -5318,6 +6016,7 @@ class QuestService:
5318
6016
  "last_delivered_batch_id": None,
5319
6017
  "last_delivered_at": None,
5320
6018
  "retry_state": None,
6019
+ "turn_message_override": None,
5321
6020
  }
5322
6021
 
5323
6022
  def _default_agent_status(self, quest_root: Path) -> dict[str, Any]:
@@ -5358,9 +6057,11 @@ class QuestService:
5358
6057
  payload = self._read_cached_json(self._message_queue_path(quest_root), self._default_message_queue())
5359
6058
  if not isinstance(payload, dict):
5360
6059
  payload = self._default_message_queue()
5361
- payload.setdefault("version", 1)
6060
+ payload.setdefault("version", 2)
5362
6061
  payload.setdefault("pending", [])
5363
6062
  payload.setdefault("completed", [])
6063
+ message_states = payload.get("message_states")
6064
+ payload["message_states"] = dict(message_states) if isinstance(message_states, dict) else {}
5364
6065
  return payload
5365
6066
 
5366
6067
  def _write_message_queue(self, quest_root: Path, payload: dict[str, Any]) -> None:
@@ -5388,6 +6089,7 @@ class QuestService:
5388
6089
  merged["continuation_anchor"] = str(merged.get("continuation_anchor") or "").strip() or None
5389
6090
  merged["continuation_reason"] = str(merged.get("continuation_reason") or "").strip() or None
5390
6091
  merged["continuation_updated_at"] = str(merged.get("continuation_updated_at") or "").strip() or None
6092
+ merged["waiting_notice"] = dict(merged.get("waiting_notice") or {}) if isinstance(merged.get("waiting_notice"), dict) else None
5391
6093
  merged["last_resume_source"] = str(merged.get("last_resume_source") or "").strip() or None
5392
6094
  merged["last_resume_at"] = str(merged.get("last_resume_at") or "").strip() or None
5393
6095
  merged["last_recovery_abandoned_run_id"] = str(merged.get("last_recovery_abandoned_run_id") or "").strip() or None
@@ -5396,6 +6098,11 @@ class QuestService:
5396
6098
  merged["last_stage_fingerprint_at"] = str(merged.get("last_stage_fingerprint_at") or "").strip() or None
5397
6099
  merged["same_fingerprint_auto_turn_count"] = int(merged.get("same_fingerprint_auto_turn_count") or 0)
5398
6100
  merged["retry_state"] = dict(merged.get("retry_state") or {}) if isinstance(merged.get("retry_state"), dict) else None
6101
+ merged["turn_message_override"] = (
6102
+ dict(merged.get("turn_message_override") or {})
6103
+ if isinstance(merged.get("turn_message_override"), dict)
6104
+ else None
6105
+ )
5399
6106
  return merged
5400
6107
 
5401
6108
  def _write_runtime_state(self, quest_root: Path, payload: dict[str, Any]) -> None:
@@ -5418,6 +6125,7 @@ class QuestService:
5418
6125
  continuation_anchor: str | None | object = _UNSET,
5419
6126
  continuation_reason: str | None | object = _UNSET,
5420
6127
  continuation_updated_at: str | None | object = _UNSET,
6128
+ waiting_notice: dict[str, Any] | None | object = _UNSET,
5421
6129
  last_resume_source: str | None | object = _UNSET,
5422
6130
  last_resume_at: str | None | object = _UNSET,
5423
6131
  last_recovery_abandoned_run_id: str | None | object = _UNSET,
@@ -5430,6 +6138,7 @@ class QuestService:
5430
6138
  last_delivered_at: str | None | object = _UNSET,
5431
6139
  display_status: str | None | object = _UNSET,
5432
6140
  retry_state: dict[str, Any] | None | object = _UNSET,
6141
+ turn_message_override: dict[str, Any] | None | object = _UNSET,
5433
6142
  ) -> dict[str, Any]:
5434
6143
  with self._runtime_state_lock(quest_root):
5435
6144
  state = self._read_runtime_state(quest_root)
@@ -5486,6 +6195,8 @@ class QuestService:
5486
6195
  state["continuation_updated_at"] = str(continuation_updated_at or "").strip() or None
5487
6196
  elif continuation_changed:
5488
6197
  state["continuation_updated_at"] = now
6198
+ if waiting_notice is not _UNSET:
6199
+ state["waiting_notice"] = dict(waiting_notice) if isinstance(waiting_notice, dict) else None
5489
6200
  if last_resume_source is not _UNSET:
5490
6201
  state["last_resume_source"] = str(last_resume_source or "").strip() or None
5491
6202
  if last_resume_at is not _UNSET:
@@ -5508,6 +6219,12 @@ class QuestService:
5508
6219
  state["last_delivered_at"] = last_delivered_at
5509
6220
  if retry_state is not _UNSET:
5510
6221
  state["retry_state"] = dict(retry_state) if isinstance(retry_state, dict) else None
6222
+ if turn_message_override is not _UNSET:
6223
+ state["turn_message_override"] = (
6224
+ dict(turn_message_override)
6225
+ if isinstance(turn_message_override, dict)
6226
+ else None
6227
+ )
5511
6228
  if last_transition_at is not _UNSET:
5512
6229
  state["last_transition_at"] = last_transition_at
5513
6230
  elif status_changed or run_changed:
@@ -5549,11 +6266,369 @@ class QuestService:
5549
6266
  continuation_reason=reason,
5550
6267
  )
5551
6268
 
6269
+ @staticmethod
6270
+ def _normalize_message_read_state(value: object, *, default: str = "read") -> str:
6271
+ normalized = str(value or "").strip().lower() or default
6272
+ return normalized if normalized in {"read", "unread"} else default
6273
+
6274
+ def _update_message_read_state(
6275
+ self,
6276
+ queue_payload: dict[str, Any],
6277
+ *,
6278
+ message_id: str | None,
6279
+ client_message_id: str | None = None,
6280
+ source: str | None = None,
6281
+ conversation_id: str | None = None,
6282
+ created_at: str | None = None,
6283
+ read_state: str,
6284
+ read_reason: str | None = None,
6285
+ read_at: str | None = None,
6286
+ read_interaction_id: str | None = None,
6287
+ read_run_id: str | None = None,
6288
+ ) -> dict[str, Any] | None:
6289
+ normalized_message_id = str(message_id or "").strip()
6290
+ normalized_client_message_id = str(client_message_id or "").strip() or None
6291
+ if not normalized_message_id and not normalized_client_message_id:
6292
+ return None
6293
+ states = dict(queue_payload.get("message_states") or {})
6294
+ key = normalized_message_id or f"client:{normalized_client_message_id}"
6295
+ current = dict(states.get(key) or {})
6296
+ current["message_id"] = normalized_message_id or current.get("message_id")
6297
+ if normalized_client_message_id:
6298
+ current["client_message_id"] = normalized_client_message_id
6299
+ if source:
6300
+ current["source"] = str(source)
6301
+ if conversation_id:
6302
+ current["conversation_id"] = str(conversation_id)
6303
+ if created_at:
6304
+ current["created_at"] = str(created_at)
6305
+ current["read_state"] = self._normalize_message_read_state(read_state)
6306
+ current["read_reason"] = str(read_reason or "").strip() or None
6307
+ current["read_at"] = str(read_at or "").strip() or None
6308
+ current["read_interaction_id"] = str(read_interaction_id or "").strip() or None
6309
+ current["read_run_id"] = str(read_run_id or "").strip() or None
6310
+ current["updated_at"] = utc_now()
6311
+ states[key] = current
6312
+ if normalized_message_id:
6313
+ for existing_key, item in list(states.items()):
6314
+ if existing_key == key or not isinstance(item, dict):
6315
+ continue
6316
+ if str(item.get("message_id") or "").strip() == normalized_message_id:
6317
+ states.pop(existing_key, None)
6318
+ if normalized_client_message_id:
6319
+ states[normalized_message_id] = current
6320
+ if key != normalized_message_id:
6321
+ states.pop(key, None)
6322
+ key = normalized_message_id
6323
+ queue_payload["message_states"] = states
6324
+ return dict(states.get(key) or current)
6325
+
6326
+ def _message_read_state(
6327
+ self,
6328
+ quest_root: Path,
6329
+ *,
6330
+ message_id: str | None = None,
6331
+ client_message_id: str | None = None,
6332
+ ) -> dict[str, Any] | None:
6333
+ queue_payload = self._read_message_queue(quest_root)
6334
+ states = queue_payload.get("message_states")
6335
+ if not isinstance(states, dict):
6336
+ return None
6337
+ normalized_message_id = str(message_id or "").strip()
6338
+ normalized_client_message_id = str(client_message_id or "").strip()
6339
+ if normalized_message_id:
6340
+ direct = states.get(normalized_message_id)
6341
+ if isinstance(direct, dict):
6342
+ return dict(direct)
6343
+ for item in states.values():
6344
+ if not isinstance(item, dict):
6345
+ continue
6346
+ if normalized_message_id and str(item.get("message_id") or "").strip() == normalized_message_id:
6347
+ return dict(item)
6348
+ if normalized_client_message_id and str(item.get("client_message_id") or "").strip() == normalized_client_message_id:
6349
+ return dict(item)
6350
+ return None
6351
+
6352
+ @staticmethod
6353
+ def _find_message_queue_entry(
6354
+ items: list[dict[str, Any]],
6355
+ *,
6356
+ message_id: str | None = None,
6357
+ client_message_id: str | None = None,
6358
+ ) -> tuple[int, dict[str, Any]] | tuple[None, None]:
6359
+ normalized_message_id = str(message_id or "").strip()
6360
+ normalized_client_message_id = str(client_message_id or "").strip()
6361
+ if not normalized_message_id and not normalized_client_message_id:
6362
+ return None, None
6363
+ for index in range(len(items) - 1, -1, -1):
6364
+ item = items[index]
6365
+ if normalized_message_id and str(item.get("message_id") or "").strip() == normalized_message_id:
6366
+ return index, dict(item)
6367
+ if normalized_client_message_id and str(item.get("client_message_id") or "").strip() == normalized_client_message_id:
6368
+ return index, dict(item)
6369
+ return None, None
6370
+
6371
+ def pending_user_message_status(
6372
+ self,
6373
+ quest_root: Path,
6374
+ *,
6375
+ message_id: str | None = None,
6376
+ client_message_id: str | None = None,
6377
+ ) -> dict[str, Any]:
6378
+ queue_payload = self._read_message_queue(quest_root)
6379
+ pending = [dict(item) for item in (queue_payload.get("pending") or []) if isinstance(item, dict)]
6380
+ completed = [dict(item) for item in (queue_payload.get("completed") or []) if isinstance(item, dict)]
6381
+ pending_index, pending_item = self._find_message_queue_entry(
6382
+ pending,
6383
+ message_id=message_id,
6384
+ client_message_id=client_message_id,
6385
+ )
6386
+ state_record = self._message_read_state(
6387
+ quest_root,
6388
+ message_id=message_id,
6389
+ client_message_id=client_message_id,
6390
+ )
6391
+ completed_index, completed_item = self._find_message_queue_entry(
6392
+ completed,
6393
+ message_id=message_id,
6394
+ client_message_id=client_message_id,
6395
+ )
6396
+ queue_state = "missing"
6397
+ if pending_index is not None and pending_item is not None:
6398
+ queue_state = "pending"
6399
+ elif state_record:
6400
+ read_reason = str(state_record.get("read_reason") or "").strip()
6401
+ queue_state = "withdrawn" if read_reason == "withdrawn_by_user" else "read"
6402
+ elif completed_index is not None and completed_item is not None:
6403
+ completed_status = str(completed_item.get("status") or "").strip()
6404
+ queue_state = "withdrawn" if completed_status == "withdrawn_by_user" else "read"
6405
+ return {
6406
+ "queue_state": queue_state,
6407
+ "pending_index": pending_index,
6408
+ "pending_item": pending_item,
6409
+ "completed_index": completed_index,
6410
+ "completed_item": completed_item,
6411
+ "message_state": state_record,
6412
+ }
6413
+
6414
+ def latest_pending_user_message(self, quest_id: str) -> dict[str, Any] | None:
6415
+ quest_root = self._quest_root(quest_id)
6416
+ queue_payload = self._read_message_queue(quest_root)
6417
+ pending = [dict(item) for item in (queue_payload.get("pending") or []) if isinstance(item, dict)]
6418
+ if not pending:
6419
+ return None
6420
+ latest = dict(pending[-1])
6421
+ return {
6422
+ "id": latest.get("message_id"),
6423
+ "message_id": latest.get("message_id"),
6424
+ "client_message_id": latest.get("client_message_id"),
6425
+ "role": "user",
6426
+ "source": latest.get("source"),
6427
+ "content": latest.get("content") or "",
6428
+ "created_at": latest.get("created_at"),
6429
+ "reply_to_interaction_id": latest.get("reply_to_interaction_id"),
6430
+ "attachments": [dict(item) for item in (latest.get("attachments") or []) if isinstance(item, dict)],
6431
+ }
6432
+
6433
+ def withdraw_pending_user_message(
6434
+ self,
6435
+ quest_id: str,
6436
+ *,
6437
+ message_id: str | None,
6438
+ source: str,
6439
+ ) -> dict[str, Any]:
6440
+ normalized_message_id = str(message_id or "").strip()
6441
+ if not normalized_message_id:
6442
+ return {
6443
+ "ok": False,
6444
+ "status": "invalid_request",
6445
+ "message": "Message id is required.",
6446
+ }
6447
+ quest_root = self._quest_root(quest_id)
6448
+ queue_payload = self._read_message_queue(quest_root)
6449
+ status_payload = self.pending_user_message_status(
6450
+ quest_root,
6451
+ message_id=normalized_message_id,
6452
+ )
6453
+ pending_index = status_payload.get("pending_index")
6454
+ pending_item = dict(status_payload.get("pending_item") or {}) if isinstance(status_payload.get("pending_item"), dict) else None
6455
+ if pending_index is None or pending_item is None:
6456
+ queue_state = str(status_payload.get("queue_state") or "missing")
6457
+ message_state = (
6458
+ dict(status_payload.get("message_state") or {})
6459
+ if isinstance(status_payload.get("message_state"), dict)
6460
+ else None
6461
+ )
6462
+ if queue_state == "read":
6463
+ return {
6464
+ "ok": False,
6465
+ "status": "already_read",
6466
+ "message": "Withdrawal failed because this message was already sent to the agent.",
6467
+ "message_id": normalized_message_id,
6468
+ "current_message_state": message_state,
6469
+ }
6470
+ if queue_state == "withdrawn":
6471
+ return {
6472
+ "ok": True,
6473
+ "status": "already_withdrawn",
6474
+ "message": "This message was already withdrawn from the waiting queue.",
6475
+ "message_id": normalized_message_id,
6476
+ "current_message_state": message_state,
6477
+ }
6478
+ return {
6479
+ "ok": False,
6480
+ "status": "missing",
6481
+ "message": f"Message `{normalized_message_id}` is not waiting in the queue.",
6482
+ "message_id": normalized_message_id,
6483
+ "current_message_state": message_state,
6484
+ }
6485
+
6486
+ pending = [dict(item) for item in (queue_payload.get("pending") or []) if isinstance(item, dict)]
6487
+ completed = [dict(item) for item in (queue_payload.get("completed") or []) if isinstance(item, dict)]
6488
+ withdrawn_at = utc_now()
6489
+ withdrawn = {
6490
+ **pending.pop(int(pending_index)),
6491
+ "status": "withdrawn_by_user",
6492
+ "withdrawn_at": withdrawn_at,
6493
+ "withdrawn_by_source": source,
6494
+ }
6495
+ queue_payload["pending"] = pending
6496
+ queue_payload["completed"] = [*completed, withdrawn][-200:]
6497
+ state_record = self._update_message_read_state(
6498
+ queue_payload,
6499
+ message_id=str(withdrawn.get("message_id") or "").strip() or None,
6500
+ client_message_id=str(withdrawn.get("client_message_id") or "").strip() or None,
6501
+ source=str(withdrawn.get("source") or "").strip() or None,
6502
+ conversation_id=str(withdrawn.get("conversation_id") or "").strip() or None,
6503
+ created_at=str(withdrawn.get("created_at") or "").strip() or None,
6504
+ read_state="read",
6505
+ read_reason="withdrawn_by_user",
6506
+ read_at=withdrawn_at,
6507
+ )
6508
+ self._write_message_queue(quest_root, queue_payload)
6509
+ self.update_runtime_state(
6510
+ quest_root=quest_root,
6511
+ pending_user_message_count=len(pending),
6512
+ )
6513
+ self.append_message_read_state_event(
6514
+ quest_id,
6515
+ message_id=str(withdrawn.get("message_id") or "").strip() or None,
6516
+ client_message_id=str(withdrawn.get("client_message_id") or "").strip() or None,
6517
+ read_state=str((state_record or {}).get("read_state") or "read"),
6518
+ read_reason=str((state_record or {}).get("read_reason") or "withdrawn_by_user"),
6519
+ read_at=str((state_record or {}).get("read_at") or withdrawn_at),
6520
+ )
6521
+ append_jsonl(
6522
+ self._interaction_journal_path(quest_root),
6523
+ {
6524
+ "event_id": generate_id("evt"),
6525
+ "type": "user_message_withdrawn",
6526
+ "quest_id": quest_id,
6527
+ "message_id": normalized_message_id,
6528
+ "source": source,
6529
+ "created_at": withdrawn_at,
6530
+ },
6531
+ )
6532
+ self._write_active_user_requirements(quest_root, latest_requirement=None)
6533
+ return {
6534
+ "ok": True,
6535
+ "status": "withdrawn",
6536
+ "message": "The message was removed from the waiting queue.",
6537
+ "message_id": normalized_message_id,
6538
+ "pending_user_message_count": len(pending),
6539
+ "current_message_state": state_record,
6540
+ }
6541
+
6542
+ def enrich_conversation_message_event(self, quest_id: str, event: dict[str, Any]) -> dict[str, Any]:
6543
+ if str(event.get("type") or "").strip() != "conversation.message":
6544
+ return dict(event)
6545
+ quest_root = self._quest_root(quest_id)
6546
+ read_state = self._message_read_state(
6547
+ quest_root,
6548
+ message_id=str(event.get("message_id") or "").strip() or None,
6549
+ client_message_id=str(event.get("client_message_id") or "").strip() or None,
6550
+ )
6551
+ enriched = dict(event)
6552
+ if read_state:
6553
+ enriched["read_state"] = read_state.get("read_state")
6554
+ enriched["read_reason"] = read_state.get("read_reason")
6555
+ enriched["read_at"] = read_state.get("read_at")
6556
+ return enriched
6557
+
6558
+ def append_message_read_state_event(
6559
+ self,
6560
+ quest_id: str,
6561
+ *,
6562
+ message_id: str | None,
6563
+ client_message_id: str | None = None,
6564
+ read_state: str,
6565
+ read_reason: str | None = None,
6566
+ read_at: str | None = None,
6567
+ ) -> dict[str, Any]:
6568
+ payload = {
6569
+ "event_id": generate_id("evt"),
6570
+ "type": "conversation.message_state",
6571
+ "quest_id": quest_id,
6572
+ "message_id": str(message_id or "").strip() or None,
6573
+ "client_message_id": str(client_message_id or "").strip() or None,
6574
+ "read_state": self._normalize_message_read_state(read_state),
6575
+ "read_reason": str(read_reason or "").strip() or None,
6576
+ "read_at": str(read_at or "").strip() or None,
6577
+ "created_at": utc_now(),
6578
+ }
6579
+ append_jsonl(self._quest_root(quest_id) / ".ds" / "events.jsonl", payload)
6580
+ return payload
6581
+
6582
+ def set_turn_message_override(
6583
+ self,
6584
+ quest_root: Path,
6585
+ *,
6586
+ turn_reason: str,
6587
+ message: str,
6588
+ message_ids: list[str] | None = None,
6589
+ delivery_batch_id: str | None = None,
6590
+ source: str | None = None,
6591
+ ) -> dict[str, Any]:
6592
+ payload = {
6593
+ "turn_reason": str(turn_reason or "").strip() or "user_message",
6594
+ "message": str(message or "").strip(),
6595
+ "message_ids": [str(item).strip() for item in (message_ids or []) if str(item).strip()],
6596
+ "delivery_batch_id": str(delivery_batch_id or "").strip() or None,
6597
+ "source": str(source or "").strip() or None,
6598
+ "created_at": utc_now(),
6599
+ }
6600
+ self.update_runtime_state(
6601
+ quest_root=quest_root,
6602
+ turn_message_override=payload,
6603
+ )
6604
+ return payload
6605
+
6606
+ def consume_turn_message_override(
6607
+ self,
6608
+ quest_root: Path,
6609
+ *,
6610
+ expected_turn_reason: str | None = None,
6611
+ ) -> dict[str, Any] | None:
6612
+ with self._runtime_state_lock(quest_root):
6613
+ state = self._read_runtime_state(quest_root)
6614
+ override = dict(state.get("turn_message_override") or {}) if isinstance(state.get("turn_message_override"), dict) else None
6615
+ if not override:
6616
+ return None
6617
+ if expected_turn_reason:
6618
+ normalized_expected = str(expected_turn_reason or "").strip()
6619
+ normalized_actual = str(override.get("turn_reason") or "").strip()
6620
+ if normalized_expected and normalized_actual != normalized_expected:
6621
+ return None
6622
+ state["turn_message_override"] = None
6623
+ self._write_runtime_state(quest_root, state)
6624
+ return override
6625
+
5552
6626
  def _enqueue_user_message(self, quest_root: Path, record: dict[str, Any]) -> dict[str, Any]:
5553
6627
  queue_payload = self._read_message_queue(quest_root)
5554
6628
  source = str(record.get("source") or "local")
5555
6629
  queue_record = {
5556
6630
  "message_id": record.get("id"),
6631
+ "client_message_id": str(record.get("client_message_id") or "").strip() or None,
5557
6632
  "source": source,
5558
6633
  "conversation_id": self._normalize_binding_source(source),
5559
6634
  "content": record.get("content") or "",
@@ -5563,6 +6638,17 @@ class QuestService:
5563
6638
  "status": "queued",
5564
6639
  }
5565
6640
  queue_payload["pending"] = [*list(queue_payload.get("pending") or []), queue_record]
6641
+ self._update_message_read_state(
6642
+ queue_payload,
6643
+ message_id=str(queue_record.get("message_id") or "").strip() or None,
6644
+ client_message_id=str(queue_record.get("client_message_id") or "").strip() or None,
6645
+ source=source,
6646
+ conversation_id=str(queue_record.get("conversation_id") or "").strip() or None,
6647
+ created_at=str(queue_record.get("created_at") or "").strip() or None,
6648
+ read_state="unread",
6649
+ read_reason="queued",
6650
+ read_at=None,
6651
+ )
5566
6652
  self._write_message_queue(quest_root, queue_payload)
5567
6653
  self.update_runtime_state(
5568
6654
  quest_root=quest_root,
@@ -5592,7 +6678,27 @@ class QuestService:
5592
6678
  for item in read_jsonl(quest_root / ".ds" / "conversations" / "main.jsonl")
5593
6679
  if str(item.get("role") or "") == "user"
5594
6680
  ]
5595
- latest = latest_requirement or (user_messages[-1] if user_messages else None)
6681
+ filtered_user_messages = []
6682
+ for item in user_messages:
6683
+ state = self._message_read_state(
6684
+ quest_root,
6685
+ message_id=str(item.get("id") or "").strip() or None,
6686
+ client_message_id=str(item.get("client_message_id") or "").strip() or None,
6687
+ )
6688
+ if str((state or {}).get("read_reason") or "").strip() == "withdrawn_by_user":
6689
+ continue
6690
+ filtered_user_messages.append(item)
6691
+ latest = latest_requirement
6692
+ if latest is not None:
6693
+ latest_state = self._message_read_state(
6694
+ quest_root,
6695
+ message_id=str(latest.get("id") or "").strip() or None,
6696
+ client_message_id=str(latest.get("client_message_id") or "").strip() or None,
6697
+ )
6698
+ if str((latest_state or {}).get("read_reason") or "").strip() == "withdrawn_by_user":
6699
+ latest = None
6700
+ if latest is None:
6701
+ latest = filtered_user_messages[-1] if filtered_user_messages else None
5596
6702
  lines = [
5597
6703
  "# Active User Requirements",
5598
6704
  "",
@@ -5611,12 +6717,17 @@ class QuestService:
5611
6717
  "",
5612
6718
  ]
5613
6719
  if latest:
6720
+ latest_rendered = self._agent_visible_user_message_content(
6721
+ quest_root,
6722
+ content=str(latest.get("content") or ""),
6723
+ attachments=[dict(item) for item in (latest.get("attachments") or []) if isinstance(item, dict)],
6724
+ )
5614
6725
  lines.extend(
5615
6726
  [
5616
6727
  f"- source: {latest.get('source') or 'local'}",
5617
6728
  f"- created_at: {latest.get('created_at') or utc_now()}",
5618
6729
  "",
5619
- str(latest.get("content") or "").strip() or "No latest requirement text was captured.",
6730
+ latest_rendered or "No latest requirement text was captured.",
5620
6731
  "",
5621
6732
  ]
5622
6733
  )
@@ -5633,11 +6744,15 @@ class QuestService:
5633
6744
  "",
5634
6745
  ]
5635
6746
  )
5636
- if user_messages:
5637
- for index, item in enumerate(user_messages[-12:], start=1):
6747
+ if filtered_user_messages:
6748
+ for index, item in enumerate(filtered_user_messages[-12:], start=1):
5638
6749
  source = str(item.get("source") or "local").strip() or "local"
5639
6750
  created_at = str(item.get("created_at") or "").strip() or "unknown"
5640
- content = str(item.get("content") or "").strip() or "(empty)"
6751
+ content = self._agent_visible_user_message_content(
6752
+ quest_root,
6753
+ content=str(item.get("content") or ""),
6754
+ attachments=[dict(value) for value in (item.get("attachments") or []) if isinstance(value, dict)],
6755
+ ) or "(empty)"
5641
6756
  lines.append(f"{index}. [{source}] [{created_at}] {content}")
5642
6757
  else:
5643
6758
  lines.append("1. No user messages yet.")
@@ -5679,11 +6794,31 @@ class QuestService:
5679
6794
  }
5680
6795
  queue_payload["pending"] = pending
5681
6796
  queue_payload["completed"] = [*list(queue_payload.get("completed") or []), claimed][-200:]
6797
+ state_record = self._update_message_read_state(
6798
+ queue_payload,
6799
+ message_id=str(claimed.get("message_id") or "").strip() or None,
6800
+ client_message_id=str(claimed.get("client_message_id") or "").strip() or None,
6801
+ source=str(claimed.get("source") or "").strip() or None,
6802
+ conversation_id=str(claimed.get("conversation_id") or "").strip() or None,
6803
+ created_at=str(claimed.get("created_at") or "").strip() or None,
6804
+ read_state="read",
6805
+ read_reason="accepted_by_run",
6806
+ read_at=now,
6807
+ read_run_id=run_id,
6808
+ )
5682
6809
  self._write_message_queue(quest_root, queue_payload)
5683
6810
  self.update_runtime_state(
5684
6811
  quest_root=quest_root,
5685
6812
  pending_user_message_count=len(pending),
5686
6813
  )
6814
+ self.append_message_read_state_event(
6815
+ quest_id,
6816
+ message_id=str(claimed.get("message_id") or "").strip() or None,
6817
+ client_message_id=str(claimed.get("client_message_id") or "").strip() or None,
6818
+ read_state=str((state_record or {}).get("read_state") or "read"),
6819
+ read_reason=str((state_record or {}).get("read_reason") or "accepted_by_run"),
6820
+ read_at=str((state_record or {}).get("read_at") or now),
6821
+ )
5687
6822
  append_jsonl(
5688
6823
  self._interaction_journal_path(quest_root),
5689
6824
  {
@@ -5733,6 +6868,26 @@ class QuestService:
5733
6868
  ]
5734
6869
  queue_payload["pending"] = []
5735
6870
  queue_payload["completed"] = [*list(queue_payload.get("completed") or []), *cancelled][-200:]
6871
+ for item in cancelled:
6872
+ self._update_message_read_state(
6873
+ queue_payload,
6874
+ message_id=str(item.get("message_id") or "").strip() or None,
6875
+ client_message_id=str(item.get("client_message_id") or "").strip() or None,
6876
+ source=str(item.get("source") or "").strip() or None,
6877
+ conversation_id=str(item.get("conversation_id") or "").strip() or None,
6878
+ created_at=str(item.get("created_at") or "").strip() or None,
6879
+ read_state="read",
6880
+ read_reason=reason,
6881
+ read_at=now,
6882
+ )
6883
+ self.append_message_read_state_event(
6884
+ quest_id,
6885
+ message_id=str(item.get("message_id") or "").strip() or None,
6886
+ client_message_id=str(item.get("client_message_id") or "").strip() or None,
6887
+ read_state="read",
6888
+ read_reason=reason,
6889
+ read_at=now,
6890
+ )
5736
6891
  self._write_message_queue(quest_root, queue_payload)
5737
6892
  append_jsonl(
5738
6893
  self._interaction_journal_path(quest_root),
@@ -5892,11 +7047,13 @@ class QuestService:
5892
7047
  *,
5893
7048
  interaction_id: str | None,
5894
7049
  limit: int = 10,
7050
+ delivery_reason: str = "artifact_mailbox",
5895
7051
  ) -> dict[str, Any]:
5896
7052
  queue_payload = self._read_message_queue(quest_root)
5897
7053
  pending = [dict(item) for item in (queue_payload.get("pending") or [])]
5898
7054
  recent_records = self.latest_artifact_interaction_records(quest_root, limit=max(limit, 10))
5899
7055
  delivered_messages: list[dict[str, Any]] = []
7056
+ delivered_state_records: list[dict[str, Any]] = []
5900
7057
  delivery_batch = None
5901
7058
  now = utc_now()
5902
7059
 
@@ -5913,6 +7070,29 @@ class QuestService:
5913
7070
  delivered_messages.append(delivered)
5914
7071
  queue_payload["pending"] = []
5915
7072
  queue_payload["completed"] = [*list(queue_payload.get("completed") or []), *delivered_messages][-200:]
7073
+ for item in delivered_messages:
7074
+ state_record = self._update_message_read_state(
7075
+ queue_payload,
7076
+ message_id=str(item.get("message_id") or "").strip() or None,
7077
+ client_message_id=str(item.get("client_message_id") or "").strip() or None,
7078
+ source=str(item.get("source") or "").strip() or None,
7079
+ conversation_id=str(item.get("conversation_id") or "").strip() or None,
7080
+ created_at=str(item.get("created_at") or "").strip() or None,
7081
+ read_state="read",
7082
+ read_reason=delivery_reason,
7083
+ read_at=now,
7084
+ read_interaction_id=interaction_id,
7085
+ )
7086
+ self.append_message_read_state_event(
7087
+ quest_root.name,
7088
+ message_id=str(item.get("message_id") or "").strip() or None,
7089
+ client_message_id=str(item.get("client_message_id") or "").strip() or None,
7090
+ read_state=str((state_record or {}).get("read_state") or "read"),
7091
+ read_reason=str((state_record or {}).get("read_reason") or delivery_reason),
7092
+ read_at=str((state_record or {}).get("read_at") or now),
7093
+ )
7094
+ if state_record:
7095
+ delivered_state_records.append(dict(state_record))
5916
7096
  self._write_message_queue(quest_root, queue_payload)
5917
7097
  append_jsonl(
5918
7098
  self._interaction_journal_path(quest_root),
@@ -5935,6 +7115,8 @@ class QuestService:
5935
7115
  delivery_batch = {
5936
7116
  "batch_id": batch_id,
5937
7117
  "message_ids": [item.get("message_id") for item in delivered_messages],
7118
+ "client_message_ids": [item.get("client_message_id") for item in delivered_messages],
7119
+ "delivered_at": now,
5938
7120
  }
5939
7121
  else:
5940
7122
  self.update_runtime_state(
@@ -5942,15 +7124,45 @@ class QuestService:
5942
7124
  pending_user_message_count=0,
5943
7125
  )
5944
7126
 
7127
+ state_by_message_id = {
7128
+ str(item.get("message_id") or "").strip(): dict(item)
7129
+ for item in delivered_state_records
7130
+ if str(item.get("message_id") or "").strip()
7131
+ }
7132
+ state_by_client_message_id = {
7133
+ str(item.get("client_message_id") or "").strip(): dict(item)
7134
+ for item in delivered_state_records
7135
+ if str(item.get("client_message_id") or "").strip()
7136
+ }
5945
7137
  recent_inbound_messages = [
5946
7138
  {
5947
7139
  "message_id": item.get("message_id"),
7140
+ "client_message_id": item.get("client_message_id"),
5948
7141
  "source": str(item.get("conversation_id") or item.get("source") or "local").split(":", 1)[0],
5949
7142
  "conversation_id": item.get("conversation_id") or self._normalize_binding_source(str(item.get("source") or "local")),
5950
7143
  "sender": "user",
5951
7144
  "created_at": item.get("created_at"),
7145
+ "read_state": (
7146
+ state_by_message_id.get(str(item.get("message_id") or "").strip())
7147
+ or state_by_client_message_id.get(str(item.get("client_message_id") or "").strip())
7148
+ or {}
7149
+ ).get("read_state") or "read",
7150
+ "read_reason": (
7151
+ state_by_message_id.get(str(item.get("message_id") or "").strip())
7152
+ or state_by_client_message_id.get(str(item.get("client_message_id") or "").strip())
7153
+ or {}
7154
+ ).get("read_reason") or delivery_reason,
7155
+ "read_at": (
7156
+ state_by_message_id.get(str(item.get("message_id") or "").strip())
7157
+ or state_by_client_message_id.get(str(item.get("client_message_id") or "").strip())
7158
+ or {}
7159
+ ).get("read_at") or now,
5952
7160
  "text": item.get("content") or "",
5953
- "content": item.get("content") or "",
7161
+ "content": self._agent_visible_user_message_content(
7162
+ quest_root,
7163
+ content=str(item.get("content") or ""),
7164
+ attachments=[dict(attachment) for attachment in (item.get("attachments") or []) if isinstance(attachment, dict)],
7165
+ ),
5954
7166
  "attachments": [dict(attachment) for attachment in (item.get("attachments") or []) if isinstance(attachment, dict)],
5955
7167
  "reply_to_interaction_id": item.get("reply_to_interaction_id"),
5956
7168
  }
@@ -5998,7 +7210,12 @@ class QuestService:
5998
7210
  ]
5999
7211
  for index, item in enumerate(delivered_messages, start=1):
6000
7212
  source = str(item.get("conversation_id") or item.get("source") or "local")
6001
- lines.append(f"{index}. [{source}] {item.get('content') or ''}")
7213
+ rendered_content = self._agent_visible_user_message_content(
7214
+ quest_root,
7215
+ content=str(item.get("content") or ""),
7216
+ attachments=[dict(attachment) for attachment in (item.get("attachments") or []) if isinstance(attachment, dict)],
7217
+ )
7218
+ lines.append(f"{index}. [{source}] {rendered_content}")
6002
7219
  agent_instruction = "\n".join(lines).strip()
6003
7220
  else:
6004
7221
  lines = [
@@ -6039,12 +7256,60 @@ class QuestService:
6039
7256
  return {
6040
7257
  "delivery_batch": delivery_batch,
6041
7258
  "recent_inbound_messages": recent_inbound_messages,
7259
+ "message_states": delivered_state_records,
6042
7260
  "recent_interaction_records": recent_records[-10:],
6043
7261
  "agent_instruction": agent_instruction,
6044
7262
  "queued_message_count_before_delivery": len(pending),
6045
7263
  "queued_message_count_after_delivery": len(queue_payload.get("pending") or []),
6046
7264
  }
6047
7265
 
7266
+ def _agent_visible_user_message_content(
7267
+ self,
7268
+ quest_root: Path,
7269
+ *,
7270
+ content: str,
7271
+ attachments: list[dict[str, Any]] | None = None,
7272
+ ) -> str:
7273
+ base = str(content or "").strip()
7274
+ normalized_attachments = [dict(item) for item in (attachments or []) if isinstance(item, dict)]
7275
+ if not normalized_attachments:
7276
+ return base
7277
+ lines: list[str] = []
7278
+ if base:
7279
+ lines.extend([base, ""])
7280
+ lines.append(
7281
+ self.localized_copy(
7282
+ quest_root=quest_root,
7283
+ zh="系统提示:用户刚刚发送了附件。请优先阅读这些 quest 本地文件,再继续处理这条请求:",
7284
+ en="System note: the user just sent attachments. Read these quest-local files first before continuing this request:",
7285
+ )
7286
+ )
7287
+ for index, item in enumerate(normalized_attachments, start=1):
7288
+ label = str(
7289
+ item.get("name")
7290
+ or item.get("file_name")
7291
+ or item.get("quest_relative_path")
7292
+ or item.get("path")
7293
+ or item.get("url")
7294
+ or f"attachment-{index}"
7295
+ ).strip()
7296
+ content_type = str(item.get("content_type") or item.get("mime_type") or "").strip()
7297
+ location = str(
7298
+ item.get("extracted_text_path")
7299
+ or item.get("ocr_text_path")
7300
+ or item.get("archive_manifest_path")
7301
+ or item.get("quest_relative_path")
7302
+ or item.get("url")
7303
+ or ("hidden" if str(item.get("path") or "").strip() else "unavailable")
7304
+ ).strip()
7305
+ error = str(item.get("download_error") or item.get("path_error") or "").strip()
7306
+ suffix = f" ({content_type})" if content_type else ""
7307
+ if error:
7308
+ lines.append(f"- {label}{suffix}: {location} | attachment_error={error}")
7309
+ else:
7310
+ lines.append(f"- {label}{suffix}: {location}")
7311
+ return "\n".join(lines).strip()
7312
+
6048
7313
  @staticmethod
6049
7314
  def _document_resolution_root(quest_root: Path, workspace_root: Path, document_id: str) -> Path:
6050
7315
  if document_id.startswith(("questpath::", "memory::")):
@@ -6406,19 +7671,36 @@ class QuestService:
6406
7671
  return True
6407
7672
  if relative.startswith(".ds/worktrees/"):
6408
7673
  return True
7674
+ heavy_runtime_roots = {
7675
+ ".ds/bash_exec",
7676
+ ".ds/runs",
7677
+ ".ds/codex_history",
7678
+ ".ds/codex_homes",
7679
+ ".ds/claude-home",
7680
+ ".ds/opencode-home",
7681
+ ".ds/evidence_packets",
7682
+ ".ds/slim_backups",
7683
+ ".ds/cold_archive",
7684
+ }
7685
+ normalized = relative.strip("/")
7686
+ if any(normalized == root or normalized.startswith(f"{root}/") for root in heavy_runtime_roots):
7687
+ return True
6409
7688
  parts = PurePosixPath(relative).parts
6410
7689
  return "__pycache__" in parts or ".pytest_cache" in parts
6411
7690
 
6412
7691
  @staticmethod
6413
7692
  def _skip_explorer_profile_relative(relative: str, profile: str | None) -> bool:
6414
- if profile != "mobile":
7693
+ profile = str(profile or "").strip().lower()
7694
+ if profile not in {"mobile", "workspace"}:
6415
7695
  return False
6416
7696
  normalized = relative.strip("/")
6417
7697
  if not normalized:
6418
7698
  return False
6419
7699
  parts = PurePosixPath(normalized).parts
6420
7700
  top = parts[0] if parts else normalized
6421
- if top in {".codex", ".claude", ".ds", "tmp", "userfiles", "artifacts"}:
7701
+ if top in {".codex", ".claude", ".kimi", ".opencode", ".ds", "tmp", "userfiles"}:
7702
+ return True
7703
+ if profile == "mobile" and top == "artifacts":
6422
7704
  return True
6423
7705
  if top.startswith(".") and normalized not in {".gitignore"}:
6424
7706
  return True
@@ -6426,7 +7708,8 @@ class QuestService:
6426
7708
 
6427
7709
  @staticmethod
6428
7710
  def _truncate_explorer_directory(relative: str, *, profile: str | None, depth: int) -> bool:
6429
- if profile != "mobile":
7711
+ profile = str(profile or "").strip().lower()
7712
+ if profile not in {"mobile", "workspace"}:
6430
7713
  return False
6431
7714
  normalized = relative.strip("/")
6432
7715
  if not normalized:
@@ -6435,11 +7718,19 @@ class QuestService:
6435
7718
  top = parts[0] if parts else normalized
6436
7719
  if top == "memory":
6437
7720
  return False
7721
+ if profile == "mobile":
7722
+ if top == "baselines":
7723
+ return depth >= 1
7724
+ if top in {"literature", "paper", "experiments", "handoffs"}:
7725
+ return depth >= 2
7726
+ return depth >= 1
6438
7727
  if top == "baselines":
7728
+ return depth >= 2
7729
+ if top == "artifacts":
6439
7730
  return depth >= 1
6440
7731
  if top in {"literature", "paper", "experiments", "handoffs"}:
6441
- return depth >= 2
6442
- return depth >= 1
7732
+ return depth >= 3
7733
+ return depth >= 2
6443
7734
 
6444
7735
  @staticmethod
6445
7736
  def _classify_path_scope(quest_root: Path, path: Path) -> tuple[str, bool]: