@furongjun1999/dsh-memory 0.4.11 → 0.5.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 (504) hide show
  1. package/README.md +16 -16
  2. package/codebuddy/CODEBUDDY.md +11 -3
  3. package/codebuddy/README.md +92 -90
  4. package/codebuddy/mcp.json +27 -27
  5. package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -169
  6. package/docs/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -144
  7. package/docs/README.md +142 -111
  8. package/docs/discipline/harnesses.yaml +244 -226
  9. package/docs/discipline/templates/full.md.tmpl +61 -61
  10. package/docs/discipline/templates/rules.mdc.tmpl +68 -0
  11. package/docs/discipline/templates/skill.md.tmpl +23 -23
  12. package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md +299 -299
  13. package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md +239 -239
  14. package/docs/eval//345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md +174 -174
  15. package/docs/eval//346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md +223 -223
  16. package/docs/{mdcg → hive}//344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md +165 -160
  17. package/docs/hive//344/273/244/347/211/214/350/257/255/344/271/211/344/277/256/346/255/243_/344/273/262/350/243/201/344/275/215_v0.1.md +76 -0
  18. package/docs/{mdcg → hive}//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/206_v0.5.md +236 -236
  19. package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -43
  20. package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +85 -0
  21. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +102 -102
  22. package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +47 -0
  23. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +503 -503
  24. package/docs/hive//350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +85 -85
  25. package/docs/hive//350/234/202/345/267/242/350/256/276/350/256/241_/347/220/206/350/256/272/345/257/271/351/275/220_v0.1.md +191 -0
  26. package/docs/hive//350/234/202/345/267/242/350/277/255/344/273/243_/345/256/217/350/247/202/344/270/216/347/276/244/344/275/223/350/260/203/345/272/246_v0.1.md +90 -0
  27. package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -216
  28. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +646 -646
  29. package/docs/mdcg/lingshu_tutorial.html +14449 -14449
  30. package/docs/mdcg/release_v0.4.11.md +49 -0
  31. package/docs/mdcg/release_v0.4.5.md +55 -55
  32. package/docs/mdcg/tool_table_v0.3.0.md +117 -117
  33. package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -600
  34. package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
  35. package/docs/mdcg//345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +275 -275
  36. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +54 -0
  37. package/docs/mdcg//346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +130 -130
  38. package/docs/mdcg//347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md +278 -278
  39. package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -172
  40. package/docs/mdcg//347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +270 -270
  41. package/docs/mdcg//350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +491 -491
  42. package/docs/mdcg//350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md +340 -340
  43. package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +588 -588
  44. package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -176
  45. package/docs/plans//345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +81 -81
  46. package/docs/swarm//350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +704 -704
  47. package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -242
  48. package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -129
  49. package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +134 -134
  50. package/docs/theory//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
  51. package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -145
  52. package/docs/theory//347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +144 -144
  53. package/docs/theory//347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +198 -198
  54. package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +221 -221
  55. package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -237
  56. package/docs//345/217/221/345/270/203/344/273/266/345/233/236/346/272/257/350/257/264/346/230/216_v0.1.md +82 -82
  57. package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +28 -2
  58. package/dsh/README.md +82 -82
  59. package/dsh/cordis.yml.example +139 -139
  60. package/dsh/update-lingshu.bat +11 -11
  61. package/lib/hooks.js +36 -2
  62. package/lib/lib/roleplay_web.js +427 -427
  63. package/md_cg/__init__.py +7 -7
  64. package/md_cg/audit.py +368 -368
  65. package/md_cg/autonomy.py +287 -287
  66. package/md_cg/backfill.py +1327 -1327
  67. package/md_cg/backfill_bigdomain.py +34 -34
  68. package/md_cg/bench6_arms.py +410 -410
  69. package/md_cg/bench6_common.py +230 -230
  70. package/md_cg/bench6_competitors.py +212 -212
  71. package/md_cg/bench_axis_domain.py +257 -257
  72. package/md_cg/bench_blind_comp.py +308 -308
  73. package/md_cg/bench_en_atoms_public.py +230 -230
  74. package/md_cg/bench_governance.py +348 -348
  75. package/md_cg/bench_lme_zh.py +410 -410
  76. package/md_cg/bench_locomo.py +121 -121
  77. package/md_cg/bench_locomo_zh.py +450 -450
  78. package/md_cg/bench_locomo_zh_public.py +147 -147
  79. package/md_cg/bench_longmem.py +112 -112
  80. package/md_cg/bench_membench.py +632 -632
  81. package/md_cg/bench_p0.py +149 -149
  82. package/md_cg/bench_progressive.py +287 -287
  83. package/md_cg/bench_role_views.py +238 -238
  84. package/md_cg/bench_task_ab.py +243 -243
  85. package/md_cg/bench_task_ab_llm.py +408 -408
  86. package/md_cg/bench_unified_en.py +204 -204
  87. package/md_cg/bench_zh_mad.py +601 -601
  88. package/md_cg/blindspot_tickets.py +123 -123
  89. package/md_cg/branches.py +285 -285
  90. package/md_cg/build_postings.py +73 -73
  91. package/md_cg/ccgc.py +1005 -948
  92. package/md_cg/census.py +132 -132
  93. package/md_cg/chain.py +300 -300
  94. package/md_cg/codeindex.py +531 -531
  95. package/md_cg/coldverify.py +292 -292
  96. package/md_cg/comment_gate.py +337 -337
  97. package/md_cg/cond_compose.py +190 -190
  98. package/md_cg/cond_facts.py +154 -154
  99. package/md_cg/cond_template.json +106 -106
  100. package/md_cg/condition_anchor.py +142 -142
  101. package/md_cg/conformance.py +726 -726
  102. package/md_cg/consistency.py +717 -717
  103. package/md_cg/consolidate.py +1536 -1439
  104. package/md_cg/corpus.py +110 -110
  105. package/md_cg/crosscheck.py +1097 -1097
  106. package/md_cg/crypto.py +437 -437
  107. package/md_cg/d_meta.py +310 -310
  108. package/md_cg/datapath.py +334 -334
  109. package/md_cg/docindex.py +473 -473
  110. package/md_cg/eval_common.py +575 -575
  111. package/md_cg/evidence.py +580 -580
  112. package/md_cg/evolution.py +477 -477
  113. package/md_cg/export.py +220 -220
  114. package/md_cg/forgetting.py +581 -581
  115. package/md_cg/fsutil.py +329 -329
  116. package/md_cg/hotcache.py +238 -214
  117. package/md_cg/hyperedge.py +251 -251
  118. package/md_cg/identity.py +390 -390
  119. package/md_cg/insight.py +500 -500
  120. package/md_cg/interop.py +199 -0
  121. package/md_cg/lexicon/build_cedict_en_zh.py +329 -329
  122. package/md_cg/lexicon/build_standard_en.py +171 -171
  123. package/md_cg/lexicon/expand_en_zh.py +211 -211
  124. package/md_cg/lifecycle.py +272 -272
  125. package/md_cg/linkref.py +280 -280
  126. package/md_cg/links.py +622 -622
  127. package/md_cg/mcp_server.py +129 -32
  128. package/md_cg/md_whitebox.py +345 -345
  129. package/md_cg/mdcg.py +176 -117
  130. package/md_cg/mdcos.py +79 -13
  131. package/md_cg/metacognition.py +591 -591
  132. package/md_cg/migrate.py +119 -119
  133. package/md_cg/migrate_aeis.py +221 -221
  134. package/md_cg/migrate_roleplay.py +293 -293
  135. package/md_cg/migrate_wisdom_graph.py +360 -360
  136. package/md_cg/mreview/__init__.py +25 -25
  137. package/md_cg/mreview/__main__.py +110 -110
  138. package/md_cg/mreview/bundle.py +178 -178
  139. package/md_cg/mreview/candidates.py +262 -262
  140. package/md_cg/mreview/govern.py +693 -693
  141. package/md_cg/mreview/locate.py +939 -939
  142. package/md_cg/mreview/pipeline.py +728 -728
  143. package/md_cg/mreview/rules/duplication.json +21 -21
  144. package/md_cg/mreview/rules/field_coverage.json +54 -54
  145. package/md_cg/mreview/rules/source_license.json +21 -21
  146. package/md_cg/mreview/rules/template_flow.json +21 -21
  147. package/md_cg/mreview/ruleset.py +252 -252
  148. package/md_cg/nodefile.py +575 -575
  149. package/md_cg/pooling.py +484 -472
  150. package/md_cg/postings.py +298 -298
  151. package/md_cg/predict.py +1100 -1100
  152. package/md_cg/progressive.py +123 -123
  153. package/md_cg/protect.py +272 -272
  154. package/md_cg/protocol/md_cg_gate.proto +33 -33
  155. package/md_cg/protocol.py +372 -372
  156. package/md_cg/provenance.py +582 -582
  157. package/md_cg/reach.py +453 -453
  158. package/md_cg/readcache.py +85 -0
  159. package/md_cg/refindex.py +833 -833
  160. package/md_cg/refine.py +604 -604
  161. package/md_cg/roleviews.py +89 -89
  162. package/md_cg/routing.py +365 -365
  163. package/md_cg/scrub.py +852 -852
  164. package/md_cg/security.py +274 -274
  165. package/md_cg/self_state.py +1029 -1029
  166. package/md_cg/selfreport.py +151 -151
  167. package/md_cg/semantic/__init__.py +10 -10
  168. package/md_cg/semantic/canonical.py +122 -122
  169. package/md_cg/semantic/en_normalizer.py +364 -364
  170. package/md_cg/semantic/en_zh_map.json +28694 -0
  171. package/md_cg/semantic/export_en_zh_map.py +64 -0
  172. package/md_cg/semantic/unify.py +45 -0
  173. package/md_cg/semantic/zh_en_atoms.py +139 -139
  174. package/md_cg/signer.py +562 -562
  175. package/md_cg/sources.py +815 -582
  176. package/md_cg/statushdr.py +179 -179
  177. package/md_cg/stg.py +48 -37
  178. package/md_cg/subgraph.py +729 -729
  179. package/md_cg/sustain.py +1138 -1138
  180. package/md_cg/tasks.py +470 -470
  181. package/md_cg/test_action_derive.py +203 -203
  182. package/md_cg/test_audit_rotate.py +270 -270
  183. package/md_cg/test_autonomy.py +143 -143
  184. package/md_cg/test_bench_governance.py +102 -102
  185. package/md_cg/test_blindspot_tickets.py +166 -166
  186. package/md_cg/test_branches.py +249 -249
  187. package/md_cg/test_ccg_perturb.py +184 -184
  188. package/md_cg/test_ccgc.py +433 -433
  189. package/md_cg/test_census_prune.py +81 -81
  190. package/md_cg/test_cond_compose_anchors.py +76 -76
  191. package/md_cg/test_cond_match.py +165 -165
  192. package/md_cg/test_condition_anchor.py +81 -81
  193. package/md_cg/test_d_meta.py +412 -412
  194. package/md_cg/test_datapath_root.py +199 -199
  195. package/md_cg/test_en_pipeline.py +166 -166
  196. package/md_cg/test_gain_gate.py +212 -212
  197. package/md_cg/test_health_scale.py +173 -173
  198. package/md_cg/test_hive_ingest.py +285 -0
  199. package/md_cg/test_hot_cold.py +215 -215
  200. package/md_cg/test_hyperedge.py +245 -245
  201. package/md_cg/test_i26_empty_first_write.py +116 -0
  202. package/md_cg/test_i27_e041_identity.py +128 -0
  203. package/md_cg/test_i28_hotcache_prodpath.py +122 -0
  204. package/md_cg/test_identity_attribution.py +147 -147
  205. package/md_cg/test_index_durability.py +224 -224
  206. package/md_cg/test_interop.py +93 -0
  207. package/md_cg/test_lifecycle.py +309 -309
  208. package/md_cg/test_linkref.py +306 -306
  209. package/md_cg/test_lock.py +43 -43
  210. package/md_cg/test_md_access_parity.py +255 -255
  211. package/md_cg/test_md_writepath.py +345 -345
  212. package/md_cg/test_mdstore_search_parity.py +160 -0
  213. package/md_cg/test_mr_m2.py +587 -587
  214. package/md_cg/test_mr_m3.py +710 -710
  215. package/md_cg/test_mr_m4.py +485 -485
  216. package/md_cg/test_p0.py +250 -250
  217. package/md_cg/test_p1.py +316 -316
  218. package/md_cg/test_p10_identity.py +173 -173
  219. package/md_cg/test_p11_consistency.py +233 -233
  220. package/md_cg/test_p12_metacognition.py +212 -212
  221. package/md_cg/test_p13_encryption.py +241 -241
  222. package/md_cg/test_p14_sustain.py +249 -249
  223. package/md_cg/test_p15_scrub.py +280 -280
  224. package/md_cg/test_p16_self_state.py +301 -301
  225. package/md_cg/test_p17_predict.py +354 -354
  226. package/md_cg/test_p18_whitebox.py +171 -171
  227. package/md_cg/test_p19_migrate_roleplay.py +149 -149
  228. package/md_cg/test_p20_evolution.py +315 -315
  229. package/md_cg/test_p21_tokens.py +293 -270
  230. package/md_cg/test_p22_theory.py +175 -175
  231. package/md_cg/test_p23_links.py +311 -311
  232. package/md_cg/test_p24_evidence.py +227 -227
  233. package/md_cg/test_p25_weights.py +156 -156
  234. package/md_cg/test_p26_refindex.py +416 -416
  235. package/md_cg/test_p27_docindex.py +765 -765
  236. package/md_cg/test_p28_refcheck.py +305 -305
  237. package/md_cg/test_p29_session_ingest_export.py +354 -333
  238. package/md_cg/test_p3.py +11 -2
  239. package/md_cg/test_p30_maintain.py +330 -330
  240. package/md_cg/test_p31_insight.py +534 -534
  241. package/md_cg/test_p32_backfill.py +298 -298
  242. package/md_cg/test_p33_ccg_wiring.py +293 -293
  243. package/md_cg/test_p34_crosscheck.py +331 -331
  244. package/md_cg/test_p35_conditioned_claim.py +252 -252
  245. package/md_cg/test_p36_kp_align.py +230 -230
  246. package/md_cg/test_p37_condition_space.py +248 -248
  247. package/md_cg/test_p38_concurrent_flush.py +102 -0
  248. package/md_cg/test_p38_contextualize.py +273 -273
  249. package/md_cg/test_p39_verify_flow.py +113 -0
  250. package/md_cg/test_p39_vision_evidence.py +369 -369
  251. package/md_cg/test_p40_refine_worklist.py +241 -241
  252. package/md_cg/test_p41_evolve_patrol.py +224 -224
  253. package/md_cg/test_p42_provenance.py +269 -269
  254. package/md_cg/test_p43_pooling.py +412 -398
  255. package/md_cg/test_p44_md_whitebox.py +231 -231
  256. package/md_cg/test_p45_session_identity.py +219 -219
  257. package/md_cg/test_p46_unit_scope.py +272 -272
  258. package/md_cg/test_p47_session_view.py +281 -0
  259. package/md_cg/test_p4_fuzzy.py +223 -223
  260. package/md_cg/test_p5_semantic.py +226 -226
  261. package/md_cg/test_p6_consolidate.py +440 -387
  262. package/md_cg/test_p7_goals_recent.py +202 -202
  263. package/md_cg/test_p8_subgraph_chain.py +200 -200
  264. package/md_cg/test_p9_forget_protect.py +231 -231
  265. package/md_cg/test_predict_beta.py +135 -135
  266. package/md_cg/test_preflight_failclosed.py +100 -100
  267. package/md_cg/test_progressive.py +146 -146
  268. package/md_cg/test_protocol.py +243 -243
  269. package/md_cg/test_reach.py +378 -378
  270. package/md_cg/test_reach_keys.py +201 -201
  271. package/md_cg/test_read_clip.py +141 -141
  272. package/md_cg/test_readcache_prodpath.py +155 -0
  273. package/md_cg/test_retr_gates_prodpath.py +140 -0
  274. package/md_cg/test_retr_s1.py +340 -340
  275. package/md_cg/test_retr_s1b.py +209 -209
  276. package/md_cg/test_retr_s3.py +194 -194
  277. package/md_cg/test_retr_s4.py +163 -163
  278. package/md_cg/test_retr_s5.py +200 -200
  279. package/md_cg/test_retr_s6.py +157 -157
  280. package/md_cg/test_retr_s7.py +384 -384
  281. package/md_cg/test_retr_s8_time.py +369 -316
  282. package/md_cg/test_retr_s9_edges.py +286 -286
  283. package/md_cg/test_retr_s9_entity_ctx.py +175 -175
  284. package/md_cg/test_review_conformance.py +367 -367
  285. package/md_cg/test_role_views.py +354 -354
  286. package/md_cg/test_sem_noise.py +242 -242
  287. package/md_cg/test_semantic_canonical.py +241 -241
  288. package/md_cg/test_subproc_encoding.py +192 -192
  289. package/md_cg/test_sustain_mutual.py +153 -153
  290. package/md_cg/test_tasks.py +409 -409
  291. package/md_cg/test_tool_face.py +189 -189
  292. package/md_cg/test_transfer.py +180 -180
  293. package/md_cg/test_trust.py +361 -361
  294. package/md_cg/test_twophase.py +286 -286
  295. package/md_cg/test_v14_fixes.py +397 -397
  296. package/md_cg/test_validity_filter.py +280 -280
  297. package/md_cg/test_verify_answer.py +138 -138
  298. package/md_cg/test_wisdom_md_store.py +292 -292
  299. package/md_cg/test_writelimit.py +197 -197
  300. package/md_cg/test_writepipe.py +214 -214
  301. package/md_cg/theory.py +273 -273
  302. package/md_cg/tokens.py +677 -663
  303. package/md_cg/tool_face.py +260 -260
  304. package/md_cg/trust.py +986 -950
  305. package/md_cg/twophase.py +231 -231
  306. package/md_cg/units.py +667 -667
  307. package/md_cg/vision_evidence.py +666 -666
  308. package/md_cg/weights.py +624 -624
  309. package/md_cg/whitebox.py +527 -527
  310. package/md_cg/whitebox_kb/__init__.py +37 -37
  311. package/md_cg/whitebox_kb/aeis_core/__init__.py +42 -42
  312. package/md_cg/whitebox_kb/aeis_core/semantic.py +280 -280
  313. package/md_cg/whitebox_kb/aeis_core/textutil.py +13 -13
  314. package/md_cg/whitebox_kb/engine.py +310 -310
  315. package/md_cg/whitebox_kb/seed_knowledge//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
  316. package/md_cg/whitebox_kb/wisdom/browser_units.py +2631 -2631
  317. package/md_cg/whitebox_kb/wisdom/causal_discover.py +432 -432
  318. package/md_cg/whitebox_kb/wisdom/chat_engine.py +1506 -1506
  319. package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
  320. package/md_cg/whitebox_kb/wisdom/compiler_code_units.py +3033 -3033
  321. package/md_cg/whitebox_kb/wisdom/condition_algebra.py +112 -112
  322. package/md_cg/whitebox_kb/wisdom/condition_frame.py +315 -315
  323. package/md_cg/whitebox_kb/wisdom/condition_kb.py +108 -108
  324. package/md_cg/whitebox_kb/wisdom/conflict_map.json +4445 -4445
  325. package/md_cg/whitebox_kb/wisdom/core/lexer.py +512 -512
  326. package/md_cg/whitebox_kb/wisdom/core/name_checker.py +1023 -1023
  327. package/md_cg/whitebox_kb/wisdom/cspmn.py +258 -258
  328. package/md_cg/whitebox_kb/wisdom/csre.py +264 -264
  329. package/md_cg/whitebox_kb/wisdom/danmaku_audit.py +252 -252
  330. package/md_cg/whitebox_kb/wisdom/distilled_condition_units.json +6417 -6417
  331. package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902b.json +1950 -1950
  332. package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902c.json +1296 -1296
  333. package/md_cg/whitebox_kb/wisdom/docs/whitebox_capability_graph_demo.json +59 -59
  334. package/md_cg/whitebox_kb/wisdom/graph_db_units.py +3089 -3089
  335. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +318 -318
  336. package/md_cg/whitebox_kb/wisdom/md_access.py +470 -470
  337. package/md_cg/whitebox_kb/wisdom/md_store.py +276 -251
  338. package/md_cg/whitebox_kb/wisdom/migrate_wisdom.py +476 -476
  339. package/md_cg/whitebox_kb/wisdom/navigate.py +248 -248
  340. package/md_cg/whitebox_kb/wisdom/neural_retrieve.py +224 -224
  341. package/md_cg/whitebox_kb/wisdom/os_units.py +2735 -2735
  342. package/md_cg/whitebox_kb/wisdom/pattern_separation.py +376 -376
  343. package/md_cg/whitebox_kb/wisdom/prereq_map.json +364 -364
  344. package/md_cg/whitebox_kb/wisdom/python_code_units.py +2766 -2766
  345. package/md_cg/whitebox_kb/wisdom/role_solidified.json +7 -7
  346. package/md_cg/whitebox_kb/wisdom/route_memory.py +244 -244
  347. package/md_cg/whitebox_kb/wisdom/scene_reconstruction.py +148 -148
  348. package/md_cg/whitebox_kb/wisdom/snr_report.json +40 -40
  349. package/md_cg/whitebox_kb/wisdom/test_code_compose_domains.py +7541 -7541
  350. package/md_cg/whitebox_kb/wisdom/test_compiler_self_bootstrap.py +354 -354
  351. package/md_cg/whitebox_kb/wisdom/test_ecosystem_assembly.py +158 -158
  352. package/md_cg/whitebox_kb/wisdom/test_ecosystem_demos.py +193 -193
  353. package/md_cg/whitebox_kb/wisdom/test_graph_db.py +292 -292
  354. package/md_cg/whitebox_kb/wisdom/test_python_self_bootstrap.py +160 -160
  355. package/md_cg/whitebox_kb/wisdom/trigger_words_index.json +4366 -4366
  356. package/md_cg/whitebox_kb/wisdom/verifier.py +1297 -1297
  357. package/md_cg/writelimit.py +356 -356
  358. package/md_cg/writepipe.py +550 -542
  359. package/package.json +97 -96
  360. package/skills/plugin.json +54 -54
  361. package/skills/skills/designer-perspective/SKILL.md +158 -158
  362. package/skills/skills/designer-perspective/references/01-observation-position.md +66 -66
  363. package/skills/skills/designer-perspective/references/02-structure-recognition.md +62 -62
  364. package/skills/skills/designer-perspective/references/03-direction-judgment.md +55 -55
  365. package/skills/skills/designer-perspective/references/04-qualification-verdict.md +72 -72
  366. package/skills/skills/designer-perspective/references/05-condition-attribution.md +74 -74
  367. package/skills/skills/designer-perspective/scripts/designer.py +545 -545
  368. package/skills/skills/designer-perspective/tests/cases.jsonl +17 -17
  369. package/skills/skills/designer-perspective/tests/selftest.py +61 -61
  370. package/skills/skills/lingshu-browser/SKILL.md +60 -60
  371. package/skills/skills/lingshu-compiler/SKILL.md +56 -56
  372. package/skills/skills/lingshu-compiler/units/analyze-type-infer/SKILL.md +45 -45
  373. package/skills/skills/lingshu-compiler/units/check-name-real/SKILL.md +45 -45
  374. package/skills/skills/lingshu-compiler/units/compile-assign/SKILL.md +45 -45
  375. package/skills/skills/lingshu-compiler/units/compile-expr-tree/SKILL.md +45 -45
  376. package/skills/skills/lingshu-compiler/units/compile-full-pipeline/SKILL.md +45 -45
  377. package/skills/skills/lingshu-compiler/units/compile-func-def/SKILL.md +45 -45
  378. package/skills/skills/lingshu-compiler/units/compile-if-then/SKILL.md +45 -45
  379. package/skills/skills/lingshu-compiler/units/compile-logic-expr/SKILL.md +45 -45
  380. package/skills/skills/lingshu-compiler/units/compile-recursive/SKILL.md +45 -45
  381. package/skills/skills/lingshu-compiler/units/compile-scope/SKILL.md +45 -45
  382. package/skills/skills/lingshu-compiler/units/compile-type-check/SKILL.md +45 -45
  383. package/skills/skills/lingshu-compiler/units/compile-while/SKILL.md +45 -45
  384. package/skills/skills/lingshu-compiler/units/compiler-0010c4bf/SKILL.md +45 -45
  385. package/skills/skills/lingshu-compiler/units/compiler-0355bffb/SKILL.md +45 -45
  386. package/skills/skills/lingshu-compiler/units/compiler-0361708a/SKILL.md +45 -45
  387. package/skills/skills/lingshu-compiler/units/compiler-054a0414/SKILL.md +45 -45
  388. package/skills/skills/lingshu-compiler/units/compiler-05a1691a/SKILL.md +45 -45
  389. package/skills/skills/lingshu-compiler/units/compiler-05eeed1e/SKILL.md +45 -45
  390. package/skills/skills/lingshu-compiler/units/compiler-0622a1f6/SKILL.md +45 -45
  391. package/skills/skills/lingshu-compiler/units/compiler-08b54217/SKILL.md +45 -45
  392. package/skills/skills/lingshu-compiler/units/compiler-0a62b70c/SKILL.md +45 -45
  393. package/skills/skills/lingshu-compiler/units/compiler-0ab24d00/SKILL.md +45 -45
  394. package/skills/skills/lingshu-compiler/units/compiler-0e093688/SKILL.md +45 -45
  395. package/skills/skills/lingshu-compiler/units/compiler-0e82b966/SKILL.md +45 -45
  396. package/skills/skills/lingshu-compiler/units/compiler-0ee9b9b9/SKILL.md +45 -45
  397. package/skills/skills/lingshu-compiler/units/compiler-0f3787e6/SKILL.md +45 -45
  398. package/skills/skills/lingshu-compiler/units/compiler-1028685f/SKILL.md +45 -45
  399. package/skills/skills/lingshu-compiler/units/compiler-11897630/SKILL.md +45 -45
  400. package/skills/skills/lingshu-compiler/units/compiler-16661b9b/SKILL.md +45 -45
  401. package/skills/skills/lingshu-compiler/units/compiler-1f722303/SKILL.md +45 -45
  402. package/skills/skills/lingshu-compiler/units/compiler-25be1262/SKILL.md +45 -45
  403. package/skills/skills/lingshu-compiler/units/compiler-2a76ba07/SKILL.md +45 -45
  404. package/skills/skills/lingshu-compiler/units/compiler-2dbea54a/SKILL.md +45 -45
  405. package/skills/skills/lingshu-compiler/units/compiler-2df52f16/SKILL.md +45 -45
  406. package/skills/skills/lingshu-compiler/units/compiler-2ec2c9d2/SKILL.md +45 -45
  407. package/skills/skills/lingshu-compiler/units/compiler-2f8c8f39/SKILL.md +45 -45
  408. package/skills/skills/lingshu-compiler/units/compiler-38378ac7/SKILL.md +45 -45
  409. package/skills/skills/lingshu-compiler/units/compiler-39457d2e/SKILL.md +45 -45
  410. package/skills/skills/lingshu-compiler/units/compiler-39fb5926/SKILL.md +45 -45
  411. package/skills/skills/lingshu-compiler/units/compiler-47f4fbfa/SKILL.md +45 -45
  412. package/skills/skills/lingshu-compiler/units/compiler-4a8cd1f1/SKILL.md +45 -45
  413. package/skills/skills/lingshu-compiler/units/compiler-4b230b6b/SKILL.md +45 -45
  414. package/skills/skills/lingshu-compiler/units/compiler-4cbbda95/SKILL.md +45 -45
  415. package/skills/skills/lingshu-compiler/units/compiler-4d5a68ff/SKILL.md +45 -45
  416. package/skills/skills/lingshu-compiler/units/compiler-56dc9bdd/SKILL.md +45 -45
  417. package/skills/skills/lingshu-compiler/units/compiler-57e76ebe/SKILL.md +45 -45
  418. package/skills/skills/lingshu-compiler/units/compiler-61ff016b/SKILL.md +45 -45
  419. package/skills/skills/lingshu-compiler/units/compiler-63eae588/SKILL.md +45 -45
  420. package/skills/skills/lingshu-compiler/units/compiler-64c4224b/SKILL.md +45 -45
  421. package/skills/skills/lingshu-compiler/units/compiler-66377955/SKILL.md +45 -45
  422. package/skills/skills/lingshu-compiler/units/compiler-674ab4f6/SKILL.md +45 -45
  423. package/skills/skills/lingshu-compiler/units/compiler-6af76fd6/SKILL.md +45 -45
  424. package/skills/skills/lingshu-compiler/units/compiler-6d979db2/SKILL.md +45 -45
  425. package/skills/skills/lingshu-compiler/units/compiler-6de326c0/SKILL.md +45 -45
  426. package/skills/skills/lingshu-compiler/units/compiler-6f1c0eea/SKILL.md +45 -45
  427. package/skills/skills/lingshu-compiler/units/compiler-724d6c9b/SKILL.md +45 -45
  428. package/skills/skills/lingshu-compiler/units/compiler-7690177f/SKILL.md +45 -45
  429. package/skills/skills/lingshu-compiler/units/compiler-7b229a7d/SKILL.md +45 -45
  430. package/skills/skills/lingshu-compiler/units/compiler-7f471b3a/SKILL.md +45 -45
  431. package/skills/skills/lingshu-compiler/units/compiler-815cad08/SKILL.md +45 -45
  432. package/skills/skills/lingshu-compiler/units/compiler-83c61634/SKILL.md +45 -45
  433. package/skills/skills/lingshu-compiler/units/compiler-8c798c81/SKILL.md +45 -45
  434. package/skills/skills/lingshu-compiler/units/compiler-8dd747ac/SKILL.md +45 -45
  435. package/skills/skills/lingshu-compiler/units/compiler-90324d0a/SKILL.md +45 -45
  436. package/skills/skills/lingshu-compiler/units/compiler-94b8d72d/SKILL.md +45 -45
  437. package/skills/skills/lingshu-compiler/units/compiler-94f12231/SKILL.md +45 -45
  438. package/skills/skills/lingshu-compiler/units/compiler-95937c16/SKILL.md +45 -45
  439. package/skills/skills/lingshu-compiler/units/compiler-98a5625b/SKILL.md +45 -45
  440. package/skills/skills/lingshu-compiler/units/compiler-98b4c42f/SKILL.md +45 -45
  441. package/skills/skills/lingshu-compiler/units/compiler-98e3894a/SKILL.md +45 -45
  442. package/skills/skills/lingshu-compiler/units/compiler-9bdfe4b8/SKILL.md +45 -45
  443. package/skills/skills/lingshu-compiler/units/compiler-9d9b4e83/SKILL.md +45 -45
  444. package/skills/skills/lingshu-compiler/units/compiler-9f7be5ad/SKILL.md +45 -45
  445. package/skills/skills/lingshu-compiler/units/compiler-a6daf076/SKILL.md +45 -45
  446. package/skills/skills/lingshu-compiler/units/compiler-a7995a0c/SKILL.md +45 -45
  447. package/skills/skills/lingshu-compiler/units/compiler-a8399248/SKILL.md +45 -45
  448. package/skills/skills/lingshu-compiler/units/compiler-aabbd099/SKILL.md +45 -45
  449. package/skills/skills/lingshu-compiler/units/compiler-afe169d8/SKILL.md +45 -45
  450. package/skills/skills/lingshu-compiler/units/compiler-b0678bda/SKILL.md +45 -45
  451. package/skills/skills/lingshu-compiler/units/compiler-b09bd196/SKILL.md +45 -45
  452. package/skills/skills/lingshu-compiler/units/compiler-b1396e23/SKILL.md +45 -45
  453. package/skills/skills/lingshu-compiler/units/compiler-b9b31ce0/SKILL.md +45 -45
  454. package/skills/skills/lingshu-compiler/units/compiler-c10264a7/SKILL.md +45 -45
  455. package/skills/skills/lingshu-compiler/units/compiler-cb1e8e4b/SKILL.md +45 -45
  456. package/skills/skills/lingshu-compiler/units/compiler-ccafd438/SKILL.md +45 -45
  457. package/skills/skills/lingshu-compiler/units/compiler-cda9c262/SKILL.md +45 -45
  458. package/skills/skills/lingshu-compiler/units/compiler-ce648068/SKILL.md +45 -45
  459. package/skills/skills/lingshu-compiler/units/compiler-cf5776a4/SKILL.md +45 -45
  460. package/skills/skills/lingshu-compiler/units/compiler-d974e5d3/SKILL.md +45 -45
  461. package/skills/skills/lingshu-compiler/units/compiler-e3979fd3/SKILL.md +45 -45
  462. package/skills/skills/lingshu-compiler/units/compiler-eb1cf2b5/SKILL.md +45 -45
  463. package/skills/skills/lingshu-compiler/units/compiler-ecb30d5b/SKILL.md +45 -45
  464. package/skills/skills/lingshu-compiler/units/compiler-f8c8b24b/SKILL.md +45 -45
  465. package/skills/skills/lingshu-compiler/units/compiler-f99fedbe/SKILL.md +45 -45
  466. package/skills/skills/lingshu-compiler/units/compiler-fa8ff5f7/SKILL.md +45 -45
  467. package/skills/skills/lingshu-compiler/units/compiler-fe1b058d/SKILL.md +45 -45
  468. package/skills/skills/lingshu-compiler/units/lex-chinese-program/SKILL.md +45 -45
  469. package/skills/skills/lingshu-compiler/units/lex-dao-de-jing/SKILL.md +45 -45
  470. package/skills/skills/lingshu-compiler/units/lex-nine-chapters/SKILL.md +45 -45
  471. package/skills/skills/lingshu-compiler/units/vm-arithmetic/SKILL.md +45 -45
  472. package/skills/skills/lingshu-compiler/units/vm-array-ops/SKILL.md +45 -45
  473. package/skills/skills/lingshu-compiler/units/vm-closure-call/SKILL.md +45 -45
  474. package/skills/skills/lingshu-compiler/units/vm-closure-create/SKILL.md +45 -45
  475. package/skills/skills/lingshu-compiler/units/vm-compare/SKILL.md +45 -45
  476. package/skills/skills/lingshu-compiler/units/vm-cond-jump/SKILL.md +45 -45
  477. package/skills/skills/lingshu-compiler/units/vm-cond-space/SKILL.md +45 -45
  478. package/skills/skills/lingshu-compiler/units/vm-exception/SKILL.md +45 -45
  479. package/skills/skills/lingshu-compiler/units/vm-func-call/SKILL.md +45 -45
  480. package/skills/skills/lingshu-compiler/units/vm-loop-run/SKILL.md +45 -45
  481. package/skills/skills/lingshu-compiler/units/vm-profiling/SKILL.md +45 -45
  482. package/skills/skills/lingshu-compiler/units/vm-refcount/SKILL.md +45 -45
  483. package/skills/skills/lingshu-compiler/units/vm-run-loop/SKILL.md +45 -45
  484. package/skills/skills/lingshu-compiler/units/vm-short-circuit/SKILL.md +45 -45
  485. package/skills/skills/lingshu-compiler/units/vm-stack-guard/SKILL.md +45 -45
  486. package/skills/skills/lingshu-compiler/units/vm-stack-ops/SKILL.md +45 -45
  487. package/skills/skills/lingshu-compiler/units/vm-trust-accum/SKILL.md +45 -45
  488. package/skills/skills/lingshu-graph/SKILL.md +63 -63
  489. package/skills/skills/lingshu-net/SKILL.md +48 -48
  490. package/skills/skills/lingshu-os/SKILL.md +64 -64
  491. package/skills/skills/lingshu-pylang/SKILL.md +71 -71
  492. package/src/bridge.ts +401 -401
  493. package/src/hooks.ts +38 -2
  494. package/src/lib/datapath.ts +326 -326
  495. package/src/lib/mdcg_client.ts +413 -413
  496. package/src/lib/mutual.ts +428 -428
  497. package/src/lib/prompt_safety.ts +62 -62
  498. package/src/lib/python_path.ts +71 -71
  499. package/src/lib/roleplay_web.ts +932 -932
  500. package/src/lib/token_store.ts +192 -192
  501. package/src/tools.ts +212 -212
  502. package/zcode/AGENTS.md +11 -3
  503. package/zcode/README.md +41 -41
  504. /package/docs/{mdcg → hive}//344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md" +0 -0
package/md_cg/units.py CHANGED
@@ -1,668 +1,668 @@
1
- # -*- coding: utf-8 -*-
2
- """units.py · 复核单元接线层:蜂巢优先 → 提示配置 → harness 端子代理降级。
3
-
4
- 定位(与 audit.py 同一条架构约束):**能力不在认知图内**。本模块不做智能判定,
5
- 只做三件事:选通道(probe/plan)→ 派发请求(submit/wait/run)→ 记账(_units.jsonl)。
6
-
7
- 使用者裁定(2026-09-16,第 17 条精神):编译器(ccgc)的复核**优先使用蜂巢的
8
- 反思单元 / 验证单元**(reflect / verify,与 crosscheck.REFLECT_UNIT/VERIFY_UNIT 同值,
9
- 不新造角色名);蜂巢不可用 → **提示使用者配置**(给出可照做的构建/启动/环境变量命令);
10
- 或经显式 allow_degrade=True 降级到 **harness 端子代理**(宿主 agent 派发,裁决经
11
- crosscheck 同款 verdicts 通道回填)。理由:使用者的复核是昂贵的——重活交给可并行的
12
- 外部单元,agent 本体只做编排与记账。
13
-
14
- 蜂巢契约(真源 hive/README.md + hive/hive_mcp/mcp_server.py;**复制契约不 import**,
15
- 保持 md_cg 对 hive 零依赖,与 ccgc「同款语义就地实现防 import 环」惯例一致):
16
- jobs 目录 = $HIVE_JOBS_DIR | <repo>/hive/jobs
17
- serve 判活 = **三层**:jobs/_serve.json 的 ts(毫秒)距今 < FRESH_S(15s)
18
- ∧ pid 存活 ∧ 该 pid 是本程序
19
- (同 hive/serve_start.serve_alive;四路口径由
20
- hive/test_serve_entry.py 机械守卫)
21
- job 目录 = jobs/<job_id>/{spec.json,status.json,result.json,kill}
22
- job_id = h<java_ms>_<uuid6> (同 _submit)
23
- result.json = {"ok":true,"content":...} | {"ok":false,"error":...}
24
- 终态 = done | error | timeout | killed
25
- 拉起 serve = <repo>/hive/target/release/hive serve --jobs <jobs>(detached)
26
-
27
- 边界(诚实声明,不猜测):
28
- · 本模块**不产生裁决**,只搬运;裁决由外部单元负责。
29
- · 契约漂移:hive 侧协议若变更,本模块以「无 result / 超时」如实失败,不假装成功。
30
- · 默认**不自动拉起 serve**(使用者裁定:不可用即提示配置);autostart=True 才拉起。
31
- """
32
- from __future__ import annotations
33
-
34
- import json
35
- import os
36
- import re
37
- import subprocess
38
- import time
39
- import uuid
40
-
41
- HIVE, SUBAGENT, CONFIGURE = "hive", "subagent", "configure"
42
- STATES = (HIVE, SUBAGENT, CONFIGURE)
43
-
44
- REFLECT, VERIFY = "reflect", "verify" # 同 crosscheck.REFLECT_UNIT/VERIFY_UNIT
45
- ROLES = (REFLECT, VERIFY)
46
-
47
- SERVE_FILE, SPEC_FILE = "_serve.json", "spec.json"
48
- STATUS_FILE, RESULT_FILE, KILL_FILE = "status.json", "result.json", "kill"
49
- LOG_NAME = "_units.jsonl"
50
- TERMINAL_STATES = ("done", "error", "timeout", "killed")
51
- #: serve 心跳新鲜窗口(秒)。**须与 hive/serve_start.FRESH_S 同值**——本模块
52
- #: 只持有「判活的第 4 处实现」(复制契约不 import hive),阈值/判据结构若与
53
- #: 权威漂移,会在通道选择面复现 v13 的「假存活」病类(v14 缺陷 E:
54
- #: 旧值 5.0 vs 权威 15,serve 崩溃后 ≤5s 窗口内判「存活」→ 选 hive 通道 →
55
- #: 提交的 job 永远无人处理)。守卫:hive/test_serve_entry.py。
56
- FRESH_S, DEFAULT_TIMEOUT_S, DEFAULT_POLL_S = 15.0, 120, 1.0
57
- #: 复核委派的 completion 预算。**必须对齐统一默认(文档:最大输出 200000)**——
58
- #: 该模型 reasoning 与正文**共享 completion 预算**,小预算会把正文吃光并静默返回空正文:
59
- #: 实测 2048 → reasoning 2048 / content 空;16384 → 时好时坏;200000 → 正常出裁决。
60
- DEFAULT_MAX_TOKENS = 200000
61
-
62
- ENV_JOBS_DIR, ENV_EXE = "HIVE_JOBS_DIR", "HIVE_EXE"
63
- ENV_MODEL, ENV_API_KEY = "MDCG_UNIT_MODEL", "HIVE_API_KEY"
64
-
65
- ACCEPT, REJECT, DEFER, BLINDSPOT = "ACCEPT", "REJECT", "DEFER", "BLINDSPOT"
66
- _VERDICT_MAP = {
67
- "accept": ACCEPT, "accepted": ACCEPT, "keep": ACCEPT, "pass": ACCEPT,
68
- "ok": ACCEPT, "approve": ACCEPT,
69
- "drop": REJECT, "reject": REJECT, "rejected": REJECT, "deny": REJECT, "fail": REJECT,
70
- "defer": DEFER, "deferred": DEFER, "unsure": DEFER, "unclear": DEFER, "skip": DEFER,
71
- "blindspot": BLINDSPOT, "blind": BLINDSPOT,
72
- }
73
-
74
-
75
- # 生效条件:无入参,恒返回模块 `__file__` 绝对路径上溯两级的目录(md_cg 的上一级),与 cwd 无关。
76
- def repo_root() -> str:
77
- """仓库根(md_cg 的上一级);不用 cwd——cwd 由宿主决定,不可作判据。"""
78
- return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
79
-
80
-
81
- # 生效条件:explicit 为真值时 d=explicit;explicit 为空串等假值而 ENV_JOBS_DIR 有真值时 d=该环境变量;两者皆假值时 d=repo_root()/hive/jobs;最终返回 os.path.abspath(d)。
82
- def jobs_dir(explicit: str = "") -> str:
83
- d = explicit or os.environ.get(ENV_JOBS_DIR) or os.path.join(repo_root(), "hive", "jobs")
84
- return os.path.abspath(d)
85
-
86
-
87
- # 生效条件:ENV_EXE 有真值时原样返回该值;ENV_EXE 缺失或为空串时返回 repo_root()/hive/target/release/ 下按 os.name=="nt" 取 hive.exe、否则取 hive 的拼接路径。
88
- def exe_path() -> str:
89
- exe = os.environ.get(ENV_EXE)
90
- if exe:
91
- return exe
92
- return os.path.join(repo_root(), "hive", "target", "release",
93
- "hive.exe" if os.name == "nt" else "hive")
94
-
95
-
96
- # 生效条件:explicit 为真值时取 explicit,否则取 ENV_MODEL,两者皆假值时按空串,返回该结果 strip() 后的字符串(可为空串)。
97
- def model_name(explicit: str = "") -> str:
98
- """复核模型(LLM 委托型执行器的 spec.model 必填)。"""
99
- return (explicit or os.environ.get(ENV_MODEL) or "").strip()
100
-
101
-
102
- # 生效条件:pid 为 int 且大于 0 时(否则 False),os.name 为 "nt" 时返回 _tasklist_row(pid) 是否非 None,非 "nt" 时 os.kill(pid, 0) 不抛 OSError 返回 True、抛 OSError 返回 False。
103
- def pid_alive(pid) -> bool:
104
- """该 pid **号**是否存在(Windows tasklist 精确列比对 / unix `kill -0`)。
105
-
106
- 只回答「这个号有没有进程」——**不足以判定「serve 还在跑」**(见
107
- `pid_is_self_program`)。实现与 hive/serve_start.pid_alive 同口径。
108
- """
109
- if not isinstance(pid, int) or pid <= 0:
110
- return False
111
- if os.name == "nt":
112
- return _tasklist_row(pid) is not None
113
- try:
114
- os.kill(pid, 0)
115
- return True
116
- except OSError:
117
- return False
118
-
119
-
120
- # 生效条件:对传入 pid 执行 tasklist /FO CSV 后,在其 stdout 中遇到的第一个按 '","' 切分、列数≥2 且第 2 列 strip 再 strip('"') 后等于 str(pid) 的行即返回 [映像名, pid 字符串],无此行或 subprocess.run 抛 OSError 时返回 None。
121
- def _tasklist_row(pid):
122
- """Windows:查该 pid 的 tasklist 行 → [映像名, pid 字符串];查不到返回 None。
123
-
124
- 按列精确比对,**不用子串包含**——子串会让 pid=441 被 4410 命中(假存活)。
125
- """
126
- try:
127
- # 显式 utf-8 + replace:只消费 ASCII 的 pid 列,但**不依赖 locale**——
128
- # locale 口径与「后代写 UTF-8」不一致时读线程会崩(见 test_subproc_encoding.py)。
129
- r = subprocess.run(["tasklist", "/FI", f"PID eq {pid}", "/NH", "/FO", "CSV"],
130
- capture_output=True, text=True,
131
- encoding="utf-8", errors="replace")
132
- except OSError:
133
- return None
134
- for line in (r.stdout or "").splitlines():
135
- cols = line.split('","')
136
- if len(cols) >= 2 and cols[1].strip().strip('"') == str(pid):
137
- return [cols[0].strip().strip('"'), cols[1].strip().strip('"')]
138
- return None
139
-
140
-
141
- # 生效条件:exe_path() 的 basename 小写非空且 pid 为 int 大于 0 时(否则 False),"nt" 下要求 _tasklist_row(pid) 非空且映像名小写等于该 basename,非 "nt" 下要求 /proc/<pid>/cmdline 首个 b"\x00" 前 token 的 basename 小写等于它(读取抛 OSError 则 False)。
142
- def pid_is_self_program(pid) -> bool:
143
- """该 pid 是否**就是本程序**(同映像名)——pid 号会被无关进程复用。
144
-
145
- 与 hive/serve_start.pid_is_self_program 同口径(复制契约)。零依赖边界:
146
- 拿不到映像名返回 False(宁可放行启动,也不误报「已有 serve 在跑」)。
147
- """
148
- want = os.path.basename(exe_path()).lower()
149
- if not want or not isinstance(pid, int) or pid <= 0:
150
- return False
151
- if os.name == "nt":
152
- row = _tasklist_row(pid)
153
- return bool(row) and row[0].lower() == want
154
- try:
155
- with open(f"/proc/{pid}/cmdline", "rb") as f:
156
- first = f.read().split(b"\x00")[0]
157
- except OSError:
158
- return False
159
- return os.path.basename(first.decode("utf-8", "replace")).lower() == want
160
-
161
-
162
- # 生效条件:以 jobs_dir(jobs) 下的 SERVE_FILE 为心跳路径——该路径不被 isfile 命中时返回 exists/alive 均 False 的未启动 reason;命中但 open/json.load 抛 OSError 或 ValueError 时返回不可读 reason;解析成功后按**三层判据**(age < fresh_s ∧ pid_alive(pid) ∧ pid_is_self_program(pid))定 alive,回填 age_s/pid/raw 与逐层明细及失败原因。
163
- def serve_state(jobs: str = "", fresh_s: float = FRESH_S) -> dict:
164
- """serve 判活:**三层**(心跳新鲜 ∧ pid 存活 ∧ 该 pid 是本程序)。
165
-
166
- 与 `hive/serve_start.serve_alive` 同口径(同值 FRESH_S=15s、同三层判据)。
167
- v14 缺陷 E:旧实现只判 ts 新鲜度且阈值 5s——双漂移,serve 崩溃后
168
- ≤5s 内判「存活」会让 `probe()` 选 hive 通道(提交的 job 永远无人处理),
169
- 这正是 v13「假存活」病类从守卫面搬到了**通道选择面**。
170
- """
171
- jd = jobs_dir(jobs)
172
- p = os.path.join(jd, SERVE_FILE)
173
- out = {"jobs_dir": jd, "heartbeat": p, "exists": os.path.isfile(p),
174
- "alive": False, "age_s": None, "pid": None, "reason": ""}
175
- if not out["exists"]:
176
- out["reason"] = "未见 _serve.json 心跳(serve 未启动或 jobs 目录不对)"
177
- return out
178
- try:
179
- with open(p, encoding="utf-8") as f:
180
- hb = json.load(f)
181
- except (OSError, ValueError) as exc:
182
- out["reason"] = "心跳不可读:%s: %s" % (type(exc).__name__, exc)
183
- return out
184
- age = time.time() - ((hb.get("ts") or 0) / 1000.0)
185
- pid = hb.get("pid")
186
- fresh_ok = age < float(fresh_s)
187
- pid_ok = pid_alive(pid)
188
- ident_ok = pid_is_self_program(pid)
189
- out.update({"age_s": round(age, 3), "pid": pid, "raw": hb,
190
- "fresh": bool(fresh_ok), "pid_alive": bool(pid_ok),
191
- "pid_is_self_program": bool(ident_ok)})
192
- out["alive"] = bool(fresh_ok and pid_ok and ident_ok)
193
- if out["alive"]:
194
- out["reason"] = ""
195
- elif not fresh_ok:
196
- out["reason"] = "心跳过期 %.1fs(阈值 %ss)——serve 可能已退出" % (age, fresh_s)
197
- elif not pid_ok:
198
- out["reason"] = "心跳新鲜但 pid=%s 已不存在——serve 已退出" % (pid,)
199
- else:
200
- out["reason"] = ("心跳新鲜且 pid=%s 存活,但该 pid 不属于本程序"
201
- "(非 %s)——pid 号被无关进程复用"
202
- % (pid, os.path.basename(exe_path())))
203
- return out
204
-
205
-
206
- # 生效条件:jobs 仅经 jobs_dir(jobs) 用于填充指引里的 jobs 路径;reason 为假值时文案首行取“serve 未存活”,否则取 reason 原文;返回模板固定嵌入 exe_path() 的目录与路径、ENV_API_KEY、ENV_MODEL、jd、jd 下的 SERVE_FILE 与 int(FRESH_S)。
207
- def setup_hint(jobs: str = "", reason: str = "") -> str:
208
- """不可用时给使用者看的**配置指引**(含降级指引)——提示即责任,须可照着做。"""
209
- jd = jobs_dir(jobs)
210
- return ("蜂巢不可用:%s\n"
211
- " [A 配置蜂巢(推荐,第 17 条)]\n"
212
- " 1) cd %s && cargo build --release\n"
213
- " 2) set %s=<LLM密钥>\n 3) set %s=<复核模型名>\n"
214
- " 4) 启动:%s serve --jobs \"%s\"(或 hive_spawn 自动拉起)\n"
215
- " 判活判据:%s 的 ts 距今 < %ss\n"
216
- " [B 降级 harness 端子代理]\n"
217
- " 重发本请求并带 allow_degrade=true:本模块返回复核请求包(prompt),\n"
218
- " 由宿主子代理执行后经 verdicts 回填裁决。"
219
- % (reason or "serve 未存活", os.path.dirname(exe_path()), ENV_API_KEY,
220
- ENV_MODEL, exe_path(), jd, os.path.join(jd, SERVE_FILE), int(FRESH_S)))
221
-
222
-
223
- # 生效条件:先取 st=serve_state(jobs, fresh_s) 与 mdl=model_name(model);st["alive"] 为真且 mdl 非空时返回 state=HIVE;否则 allow_degrade 为真时返回 state=SUBAGENT 且 channel 取 channel 或 "harness-subagent";否则返回 state=CONFIGURE、transport=None 并附 setup_hint(jobs, why)。
224
- def probe(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
225
- allow_degrade: bool = False, channel: str = "") -> dict:
226
- """三级能力探测:hive(可派发)→ configure(提示配置)→ subagent(显式降级)。
227
-
228
- 判据只认**心跳 + 模型名**两项硬前提;缺任一即如实降级,绝不用相似度
229
- (如「hive 目录存在」)冒充可用(资格由条件证据裁决,不由相似度裁决)。
230
- """
231
- st = serve_state(jobs, fresh_s)
232
- mdl = model_name(model)
233
- if st["alive"] and mdl:
234
- return {"state": HIVE, "transport": HIVE, "model": mdl, "jobs_dir": st["jobs_dir"],
235
- "serve": st, "channel": "", "hint": ""}
236
- why = st["reason"] if not st["alive"] else "serve 存活但未配置复核模型(%s)" % ENV_MODEL
237
- if allow_degrade:
238
- return {"state": SUBAGENT, "transport": SUBAGENT, "model": mdl,
239
- "jobs_dir": st["jobs_dir"], "serve": st,
240
- "channel": channel or "harness-subagent",
241
- "hint": "已按 allow_degrade 降级到 harness 端子代理:请把 prompt 交给宿主子代理,"
242
- "裁决经 verdicts 回填(蜂巢不可用原因:%s)" % why}
243
- return {"state": CONFIGURE, "transport": None, "model": mdl,
244
- "jobs_dir": st["jobs_dir"], "serve": st, "channel": "",
245
- "hint": setup_hint(jobs, why)}
246
-
247
-
248
- # 生效条件:state/transport/model/jobs_dir/serve/channel/hint 全部直接取自 probe(jobs, model, fresh_s, allow_degrade, channel) 的对应键,本函数不另做判活或模型判断,仅在 chain 文案中固定引用 int(FRESH_S) 与 ENV_MODEL。
249
- def plan(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
250
- allow_degrade: bool = False, channel: str = "") -> dict:
251
- """优先级链自描述(供 cg op=ccg action=units 与审计查看)。"""
252
- p = probe(jobs, model, fresh_s, allow_degrade, channel)
253
- return {"state": p["state"], "transport": p["transport"], "model": p["model"],
254
- "jobs_dir": p["jobs_dir"], "serve": p["serve"], "channel": p["channel"],
255
- "chain": [
256
- {"order": 1, "transport": HIVE, "action": "派发 reflect/verify 单元并等待 result.json",
257
- "when": ("jobs/_serve.json 心跳 < %ss 且(%s 已配置 **或调用方显式传 model**)"
258
- % (int(FRESH_S), ENV_MODEL))},
259
- {"order": 2, "transport": CONFIGURE, "action": "返回配置指引(不自动拉起、不假装通过)",
260
- "when": "蜂巢不可用"},
261
- {"order": 3, "transport": SUBAGENT, "action": "返回复核请求包,由 harness 端子代理执行并回填",
262
- "when": "调用方显式 allow_degrade=True"},
263
- ],
264
- "hint": p["hint"]}
265
-
266
-
267
- # ---------------------------------------------------------------- 派发与收取
268
-
269
- # 生效条件:无入参,恒返回 "h"+int(time.time()*1000)+"_"+uuid.uuid4().hex 前 6 位组成的字符串。
270
- def _job_id() -> str:
271
- """同 _submit:serve 侧按 'h' 前缀识别任务目录。"""
272
- return "h%d_%s" % (int(time.time() * 1000), uuid.uuid4().hex[:6])
273
-
274
-
275
- # 生效条件:cg.root 与 cg.cg.root 都取不到真值时不写、直接返回 None;取到 root 时向 root/LOG_NAME 追加一行 rec(副本,setdefault ts)的 JSON,写入抛 OSError 时被吞掉静默返回 None。
276
- def _log(cg, rec: dict) -> None:
277
- """留痕 _units.jsonl(对齐 _crosscheck.jsonl / _backfill.jsonl 纪律)。"""
278
- root = getattr(cg, "root", None) or getattr(getattr(cg, "cg", None), "root", None)
279
- if not root:
280
- return
281
- try:
282
- rec = dict(rec)
283
- rec.setdefault("ts", int(time.time() * 1000))
284
- with open(os.path.join(str(root), LOG_NAME), "a", encoding="utf-8") as f:
285
- f.write(json.dumps(rec, ensure_ascii=False) + "\n")
286
- except OSError:
287
- pass
288
-
289
-
290
- # 生效条件:role 不在 ROLES 时返回 ok=False 的未知角色 error;否则 model_name(model) 为空时返回 ok=False 的未配置复核模型;否则 str(prompt or "").strip() 为空时返回 ok=False 的 prompt 为空;否则在 jobs_dir(jobs)/_job_id() 下写 spec.json 与 status.json(context_files 为真、temperature 非 None、extra 为真时才并入 spec),OSError 时返回 ok=False 的写入失败,全部成功返回 ok=True 与 job_id/job_dir/spec/model。
291
- def submit(*, prompt: str, role: str = REFLECT, model: str = "", system_prompt: str = "",
292
- context_files=None, timeout_s: int = DEFAULT_TIMEOUT_S,
293
- max_tokens: int = DEFAULT_MAX_TOKENS, temperature=None, jobs: str = "",
294
- actor: str = "", extra: dict = None, cg=None) -> dict:
295
- """写一条 LLM 委托型 job(spec.json + status.json,形态同 _submit);只写文件不起进程。"""
296
- if role not in ROLES:
297
- return {"ok": False, "error": "未知复核角色:%r(可选 %s)" % (role, list(ROLES))}
298
- mdl = model_name(model)
299
- if not mdl:
300
- return {"ok": False, "error": "未配置复核模型:请设 %s 或显式传 model" % ENV_MODEL}
301
- if not str(prompt or "").strip():
302
- return {"ok": False, "error": "prompt 为空"}
303
- jd, job_id = jobs_dir(jobs), _job_id()
304
- d = os.path.join(jd, job_id)
305
- spec = {"model": mdl, "user_prompt": str(prompt), "system_prompt": str(system_prompt or ""),
306
- "timeout_s": int(timeout_s), "max_tokens": int(max_tokens),
307
- "role": role, "unit_role": role}
308
- if context_files:
309
- spec["context_files"] = list(context_files)
310
- if temperature is not None:
311
- spec["temperature"] = temperature
312
- if extra:
313
- spec.update(extra)
314
- try:
315
- os.makedirs(d, exist_ok=True)
316
- with open(os.path.join(d, SPEC_FILE), "w", encoding="utf-8") as f:
317
- json.dump(spec, f, ensure_ascii=False)
318
- with open(os.path.join(d, STATUS_FILE), "w", encoding="utf-8") as f:
319
- json.dump({"job_id": job_id, "state": "pending",
320
- "created_ts": int(time.time() * 1000), "started_ts": None,
321
- "heartbeat_ts": None, "elapsed_s": 0.0, "timeout_s": int(timeout_s),
322
- "model": mdl, "pid": None, "error": None}, f, ensure_ascii=False)
323
- except OSError as exc:
324
- return {"ok": False, "job_dir": d,
325
- "error": "job 写入失败:%s: %s" % (type(exc).__name__, exc)}
326
- _log(cg, {"action": "submit", "job_id": job_id, "role": role, "model": mdl,
327
- "timeout_s": int(timeout_s), "actor": actor, "prompt_head": str(prompt)[:200]})
328
- return {"ok": True, "job_id": job_id, "job_dir": d, "spec": spec,
329
- "unit_role": role, "model": mdl}
330
-
331
-
332
- # 生效条件:jobs_dir(jobs)/str(job_id or "") 不是目录时返回 state=missing、terminal=False 的目录不存在 error;是目录时读 STATUS_FILE(读失败则 status 置 None)并用其 state 覆盖 state/terminal(state 属 TERMINAL_STATES 才 terminal=True);RESULT_FILE 被 isfile 命中则 terminal=True、state=st or "done",解析抛 OSError/ValueError 时提前返回该 error;解析为 dict 时取 ok/content/error/usage/model,且 ok 为真而 content 去空白为空时把 ok 改 False 并写空正文 error,解析为非 dict 时 ok=True 且 content 为原值。
333
- def poll(job_id: str, jobs: str = "") -> dict:
334
- """读 job 终态视图:**以 result.json 出现为终态主判据**,status.json 仅作辅助。"""
335
- d = os.path.join(jobs_dir(jobs), str(job_id or ""))
336
- out = {"job_id": job_id, "job_dir": d, "state": "missing", "terminal": False,
337
- "ok": False, "content": None, "error": None, "status": None}
338
- if not os.path.isdir(d):
339
- out["error"] = "job 目录不存在:%s" % d
340
- return out
341
- sp = os.path.join(d, STATUS_FILE)
342
- if os.path.isfile(sp):
343
- try:
344
- with open(sp, encoding="utf-8") as f:
345
- out["status"] = json.load(f)
346
- except (OSError, ValueError):
347
- out["status"] = None
348
- st = (out["status"] or {}).get("state")
349
- if st:
350
- out.update({"state": st, "terminal": st in TERMINAL_STATES})
351
- rp = os.path.join(d, RESULT_FILE)
352
- if os.path.isfile(rp):
353
- out["state"] = st or "done"
354
- out["terminal"] = True
355
- try:
356
- with open(rp, encoding="utf-8") as f:
357
- res = json.load(f)
358
- except (OSError, ValueError) as exc:
359
- out["error"] = "result.json 不可读:%s" % type(exc).__name__
360
- return out
361
- if isinstance(res, dict):
362
- out["ok"] = bool(res.get("ok"))
363
- out["content"] = res.get("content")
364
- out["error"] = res.get("error") or out["error"]
365
- out["usage"] = res.get("usage")
366
- out["model"] = res.get("model")
367
- # 空正文**不得报成功**:reasoning 与正文共享 completion 预算,预算不足时
368
- # job 仍自称 ok 但 content 为空 → 若不拦,会被误读成「复核单元无答复」
369
- # (假死锁)。run() 早已有同等校验,此处补齐,消除两处口径不一致。
370
- if out["ok"] and not str(out["content"] or "").strip():
371
- out["ok"] = False
372
- out["error"] = ("空正文:job 自称 ok 但 content 为空(多为 reasoning 吃尽"
373
- "完成预算;提高 max_tokens,勿把预算不足当通道不可用)")
374
- else:
375
- out["ok"], out["content"] = True, res
376
- return out
377
-
378
-
379
- # 生效条件:循环 poll(job_id, jobs),结果 terminal 为真即补 waited_s 后返回;否则 time.time()-t0 >= float(timeout_s) 时返回 terminal=False、timeout=True 与超时 error;两者皆不满足则 sleep(float(poll_s)) 后重试(timeout_s=0 时首次 poll 非终态即超时返回)。
380
- def wait(job_id: str, *, jobs: str = "", timeout_s: float = DEFAULT_TIMEOUT_S,
381
- poll_s: float = DEFAULT_POLL_S) -> dict:
382
- """阻塞等终态;超时如实返回(不假装成功、不强杀 job)。"""
383
- t0 = time.time()
384
- while True:
385
- cur = poll(job_id, jobs)
386
- if cur.get("terminal"):
387
- cur["waited_s"] = round(time.time() - t0, 3)
388
- return cur
389
- if (time.time() - t0) >= float(timeout_s):
390
- cur.update({"terminal": False, "timeout": True, "waited_s": round(time.time() - t0, 3),
391
- "error": "等待超时 %.0fs(job 仍在 %s)" % (float(timeout_s),
392
- cur.get("state"))})
393
- return cur
394
- time.sleep(float(poll_s))
395
-
396
-
397
- # 生效条件:submit(prompt=prompt, role=role, cg=cg, actor=actor, **kw) 的 ok 为假时返回 stage="submit" 的失败 dict(含 **sub);ok 为真时 wait(sub["job_id"], jobs=kw.get("jobs",""), timeout_s=wait_s),补 stage="wait"/job_id/unit_role/model,并把 ok 改为 res 的 ok 与 content 同时为真。
398
- def run(*, prompt: str, role: str = REFLECT, cg=None, actor: str = "",
399
- wait_s: float = DEFAULT_TIMEOUT_S, **kw) -> dict:
400
- """submit + wait 组合(阻塞式复核,MCP action=review 主路径)。"""
401
- sub = submit(prompt=prompt, role=role, cg=cg, actor=actor, **kw)
402
- if not sub.get("ok"):
403
- return {"ok": False, "stage": "submit", "error": sub.get("error"), **sub}
404
- res = wait(sub["job_id"], jobs=kw.get("jobs", ""), timeout_s=wait_s)
405
- res.update({"stage": "wait", "job_id": sub["job_id"], "unit_role": role,
406
- "model": sub.get("model")})
407
- res["ok"] = bool(res.get("ok")) and bool(res.get("content"))
408
- return res
409
-
410
-
411
- # ---------------------------------------------------------------- 裁决解析与搬运
412
-
413
- _FENCE_RE = re.compile(r"```(?:json)?\s*(.+?)\s*```", re.S)
414
-
415
-
416
- # 生效条件:raw 是 dict/list 时原样返回;否则取 str(raw or "").strip(),为空串(含 0/False/None 等假值)返回 None;否则依次尝试 _FENCE_RE 第 1 组、整段文本、首个 "{" 到末个 "}" 及首个 "[" 到末个 "]" 的切片,返回首个能 json.loads 成 dict 或 list 的候选;全部失败返回 None。
417
- def _json_of(raw):
418
- """从单元输出抽第一个 JSON 对象/数组(容忍 ```json 围栏与前后噪声)。"""
419
- if isinstance(raw, (dict, list)):
420
- return raw
421
- text = str(raw or "").strip()
422
- if not text:
423
- return None
424
- cands = []
425
- m = _FENCE_RE.search(text)
426
- if m:
427
- cands.append(m.group(1))
428
- cands.append(text)
429
- for opener, closer in (("{", "}"), ("[", "]")):
430
- i, j = text.find(opener), text.rfind(closer)
431
- if 0 <= i < j:
432
- cands.append(text[i:j + 1])
433
- for c in cands:
434
- try:
435
- v = json.loads(c)
436
- except ValueError:
437
- continue
438
- if isinstance(v, (dict, list)):
439
- return v
440
- return None
441
-
442
-
443
- # 生效条件:data=_json_of(raw) 为 list 时取 data[0](仅当它是 dict,否则 None);非 dict 时返回 verdict=DEFER、parsed=False;dict 时 word 取 verdict/decision/state/result 中首个真值后 strip().lower(),_VERDICT_MAP.get(word) 为 None 时返回 DEFER、parsed=True 并带 corr 与 reason,命中时返回该裁决及 reason、slot_corrections(取自 slot_corrections/corrections,非 dict 则置 {})、checks、raw 前 500 字符。
444
- def verdict_of(raw) -> dict:
445
- """单元输出 → 裁决四态;解析不出 → DEFER(不猜测、不当通过)。"""
446
- data = _json_of(raw)
447
- if isinstance(data, list):
448
- data = data[0] if data and isinstance(data[0], dict) else None
449
- if not isinstance(data, dict):
450
- return {"verdict": DEFER, "reason": "单元输出无法解析为裁决 JSON",
451
- "slot_corrections": {}, "raw": str(raw)[:500], "parsed": False}
452
- word = str(data.get("verdict") or data.get("decision") or data.get("state")
453
- or data.get("result") or "").strip().lower()
454
- reason = str(data.get("reason") or data.get("why") or data.get("detail") or "").strip()
455
- corr = data.get("slot_corrections") or data.get("corrections") or {}
456
- corr = corr if isinstance(corr, dict) else {}
457
- got = _VERDICT_MAP.get(word)
458
- if got is None:
459
- return {"verdict": DEFER, "slot_corrections": corr, "parsed": True, "raw": str(raw)[:500],
460
- "reason": "单元未给出可识别的裁决(%r)%s" % (word, (":" + reason) if reason else "")}
461
- return {"verdict": got, "reason": reason, "slot_corrections": corr,
462
- "checks": data.get("checks"), "raw": str(raw)[:500], "parsed": True}
463
-
464
-
465
- # 生效条件:state==HIVE 时返回 "hive:<role>:<job_id 或 'unknown'>";state==SUBAGENT 时返回 "subagent:<role>:<channel 或 'harness'>";其余 state 返回空串。
466
- def transport_name(state: str, role: str, job_id: str = "", channel: str = "") -> str:
467
- """裁决来源标识:**结构上不可能等于编译者**,保证 A 裁定「不得自证」不被误伤。"""
468
- if state == HIVE:
469
- return "hive:%s:%s" % (role, job_id or "unknown")
470
- if state == SUBAGENT:
471
- return "subagent:%s:%s" % (role, channel or "harness")
472
- return ""
473
-
474
-
475
- # 生效条件:unit 为假值时按 {} 处理;verdict=unit.get("verdict") or DEFER,verifier=transport_name(state, role, job_id, channel),evidence 取 unit 的 reason,为空则退回 str(unit["raw"])[:300],job_id 为真时再追加“(job=<job_id>)”;仅当 unit 的 slot_corrections 为真才带该键,仅当 compiled_by 与 verifier 皆非空且相等才置 self_verify=True。
476
- def to_attest_args(unit: dict, *, state: str = "", role: str = REFLECT, job_id: str = "",
477
- channel: str = "", compiled_by: str = "") -> dict:
478
- """单元裁决 → ccgc.attest(...) 入参(纯函数,离线可测)。
479
-
480
- 诚实边界:只做搬运与命名;裁决正确性由外部单元负责,本模块不为其背书。
481
- """
482
- unit = unit or {}
483
- verdict = unit.get("verdict") or DEFER
484
- verifier = transport_name(state, role, job_id, channel)
485
- evidence = str(unit.get("reason") or "")
486
- if unit.get("raw") and not evidence:
487
- evidence = str(unit["raw"])[:300]
488
- if job_id:
489
- evidence = "%s(job=%s)" % (evidence, job_id)
490
- args = {"verdict": verdict, "verifier": verifier, "evidence": evidence}
491
- if unit.get("slot_corrections"):
492
- args["slot_corrections"] = unit["slot_corrections"]
493
- if compiled_by and verifier and verifier == compiled_by:
494
- args["self_verify"] = True
495
- return args
496
-
497
-
498
- # ---------------------------------------------------------------- 复核请求包
499
-
500
- _REFLECT_TPL = """你是认知图记忆节点的**反思单元**(reflect)。下面是一份由 ccgc 编译出的
501
- CCG 六要素候选(功能名/生效条件/子功能/执行/验证方式/不适用条件)。请逐项反思:
502
-
503
- 1) 六要素是否齐备,是否都能在「对话记录」里找到字面依据;
504
- 2) 「生效条件」四槽(场景/时窗/对象/前置)是否与实际适用范围一致,有无过度泛化;
505
- 3) 「不适用条件」(拒绝域)是否漏掉了明显该拒的情形;
506
- 4) 若有问题,给出**最小修正**(只改该字段,不重写)。
507
-
508
- 编译产物(JSON):
509
- {digest}
510
-
511
- 原始对话记录(真源,引用必须出自此处):
512
- {dialog}
513
-
514
- 只输出一个 JSON 对象,不要任何解释文字:
515
- {{"verdict":"accept|drop|defer","reason":"一句话依据","slot_corrections":{{}},
516
- "checks":[{{"element":"功能名","ok":true,"note":""}}]}}"""
517
-
518
- _VERIFY_TPL = """你是独立**验证单元**(verify)。对下面这份 CCG 六要素候选与反思单元的结论
519
- 逐条复核:来源是否真实可追溯、结论是否被对话记录支持、修正是否越权(不得新增事实)。
520
-
521
- **你只能否决或存疑,不得新增主张、不得改写事实**(可给出 slot_corrections 修正已有槽位)。
522
-
523
- 候选(JSON):
524
- {digest}
525
-
526
- 反思单元结论:
527
- {reflect_rows}
528
-
529
- 对话记录(真源):
530
- {dialog}
531
-
532
- 只输出一个 JSON 对象:
533
- {{"verdict":"accept|drop|defer","reason":"一句话依据","slot_corrections":{{}}}}"""
534
-
535
-
536
- # 生效条件:digest is None 时返回 "{}";digest 是 str 时原样返回;否则先试 to_dict/as_dict 可调用方法并 json.dumps 其返回值(抛异常即跳出改走后续分支);是 dict 时 json.dumps(digest);其余取 vars(digest) 中不以 "_" 开头的属性 json.dumps(default=str)。
537
- def _digest_of(digest) -> str:
538
- if digest is None:
539
- return "{}"
540
- if isinstance(digest, str):
541
- return digest
542
- for attr in ("to_dict", "as_dict"):
543
- fn = getattr(digest, attr, None)
544
- if callable(fn):
545
- try:
546
- return json.dumps(fn(), ensure_ascii=False, indent=1)
547
- except Exception: # noqa: BLE001
548
- break
549
- if isinstance(digest, dict):
550
- return json.dumps(digest, ensure_ascii=False, indent=1)
551
- return json.dumps({k: v for k, v in vars(digest).items() if not k.startswith("_")},
552
- ensure_ascii=False, indent=1, default=str)
553
-
554
-
555
- # 生效条件:role==VERIFY 选 _VERIFY_TPL,否则选 _REFLECT_TPL;reflect_rows 是 str 时直接作为 rows,否则 json.dumps(reflect_rows or [], ensure_ascii=False, indent=1);最终返回 tpl.format(digest=_digest_of(digest), dialog=str(dialog or "(未提供)")[:6000], reflect_rows=rows[:3000])。
556
- def prompt_for(role: str, digest=None, *, dialog: str = "", reflect_rows="") -> str:
557
- """按角色生成复核请求包正文(reflect/verify 共用一处模板真源)。"""
558
- tpl = _VERIFY_TPL if role == VERIFY else _REFLECT_TPL
559
- rows = reflect_rows if isinstance(reflect_rows, str) else json.dumps(
560
- reflect_rows or [], ensure_ascii=False, indent=1)
561
- return tpl.format(digest=_digest_of(digest), dialog=str(dialog or "(未提供)")[:6000],
562
- reflect_rows=rows[:3000])
563
-
564
-
565
- # 生效条件:text 取 prompt 真值或 prompt_for(role, digest, dialog=dialog, reflect_rows=reflect_rows);pr=probe(jobs, model, allow_degrade=allow_degrade, channel=channel) 为 SUBAGENT 时原样返回未派发的 SUBAGENT 包、为 CONFIGURE 时返回未派发的 CONFIGURE 包;为 HIVE 时若 autostart 为真且 serve_state(jobs) 非 alive 先 autostart_serve(jobs),再 submit(...),submit 失败回 CONFIGURE 且把 error 拼进 hint,成功则给出 job_id,blocking=False 直接返回;blocking=True 时 wait(sub["job_id"], jobs=jobs, timeout_s=wait_s),res.ok 为假则 unit 记 DEFER 并附未产出有效结果的 hint,为真则 unit=verdict_of(res.get("content")),随后填 transport/attest/result_ok/waited_s 并 _log。
566
- def review(*, prompt: str = "", role: str = REFLECT, digest=None, dialog: str = "",
567
- reflect_rows="", node_id: str = "", jobs: str = "", model: str = "",
568
- timeout_s: int = DEFAULT_TIMEOUT_S, allow_degrade: bool = False,
569
- channel: str = "", wait_s: float = DEFAULT_TIMEOUT_S, blocking: bool = True,
570
- cg=None, actor: str = "", autostart: bool = False) -> dict:
571
- """复核主入口:**蜂巢优先 → 提示配置 → 端子代理降级**(三态各自如实返回)。
572
-
573
- 返回统一形态:{state, role, node_id, transport, unit, attest, prompt, job_id, hint}
574
- - state=hive :已派发;blocking=True 时等终态并给出 unit/attest;否则只给 job_id
575
- - state=configure:**未派发**,hint 为配置指引(使用者裁定:不可用即提示配置)
576
- - state=subagent :**未派发**,返回 prompt 包供宿主子代理执行并回填
577
- """
578
- text = prompt or prompt_for(role, digest, dialog=dialog, reflect_rows=reflect_rows)
579
- pr = probe(jobs, model, allow_degrade=allow_degrade, channel=channel)
580
- base = {"role": role, "node_id": node_id, "prompt": text,
581
- "transport": None, "unit": None, "attest": None,
582
- "job_id": None, "hint": pr.get("hint") or "", "channel": pr.get("channel") or "",
583
- "degrade": {"channel": channel or "harness-subagent",
584
- "how": "宿主子代理执行 prompt → 裁决经 verdicts 回填 attest"}}
585
- if pr["state"] == SUBAGENT:
586
- return {"state": SUBAGENT, **base,
587
- "degrade": {"available": True, "channel": channel or "harness-subagent",
588
- "how": "把 prompt 交给宿主子代理,取其 JSON 裁决后调 "
589
- "cg(op=ccg, action=attest, verdict=<accept|drop|defer>, "
590
- "verifier=subagent:<role>:<channel>)"}}
591
- if pr["state"] == CONFIGURE:
592
- return {"state": CONFIGURE, **base,
593
- "degrade": {"available": True, "channel": channel or "harness-subagent",
594
- "how": "① 按 hint 配置蜂巢后重试;② 或带 allow_degrade=true 降级子代理"}}
595
- if autostart and not serve_state(jobs)["alive"]:
596
- pr["autostart"] = autostart_serve(jobs)
597
- sub = submit(prompt=text, role=role, jobs=jobs, model=model, timeout_s=timeout_s,
598
- cg=cg, actor=actor)
599
- if not sub.get("ok"):
600
- return {"state": CONFIGURE, **base, "hint": (pr.get("hint") or "") + "\n" + str(sub.get("error"))}
601
- out = {"state": HIVE, **base, "job_id": sub["job_id"], "unit_role": role,
602
- "model": sub.get("model")}
603
- if not blocking:
604
- return out
605
- res = wait(sub["job_id"], jobs=jobs, timeout_s=wait_s)
606
- unit = verdict_of(res.get("content")) if res.get("ok") else {
607
- "verdict": DEFER, "reason": res.get("error") or "单元未返回可用结果",
608
- "slot_corrections": {}, "parsed": False}
609
- out["unit"] = unit
610
- out["transport"] = transport_name(HIVE, role, sub["job_id"])
611
- out["attest"] = to_attest_args(unit, state=HIVE, role=role, job_id=sub["job_id"])
612
- out["result_ok"] = bool(res.get("ok"))
613
- out["waited_s"] = res.get("waited_s")
614
- if not res.get("ok"):
615
- out["hint"] = "蜂巢 job 未产出有效结果:%s(裁决按 DEFER 处理,未假装通过)" % res.get("error")
616
- _log(cg, {"action": "review", "job_id": sub["job_id"], "role": role,
617
- "node_id": node_id, "actor": actor, "unit_verdict": unit.get("verdict"),
618
- "result_ok": bool(res.get("ok"))})
619
- return out
620
-
621
-
622
- # ---------------------------------------------------------------- serve 拉起(显式)
623
-
624
- # 生效条件:serve_state(jobs_dir(jobs)) 已 alive 时返回 started=False 的“serve 存活”;否则 exe_path() 未被 isfile 命中时返回 started=False 并提示先 cargo build --release;否则以 detached/新会话 Popen 拉起 exe serve --jobs jd 并把 stdout/stderr 写入 jd/_serve.log,Popen 抛 OSError 时返回 started=False 的拉起失败;拉起后在 float(wait_s) 内轮询到 alive 返回 started=True 的“serve 已拉起”,轮询超时仍返回 started=True 但标注心跳未就绪。
625
- def autostart_serve(jobs: str = "", wait_s: float = 5.0) -> dict:
626
- """**显式**拉起 serve(默认不启用;使用者裁定:不可用即提示配置)。
627
-
628
- 形态同 hive_mcp._ensure_serve:detached + 独立日志,不阻塞调用方。
629
- """
630
- jd = jobs_dir(jobs)
631
- if serve_state(jd)["alive"]:
632
- return {"started": False, "note": "serve 存活"}
633
- exe = exe_path()
634
- if not os.path.isfile(exe):
635
- return {"started": False, "note": "未找到可执行文件 %s——先 cargo build --release" % exe}
636
- kwargs = {}
637
- if os.name == "nt":
638
- kwargs["creationflags"] = subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW
639
- else:
640
- kwargs["start_new_session"] = True
641
- try:
642
- os.makedirs(jd, exist_ok=True)
643
- with open(os.path.join(jd, "_serve.log"), "ab") as logf:
644
- subprocess.Popen([exe, "serve", "--jobs", jd], stdout=logf, stderr=logf,
645
- stdin=subprocess.DEVNULL, **kwargs)
646
- except OSError as exc:
647
- return {"started": False, "note": "拉起失败:%s: %s" % (type(exc).__name__, exc)}
648
- t0 = time.time()
649
- while time.time() - t0 < float(wait_s):
650
- if serve_state(jd)["alive"]:
651
- return {"started": True, "note": "serve 已拉起"}
652
- time.sleep(0.1)
653
- return {"started": True, "note": "serve 已拉起(心跳未就绪,稍后自愈)"}
654
-
655
-
656
- # 生效条件:返回 dict 的 ok=p["state"]==HIVE,其中 p=plan(jobs)(model/fresh_s/allow_degrade/channel 全走默认),state/serve_alive/serve_age_s/chain/hint/model 取自该 p;jobs_dir 取 jobs_dir(jobs),exe_found 与 exe_path 取 exe_path() 是否被 isfile 命中,model_set=bool(model_name())(无参,读 ENV_MODEL),api_key_set=bool(os.environ.get(ENV_API_KEY)),env 记录 ENV_JOBS_DIR 与 ENV_MODEL 的原值。
657
- def doctor(jobs: str = "") -> dict:
658
- """能力体检(形态对齐 hive_doctor):判活 + exe + 模型 + 优先级链。"""
659
- jd = jobs_dir(jobs)
660
- p = plan(jobs)
661
- return {"ok": p["state"] == HIVE, "state": p["state"], "jobs_dir": jd,
662
- "serve_alive": p["serve"]["alive"], "serve_age_s": p["serve"]["age_s"],
663
- "exe_found": os.path.isfile(exe_path()), "exe_path": exe_path(),
664
- "model_set": bool(model_name()), "model": p["model"],
665
- "api_key_set": bool(os.environ.get(ENV_API_KEY)),
666
- "env": {"HIVE_JOBS_DIR": os.environ.get(ENV_JOBS_DIR, ""),
667
- "MDCG_UNIT_MODEL": os.environ.get(ENV_MODEL, "")},
1
+ # -*- coding: utf-8 -*-
2
+ """units.py · 复核单元接线层:蜂巢优先 → 提示配置 → harness 端子代理降级。
3
+
4
+ 定位(与 audit.py 同一条架构约束):**能力不在认知图内**。本模块不做智能判定,
5
+ 只做三件事:选通道(probe/plan)→ 派发请求(submit/wait/run)→ 记账(_units.jsonl)。
6
+
7
+ 使用者裁定(2026-09-16,第 17 条精神):编译器(ccgc)的复核**优先使用蜂巢的
8
+ 反思单元 / 验证单元**(reflect / verify,与 crosscheck.REFLECT_UNIT/VERIFY_UNIT 同值,
9
+ 不新造角色名);蜂巢不可用 → **提示使用者配置**(给出可照做的构建/启动/环境变量命令);
10
+ 或经显式 allow_degrade=True 降级到 **harness 端子代理**(宿主 agent 派发,裁决经
11
+ crosscheck 同款 verdicts 通道回填)。理由:使用者的复核是昂贵的——重活交给可并行的
12
+ 外部单元,agent 本体只做编排与记账。
13
+
14
+ 蜂巢契约(真源 hive/README.md + hive/hive_mcp/mcp_server.py;**复制契约不 import**,
15
+ 保持 md_cg 对 hive 零依赖,与 ccgc「同款语义就地实现防 import 环」惯例一致):
16
+ jobs 目录 = $HIVE_JOBS_DIR | <repo>/hive/jobs
17
+ serve 判活 = **三层**:jobs/_serve.json 的 ts(毫秒)距今 < FRESH_S(15s)
18
+ ∧ pid 存活 ∧ 该 pid 是本程序
19
+ (同 hive/serve_start.serve_alive;四路口径由
20
+ hive/test_serve_entry.py 机械守卫)
21
+ job 目录 = jobs/<job_id>/{spec.json,status.json,result.json,kill}
22
+ job_id = h<java_ms>_<uuid6> (同 _submit)
23
+ result.json = {"ok":true,"content":...} | {"ok":false,"error":...}
24
+ 终态 = done | error | timeout | killed
25
+ 拉起 serve = <repo>/hive/target/release/hive serve --jobs <jobs>(detached)
26
+
27
+ 边界(诚实声明,不猜测):
28
+ · 本模块**不产生裁决**,只搬运;裁决由外部单元负责。
29
+ · 契约漂移:hive 侧协议若变更,本模块以「无 result / 超时」如实失败,不假装成功。
30
+ · 默认**不自动拉起 serve**(使用者裁定:不可用即提示配置);autostart=True 才拉起。
31
+ """
32
+ from __future__ import annotations
33
+
34
+ import json
35
+ import os
36
+ import re
37
+ import subprocess
38
+ import time
39
+ import uuid
40
+
41
+ HIVE, SUBAGENT, CONFIGURE = "hive", "subagent", "configure"
42
+ STATES = (HIVE, SUBAGENT, CONFIGURE)
43
+
44
+ REFLECT, VERIFY = "reflect", "verify" # 同 crosscheck.REFLECT_UNIT/VERIFY_UNIT
45
+ ROLES = (REFLECT, VERIFY)
46
+
47
+ SERVE_FILE, SPEC_FILE = "_serve.json", "spec.json"
48
+ STATUS_FILE, RESULT_FILE, KILL_FILE = "status.json", "result.json", "kill"
49
+ LOG_NAME = "_units.jsonl"
50
+ TERMINAL_STATES = ("done", "error", "timeout", "killed")
51
+ #: serve 心跳新鲜窗口(秒)。**须与 hive/serve_start.FRESH_S 同值**——本模块
52
+ #: 只持有「判活的第 4 处实现」(复制契约不 import hive),阈值/判据结构若与
53
+ #: 权威漂移,会在通道选择面复现 v13 的「假存活」病类(v14 缺陷 E:
54
+ #: 旧值 5.0 vs 权威 15,serve 崩溃后 ≤5s 窗口内判「存活」→ 选 hive 通道 →
55
+ #: 提交的 job 永远无人处理)。守卫:hive/test_serve_entry.py。
56
+ FRESH_S, DEFAULT_TIMEOUT_S, DEFAULT_POLL_S = 15.0, 120, 1.0
57
+ #: 复核委派的 completion 预算。**必须对齐统一默认(文档:最大输出 200000)**——
58
+ #: 该模型 reasoning 与正文**共享 completion 预算**,小预算会把正文吃光并静默返回空正文:
59
+ #: 实测 2048 → reasoning 2048 / content 空;16384 → 时好时坏;200000 → 正常出裁决。
60
+ DEFAULT_MAX_TOKENS = 200000
61
+
62
+ ENV_JOBS_DIR, ENV_EXE = "HIVE_JOBS_DIR", "HIVE_EXE"
63
+ ENV_MODEL, ENV_API_KEY = "MDCG_UNIT_MODEL", "HIVE_API_KEY"
64
+
65
+ ACCEPT, REJECT, DEFER, BLINDSPOT = "ACCEPT", "REJECT", "DEFER", "BLINDSPOT"
66
+ _VERDICT_MAP = {
67
+ "accept": ACCEPT, "accepted": ACCEPT, "keep": ACCEPT, "pass": ACCEPT,
68
+ "ok": ACCEPT, "approve": ACCEPT,
69
+ "drop": REJECT, "reject": REJECT, "rejected": REJECT, "deny": REJECT, "fail": REJECT,
70
+ "defer": DEFER, "deferred": DEFER, "unsure": DEFER, "unclear": DEFER, "skip": DEFER,
71
+ "blindspot": BLINDSPOT, "blind": BLINDSPOT,
72
+ }
73
+
74
+
75
+ # 生效条件:无入参,恒返回模块 `__file__` 绝对路径上溯两级的目录(md_cg 的上一级),与 cwd 无关。
76
+ def repo_root() -> str:
77
+ """仓库根(md_cg 的上一级);不用 cwd——cwd 由宿主决定,不可作判据。"""
78
+ return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
79
+
80
+
81
+ # 生效条件:explicit 为真值时 d=explicit;explicit 为空串等假值而 ENV_JOBS_DIR 有真值时 d=该环境变量;两者皆假值时 d=repo_root()/hive/jobs;最终返回 os.path.abspath(d)。
82
+ def jobs_dir(explicit: str = "") -> str:
83
+ d = explicit or os.environ.get(ENV_JOBS_DIR) or os.path.join(repo_root(), "hive", "jobs")
84
+ return os.path.abspath(d)
85
+
86
+
87
+ # 生效条件:ENV_EXE 有真值时原样返回该值;ENV_EXE 缺失或为空串时返回 repo_root()/hive/target/release/ 下按 os.name=="nt" 取 hive.exe、否则取 hive 的拼接路径。
88
+ def exe_path() -> str:
89
+ exe = os.environ.get(ENV_EXE)
90
+ if exe:
91
+ return exe
92
+ return os.path.join(repo_root(), "hive", "target", "release",
93
+ "hive.exe" if os.name == "nt" else "hive")
94
+
95
+
96
+ # 生效条件:explicit 为真值时取 explicit,否则取 ENV_MODEL,两者皆假值时按空串,返回该结果 strip() 后的字符串(可为空串)。
97
+ def model_name(explicit: str = "") -> str:
98
+ """复核模型(LLM 委托型执行器的 spec.model 必填)。"""
99
+ return (explicit or os.environ.get(ENV_MODEL) or "").strip()
100
+
101
+
102
+ # 生效条件:pid 为 int 且大于 0 时(否则 False),os.name 为 "nt" 时返回 _tasklist_row(pid) 是否非 None,非 "nt" 时 os.kill(pid, 0) 不抛 OSError 返回 True、抛 OSError 返回 False。
103
+ def pid_alive(pid) -> bool:
104
+ """该 pid **号**是否存在(Windows tasklist 精确列比对 / unix `kill -0`)。
105
+
106
+ 只回答「这个号有没有进程」——**不足以判定「serve 还在跑」**(见
107
+ `pid_is_self_program`)。实现与 hive/serve_start.pid_alive 同口径。
108
+ """
109
+ if not isinstance(pid, int) or pid <= 0:
110
+ return False
111
+ if os.name == "nt":
112
+ return _tasklist_row(pid) is not None
113
+ try:
114
+ os.kill(pid, 0)
115
+ return True
116
+ except OSError:
117
+ return False
118
+
119
+
120
+ # 生效条件:对传入 pid 执行 tasklist /FO CSV 后,在其 stdout 中遇到的第一个按 '","' 切分、列数≥2 且第 2 列 strip 再 strip('"') 后等于 str(pid) 的行即返回 [映像名, pid 字符串],无此行或 subprocess.run 抛 OSError 时返回 None。
121
+ def _tasklist_row(pid):
122
+ """Windows:查该 pid 的 tasklist 行 → [映像名, pid 字符串];查不到返回 None。
123
+
124
+ 按列精确比对,**不用子串包含**——子串会让 pid=441 被 4410 命中(假存活)。
125
+ """
126
+ try:
127
+ # 显式 utf-8 + replace:只消费 ASCII 的 pid 列,但**不依赖 locale**——
128
+ # locale 口径与「后代写 UTF-8」不一致时读线程会崩(见 test_subproc_encoding.py)。
129
+ r = subprocess.run(["tasklist", "/FI", f"PID eq {pid}", "/NH", "/FO", "CSV"],
130
+ capture_output=True, text=True,
131
+ encoding="utf-8", errors="replace")
132
+ except OSError:
133
+ return None
134
+ for line in (r.stdout or "").splitlines():
135
+ cols = line.split('","')
136
+ if len(cols) >= 2 and cols[1].strip().strip('"') == str(pid):
137
+ return [cols[0].strip().strip('"'), cols[1].strip().strip('"')]
138
+ return None
139
+
140
+
141
+ # 生效条件:exe_path() 的 basename 小写非空且 pid 为 int 大于 0 时(否则 False),"nt" 下要求 _tasklist_row(pid) 非空且映像名小写等于该 basename,非 "nt" 下要求 /proc/<pid>/cmdline 首个 b"\x00" 前 token 的 basename 小写等于它(读取抛 OSError 则 False)。
142
+ def pid_is_self_program(pid) -> bool:
143
+ """该 pid 是否**就是本程序**(同映像名)——pid 号会被无关进程复用。
144
+
145
+ 与 hive/serve_start.pid_is_self_program 同口径(复制契约)。零依赖边界:
146
+ 拿不到映像名返回 False(宁可放行启动,也不误报「已有 serve 在跑」)。
147
+ """
148
+ want = os.path.basename(exe_path()).lower()
149
+ if not want or not isinstance(pid, int) or pid <= 0:
150
+ return False
151
+ if os.name == "nt":
152
+ row = _tasklist_row(pid)
153
+ return bool(row) and row[0].lower() == want
154
+ try:
155
+ with open(f"/proc/{pid}/cmdline", "rb") as f:
156
+ first = f.read().split(b"\x00")[0]
157
+ except OSError:
158
+ return False
159
+ return os.path.basename(first.decode("utf-8", "replace")).lower() == want
160
+
161
+
162
+ # 生效条件:以 jobs_dir(jobs) 下的 SERVE_FILE 为心跳路径——该路径不被 isfile 命中时返回 exists/alive 均 False 的未启动 reason;命中但 open/json.load 抛 OSError 或 ValueError 时返回不可读 reason;解析成功后按**三层判据**(age < fresh_s ∧ pid_alive(pid) ∧ pid_is_self_program(pid))定 alive,回填 age_s/pid/raw 与逐层明细及失败原因。
163
+ def serve_state(jobs: str = "", fresh_s: float = FRESH_S) -> dict:
164
+ """serve 判活:**三层**(心跳新鲜 ∧ pid 存活 ∧ 该 pid 是本程序)。
165
+
166
+ 与 `hive/serve_start.serve_alive` 同口径(同值 FRESH_S=15s、同三层判据)。
167
+ v14 缺陷 E:旧实现只判 ts 新鲜度且阈值 5s——双漂移,serve 崩溃后
168
+ ≤5s 内判「存活」会让 `probe()` 选 hive 通道(提交的 job 永远无人处理),
169
+ 这正是 v13「假存活」病类从守卫面搬到了**通道选择面**。
170
+ """
171
+ jd = jobs_dir(jobs)
172
+ p = os.path.join(jd, SERVE_FILE)
173
+ out = {"jobs_dir": jd, "heartbeat": p, "exists": os.path.isfile(p),
174
+ "alive": False, "age_s": None, "pid": None, "reason": ""}
175
+ if not out["exists"]:
176
+ out["reason"] = "未见 _serve.json 心跳(serve 未启动或 jobs 目录不对)"
177
+ return out
178
+ try:
179
+ with open(p, encoding="utf-8") as f:
180
+ hb = json.load(f)
181
+ except (OSError, ValueError) as exc:
182
+ out["reason"] = "心跳不可读:%s: %s" % (type(exc).__name__, exc)
183
+ return out
184
+ age = time.time() - ((hb.get("ts") or 0) / 1000.0)
185
+ pid = hb.get("pid")
186
+ fresh_ok = age < float(fresh_s)
187
+ pid_ok = pid_alive(pid)
188
+ ident_ok = pid_is_self_program(pid)
189
+ out.update({"age_s": round(age, 3), "pid": pid, "raw": hb,
190
+ "fresh": bool(fresh_ok), "pid_alive": bool(pid_ok),
191
+ "pid_is_self_program": bool(ident_ok)})
192
+ out["alive"] = bool(fresh_ok and pid_ok and ident_ok)
193
+ if out["alive"]:
194
+ out["reason"] = ""
195
+ elif not fresh_ok:
196
+ out["reason"] = "心跳过期 %.1fs(阈值 %ss)——serve 可能已退出" % (age, fresh_s)
197
+ elif not pid_ok:
198
+ out["reason"] = "心跳新鲜但 pid=%s 已不存在——serve 已退出" % (pid,)
199
+ else:
200
+ out["reason"] = ("心跳新鲜且 pid=%s 存活,但该 pid 不属于本程序"
201
+ "(非 %s)——pid 号被无关进程复用"
202
+ % (pid, os.path.basename(exe_path())))
203
+ return out
204
+
205
+
206
+ # 生效条件:jobs 仅经 jobs_dir(jobs) 用于填充指引里的 jobs 路径;reason 为假值时文案首行取“serve 未存活”,否则取 reason 原文;返回模板固定嵌入 exe_path() 的目录与路径、ENV_API_KEY、ENV_MODEL、jd、jd 下的 SERVE_FILE 与 int(FRESH_S)。
207
+ def setup_hint(jobs: str = "", reason: str = "") -> str:
208
+ """不可用时给使用者看的**配置指引**(含降级指引)——提示即责任,须可照着做。"""
209
+ jd = jobs_dir(jobs)
210
+ return ("蜂巢不可用:%s\n"
211
+ " [A 配置蜂巢(推荐,第 17 条)]\n"
212
+ " 1) cd %s && cargo build --release\n"
213
+ " 2) set %s=<LLM密钥>\n 3) set %s=<复核模型名>\n"
214
+ " 4) 启动:%s serve --jobs \"%s\"(或 hive_spawn 自动拉起)\n"
215
+ " 判活判据:%s 的 ts 距今 < %ss\n"
216
+ " [B 降级 harness 端子代理]\n"
217
+ " 重发本请求并带 allow_degrade=true:本模块返回复核请求包(prompt),\n"
218
+ " 由宿主子代理执行后经 verdicts 回填裁决。"
219
+ % (reason or "serve 未存活", os.path.dirname(exe_path()), ENV_API_KEY,
220
+ ENV_MODEL, exe_path(), jd, os.path.join(jd, SERVE_FILE), int(FRESH_S)))
221
+
222
+
223
+ # 生效条件:先取 st=serve_state(jobs, fresh_s) 与 mdl=model_name(model);st["alive"] 为真且 mdl 非空时返回 state=HIVE;否则 allow_degrade 为真时返回 state=SUBAGENT 且 channel 取 channel 或 "harness-subagent";否则返回 state=CONFIGURE、transport=None 并附 setup_hint(jobs, why)。
224
+ def probe(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
225
+ allow_degrade: bool = False, channel: str = "") -> dict:
226
+ """三级能力探测:hive(可派发)→ configure(提示配置)→ subagent(显式降级)。
227
+
228
+ 判据只认**心跳 + 模型名**两项硬前提;缺任一即如实降级,绝不用相似度
229
+ (如「hive 目录存在」)冒充可用(资格由条件证据裁决,不由相似度裁决)。
230
+ """
231
+ st = serve_state(jobs, fresh_s)
232
+ mdl = model_name(model)
233
+ if st["alive"] and mdl:
234
+ return {"state": HIVE, "transport": HIVE, "model": mdl, "jobs_dir": st["jobs_dir"],
235
+ "serve": st, "channel": "", "hint": ""}
236
+ why = st["reason"] if not st["alive"] else "serve 存活但未配置复核模型(%s)" % ENV_MODEL
237
+ if allow_degrade:
238
+ return {"state": SUBAGENT, "transport": SUBAGENT, "model": mdl,
239
+ "jobs_dir": st["jobs_dir"], "serve": st,
240
+ "channel": channel or "harness-subagent",
241
+ "hint": "已按 allow_degrade 降级到 harness 端子代理:请把 prompt 交给宿主子代理,"
242
+ "裁决经 verdicts 回填(蜂巢不可用原因:%s)" % why}
243
+ return {"state": CONFIGURE, "transport": None, "model": mdl,
244
+ "jobs_dir": st["jobs_dir"], "serve": st, "channel": "",
245
+ "hint": setup_hint(jobs, why)}
246
+
247
+
248
+ # 生效条件:state/transport/model/jobs_dir/serve/channel/hint 全部直接取自 probe(jobs, model, fresh_s, allow_degrade, channel) 的对应键,本函数不另做判活或模型判断,仅在 chain 文案中固定引用 int(FRESH_S) 与 ENV_MODEL。
249
+ def plan(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
250
+ allow_degrade: bool = False, channel: str = "") -> dict:
251
+ """优先级链自描述(供 cg op=ccg action=units 与审计查看)。"""
252
+ p = probe(jobs, model, fresh_s, allow_degrade, channel)
253
+ return {"state": p["state"], "transport": p["transport"], "model": p["model"],
254
+ "jobs_dir": p["jobs_dir"], "serve": p["serve"], "channel": p["channel"],
255
+ "chain": [
256
+ {"order": 1, "transport": HIVE, "action": "派发 reflect/verify 单元并等待 result.json",
257
+ "when": ("jobs/_serve.json 心跳 < %ss 且(%s 已配置 **或调用方显式传 model**)"
258
+ % (int(FRESH_S), ENV_MODEL))},
259
+ {"order": 2, "transport": CONFIGURE, "action": "返回配置指引(不自动拉起、不假装通过)",
260
+ "when": "蜂巢不可用"},
261
+ {"order": 3, "transport": SUBAGENT, "action": "返回复核请求包,由 harness 端子代理执行并回填",
262
+ "when": "调用方显式 allow_degrade=True"},
263
+ ],
264
+ "hint": p["hint"]}
265
+
266
+
267
+ # ---------------------------------------------------------------- 派发与收取
268
+
269
+ # 生效条件:无入参,恒返回 "h"+int(time.time()*1000)+"_"+uuid.uuid4().hex 前 6 位组成的字符串。
270
+ def _job_id() -> str:
271
+ """同 _submit:serve 侧按 'h' 前缀识别任务目录。"""
272
+ return "h%d_%s" % (int(time.time() * 1000), uuid.uuid4().hex[:6])
273
+
274
+
275
+ # 生效条件:cg.root 与 cg.cg.root 都取不到真值时不写、直接返回 None;取到 root 时向 root/LOG_NAME 追加一行 rec(副本,setdefault ts)的 JSON,写入抛 OSError 时被吞掉静默返回 None。
276
+ def _log(cg, rec: dict) -> None:
277
+ """留痕 _units.jsonl(对齐 _crosscheck.jsonl / _backfill.jsonl 纪律)。"""
278
+ root = getattr(cg, "root", None) or getattr(getattr(cg, "cg", None), "root", None)
279
+ if not root:
280
+ return
281
+ try:
282
+ rec = dict(rec)
283
+ rec.setdefault("ts", int(time.time() * 1000))
284
+ with open(os.path.join(str(root), LOG_NAME), "a", encoding="utf-8") as f:
285
+ f.write(json.dumps(rec, ensure_ascii=False) + "\n")
286
+ except OSError:
287
+ pass
288
+
289
+
290
+ # 生效条件:role 不在 ROLES 时返回 ok=False 的未知角色 error;否则 model_name(model) 为空时返回 ok=False 的未配置复核模型;否则 str(prompt or "").strip() 为空时返回 ok=False 的 prompt 为空;否则在 jobs_dir(jobs)/_job_id() 下写 spec.json 与 status.json(context_files 为真、temperature 非 None、extra 为真时才并入 spec),OSError 时返回 ok=False 的写入失败,全部成功返回 ok=True 与 job_id/job_dir/spec/model。
291
+ def submit(*, prompt: str, role: str = REFLECT, model: str = "", system_prompt: str = "",
292
+ context_files=None, timeout_s: int = DEFAULT_TIMEOUT_S,
293
+ max_tokens: int = DEFAULT_MAX_TOKENS, temperature=None, jobs: str = "",
294
+ actor: str = "", extra: dict = None, cg=None) -> dict:
295
+ """写一条 LLM 委托型 job(spec.json + status.json,形态同 _submit);只写文件不起进程。"""
296
+ if role not in ROLES:
297
+ return {"ok": False, "error": "未知复核角色:%r(可选 %s)" % (role, list(ROLES))}
298
+ mdl = model_name(model)
299
+ if not mdl:
300
+ return {"ok": False, "error": "未配置复核模型:请设 %s 或显式传 model" % ENV_MODEL}
301
+ if not str(prompt or "").strip():
302
+ return {"ok": False, "error": "prompt 为空"}
303
+ jd, job_id = jobs_dir(jobs), _job_id()
304
+ d = os.path.join(jd, job_id)
305
+ spec = {"model": mdl, "user_prompt": str(prompt), "system_prompt": str(system_prompt or ""),
306
+ "timeout_s": int(timeout_s), "max_tokens": int(max_tokens),
307
+ "role": role, "unit_role": role}
308
+ if context_files:
309
+ spec["context_files"] = list(context_files)
310
+ if temperature is not None:
311
+ spec["temperature"] = temperature
312
+ if extra:
313
+ spec.update(extra)
314
+ try:
315
+ os.makedirs(d, exist_ok=True)
316
+ with open(os.path.join(d, SPEC_FILE), "w", encoding="utf-8") as f:
317
+ json.dump(spec, f, ensure_ascii=False)
318
+ with open(os.path.join(d, STATUS_FILE), "w", encoding="utf-8") as f:
319
+ json.dump({"job_id": job_id, "state": "pending",
320
+ "created_ts": int(time.time() * 1000), "started_ts": None,
321
+ "heartbeat_ts": None, "elapsed_s": 0.0, "timeout_s": int(timeout_s),
322
+ "model": mdl, "pid": None, "error": None}, f, ensure_ascii=False)
323
+ except OSError as exc:
324
+ return {"ok": False, "job_dir": d,
325
+ "error": "job 写入失败:%s: %s" % (type(exc).__name__, exc)}
326
+ _log(cg, {"action": "submit", "job_id": job_id, "role": role, "model": mdl,
327
+ "timeout_s": int(timeout_s), "actor": actor, "prompt_head": str(prompt)[:200]})
328
+ return {"ok": True, "job_id": job_id, "job_dir": d, "spec": spec,
329
+ "unit_role": role, "model": mdl}
330
+
331
+
332
+ # 生效条件:jobs_dir(jobs)/str(job_id or "") 不是目录时返回 state=missing、terminal=False 的目录不存在 error;是目录时读 STATUS_FILE(读失败则 status 置 None)并用其 state 覆盖 state/terminal(state 属 TERMINAL_STATES 才 terminal=True);RESULT_FILE 被 isfile 命中则 terminal=True、state=st or "done",解析抛 OSError/ValueError 时提前返回该 error;解析为 dict 时取 ok/content/error/usage/model,且 ok 为真而 content 去空白为空时把 ok 改 False 并写空正文 error,解析为非 dict 时 ok=True 且 content 为原值。
333
+ def poll(job_id: str, jobs: str = "") -> dict:
334
+ """读 job 终态视图:**以 result.json 出现为终态主判据**,status.json 仅作辅助。"""
335
+ d = os.path.join(jobs_dir(jobs), str(job_id or ""))
336
+ out = {"job_id": job_id, "job_dir": d, "state": "missing", "terminal": False,
337
+ "ok": False, "content": None, "error": None, "status": None}
338
+ if not os.path.isdir(d):
339
+ out["error"] = "job 目录不存在:%s" % d
340
+ return out
341
+ sp = os.path.join(d, STATUS_FILE)
342
+ if os.path.isfile(sp):
343
+ try:
344
+ with open(sp, encoding="utf-8") as f:
345
+ out["status"] = json.load(f)
346
+ except (OSError, ValueError):
347
+ out["status"] = None
348
+ st = (out["status"] or {}).get("state")
349
+ if st:
350
+ out.update({"state": st, "terminal": st in TERMINAL_STATES})
351
+ rp = os.path.join(d, RESULT_FILE)
352
+ if os.path.isfile(rp):
353
+ out["state"] = st or "done"
354
+ out["terminal"] = True
355
+ try:
356
+ with open(rp, encoding="utf-8") as f:
357
+ res = json.load(f)
358
+ except (OSError, ValueError) as exc:
359
+ out["error"] = "result.json 不可读:%s" % type(exc).__name__
360
+ return out
361
+ if isinstance(res, dict):
362
+ out["ok"] = bool(res.get("ok"))
363
+ out["content"] = res.get("content")
364
+ out["error"] = res.get("error") or out["error"]
365
+ out["usage"] = res.get("usage")
366
+ out["model"] = res.get("model")
367
+ # 空正文**不得报成功**:reasoning 与正文共享 completion 预算,预算不足时
368
+ # job 仍自称 ok 但 content 为空 → 若不拦,会被误读成「复核单元无答复」
369
+ # (假死锁)。run() 早已有同等校验,此处补齐,消除两处口径不一致。
370
+ if out["ok"] and not str(out["content"] or "").strip():
371
+ out["ok"] = False
372
+ out["error"] = ("空正文:job 自称 ok 但 content 为空(多为 reasoning 吃尽"
373
+ "完成预算;提高 max_tokens,勿把预算不足当通道不可用)")
374
+ else:
375
+ out["ok"], out["content"] = True, res
376
+ return out
377
+
378
+
379
+ # 生效条件:循环 poll(job_id, jobs),结果 terminal 为真即补 waited_s 后返回;否则 time.time()-t0 >= float(timeout_s) 时返回 terminal=False、timeout=True 与超时 error;两者皆不满足则 sleep(float(poll_s)) 后重试(timeout_s=0 时首次 poll 非终态即超时返回)。
380
+ def wait(job_id: str, *, jobs: str = "", timeout_s: float = DEFAULT_TIMEOUT_S,
381
+ poll_s: float = DEFAULT_POLL_S) -> dict:
382
+ """阻塞等终态;超时如实返回(不假装成功、不强杀 job)。"""
383
+ t0 = time.time()
384
+ while True:
385
+ cur = poll(job_id, jobs)
386
+ if cur.get("terminal"):
387
+ cur["waited_s"] = round(time.time() - t0, 3)
388
+ return cur
389
+ if (time.time() - t0) >= float(timeout_s):
390
+ cur.update({"terminal": False, "timeout": True, "waited_s": round(time.time() - t0, 3),
391
+ "error": "等待超时 %.0fs(job 仍在 %s)" % (float(timeout_s),
392
+ cur.get("state"))})
393
+ return cur
394
+ time.sleep(float(poll_s))
395
+
396
+
397
+ # 生效条件:submit(prompt=prompt, role=role, cg=cg, actor=actor, **kw) 的 ok 为假时返回 stage="submit" 的失败 dict(含 **sub);ok 为真时 wait(sub["job_id"], jobs=kw.get("jobs",""), timeout_s=wait_s),补 stage="wait"/job_id/unit_role/model,并把 ok 改为 res 的 ok 与 content 同时为真。
398
+ def run(*, prompt: str, role: str = REFLECT, cg=None, actor: str = "",
399
+ wait_s: float = DEFAULT_TIMEOUT_S, **kw) -> dict:
400
+ """submit + wait 组合(阻塞式复核,MCP action=review 主路径)。"""
401
+ sub = submit(prompt=prompt, role=role, cg=cg, actor=actor, **kw)
402
+ if not sub.get("ok"):
403
+ return {"ok": False, "stage": "submit", "error": sub.get("error"), **sub}
404
+ res = wait(sub["job_id"], jobs=kw.get("jobs", ""), timeout_s=wait_s)
405
+ res.update({"stage": "wait", "job_id": sub["job_id"], "unit_role": role,
406
+ "model": sub.get("model")})
407
+ res["ok"] = bool(res.get("ok")) and bool(res.get("content"))
408
+ return res
409
+
410
+
411
+ # ---------------------------------------------------------------- 裁决解析与搬运
412
+
413
+ _FENCE_RE = re.compile(r"```(?:json)?\s*(.+?)\s*```", re.S)
414
+
415
+
416
+ # 生效条件:raw 是 dict/list 时原样返回;否则取 str(raw or "").strip(),为空串(含 0/False/None 等假值)返回 None;否则依次尝试 _FENCE_RE 第 1 组、整段文本、首个 "{" 到末个 "}" 及首个 "[" 到末个 "]" 的切片,返回首个能 json.loads 成 dict 或 list 的候选;全部失败返回 None。
417
+ def _json_of(raw):
418
+ """从单元输出抽第一个 JSON 对象/数组(容忍 ```json 围栏与前后噪声)。"""
419
+ if isinstance(raw, (dict, list)):
420
+ return raw
421
+ text = str(raw or "").strip()
422
+ if not text:
423
+ return None
424
+ cands = []
425
+ m = _FENCE_RE.search(text)
426
+ if m:
427
+ cands.append(m.group(1))
428
+ cands.append(text)
429
+ for opener, closer in (("{", "}"), ("[", "]")):
430
+ i, j = text.find(opener), text.rfind(closer)
431
+ if 0 <= i < j:
432
+ cands.append(text[i:j + 1])
433
+ for c in cands:
434
+ try:
435
+ v = json.loads(c)
436
+ except ValueError:
437
+ continue
438
+ if isinstance(v, (dict, list)):
439
+ return v
440
+ return None
441
+
442
+
443
+ # 生效条件:data=_json_of(raw) 为 list 时取 data[0](仅当它是 dict,否则 None);非 dict 时返回 verdict=DEFER、parsed=False;dict 时 word 取 verdict/decision/state/result 中首个真值后 strip().lower(),_VERDICT_MAP.get(word) 为 None 时返回 DEFER、parsed=True 并带 corr 与 reason,命中时返回该裁决及 reason、slot_corrections(取自 slot_corrections/corrections,非 dict 则置 {})、checks、raw 前 500 字符。
444
+ def verdict_of(raw) -> dict:
445
+ """单元输出 → 裁决四态;解析不出 → DEFER(不猜测、不当通过)。"""
446
+ data = _json_of(raw)
447
+ if isinstance(data, list):
448
+ data = data[0] if data and isinstance(data[0], dict) else None
449
+ if not isinstance(data, dict):
450
+ return {"verdict": DEFER, "reason": "单元输出无法解析为裁决 JSON",
451
+ "slot_corrections": {}, "raw": str(raw)[:500], "parsed": False}
452
+ word = str(data.get("verdict") or data.get("decision") or data.get("state")
453
+ or data.get("result") or "").strip().lower()
454
+ reason = str(data.get("reason") or data.get("why") or data.get("detail") or "").strip()
455
+ corr = data.get("slot_corrections") or data.get("corrections") or {}
456
+ corr = corr if isinstance(corr, dict) else {}
457
+ got = _VERDICT_MAP.get(word)
458
+ if got is None:
459
+ return {"verdict": DEFER, "slot_corrections": corr, "parsed": True, "raw": str(raw)[:500],
460
+ "reason": "单元未给出可识别的裁决(%r)%s" % (word, (":" + reason) if reason else "")}
461
+ return {"verdict": got, "reason": reason, "slot_corrections": corr,
462
+ "checks": data.get("checks"), "raw": str(raw)[:500], "parsed": True}
463
+
464
+
465
+ # 生效条件:state==HIVE 时返回 "hive:<role>:<job_id 或 'unknown'>";state==SUBAGENT 时返回 "subagent:<role>:<channel 或 'harness'>";其余 state 返回空串。
466
+ def transport_name(state: str, role: str, job_id: str = "", channel: str = "") -> str:
467
+ """裁决来源标识:**结构上不可能等于编译者**,保证 A 裁定「不得自证」不被误伤。"""
468
+ if state == HIVE:
469
+ return "hive:%s:%s" % (role, job_id or "unknown")
470
+ if state == SUBAGENT:
471
+ return "subagent:%s:%s" % (role, channel or "harness")
472
+ return ""
473
+
474
+
475
+ # 生效条件:unit 为假值时按 {} 处理;verdict=unit.get("verdict") or DEFER,verifier=transport_name(state, role, job_id, channel),evidence 取 unit 的 reason,为空则退回 str(unit["raw"])[:300],job_id 为真时再追加“(job=<job_id>)”;仅当 unit 的 slot_corrections 为真才带该键,仅当 compiled_by 与 verifier 皆非空且相等才置 self_verify=True。
476
+ def to_attest_args(unit: dict, *, state: str = "", role: str = REFLECT, job_id: str = "",
477
+ channel: str = "", compiled_by: str = "") -> dict:
478
+ """单元裁决 → ccgc.attest(...) 入参(纯函数,离线可测)。
479
+
480
+ 诚实边界:只做搬运与命名;裁决正确性由外部单元负责,本模块不为其背书。
481
+ """
482
+ unit = unit or {}
483
+ verdict = unit.get("verdict") or DEFER
484
+ verifier = transport_name(state, role, job_id, channel)
485
+ evidence = str(unit.get("reason") or "")
486
+ if unit.get("raw") and not evidence:
487
+ evidence = str(unit["raw"])[:300]
488
+ if job_id:
489
+ evidence = "%s(job=%s)" % (evidence, job_id)
490
+ args = {"verdict": verdict, "verifier": verifier, "evidence": evidence}
491
+ if unit.get("slot_corrections"):
492
+ args["slot_corrections"] = unit["slot_corrections"]
493
+ if compiled_by and verifier and verifier == compiled_by:
494
+ args["self_verify"] = True
495
+ return args
496
+
497
+
498
+ # ---------------------------------------------------------------- 复核请求包
499
+
500
+ _REFLECT_TPL = """你是认知图记忆节点的**反思单元**(reflect)。下面是一份由 ccgc 编译出的
501
+ CCG 六要素候选(功能名/生效条件/子功能/执行/验证方式/不适用条件)。请逐项反思:
502
+
503
+ 1) 六要素是否齐备,是否都能在「对话记录」里找到字面依据;
504
+ 2) 「生效条件」四槽(场景/时窗/对象/前置)是否与实际适用范围一致,有无过度泛化;
505
+ 3) 「不适用条件」(拒绝域)是否漏掉了明显该拒的情形;
506
+ 4) 若有问题,给出**最小修正**(只改该字段,不重写)。
507
+
508
+ 编译产物(JSON):
509
+ {digest}
510
+
511
+ 原始对话记录(真源,引用必须出自此处):
512
+ {dialog}
513
+
514
+ 只输出一个 JSON 对象,不要任何解释文字:
515
+ {{"verdict":"accept|drop|defer","reason":"一句话依据","slot_corrections":{{}},
516
+ "checks":[{{"element":"功能名","ok":true,"note":""}}]}}"""
517
+
518
+ _VERIFY_TPL = """你是独立**验证单元**(verify)。对下面这份 CCG 六要素候选与反思单元的结论
519
+ 逐条复核:来源是否真实可追溯、结论是否被对话记录支持、修正是否越权(不得新增事实)。
520
+
521
+ **你只能否决或存疑,不得新增主张、不得改写事实**(可给出 slot_corrections 修正已有槽位)。
522
+
523
+ 候选(JSON):
524
+ {digest}
525
+
526
+ 反思单元结论:
527
+ {reflect_rows}
528
+
529
+ 对话记录(真源):
530
+ {dialog}
531
+
532
+ 只输出一个 JSON 对象:
533
+ {{"verdict":"accept|drop|defer","reason":"一句话依据","slot_corrections":{{}}}}"""
534
+
535
+
536
+ # 生效条件:digest is None 时返回 "{}";digest 是 str 时原样返回;否则先试 to_dict/as_dict 可调用方法并 json.dumps 其返回值(抛异常即跳出改走后续分支);是 dict 时 json.dumps(digest);其余取 vars(digest) 中不以 "_" 开头的属性 json.dumps(default=str)。
537
+ def _digest_of(digest) -> str:
538
+ if digest is None:
539
+ return "{}"
540
+ if isinstance(digest, str):
541
+ return digest
542
+ for attr in ("to_dict", "as_dict"):
543
+ fn = getattr(digest, attr, None)
544
+ if callable(fn):
545
+ try:
546
+ return json.dumps(fn(), ensure_ascii=False, indent=1)
547
+ except Exception: # noqa: BLE001
548
+ break
549
+ if isinstance(digest, dict):
550
+ return json.dumps(digest, ensure_ascii=False, indent=1)
551
+ return json.dumps({k: v for k, v in vars(digest).items() if not k.startswith("_")},
552
+ ensure_ascii=False, indent=1, default=str)
553
+
554
+
555
+ # 生效条件:role==VERIFY 选 _VERIFY_TPL,否则选 _REFLECT_TPL;reflect_rows 是 str 时直接作为 rows,否则 json.dumps(reflect_rows or [], ensure_ascii=False, indent=1);最终返回 tpl.format(digest=_digest_of(digest), dialog=str(dialog or "(未提供)")[:6000], reflect_rows=rows[:3000])。
556
+ def prompt_for(role: str, digest=None, *, dialog: str = "", reflect_rows="") -> str:
557
+ """按角色生成复核请求包正文(reflect/verify 共用一处模板真源)。"""
558
+ tpl = _VERIFY_TPL if role == VERIFY else _REFLECT_TPL
559
+ rows = reflect_rows if isinstance(reflect_rows, str) else json.dumps(
560
+ reflect_rows or [], ensure_ascii=False, indent=1)
561
+ return tpl.format(digest=_digest_of(digest), dialog=str(dialog or "(未提供)")[:6000],
562
+ reflect_rows=rows[:3000])
563
+
564
+
565
+ # 生效条件:text 取 prompt 真值或 prompt_for(role, digest, dialog=dialog, reflect_rows=reflect_rows);pr=probe(jobs, model, allow_degrade=allow_degrade, channel=channel) 为 SUBAGENT 时原样返回未派发的 SUBAGENT 包、为 CONFIGURE 时返回未派发的 CONFIGURE 包;为 HIVE 时若 autostart 为真且 serve_state(jobs) 非 alive 先 autostart_serve(jobs),再 submit(...),submit 失败回 CONFIGURE 且把 error 拼进 hint,成功则给出 job_id,blocking=False 直接返回;blocking=True 时 wait(sub["job_id"], jobs=jobs, timeout_s=wait_s),res.ok 为假则 unit 记 DEFER 并附未产出有效结果的 hint,为真则 unit=verdict_of(res.get("content")),随后填 transport/attest/result_ok/waited_s 并 _log。
566
+ def review(*, prompt: str = "", role: str = REFLECT, digest=None, dialog: str = "",
567
+ reflect_rows="", node_id: str = "", jobs: str = "", model: str = "",
568
+ timeout_s: int = DEFAULT_TIMEOUT_S, allow_degrade: bool = False,
569
+ channel: str = "", wait_s: float = DEFAULT_TIMEOUT_S, blocking: bool = True,
570
+ cg=None, actor: str = "", autostart: bool = False) -> dict:
571
+ """复核主入口:**蜂巢优先 → 提示配置 → 端子代理降级**(三态各自如实返回)。
572
+
573
+ 返回统一形态:{state, role, node_id, transport, unit, attest, prompt, job_id, hint}
574
+ - state=hive :已派发;blocking=True 时等终态并给出 unit/attest;否则只给 job_id
575
+ - state=configure:**未派发**,hint 为配置指引(使用者裁定:不可用即提示配置)
576
+ - state=subagent :**未派发**,返回 prompt 包供宿主子代理执行并回填
577
+ """
578
+ text = prompt or prompt_for(role, digest, dialog=dialog, reflect_rows=reflect_rows)
579
+ pr = probe(jobs, model, allow_degrade=allow_degrade, channel=channel)
580
+ base = {"role": role, "node_id": node_id, "prompt": text,
581
+ "transport": None, "unit": None, "attest": None,
582
+ "job_id": None, "hint": pr.get("hint") or "", "channel": pr.get("channel") or "",
583
+ "degrade": {"channel": channel or "harness-subagent",
584
+ "how": "宿主子代理执行 prompt → 裁决经 verdicts 回填 attest"}}
585
+ if pr["state"] == SUBAGENT:
586
+ return {"state": SUBAGENT, **base,
587
+ "degrade": {"available": True, "channel": channel or "harness-subagent",
588
+ "how": "把 prompt 交给宿主子代理,取其 JSON 裁决后调 "
589
+ "cg(op=ccg, action=attest, verdict=<accept|drop|defer>, "
590
+ "verifier=subagent:<role>:<channel>)"}}
591
+ if pr["state"] == CONFIGURE:
592
+ return {"state": CONFIGURE, **base,
593
+ "degrade": {"available": True, "channel": channel or "harness-subagent",
594
+ "how": "① 按 hint 配置蜂巢后重试;② 或带 allow_degrade=true 降级子代理"}}
595
+ if autostart and not serve_state(jobs)["alive"]:
596
+ pr["autostart"] = autostart_serve(jobs)
597
+ sub = submit(prompt=text, role=role, jobs=jobs, model=model, timeout_s=timeout_s,
598
+ cg=cg, actor=actor)
599
+ if not sub.get("ok"):
600
+ return {"state": CONFIGURE, **base, "hint": (pr.get("hint") or "") + "\n" + str(sub.get("error"))}
601
+ out = {"state": HIVE, **base, "job_id": sub["job_id"], "unit_role": role,
602
+ "model": sub.get("model")}
603
+ if not blocking:
604
+ return out
605
+ res = wait(sub["job_id"], jobs=jobs, timeout_s=wait_s)
606
+ unit = verdict_of(res.get("content")) if res.get("ok") else {
607
+ "verdict": DEFER, "reason": res.get("error") or "单元未返回可用结果",
608
+ "slot_corrections": {}, "parsed": False}
609
+ out["unit"] = unit
610
+ out["transport"] = transport_name(HIVE, role, sub["job_id"])
611
+ out["attest"] = to_attest_args(unit, state=HIVE, role=role, job_id=sub["job_id"])
612
+ out["result_ok"] = bool(res.get("ok"))
613
+ out["waited_s"] = res.get("waited_s")
614
+ if not res.get("ok"):
615
+ out["hint"] = "蜂巢 job 未产出有效结果:%s(裁决按 DEFER 处理,未假装通过)" % res.get("error")
616
+ _log(cg, {"action": "review", "job_id": sub["job_id"], "role": role,
617
+ "node_id": node_id, "actor": actor, "unit_verdict": unit.get("verdict"),
618
+ "result_ok": bool(res.get("ok"))})
619
+ return out
620
+
621
+
622
+ # ---------------------------------------------------------------- serve 拉起(显式)
623
+
624
+ # 生效条件:serve_state(jobs_dir(jobs)) 已 alive 时返回 started=False 的“serve 存活”;否则 exe_path() 未被 isfile 命中时返回 started=False 并提示先 cargo build --release;否则以 detached/新会话 Popen 拉起 exe serve --jobs jd 并把 stdout/stderr 写入 jd/_serve.log,Popen 抛 OSError 时返回 started=False 的拉起失败;拉起后在 float(wait_s) 内轮询到 alive 返回 started=True 的“serve 已拉起”,轮询超时仍返回 started=True 但标注心跳未就绪。
625
+ def autostart_serve(jobs: str = "", wait_s: float = 5.0) -> dict:
626
+ """**显式**拉起 serve(默认不启用;使用者裁定:不可用即提示配置)。
627
+
628
+ 形态同 hive_mcp._ensure_serve:detached + 独立日志,不阻塞调用方。
629
+ """
630
+ jd = jobs_dir(jobs)
631
+ if serve_state(jd)["alive"]:
632
+ return {"started": False, "note": "serve 存活"}
633
+ exe = exe_path()
634
+ if not os.path.isfile(exe):
635
+ return {"started": False, "note": "未找到可执行文件 %s——先 cargo build --release" % exe}
636
+ kwargs = {}
637
+ if os.name == "nt":
638
+ kwargs["creationflags"] = subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW
639
+ else:
640
+ kwargs["start_new_session"] = True
641
+ try:
642
+ os.makedirs(jd, exist_ok=True)
643
+ with open(os.path.join(jd, "_serve.log"), "ab") as logf:
644
+ subprocess.Popen([exe, "serve", "--jobs", jd], stdout=logf, stderr=logf,
645
+ stdin=subprocess.DEVNULL, **kwargs)
646
+ except OSError as exc:
647
+ return {"started": False, "note": "拉起失败:%s: %s" % (type(exc).__name__, exc)}
648
+ t0 = time.time()
649
+ while time.time() - t0 < float(wait_s):
650
+ if serve_state(jd)["alive"]:
651
+ return {"started": True, "note": "serve 已拉起"}
652
+ time.sleep(0.1)
653
+ return {"started": True, "note": "serve 已拉起(心跳未就绪,稍后自愈)"}
654
+
655
+
656
+ # 生效条件:返回 dict 的 ok=p["state"]==HIVE,其中 p=plan(jobs)(model/fresh_s/allow_degrade/channel 全走默认),state/serve_alive/serve_age_s/chain/hint/model 取自该 p;jobs_dir 取 jobs_dir(jobs),exe_found 与 exe_path 取 exe_path() 是否被 isfile 命中,model_set=bool(model_name())(无参,读 ENV_MODEL),api_key_set=bool(os.environ.get(ENV_API_KEY)),env 记录 ENV_JOBS_DIR 与 ENV_MODEL 的原值。
657
+ def doctor(jobs: str = "") -> dict:
658
+ """能力体检(形态对齐 hive_doctor):判活 + exe + 模型 + 优先级链。"""
659
+ jd = jobs_dir(jobs)
660
+ p = plan(jobs)
661
+ return {"ok": p["state"] == HIVE, "state": p["state"], "jobs_dir": jd,
662
+ "serve_alive": p["serve"]["alive"], "serve_age_s": p["serve"]["age_s"],
663
+ "exe_found": os.path.isfile(exe_path()), "exe_path": exe_path(),
664
+ "model_set": bool(model_name()), "model": p["model"],
665
+ "api_key_set": bool(os.environ.get(ENV_API_KEY)),
666
+ "env": {"HIVE_JOBS_DIR": os.environ.get(ENV_JOBS_DIR, ""),
667
+ "MDCG_UNIT_MODEL": os.environ.get(ENV_MODEL, "")},
668
668
  "chain": p["chain"], "hint": p["hint"]}