@furongjun1999/dsh-memory 0.4.11 → 0.5.1

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