dsh-vibe-math 2.7.1 → 2.7.3

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 (114) hide show
  1. package/README.en.md +682 -690
  2. package/README.md +19 -27
  3. package/cordis.patch.yml +112 -16
  4. package/docs/AUDIT-CHECKLIST.md +744 -518
  5. package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
  6. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  7. package/docs/final-paper.md +172 -172
  8. package/docs/formal-verification.md +588 -588
  9. package/docs/math-computation.md +194 -163
  10. package/docs/parameter-schema.md +37 -0
  11. package/docs/release-notes/README.en.md +49 -0
  12. package/docs/release-notes/README.md +48 -0
  13. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
  14. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
  15. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  16. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  17. package/docs/release-notes/TEMPLATE.md +126 -0
  18. package/docs/status-report-fields.md +167 -0
  19. package/docs/test-timing.md +157 -154
  20. package/installer.js +27 -17
  21. package/package.json +55 -3
  22. package/prompt-corpus-persona/persona-corpus.json +9 -9
  23. package/prompt-corpus-persona/persona-corpus.md +225 -33
  24. package/prompt-corpus-v2/formal-verify-v2.json +305 -113
  25. package/prompt-corpus-v2/formal-verify-v2.md +2399 -279
  26. package/prompt-corpus-v3/formal-verify-v3.json +134 -110
  27. package/prompt-corpus-v3/formal-verify-v3.md +409 -141
  28. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  29. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  30. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  31. package/prompt-corpus-v5/prompt-corpus-v5.md +513 -259
  32. package/scripts/update-doc-counts.mjs +71 -0
  33. package/tests/audit-artifact-docs.mjs +72 -0
  34. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  35. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  36. package/tests/audit-installer-compat.test.mjs +52 -4
  37. package/tests/audit-installer-policy.test.mjs +41 -8
  38. package/tests/audit-math-computation-contract.mjs +107 -9
  39. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  40. package/tests/audit-math-computation-parity.mjs +164 -3
  41. package/tests/audit-math-computation-sensitivity.mjs +6 -4
  42. package/tests/audit-package-membership.mjs +57 -0
  43. package/tests/audit-package-membership.mutants.mjs +28 -0
  44. package/tests/audit-participant-set-parity.mjs +160 -0
  45. package/tests/audit-path-discipline.mjs +83 -0
  46. package/tests/audit-path-discipline.mutants.mjs +51 -0
  47. package/tests/audit-persona-sensitivity.mjs +14 -0
  48. package/tests/audit-persona-surface.mutants.mjs +69 -0
  49. package/tests/audit-persona-surface.test.mjs +63 -22
  50. package/tests/audit-prompt-duplication.mjs +143 -0
  51. package/tests/audit-prompt-duplication.mutants.mjs +50 -0
  52. package/tests/audit-prompt-invariants.mjs +43 -5
  53. package/tests/audit-readme-bilingual.test.mjs +152 -0
  54. package/tests/audit-readme-counts.mjs +95 -0
  55. package/tests/audit-readme-counts.mutants.mjs +52 -0
  56. package/tests/audit-registration.mjs +78 -0
  57. package/tests/audit-status-report-fields.mjs +172 -0
  58. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  59. package/tests/audit-v3-registration-parity.mjs +156 -0
  60. package/tests/audit-v5-integrity.mjs +300 -13
  61. package/tests/audit-v5-lean-abstention.mjs +392 -0
  62. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  63. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  64. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  65. package/tests/audit-v5-sensitivity.mjs +15 -8
  66. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  67. package/tests/e2e-v5-round2.test.mjs +566 -12
  68. package/tests/formal-verify-v2.test.mjs +91 -5
  69. package/tests/formal-verify-v3.mutants.mjs +117 -0
  70. package/tests/formal-verify-v3.test.mjs +85 -6
  71. package/tests/formal-verify-v4.mutants.mjs +152 -0
  72. package/tests/formal-verify-v4.test.mjs +628 -9
  73. package/tests/formal-verify-v5.test.mjs +68 -9
  74. package/tests/helpers/doc-counts-format.mjs +41 -0
  75. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  76. package/tests/math-computation-a1.mutants.mjs +72 -0
  77. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  78. package/tests/math-computation-discovery.mutants.mjs +76 -0
  79. package/tests/math-computation-shared.test.mjs +396 -2
  80. package/tests/math-computation-v2.test.mjs +767 -0
  81. package/tests/math-computation-v3.test.mjs +759 -0
  82. package/tests/math-computation-v4.mutants.mjs +133 -0
  83. package/tests/math-computation-v4.test.mjs +739 -0
  84. package/tests/prompt-v5-integrity.test.mjs +13 -2
  85. package/tests/run-tests.mjs +139 -9
  86. package/tests/run-tests.mutants.mjs +70 -0
  87. package/tests/selfdrive-v5.mjs +53 -1
  88. package/tests/selfdrive-v5.mutants.mjs +132 -0
  89. package/tests/v2-fix-probes.mutants.mjs +100 -0
  90. package/tests/v2-fix-probes.test.mjs +1039 -0
  91. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  92. package/tests/v3-fix-probes.mutants.mjs +95 -0
  93. package/tests/v3-fix-probes.test.mjs +891 -0
  94. package/tests/v5-institute-fixes.mutants.mjs +190 -0
  95. package/vibe-math-v2/agent.cordis.yml +62 -6
  96. package/vibe-math-v2/math-computation.js +229 -29
  97. package/vibe-math-v2/math-engines.js +12 -12
  98. package/vibe-math-v2/vibe-math-v2.js +458 -119
  99. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
  100. package/vibe-math-v3/agent.cordis.yml +72 -6
  101. package/vibe-math-v3/math-computation.js +229 -29
  102. package/vibe-math-v3/math-engines.js +12 -12
  103. package/vibe-math-v3/vibe-math-v3.js +482 -139
  104. package/vibe-math-v4/agent.cordis.yml +44 -8
  105. package/vibe-math-v4/math-computation.js +229 -29
  106. package/vibe-math-v4/math-engines.js +12 -12
  107. package/vibe-math-v4/vibe-math-v4.js +245 -52
  108. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
  109. package/vibe-math-v5/agent.cordis.yml +46 -12
  110. package/vibe-math-v5/math-computation.js +229 -29
  111. package/vibe-math-v5/math-engines.js +12 -12
  112. package/vibe-math-v5/vibe-math-v5.js +1307 -243
  113. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1655
  114. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -1,3 +1,106 @@
1
+ // ================= F-B: 参数机读 schema 的单一真源(模块作用域) =================
2
+ // 工具面 `vibe_math_set_params` 的 `parameters` 由 PARAM_SCHEMA 派生,描述/类型只有一处真源
3
+ //(真源与例外说明见 docs/parameter-schema.md)。本块整体位于**模块作用域**,因为本文件定义了两处
4
+ // `objParams`(会话层 makeSession + apply 层)——只有模块作用域能被两层同时看见(rehearsal 曾因此
5
+ // 让 apply 层拿到只有例外项的残缺 schema)。下列常量与 PARAM_SCHEMA 是从 makeSession 内**逐字上移**的纯
6
+ // 常量(值不变、作用域上移),会话层原有引用照旧解析到它们。
7
+ const IS_WINDOWS = process.platform === 'win32'
8
+ const SCRIPT_TOOLS = IS_WINDOWS ? ['pwsh'] : ['bash']
9
+ const NETWORK_TOOLS = ['web_search', 'web_fetch']
10
+ const ACTIVITY_REPORT_MAX = 30
11
+ const LEAN_INITIATIVE_MODES = ['off', 'normal', 'eager']
12
+ const PARAM_SCHEMA = [
13
+ { name: 'mode', type: 'enum', options: ['auto', 'manual'], description: 'auto = 无人值守自动通过关键节点;manual = 关键节点挂起人工决策', suggestion: 'auto' },
14
+ { name: 'maxParallelThreshold', type: 'integer', description: '全局最大并发子代理轮数(新派发前须满足 active < 阈值)', suggestion: 4 },
15
+ { name: 'solverMaxRounds', type: 'integer', description: '每个求解方向的最大迭代轮数(agent_self_iteration 上限)', suggestion: 3 },
16
+ { name: 'directionsPerSolver', type: 'integer', description: '每个 solver 提示词附带的其他活跃方向摘要数量:1 = 只看自己方向(互不干扰);N>1 = 额外附带最多 N 个其他活跃方向摘要用于协调', suggestion: 1 },
17
+ { name: 'verifierCount', type: 'integer', description: '每个验证对象的独立验证器数量(下限 2:一票不裁决)。并发预算紧张时单个对象同时最多占用 maxParallelThreshold-2 个槽位(至少 2),以保证另一个对象也凑得齐 2 票', suggestion: 3 },
18
+ { name: 'debateMaxRounds', type: 'integer', description: '验证辩论(交流群)最大轮数', suggestion: 5 },
19
+ { name: 'verdictMode', type: 'enum', options: ['flat', 'forced'], description: 'flat = 均衡机制(不一致时取各评审自报概率的**均值**,绝不折叠成固定的 0.5);forced = 强制裁决(按验证者历史准确率+布尔票置信度加权)。准确率按 provider/model 这种稳定身份记,且只在对象**后来**取得布尔定论时才计分', suggestion: 'flat' },
20
+ { name: 'provider', type: 'string', description: '子代理模型 provider(空 = 继承根代理)', suggestion: '' },
21
+ { name: 'model', type: 'string', description: '子代理模型 id(空 = 继承根代理)', suggestion: '' },
22
+ { name: 'solverPersona', type: 'string', description: '注入每个求解器提示词开头的人格/要求', suggestion: '' },
23
+ { name: 'verifierPersona', type: 'string', description: '注入每个验证器提示词开头的人格/要求', suggestion: '' },
24
+ { name: 'explorerPersona', type: 'string', description: '注入每个 explorer/重派生提示词开头的人格/要求', suggestion: '' },
25
+ { name: 'knowledgeContext', type: 'string', description: '共享知识/数据模型说明(空 = 内置完整版;非空 = 覆盖,注入 explorer/solver/verifier 提示词)', suggestion: '' },
26
+ { name: 'solverToolAllow', type: 'string[]', description: '求解器允许的工具名列表(空 = 继承全部工具)', suggestion: [] },
27
+ { name: 'solverToolDeny', type: 'string[]', description: '求解器禁止的工具名列表', suggestion: [] },
28
+ { name: 'verifierToolAllow', type: 'string[]', description: '验证器允许的工具名列表', suggestion: [] },
29
+ { name: 'verifierToolDeny', type: 'string[]', description: '验证器禁止的工具名列表', suggestion: [] },
30
+ { name: 'solverAllowNetwork', type: 'boolean', description: '求解器网络工具开关:空=继承全部;true=允许(在已有 allow 列表时补入本次组合真正注册的网络工具);false=禁止网络工具', suggestion: '' },
31
+ { name: 'verifierAllowNetwork', type: 'boolean', description: '验证器网络工具开关(同 solverAllowNetwork)', suggestion: '' },
32
+ { name: 'solverAllowScripts', type: 'boolean', description: '求解器脚本工具开关:空=继承全部;true=允许(在已有 allow 列表时补入);false=禁止 ' + SCRIPT_TOOLS.join('/'), suggestion: '' },
33
+ { name: 'verifierAllowScripts', type: 'boolean', description: '验证器脚本工具开关(同 solverAllowScripts)', suggestion: '' },
34
+ { name: 'solverMaxToolCalls', type: 'integer', description: '求解器每轮外部工具调用**建议**上限(0 = 不限)。框架只把它写进提示词,不计数、不强制', suggestion: 0 },
35
+ { name: 'verifierMaxToolCalls', type: 'integer', description: '验证器每轮外部工具调用**建议**上限(0 = 不限)。框架只把它写进提示词,不计数、不强制', suggestion: 0 },
36
+ { name: 'reportIntervalMs', type: 'integer', description: '进度汇报间隔(毫秒):0 = 仅事件驱动(有代理状态更新等事件才写/推报告);>0 = 同时按该间隔定时自动汇报', suggestion: 0 },
37
+ { name: 'reportMode', type: 'enum', options: ['file', 'push', 'both'], description: 'file = 写报告文件;push = 推送消息让主代理主动汇报;both = 两者都做', suggestion: 'file' },
38
+ { name: 'promoteValueThreshold', type: 'number', description: 'Propos 中「价值/关键性」≥ 该值且未决(0,1) 的命题自动加入 qs.json', suggestion: 0.7 },
39
+ { name: 'priorityAdjust', type: 'enum', options: ['none', 'deadend-deprioritize', 'survival-map'], description: '优先级动态调整策略:none=不自动调;deadend-deprioritize=方向全死路时降优先级;survival-map=按最高方向存活率重算(存活率高越优先)', suggestion: 'none' },
40
+ { name: 'proposPriorityAdjust', type: 'enum', options: ['none', 'progress-graded'], description: '命题优先级动态调整:none=不自动调;progress-graded=按「定论接近度(|布尔估计-0.5|)+ 证明/证伪材料量」重算,越接近定论越优先验证', suggestion: 'none' },
41
+ { name: 'tickIntervalMs', type: 'integer', description: '调度器心跳间隔(毫秒):多久扫描一次子代理状态并推进(越小越灵敏、越大越省资源)', suggestion: 2000 },
42
+ { name: 'activityLogCap', type: 'integer', description: '活动日志保留条数(影响 status/report 里 recentActivity 的细节量,两者最多显示 ' + ACTIVITY_REPORT_MAX + ' 条)', suggestion: 100 },
43
+ { name: 'maxExplorerRetries', type: 'integer', description: 'explorer 拆方向失败的重派生上限(达到后该问题标记为方向耗尽)', suggestion: 3 },
44
+ { name: 'formalVerify', type: 'enum', options: ['off', 'encourage', 'require'], description: 'Lean 形式化验证档位:off=不额外要求(默认,提示词里不出现 Lean);encourage=鼓励按实现难度自行形式化,一旦 Lean 通过则验证重点转为「忠实性审查」;require=同 encourage 且加门禁——对象的 formal.status 未达到 passed/blocked 前,真/假裁定记为未定论(原因 formal-required)并写入 Formal/TODO.md', suggestion: 'off' },
45
+ { name: 'leanCommand', type: 'string', description: '要执行的 Lean 可执行文件(默认 lean;用 lake 时配合 leanArgs=["env","lean"])', suggestion: 'lean' },
46
+ { name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加命令行参数(默认空)', suggestion: [] },
47
+ { name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,默认 120000,最小 1000);异步档下它同时是**每个后台编译作业**的预算(到时主动 terminate,作业记 timeout、对象留在 attempted)', suggestion: 120000 },
48
+ { name: 'leanAsync', type: 'boolean', description: 'Lean 编译模式:true(默认)= 后台队列,vibe_math_lean_run / vibe_math_lean_archive{run:true} 立即返回 async.jobId 入队,成员不阻塞,结果由下一轮提示的【形式化结果】行与 vibe_math_lean_lib / vibe_math_lean_job 公告(**只有作业落地为 ok 才会置 passed 并写归档证明**);false = 完全同步 await(与旧行为逐字一致)', suggestion: true },
49
+ { name: 'leanJobsMaxParallel', type: 'integer', description: '后台 Lean 编译的并发上限(默认 1 = 串行,保持可预测的资源占用;调大可并行编译多个作业)', suggestion: 1 },
50
+ { name: 'leanInitiative', type: 'string', enum: LEAN_INITIATIVE_MODES.slice(), description: '日常流程中的形式化主动性:off(不主动,只在验证提示词按 formalVerify 的要求做)| normal(默认:顺手把有价值且可能复用的东西形式化)| eager(更主动:日常就主动把有价值的小引理/命题/定义形式化)。注意它与 formalVerify(验证时的要求强度:off|encourage|require)是**两件事**', suggestion: 'normal' },
51
+ { name: 'leanSearchPaths', type: 'array', items: { type: 'string' }, description: '额外 Lean 搜索路径(默认空数组 = 只用框架自动注入的 VibeMath 根)。非空时按顺序先注入这里给的路径、再注入自动根(去重);若 leanArgs 里已显式给了 --search-path/-R/--root,则完全尊重用户配置、不注入任何东西', suggestion: [] },
52
+ { name: 'finalPaper', type: 'boolean', description: '收口(严格终止)时自动撰写最终论文:派遣一名专职「论文撰写」子代理,把已检验通过的命题/解法/成果整理成 Paper/<项目>/{paper.md,paper.tex,paper.meta.json,paper.log.md}。false = 只关自动触发,/vibe paper 手动命令仍可用', suggestion: true },
53
+ { name: 'paperFormat', type: 'enum', options: ['both', 'md', 'tex'], description: '论文产出格式:both = markdown + latex;md = 只写 paper.md;tex = 只写 paper.tex(tex 才会尝试编译 pdf)', suggestion: 'both' },
54
+ { name: 'paperLanguage', type: 'enum', options: ['zh', 'en'], description: '论文语言:zh = 中文(LaTeX 用 ctexart,引擎优先 xelatex);en = 英文(article,引擎优先 pdflatex/latexmk)', suggestion: 'zh' },
55
+ { name: 'paperCompilePdf', type: 'boolean', description: '检测到 LaTeX 时是否编译 paper.pdf(-interaction=nonstopmode 跑两遍;失败先尝试修复:换引擎/去不支持宏包/最小模板)。false 或无 LaTeX 时只保留 tex+md 并记日志', suggestion: true },
56
+ { name: 'paperLatexCommand', type: 'string', description: '指定 LaTeX 引擎可执行文件(空 = 按语言探测:中文 xelatex > latexmk > pdflatex > lualatex > tectonic;英文 pdflatex 优先)。该命令解析不到时按"未检测到"降级(只留 tex+md)', suggestion: '' },
57
+ // ---- 数学计算 math_computation(六参数冻结;描述与 prompts.md §1/§6 口径一致)----
58
+ { name: 'mathComputation', type: 'enum', options: ['off', 'auto', 'on'], description: '数学计算总开关:off = 真无操作(提示词零提及);auto = 探测到 mathEngines 里任一允许引擎才工作;on = 同上(探测失败时工具仍返回可执行的安装指引,而不是假装可用)', suggestion: 'auto' },
59
+ { name: 'mathMode', type: 'enum', options: ['typed', 'typed+shell'], description: '计算策略:typed+shell(默认)= 工具不可用时允许宿主 shell 兜底,但结论必须标注"未经工具归档(shell 路径)";typed = 只用工具路径(提示词里不出现 shell 兜底段,engine:"cli" 返回 REFUSED{reason:policy})', suggestion: 'typed+shell' },
60
+ { name: 'mathEngines', type: 'string[]', description: '允许的引擎列表(默认 python|r|octave|julia|matlab|maple|wolfram|cli)。cli 默认开启且走同一套超时/输出上限/回执;从列表里移除某引擎即禁用(商业引擎只探测+许可,永不安装)', suggestion: MATH_PARAM_DEFAULTS.mathEngines.slice() },
61
+ { name: 'mathTimeoutMs', type: 'integer', description: '单次数学计算的超时上限(毫秒,默认 60000,最小 1000);到时主动 terminate 并把回执标为 MATH_TIMEOUT', suggestion: 60000 },
62
+ { name: 'mathPackages', type: 'string[]', description: '需要预检的包(默认空)。缺包只报告 + 给"用户自装指引"或"代理代装计划",不会执行脚本,也永不自动安装', suggestion: [] },
63
+ { name: 'mathInstallScope', type: 'string', enum: ['user', 'system'], description: '安装作用域:user(默认,用户级目录);system 只对当次显式调用生效、永不记忆(不会写进状态文件)', suggestion: 'user' },
64
+ ]
65
+
66
+ // 钉住"机读键集合"(现状 v2=49 / v3=63 个,不随 PARAM_SCHEMA 增删而漂移)。
67
+ const PARAM_PROPS_KEYS = ['maxParallelThreshold', 'solverMaxRounds', 'verifierCount', 'debateMaxRounds', 'verdictMode', 'reportMode', 'promoteValueThreshold', 'priorityAdjust', 'proposPriorityAdjust', 'provider', 'model', 'solverPersona', 'verifierPersona', 'explorerPersona', 'knowledgeContext', 'solverToolAllow', 'solverToolDeny', 'verifierToolAllow', 'verifierToolDeny', 'solverAllowNetwork', 'verifierAllowNetwork', 'solverAllowScripts', 'verifierAllowScripts', 'solverMaxToolCalls', 'verifierMaxToolCalls', 'reportIntervalMs', 'tickIntervalMs', 'activityLogCap', 'maxExplorerRetries', 'directionsPerSolver', 'formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs', 'leanAsync', 'leanJobsMaxParallel', 'leanInitiative', 'leanSearchPaths', 'mathComputation', 'mathMode', 'mathEngines', 'mathTimeoutMs', 'mathPackages', 'mathInstallScope', 'finalPaper', 'paperFormat', 'paperLanguage', 'paperCompilePdf', 'paperLatexCommand']
68
+ // 源无法表达的条目:数组 items / oneOf / mode 的现状枚举顺序 —— 逐字沿用,并由
69
+ // "例外集合 == docs/parameter-schema.md" 断言钉住。
70
+ const PARAM_PROPS_EXTRA = {
71
+ solverToolAllow: { type: 'array', items: { type: 'string' } },
72
+ solverToolDeny: { type: 'array', items: { type: 'string' } },
73
+ verifierToolAllow: { type: 'array', items: { type: 'string' } },
74
+ verifierToolDeny: { type: 'array', items: { type: 'string' } },
75
+ leanArgs: { type: 'array', items: { type: 'string' } },
76
+ mathEngines: { type: 'array', items: { type: 'string' } },
77
+ mathPackages: { type: 'array', items: { type: 'string' } },
78
+ solverAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] },
79
+ verifierAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] },
80
+ solverAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] },
81
+ verifierAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] },
82
+ }
83
+ function paramProps() {
84
+ const props = {}
85
+ for (const name of PARAM_PROPS_KEYS) {
86
+ const e = PARAM_SCHEMA.find(function (x) { return x && x.name === name })
87
+ if (!e) continue
88
+ const enumVals = (Array.isArray(e.enum) && e.enum.length) ? e.enum : ((Array.isArray(e.options) && e.options.length) ? e.options : null)
89
+ const isEnum = e.type === 'enum' || !!enumVals
90
+ const p = isEnum ? { type: 'string', enum: (enumVals || []).slice() } : { type: e.type }
91
+ if (e.description) p.description = e.description
92
+ props[name] = p
93
+ }
94
+ // 例外表覆盖"源无法表达"的字段,但**保留源里的描述**(覆盖度 v2 49/49、v3 63/63)
95
+ for (const k of Object.keys(PARAM_PROPS_EXTRA)) {
96
+ const base = props[k] || {}
97
+ props[k] = Object.assign({}, PARAM_PROPS_EXTRA[k])
98
+ if (base.description) props[k].description = base.description
99
+ }
100
+ return props
101
+ }
102
+ // ================= end F-B =================
103
+
1
104
  // Vibe Math V2 — host plugin implementing the classic-architecture spec
2
105
  // ("vibe-math-v2/实现方案.md"): probability-driven scheduling over a
3
106
  // problem list (qs/qs.json) and a proposition knowledge base (Propos/),
@@ -218,7 +321,7 @@ export function apply(ctx) {
218
321
  let explorerRetries = {}
219
322
  // 状态/报告里最近活动最多显示多少条(**一个常量**):此前 buildReport 用 min(30, cap)、getStatus
220
323
  // 硬编码 min(10, cap),同一个字段两个端点给出不同答案,而参数说明承诺的是 30(审计 M15)。
221
- const ACTIVITY_REPORT_MAX = 30
324
+ const ACTIVITY_PERSIST_MAX = 200 // P5:活动日志落盘上限(status/report 只显示 30 条,但恢复时保留更多线索)
222
325
  // 至少 2 名独立评审才能出裁决(审计 M11):一票裁决会把单个验证者的判断写成"完全验证"的布尔结论。
223
326
  const MIN_REVIEWERS = 2
224
327
  // 中段裁决(0<正确概率<1)的**重验冷却**(审计 M12):中段值是"为真的概率",不是定论,对象不能
@@ -234,12 +337,30 @@ export function apply(ctx) {
234
337
  function projectRoot(slug) { return vibeRoot() + '/Projects/' + slug }
235
338
  function frameworkRoot() { return projectRoot(currentProject) }
236
339
  let warnedNoPolicy = false
340
+ // F-5:围栏漂移检测(只告警不拒写)。边界:若解析结果**不暴露**任何 root 字段,本比对无法运行。
341
+ function policyRootOf(p) {
342
+ if (!p || typeof p !== 'object') return ''
343
+ const cand = p.workspaceRoot || p.root || p.cwd || (p.config && p.config.workspaceRoot) || ''
344
+ return String(cand || '').replace(/\\/g, '/').replace(/\/+$/, '')
345
+ }
346
+ let warnedFenceDrift = false
347
+ function warnFenceDriftOnce(actual, expected) {
348
+ if (warnedFenceDrift) return
349
+ warnedFenceDrift = true
350
+ console.error('vibe-math-v2' + ': sandbox fence root differs from this session workspace (resolved="' + actual + '", session="' + expected + '") — writes may be fenced to the host-configured root; boundary: when the policy object exposes no root field this comparison cannot run')
351
+ }
352
+ function checkFenceRoot(p) {
353
+ const actual = policyRootOf(p)
354
+ const expected = String(workspaceRoot() || '').replace(/\\/g, '/').replace(/\/+$/, '')
355
+ if (actual && expected && actual !== expected) warnFenceDriftOnce(actual, expected)
356
+ return p
357
+ }
237
358
  function warnNoPolicyOnce() { if (!warnedNoPolicy) { warnedNoPolicy = true; console.error('vibe-math-v2: sandboxPolicy unavailable; writes go out with no explicit policy') } }
238
359
  // Sandbox fence for our own writes. The `resolve({})` fallback is a last resort and is
239
360
  // deliberately reported (once): with no session it resolves the policy's CONFIGURED root
240
361
  // (dsh-sandbox-policy: resolveWorkspaceRoot(config.workspaceRoot ?? process.cwd())),
241
362
  // which is not necessarily this session's workspace — a silently different fence.
242
- function getPolicy() { const sp = sandboxPolicyOf(); if (!sp) { warnNoPolicyOnce(); return undefined } try { if (rootAgent && rootAgent.session) return sp.resolve({ session: rootAgent.session }) } catch (e) { warnNoPolicyOnce() } try { const p = sp.resolve({}); if (!warnedNoPolicy) { warnedNoPolicy = true; console.error('vibe-math-v2: falling back to sandboxPolicy.resolve({}) — the fence root is the host-configured workspace, not necessarily this session cwd') } return p } catch (e) { warnNoPolicyOnce() } return undefined }
363
+ function getPolicy() { const sp = sandboxPolicyOf(); if (!sp) { warnNoPolicyOnce(); return undefined } try { if (rootAgent && rootAgent.session) return checkFenceRoot(sp.resolve({ session: rootAgent.session })) } catch (e) { warnNoPolicyOnce() } try { const p = sp.resolve({}); if (!warnedNoPolicy) { warnedNoPolicy = true; console.error('vibe-math-v2: falling back to sandboxPolicy.resolve({}) — the fence root is the host-configured workspace, not necessarily this session cwd') } return checkFenceRoot(p) } catch (e) { warnNoPolicyOnce() } return undefined }
243
364
  function makeSignal(ms) { return AbortSignal.timeout(ms || 30000) }
244
365
 
245
366
  // ================= capability / tool-name lists =================
@@ -253,14 +374,11 @@ export function apply(ctx) {
253
374
  // host applies the filter when establishing a continuable child
254
375
  // (dsh-subagent: childCtx.tools.restrict(...)), so a stale name meant the child
255
376
  // was never created at all.
256
- const IS_WINDOWS = process.platform === 'win32'
257
- const SCRIPT_TOOLS = IS_WINDOWS ? ['pwsh'] : ['bash']
258
377
  // 这两个名字是否真的存在取决于**本次组合**:v2 自己的 agent.cordis.yml 把 tool-web 行设成
259
378
  // fetch:false,于是 web_search 在、web_fetch 不在,而 filter 里只要有一个宿主不注册的名字,
260
379
  // restrict() 就整条拒绝 → 子代理先失败一次再走 sanitizeToolFilter 重试,日志还会谎称"你的配置过期"
261
380
  // (审计 M8/M9)。所以能力清单**向宿主的可见工具面查询**(tools.schemas,宿主自己在
262
381
  // dsh-tools/lib/index.js 里就是这么用的),查不到/不可信时才退回静态候选表。
263
- const NETWORK_TOOLS = ['web_search', 'web_fetch']
264
382
  /**
265
383
  * 本会话可见的工具名集合;null = 无法可信地确定(此时一律保留候选名,行为与改动前一致)。
266
384
  * 自校验:这张表必须包含本插件自己注册的工具,否则说明这个 scope 视图不是子代理真正看到的
@@ -285,64 +403,17 @@ export function apply(ctx) {
285
403
  }
286
404
 
287
405
  // ================= parameter schema =================
288
- const PARAM_SCHEMA = [
289
- { name: 'mode', type: 'enum', options: ['auto', 'manual'], description: 'auto = 无人值守自动通过关键节点;manual = 关键节点挂起人工决策', suggestion: 'auto' },
290
- { name: 'maxParallelThreshold', type: 'integer', description: '全局最大并发子代理轮数(新派发前须满足 active < 阈值)', suggestion: 4 },
291
- { name: 'solverMaxRounds', type: 'integer', description: '每个求解方向的最大迭代轮数(agent_self_iteration 上限)', suggestion: 3 },
292
- { name: 'directionsPerSolver', type: 'integer', description: '每个 solver 提示词附带的其他活跃方向摘要数量:1 = 只看自己方向(互不干扰);N>1 = 额外附带最多 N 个其他活跃方向摘要用于协调', suggestion: 1 },
293
- { name: 'verifierCount', type: 'integer', description: '每个验证对象的独立验证器数量(下限 2:一票不裁决)。并发预算紧张时单个对象同时最多占用 maxParallelThreshold-2 个槽位(至少 2),以保证另一个对象也凑得齐 2 票', suggestion: 3 },
294
- { name: 'debateMaxRounds', type: 'integer', description: '验证辩论(交流群)最大轮数', suggestion: 5 },
295
- { name: 'verdictMode', type: 'enum', options: ['flat', 'forced'], description: 'flat = 均衡机制(不一致时取各评审自报概率的**均值**,绝不折叠成固定的 0.5);forced = 强制裁决(按验证者历史准确率+布尔票置信度加权)。准确率按 provider/model 这种稳定身份记,且只在对象**后来**取得布尔定论时才计分', suggestion: 'flat' },
296
- { name: 'provider', type: 'string', description: '子代理模型 provider(空 = 继承根代理)', suggestion: '' },
297
- { name: 'model', type: 'string', description: '子代理模型 id(空 = 继承根代理)', suggestion: '' },
298
- { name: 'solverPersona', type: 'string', description: '注入每个求解器提示词开头的人格/要求', suggestion: '' },
299
- { name: 'verifierPersona', type: 'string', description: '注入每个验证器提示词开头的人格/要求', suggestion: '' },
300
- { name: 'explorerPersona', type: 'string', description: '注入每个 explorer/重派生提示词开头的人格/要求', suggestion: '' },
301
- { name: 'knowledgeContext', type: 'string', description: '共享知识/数据模型说明(空 = 内置完整版;非空 = 覆盖,注入 explorer/solver/verifier 提示词)', suggestion: '' },
302
- { name: 'solverToolAllow', type: 'string[]', description: '求解器允许的工具名列表(空 = 继承全部工具)', suggestion: [] },
303
- { name: 'solverToolDeny', type: 'string[]', description: '求解器禁止的工具名列表', suggestion: [] },
304
- { name: 'verifierToolAllow', type: 'string[]', description: '验证器允许的工具名列表', suggestion: [] },
305
- { name: 'verifierToolDeny', type: 'string[]', description: '验证器禁止的工具名列表', suggestion: [] },
306
- { name: 'solverAllowNetwork', type: 'boolean', description: '求解器网络工具开关:空=继承全部;true=允许(在已有 allow 列表时补入本次组合真正注册的网络工具);false=禁止网络工具', suggestion: '' },
307
- { name: 'verifierAllowNetwork', type: 'boolean', description: '验证器网络工具开关(同 solverAllowNetwork)', suggestion: '' },
308
- { name: 'solverAllowScripts', type: 'boolean', description: '求解器脚本工具开关:空=继承全部;true=允许(在已有 allow 列表时补入);false=禁止 ' + SCRIPT_TOOLS.join('/'), suggestion: '' },
309
- { name: 'verifierAllowScripts', type: 'boolean', description: '验证器脚本工具开关(同 solverAllowScripts)', suggestion: '' },
310
- { name: 'solverMaxToolCalls', type: 'integer', description: '求解器每轮外部工具调用**建议**上限(0 = 不限)。框架只把它写进提示词,不计数、不强制', suggestion: 0 },
311
- { name: 'verifierMaxToolCalls', type: 'integer', description: '验证器每轮外部工具调用**建议**上限(0 = 不限)。框架只把它写进提示词,不计数、不强制', suggestion: 0 },
312
- { name: 'reportIntervalMs', type: 'integer', description: '进度汇报间隔(毫秒):0 = 仅事件驱动(有代理状态更新等事件才写/推报告);>0 = 同时按该间隔定时自动汇报', suggestion: 0 },
313
- { name: 'reportMode', type: 'enum', options: ['file', 'push', 'both'], description: 'file = 写报告文件;push = 推送消息让主代理主动汇报;both = 两者都做', suggestion: 'file' },
314
- { name: 'promoteValueThreshold', type: 'number', description: 'Propos 中「价值/关键性」≥ 该值且未决(0,1) 的命题自动加入 qs.json', suggestion: 0.7 },
315
- { name: 'priorityAdjust', type: 'enum', options: ['none', 'deadend-deprioritize', 'survival-map'], description: '优先级动态调整策略:none=不自动调;deadend-deprioritize=方向全死路时降优先级;survival-map=按最高方向存活率重算(存活率高越优先)', suggestion: 'none' },
316
- { name: 'proposPriorityAdjust', type: 'enum', options: ['none', 'progress-graded'], description: '命题优先级动态调整:none=不自动调;progress-graded=按「定论接近度(|布尔估计-0.5|)+ 证明/证伪材料量」重算,越接近定论越优先验证', suggestion: 'none' },
317
- { name: 'tickIntervalMs', type: 'integer', description: '调度器心跳间隔(毫秒):多久扫描一次子代理状态并推进(越小越灵敏、越大越省资源)', suggestion: 2000 },
318
- { name: 'activityLogCap', type: 'integer', description: '活动日志保留条数(影响 status/report 里 recentActivity 的细节量,两者最多显示 ' + ACTIVITY_REPORT_MAX + ' 条)', suggestion: 100 },
319
- { name: 'maxExplorerRetries', type: 'integer', description: 'explorer 拆方向失败的重派生上限(达到后该问题标记为方向耗尽)', suggestion: 3 },
320
- { name: 'formalVerify', type: 'enum', options: ['off', 'encourage', 'require'], description: 'Lean 形式化验证档位:off=不额外要求(默认,提示词里不出现 Lean);encourage=鼓励按实现难度自行形式化,一旦 Lean 通过则验证重点转为「忠实性审查」;require=同 encourage 且加门禁——对象的 formal.status 未达到 passed/blocked 前,真/假裁定记为未定论(原因 formal-required)并写入 Formal/TODO.md', suggestion: 'off' },
321
- { name: 'leanCommand', type: 'string', description: '要执行的 Lean 可执行文件(默认 lean;用 lake 时配合 leanArgs=["env","lean"])', suggestion: 'lean' },
322
- { name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加命令行参数(默认空)', suggestion: [] },
323
- { name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,默认 120000,最小 1000);异步档下它同时是**每个后台编译作业**的预算(到时主动 terminate,作业记 timeout、对象留在 attempted)', suggestion: 120000 },
324
- { name: 'leanAsync', type: 'boolean', description: 'Lean 编译模式:true(默认)= 后台队列,vibe_math_lean_run / vibe_math_lean_archive{run:true} 立即返回 async.jobId 入队,成员不阻塞,结果由下一轮提示的【形式化结果】行与 vibe_math_lean_lib / vibe_math_lean_job 公告(**只有作业落地为 ok 才会置 passed 并写归档证明**);false = 完全同步 await(与旧行为逐字一致)', suggestion: true },
325
- { name: 'leanJobsMaxParallel', type: 'integer', description: '后台 Lean 编译的并发上限(默认 1 = 串行,保持可预测的资源占用;调大可并行编译多个作业)', suggestion: 1 },
326
- { name: 'leanInitiative', type: 'string', enum: LEAN_INITIATIVE_MODES.slice(), description: '日常流程中的形式化主动性:off(不主动,只在验证提示词按 formalVerify 的要求做)| normal(默认:顺手把有价值且可能复用的东西形式化)| eager(更主动:日常就主动把有价值的小引理/命题/定义形式化)。注意它与 formalVerify(验证时的要求强度:off|encourage|require)是**两件事**', suggestion: 'normal' },
327
- { name: 'leanSearchPaths', type: 'array', items: { type: 'string' }, description: '额外 Lean 搜索路径(默认空数组 = 只用框架自动注入的 VibeMath 根)。非空时按顺序先注入这里给的路径、再注入自动根(去重);若 leanArgs 里已显式给了 --search-path/-R/--root,则完全尊重用户配置、不注入任何东西', suggestion: [] },
328
- { name: 'finalPaper', type: 'boolean', description: '收口(严格终止)时自动撰写最终论文:派遣一名专职「论文撰写」子代理,把已检验通过的命题/解法/成果整理成 Paper/<项目>/{paper.md,paper.tex,paper.meta.json,paper.log.md}。false = 只关自动触发,/vibe paper 手动命令仍可用', suggestion: true },
329
- { name: 'paperFormat', type: 'enum', options: ['both', 'md', 'tex'], description: '论文产出格式:both = markdown + latex;md = 只写 paper.md;tex = 只写 paper.tex(tex 才会尝试编译 pdf)', suggestion: 'both' },
330
- { name: 'paperLanguage', type: 'enum', options: ['zh', 'en'], description: '论文语言:zh = 中文(LaTeX 用 ctexart,引擎优先 xelatex);en = 英文(article,引擎优先 pdflatex/latexmk)', suggestion: 'zh' },
331
- { name: 'paperCompilePdf', type: 'boolean', description: '检测到 LaTeX 时是否编译 paper.pdf(-interaction=nonstopmode 跑两遍;失败先尝试修复:换引擎/去不支持宏包/最小模板)。false 或无 LaTeX 时只保留 tex+md 并记日志', suggestion: true },
332
- { name: 'paperLatexCommand', type: 'string', description: '指定 LaTeX 引擎可执行文件(空 = 按语言探测:中文 xelatex > latexmk > pdflatex > lualatex > tectonic;英文 pdflatex 优先)。该命令解析不到时按"未检测到"降级(只留 tex+md)', suggestion: '' },
333
- // ---- 数学计算 math_computation(六参数冻结;描述与 prompts.md §1/§6 口径一致)----
334
- { name: 'mathComputation', type: 'enum', options: ['off', 'auto', 'on'], description: '数学计算总开关:off = 真无操作(提示词零提及);auto = 探测到 mathEngines 里任一允许引擎才工作;on = 同上(探测失败时工具仍返回可执行的安装指引,而不是假装可用)', suggestion: 'auto' },
335
- { name: 'mathMode', type: 'enum', options: ['typed', 'typed+shell'], description: '计算策略:typed+shell(默认)= 工具不可用时允许宿主 shell 兜底,但结论必须标注"未经工具归档(shell 路径)";typed = 只用工具路径(提示词里不出现 shell 兜底段,engine:"cli" 返回 REFUSED{reason:policy})', suggestion: 'typed+shell' },
336
- { name: 'mathEngines', type: 'string[]', description: '允许的引擎列表(默认 python|r|octave|julia|matlab|maple|wolfram|cli)。cli 默认开启且走同一套超时/输出上限/回执;从列表里移除某引擎即禁用(商业引擎只探测+许可,永不安装)', suggestion: MATH_PARAM_DEFAULTS.mathEngines.slice() },
337
- { name: 'mathTimeoutMs', type: 'integer', description: '单次数学计算的超时上限(毫秒,默认 60000,最小 1000);到时主动 terminate 并把回执标为 MATH_TIMEOUT', suggestion: 60000 },
338
- { name: 'mathPackages', type: 'string[]', description: '需要预检的包(默认空)。缺包只报告 + 给"用户自装指引"或"代理代装计划",不会执行脚本,也永不自动安装', suggestion: [] },
339
- { name: 'mathInstallScope', type: 'string', enum: ['user', 'system'], description: '安装作用域:user(默认,用户级目录);system 只对当次显式调用生效、永不记忆(不会写进状态文件)', suggestion: 'user' },
340
- ]
341
406
 
342
407
  // ================= fs =================
343
408
  async function fsTarget(rel) { return await fs.resolve(rel, { cwd: frameworkRoot() }) }
344
409
  async function readText(rel) { try { const t = await fsTarget(rel); const s = await fs.stat(t); if (s === undefined) return undefined; return await fs.readText(t) } catch (e) { return undefined } }
345
- async function writeText(rel, content) { const t = await fsTarget(rel); await fs.writeText(t, content, undefined, undefined, getPolicy()); return true }
410
+ async function writeText(rel, content) {
411
+ const t = await fsTarget(rel)
412
+ const r = await fs.writeText(t, content, undefined, undefined, getPolicy())
413
+ // P4:宿主若用返回值(而非抛异常)报告写失败,也要留痕——旧实现无条件 return true 会把它吞掉。
414
+ if (r && r.ok === false) noteStateWriteFailure(rel, 'host reported write failure' + (r.error ? (': ' + r.error) : ''))
415
+ return true
416
+ }
346
417
  async function readJson(rel) { const t = await readText(rel); if (t === undefined || t === '') return undefined; try { return JSON.parse(t) } catch (e) { noteSuspect(rel); return undefined } }
347
418
  /**
348
419
  * Corruption guard. `readJson` cannot tell "no file yet" from "file present but
@@ -368,18 +439,26 @@ export function apply(ctx) {
368
439
  console.error('vibe-math-v2: write to ' + rel + ' blocked (file is unparseable; see the earlier warning)')
369
440
  return false
370
441
  }
371
- async function writeJson(rel, obj) { if (!assertWritable(rel)) return false; return await writeText(rel, JSON.stringify(obj, null, 2)) }
442
+ async function writeJson(rel, obj) {
443
+ if (!assertWritable(rel)) {
444
+ // P4:损坏守卫拒绝写入过去是**完全静默**的(返回 false 没人看)⇒ 会话以为已提交、磁盘还是旧内容。
445
+ noteStateWriteFailure(rel, 'corruption guard refused write')
446
+ logActivity('state', '拒绝写入(该 JSON 已存在但无法解析,先修/删它):' + rel)
447
+ return false
448
+ }
449
+ return await writeText(rel, JSON.stringify(obj, null, 2))
450
+ }
372
451
  async function listFiles(rel) { try { const t = await fsTarget(rel); const s = await fs.stat(t); if (s === undefined) return []; const entries = await fs.listDir(t); return entries.filter(function (e) { return e && e.type === 'file' }).map(function (e) { return e.name }) } catch (e) { return [] } }
373
452
  async function listDirsAt(base, rel) { try { const t = await fs.resolve(rel, { cwd: base }); const s = await fs.stat(t); if (s === undefined) return []; const entries = await fs.listDir(t); return entries.filter(function (e) { return e && e.type === 'directory' }).map(function (e) { return e.name }) } catch (e) { return [] } }
374
453
  async function readTextAbs(path) { try { const t = await fs.resolve(path); const s = await fs.stat(t); if (s === undefined) return undefined; return await fs.readText(t) } catch (e) { return undefined } }
375
454
  async function writeTextAbs(path, content) { try { const t = await fs.resolve(path); await fs.writeText(t, content, undefined, undefined, getPolicy()); return true } catch (e) { return false } }
376
455
  async function readCurrentProject() {
377
456
  // 按会话隔离的 current 文件(多会话并行时互不覆盖);无则回退旧共享文件
378
- try { const t = await fs.resolve('current.' + safeId(sessionId) + '.json', { cwd: vibeRoot() }); const s = await fs.stat(t); if (s !== undefined) { const txt = await fs.readText(t); const j = safeJson(txt, null); const p = (j && j.project) ? String(j.project) : 'default'; return slugify(p) } } catch (e) {}
379
- try { const t = await fs.resolve('current.json', { cwd: vibeRoot() }); const s = await fs.stat(t); if (s === undefined) return 'default'; const txt = await fs.readText(t); const j = safeJson(txt, null); const p = (j && j.project) ? String(j.project) : 'default'; return slugify(p) } catch (e) { return 'default' }
457
+ try { const t = await fs.resolve('current.' + safeId(sessionId) + '.json', { cwd: vibeRoot() }); const s = await fs.stat(t); if (s !== undefined) { const txt = await fs.readText(t); const j = safeJson(txt, null); const p = (j && j.project) ? String(j.project) : 'default'; return slugify(p) } } catch (e) { noteStateWriteFailure('current.' + safeId(sessionId) + '.json', 'read failed: ' + ((e && e.message) || e)) }
458
+ try { const t = await fs.resolve('current.json', { cwd: vibeRoot() }); const s = await fs.stat(t); if (s === undefined) return 'default'; const txt = await fs.readText(t); const j = safeJson(txt, null); const p = (j && j.project) ? String(j.project) : 'default'; return slugify(p) } catch (e) { noteStateWriteFailure('current.json', 'read failed: ' + ((e && e.message) || e)); return 'default' }
380
459
  }
381
460
  async function writeCurrentProject() {
382
- try { const t = await fs.resolve('current.' + safeId(sessionId) + '.json', { cwd: vibeRoot() }); await fs.writeText(t, JSON.stringify({ project: currentProject }), undefined, undefined, getPolicy()) } catch (e) {}
461
+ try { const t = await fs.resolve('current.' + safeId(sessionId) + '.json', { cwd: vibeRoot() }); await fs.writeText(t, JSON.stringify({ project: currentProject }), undefined, undefined, getPolicy()) } catch (e) { noteStateWriteFailure('current.' + safeId(sessionId) + '.json', (e && e.message) || e) } // F-6c: 内存已切/磁盘未落必须可观察,否则重启丢失项目切换
383
462
  }
384
463
 
385
464
  // ================= subprocess =================
@@ -732,6 +811,75 @@ export function apply(ctx) {
732
811
  }
733
812
  return out
734
813
  }
814
+ /** P2:状态提交的原子性 + 完整性标记。
815
+ *
816
+ * 可用能力只有 fs.resolve/stat/readText/writeText(**宿主没有 rename**)。所以提交协议是
817
+ * 「**暂存 → 回读校验 → 提交 → 标记**」:
818
+ * 1) 每个状态文件先写 `<rel>.tmp`,再回读确认**非空**(截断/失败写在这里就被抓住);
819
+ * 2) 暂存成功才把它写进最终路径(`writeText(rel, …)`),失败则记入 `stateWriteFailures`;
820
+ * 3) 全部落地后**最后**写 `VibeMath_State/commit.json`:`{seq, at, checkpoint, files, missing, mode}`
821
+ * —— 它是"这次提交完整"的唯一标记,恢复时用它判断上一次提交是否撕裂。
822
+ *
823
+ * **不用 shell move**:宿主可能只提供受控/模拟的 subprocess(本轮实测:假 subprocess 返回 exit 0
824
+ * 但不会真的移动文件 ⇒ "搬"成功、文件却不存在,状态直接丢失)。有鉴于此,这里坚持只走宿主 fs API;
825
+ * 代价是没有 rename 级原子性,收益是任何宿主上都不会静默丢文件。
826
+ */
827
+ let stateCommitSeq = 0
828
+ let stateCommit = { seq: 0, at: 0, complete: false, mode: '', files: [], missing: [] }
829
+ const stateWriteFailures = [] // P4:被拒绝/失败的落盘(status/report 可见)
830
+ function noteStateWriteFailure(rel, error) {
831
+ stateWriteFailures.push({ rel: String(rel), error: String(error || 'unknown'), at: now() })
832
+ if (stateWriteFailures.length > 32) stateWriteFailures.shift()
833
+ }
834
+ async function relNonEmpty(rel) {
835
+ try { const t = await fsTarget(rel); const st = await fs.stat(t); return !!(st && (st.size === undefined || st.size > 0)) } catch (e) { return false }
836
+ }
837
+ async function writeJsonAtomic(rel, obj) {
838
+ // P4:原子路径也必须过损坏守卫(否则「存在但解析失败」的状态文件会被静默覆写)
839
+ if (!assertWritable(rel)) {
840
+ noteStateWriteFailure(rel, 'corruption guard refused write')
841
+ logActivity('state', '拒绝写入(该 JSON 已存在但无法解析,先修/删它):' + rel)
842
+ return 'refused'
843
+ }
844
+ const text = JSON.stringify(obj, null, 2)
845
+ // 没有 shell 就没法清理暂存文件(宿主 fs 无 delete/rename)⇒ 直接写,避免留下 .tmp 垃圾与告警
846
+ if (!subprocessOf()) {
847
+ try { await writeText(rel, text) } catch (e) { noteStateWriteFailure(rel, (e && e.message) || e) }
848
+ if (!(await relNonEmpty(rel))) noteStateWriteFailure(rel, 'written but empty/missing')
849
+ return 'direct'
850
+ }
851
+ const tmp = rel + '.tmp'
852
+ let staged = false
853
+ try { await writeText(tmp, text); staged = await relNonEmpty(tmp) } catch (e) { staged = false }
854
+ let mode = 'direct'
855
+ if (staged) {
856
+ try { await writeText(rel, text); mode = 'stage-commit' } catch (e) { noteStateWriteFailure(rel, (e && e.message) || e) }
857
+ try { await removeFile(tmp) } catch (e) { /* 清理暂存文件失败不影响提交内容 */ }
858
+ } else {
859
+ try { await writeText(rel, text) } catch (e) { noteStateWriteFailure(rel, (e && e.message) || e) }
860
+ }
861
+ if (!(await relNonEmpty(rel))) noteStateWriteFailure(rel, 'written but empty/missing')
862
+ return mode
863
+ }
864
+ async function commitState(files) {
865
+ stateCommitSeq += 1
866
+ const modes = []
867
+ const missing = []
868
+ for (let i = 0; i < files.length; i++) {
869
+ const mode = await writeJsonAtomic(files[i].rel, files[i].obj)
870
+ modes.push(mode)
871
+ if (!(await relNonEmpty(files[i].rel))) missing.push(files[i].rel)
872
+ }
873
+ const commit = {
874
+ seq: stateCommitSeq, at: now(), epoch: processEpoch,
875
+ checkpoint: Number(scheduler.lastCheckpoint) || now(),
876
+ files: files.map(function (x) { return x.rel }), missing: missing,
877
+ mode: modes.indexOf('stage-commit') === -1 ? 'direct' : 'stage-commit',
878
+ }
879
+ await writeJsonAtomic('VibeMath_State/commit.json', commit)
880
+ stateCommit = Object.assign({ complete: missing.length === 0 }, commit)
881
+ return commit
882
+ }
735
883
  async function loadState() {
736
884
  const s = await readJson('VibeMath_State/scheduler_state.json')
737
885
  // 丢弃历史持久化的 activeCount:旧值可能已经漂移,绝不能覆盖推导值(见 activeCount())。
@@ -741,23 +889,55 @@ export function apply(ctx) {
741
889
  const va = await readJson('VibeMath_State/verifier_accuracy.json'); if (va) verifierAccuracy = sanitizeAccuracy(va)
742
890
  const tk = await readJson('VibeMath_State/tasks.json'); if (tk) tasks = tk
743
891
  const er = await readJson('VibeMath_State/explorer_retries.json'); if (er) explorerRetries = er
892
+ // P3:恢复待计分样本(有上限、有溢出告警,见 recordReviewForScoring)
893
+ // P5:把磁盘上的活动日志并回内存(旧实现只落 report 快照且从不回读 ⇒ 恢复后 status 从零开始)
894
+ const al = await readJson('VibeMath_State/activity_log.json')
895
+ if (Array.isArray(al) && al.length) { const seen = {}; const merged = []; for (const e of al.concat(activityLog)) { const k = String(e && e.at) + '|' + String(e && e.detail); if (seen[k]) continue; seen[k] = true; merged.push(e) } activityLog = merged.slice(-ACTIVITY_PERSIST_MAX) }
896
+ // P6:论文排队/回收计数恢复;在途撰写者跨进程不可恢复 ⇒ 清掉留痕
897
+ const pj = await readJson('VibeMath_State/paper.json')
898
+ if (pj && typeof pj === 'object') { if (pj.pending) paperPending = pj.pending; paperReaps = Number(pj.reaps) || 0 }
899
+ if (paperInFlight) { logActivity('paper', '上一进程的在途撰写者 ' + paperInFlight + ' 不可恢复(child 随进程结束)⇒ 交由 paper 锁/回收逻辑重新触发'); paperInFlight = ''; paperInFlightAt = 0 }
900
+ const prs = await readJson('VibeMath_State/pending_review_scores.json')
901
+ if (prs && typeof prs === 'object' && !Array.isArray(prs)) pendingReviewScores = prs
744
902
  // 形式化记录(docs/formal-verification.md §4):v2 没有会话投影,这条状态必须自己持久化,
745
903
  // 否则一次 resume 就让 require 门禁失忆(已 passed 的对象被再当"未尝试")。
904
+ // P2:先校验上一次提交的完整性(不完整 ⇒ 留痕,再按逐文件容错读取)
905
+ await loadCommitMarker()
746
906
  const fm = await readJson('VibeMath_State/formal.json')
747
907
  formalState = {
748
908
  records: (fm && fm.records && typeof fm.records === 'object' && !Array.isArray(fm.records)) ? fm.records : {},
749
909
  todo: (fm && Array.isArray(fm.todo)) ? fm.todo : [],
750
910
  }
751
911
  }
912
+ /** P2:恢复时校验上一次提交的完整性(标记列出但缺失/为空的状态文件 = 撕裂提交)。 */
913
+ async function loadCommitMarker() {
914
+ const cm = await readJson('VibeMath_State/commit.json')
915
+ if (!cm || typeof cm !== 'object') { stateCommit = { seq: 0, at: 0, complete: false, mode: '', files: [], missing: [] }; return stateCommit }
916
+ const missing = []
917
+ const files = Array.isArray(cm.files) ? cm.files : []
918
+ for (let i = 0; i < files.length; i++) { if (!(await relNonEmpty(files[i]))) missing.push(files[i]) }
919
+ stateCommitSeq = Number(cm.seq) || 0
920
+ stateCommit = Object.assign({}, cm, { complete: missing.length === 0, missing: missing })
921
+ if (missing.length && files.length) logActivity('state', '上一次状态提交不完整(缺失/为空:' + missing.join(', ') + ')⇒ 按逐文件容错读取,最后一次提交的部分字段可能丢失')
922
+ return stateCommit
923
+ }
752
924
  async function saveAll() {
753
- await writeJson('VibeMath_State/scheduler_state.json', scheduler)
754
- await writeJson('VibeMath_State/agent_registry.json', agentRegistry)
755
- await writeJson('VibeMath_State/decision_queue.json', decisionQueue)
756
- await writeJson('VibeMath_State/verifier_accuracy.json', verifierAccuracy)
757
- await writeJson('VibeMath_State/tasks.json', tasks)
758
- await writeJson('VibeMath_State/explorer_retries.json', explorerRetries)
759
- await writeJson('VibeMath_State/formal.json', { records: formalRecords(), todo: formalTodo() })
925
+ // P2:checkpoint 先落进内存再提交(旧顺序是提交完才改内存 ⇒ 磁盘上的 checkpoint 永远滞后一次)
760
926
  scheduler.lastCheckpoint = now()
927
+ await commitState([
928
+ { rel: 'VibeMath_State/scheduler_state.json', obj: scheduler },
929
+ { rel: 'VibeMath_State/agent_registry.json', obj: agentRegistry },
930
+ { rel: 'VibeMath_State/decision_queue.json', obj: decisionQueue },
931
+ { rel: 'VibeMath_State/verifier_accuracy.json', obj: verifierAccuracy },
932
+ { rel: 'VibeMath_State/tasks.json', obj: tasks },
933
+ { rel: 'VibeMath_State/explorer_retries.json', obj: explorerRetries },
934
+ { rel: 'VibeMath_State/formal.json', obj: { records: formalRecords(), todo: formalTodo() } },
935
+ // P3:待计分样本也必须落盘——旧实现只在内存,崩溃/恢复后这批评审永远不会被计分(准确率静默丢样本)
936
+ { rel: 'VibeMath_State/pending_review_scores.json', obj: pendingReviewScores },
937
+ // P5:活动日志(有界)落盘;P6:论文排队/回收计数落盘(在途 childId 不落,见 loadState)
938
+ { rel: 'VibeMath_State/activity_log.json', obj: activityLog.slice(-ACTIVITY_PERSIST_MAX) },
939
+ { rel: 'VibeMath_State/paper.json', obj: { pending: paperPending, reaps: paperReaps } },
940
+ ])
761
941
  }
762
942
  // 查询类工具(status/setup/report)汇报前重读设置文件:文件是唯一持久化源,可能在会话启动后被用户手改或由本进程外编辑更新。
763
943
  async function refreshParams() {
@@ -1969,7 +2149,9 @@ export function apply(ctx) {
1969
2149
  const rec = formalGateRecord(t)
1970
2150
  if (formalGateOk(rec)) return false
1971
2151
  const own = formalOf(t)
1972
- const reason = 'formal-required:尚未取得 Lean 形式化通过,也没有显式阻塞记录(当前状态 ' + (rec.status || 'none') + ')' + (why ? '|' + why : '')
2152
+ const statusZh = ({ none: '未尝试', attempted: '已尝试未通过', blocked: '已记录阻塞', passed: '已通过' })[String(rec.status || 'none')] || String(rec.status || 'none')
2153
+ const reason = 'formal-required:尚未取得 Lean 形式化通过,也没有显式阻塞记录(当前状态 ' + statusZh + ';代码 formal-required)' + (why ? '|' + why : '') +
2154
+ ';下一步:把该对象形式化到 Lean 通过(vibe_math_lean_archive,kind=proof/lemma),或写入显式阻塞记录(kind=blocked)后重新提议该对象,即可再次表决'
1973
2155
  const list = formalTodo().filter(function (x) { return x.id !== t })
1974
2156
  list.push({ id: t, at: now(), why: reason })
1975
2157
  // 不改动对象的既有权重/概率字段:只补一条 formal 记录(status 保持 none/attempted)。
@@ -2016,6 +2198,21 @@ export function apply(ctx) {
2016
2198
  return out
2017
2199
  }
2018
2200
  async function findProposition(pId) { const all = await getPropos(); return all.find(function (p) { return p.id === pId }) }
2201
+ /**
2202
+ * P1:`add_proposition`/`/vibe add-proposition` 只用于**新建**。id 已存在时**拒绝**,绝不覆盖——
2203
+ * 旧实现是"按 id upsert",会把已有卡的 陈述/证明列表/证伪列表 换成全新的空列表(静默丢数据),
2204
+ * 而 sync_meta 的引理路径(v3 侧)本来就有 `propos.has` + 文件存在双守卫。这里把两者语义拉齐。
2205
+ */
2206
+ function propositionIdConflict(id, all) {
2207
+ const hit = (all || []).filter(function (x) { return String(x.id) === String(id) })[0]
2208
+ if (!hit) return null
2209
+ return {
2210
+ code: 'PROPOSITION_ID_EXISTS',
2211
+ message: '命题 id "' + id + '" 已存在:add_proposition 只用于**新建**,不会覆盖已有卡(其 陈述/证明列表/证伪列表 一律保留)。',
2212
+ next: '换一个新 id 新建(例如 p-' + shortId() + ');要修改已有卡请用文件工具直接编辑 Propos/<分类>/<id>.md。',
2213
+ existing: { id: hit.id, 概述: hit.概述, 布尔估计: hit.布尔估计, category: hit._category },
2214
+ }
2215
+ }
2019
2216
  async function upsertProposition(p) {
2020
2217
  const cat = p._category || categoryOf(p)
2021
2218
  delete p._category
@@ -2025,13 +2222,6 @@ export function apply(ctx) {
2025
2222
  await writeJson(proposFile(cat), list)
2026
2223
  return cat
2027
2224
  }
2028
- async function deleteProposition(p) {
2029
- const cat = p._category || categoryOf(p)
2030
- const list = await readProposCategory(cat)
2031
- const next = list.filter(function (x) { return x.id !== p.id })
2032
- await writeJson(proposFile(cat), next)
2033
- }
2034
-
2035
2225
  // ================= data layer: Verified / Reliable =================
2036
2226
  async function readVerifiedCategory(cat) { const a = await readJson('Verified/' + String(cat) + '_Verified.json'); return Array.isArray(a) ? a : [] }
2037
2227
  async function reliableFiles() { return await listFiles('Reliable') }
@@ -2042,13 +2232,18 @@ export function apply(ctx) {
2042
2232
  const qs = await getQs()
2043
2233
  const propos = await getPropos()
2044
2234
  return {
2045
- ok: true, at: now(), project: currentProject, frameworkRoot: frameworkRoot(),
2235
+ ok: true, at: now(), project: currentProject, projectExists: await projectExistsOnDisk(), frameworkRoot: frameworkRoot(),
2236
+ stateWriteFailures: { count: stateWriteFailures.length, last: stateWriteFailures[stateWriteFailures.length - 1] || null },
2046
2237
  running: scheduler.running, mode: params.mode,
2047
2238
  activeCount: activeCount(), maxParallelThreshold: params.maxParallelThreshold,
2048
2239
  problems: { total: qs.length, solved: qs.filter(function (q) { return q.已解决 }).length },
2049
2240
  propositions: { total: propos.length, resolved: propos.filter(function (p) { return p.布尔估计 === 1 || p.布尔估计 === 0 }).length },
2050
- pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }),
2241
+ // F-A(有意拆分):pendingDecisions 是**合同**(两面同形的数字);pendingDecisionItems 是**诊断**(明细),只出现在 report——status 面向代理自省、report 面向文件快照。改动此处需同步 docs/status-report-fields.md(该文档租约在共享层 owner)。
2242
+ pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).length,
2243
+ pendingDecisionItems: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }),
2051
2244
  registeredAgents: Object.keys(agentRegistry).length,
2245
+ // D4 审计面:每个在飞验证任务的参与集/票数都由 voteCount 投影出来(两个视图可对照)。
2246
+ verifyTasks: verifyTasksView(tasks),
2052
2247
  recentActivity: activityLog.slice(-Math.min(ACTIVITY_REPORT_MAX, Number(params.activityLogCap) || 100)),
2053
2248
  // Lean 形式化:可调档位与开关同处可读参数表(契约 §1),并附当前形式化记录概况。
2054
2249
  formal: {
@@ -2056,9 +2251,12 @@ export function apply(ctx) {
2056
2251
  leanCommand: params.leanCommand, leanArgs: params.leanArgs, leanTimeoutMs: params.leanTimeoutMs,
2057
2252
  objects: Object.keys(formalRecords()).map(function (k) { const r = formalRecords()[k] || {}; return { target: k, status: r.status, file: r.file, proof: r.proof, note: r.note } }),
2058
2253
  todo: formalTodo(),
2059
- paths: { project: 'Formal/', lib: vibeRoot() + '/Formal/Lib/', proved: vibeRoot() + '/Formal/Proved/', proofs: 'Verified/Lean/' },
2254
+ // F6:四项路径基准不同 ⇒ 必须点名 base(project/proofs 相对 base,lib/proved 已绝对)。
2255
+ paths: { base: frameworkRoot() + '/', project: 'Formal/', proofs: 'Verified/Lean/', lib: vibeRoot() + '/Formal/Lib/', proved: vibeRoot() + '/Formal/Proved/', note: 'project/proofs 相对 base(项目根);lib/proved 已含绝对前缀' },
2060
2256
  },
2061
2257
  params: params,
2258
+ // F5:report/status 两面字段集对齐(旧实现只有 status 看得到论文/编译状态)。
2259
+ paper: await paperStatusView(),
2062
2260
  }
2063
2261
  }
2064
2262
  async function maybeWriteReport(force) {
@@ -2084,8 +2282,8 @@ export function apply(ctx) {
2084
2282
  const report = await buildReport()
2085
2283
  const text = '[Vibe Math V2] 进度更新:当前项目 "' + currentProject + '" 运行中=' + report.running +
2086
2284
  ',问题 ' + report.problems.solved + '/' + report.problems.total + ' 已解决,命题 ' + report.propositions.resolved + '/' + report.propositions.total + ' 已定论,' +
2087
- '活跃代理轮数=' + report.activeCount + ',待人工决策=' + report.pendingDecisions.length + '。' +
2088
- '请调用 vibe_math_report 汇总当前进展及各代理状态,并用人话简要汇报(不打断用户,简短即可)。'
2285
+ '活跃代理轮数=' + report.activeCount + ',待人工决策=' + report.pendingDecisions + '。' +
2286
+ '请调用 vibe_math_report 汇总当前进展,并用 vibe_math_list_agents 取各代理(id/角色/目标)状态,再用人话简要汇报(不打断用户,简短即可)。'
2089
2287
  // 来源 kind 必须是**已声明**的:`MessageSourceMap` 是 merge-extensible 的联合,但没有共享的
2090
2288
  // catch-all `plugin` kind(dsh-llm message.d.ts),{kind:'plugin'} 是契约外形状。role 本来就是
2091
2289
  // 'user',正文也自带 "[Vibe Math V2] 进度更新" 的真署名,所以用核心声明的 {kind:'user'}。
@@ -2097,7 +2295,14 @@ export function apply(ctx) {
2097
2295
  }
2098
2296
 
2099
2297
  // ================= child spawn / followup =================
2100
- function pickProvider() { try { const names = subagents.list ? subagents.list() : []; if (names.indexOf('spawn') !== -1) return 'spawn'; if (names.indexOf('fork') !== -1) return 'fork' } catch (e) {} return 'spawn' }
2298
+ // F-6a:宿主 list() 失败(或既无 spawn 也无 fork)时回退到假定值必须**留痕**,不能静默猜。
2299
+ let providerFallbackWarned = false
2300
+ function warnProviderFallback(why) {
2301
+ if (providerFallbackWarned) return
2302
+ providerFallbackWarned = true
2303
+ console.error('vibe-math-v2: pickProvider() falling back to \'spawn\': ' + why)
2304
+ }
2305
+ function pickProvider() { let names = []; let listed = false; try { names = subagents.list ? subagents.list() : []; listed = true } catch (e) { warnProviderFallback('subagents.list() failed: ' + ((e && e.message) || e)) } if (names.indexOf('spawn') !== -1) return 'spawn'; if (names.indexOf('fork') !== -1) return 'fork'; if (listed) warnProviderFallback('host exposes neither spawn nor fork (list=' + JSON.stringify(names) + ')'); return 'spawn' }
2101
2306
  function childAgentOptions() { const o = {}; try { if (rootAgent && rootAgent.options) { if (rootAgent.options.provider) o.provider = rootAgent.options.provider; if (rootAgent.options.model) o.model = rootAgent.options.model } } catch (e) {} if (params.provider) o.provider = params.provider; if (params.model) o.model = params.model; return o }
2102
2307
  /**
2103
2308
  * Drop filter names this host does not register. `known` comes from the host's
@@ -2165,7 +2370,28 @@ export function apply(ctx) {
2165
2370
  // 不再手工累加并发计数:唤醒的是 registry 里已登记的 child,计数已由 registry 长度体现。
2166
2371
  await saveAll()
2167
2372
  }
2168
- async function interruptChild(childId) { try { subagents.interrupt(childId, { kind: 'ancestor', agent: rootAgent }) } catch (e) {} }
2373
+ /**
2374
+ * 中断一个**在册**子代理。审计 D1:旧实现 `try{…}catch(e){}` 把"没有这个 child / 宿主拒绝中断"
2375
+ * 都吞成静默成功(调用方拿到 undefined,还以为已经中断)。现在:空/未知 id ⇒ `code` +
2376
+ * `next{tool,hint}`,宿主抛错 ⇒ 保留错误文本并给替代出口;只有真的发出中断才 `ok:true`。
2377
+ */
2378
+ /** F-6b:中断失败必须留痕(调用点此前丢掉 {ok:false},界面看起来一切正常)。 */
2379
+ async function interruptTraced(cid, why) {
2380
+ const r = await interruptChild(cid)
2381
+ if (r && r.ok === false) logActivity('interrupt', '中断失败(' + why + '):' + String(cid) + ' — ' + String(r.message || r.code || ''))
2382
+ return r
2383
+ }
2384
+ async function interruptChild(childId) {
2385
+ const id = String(childId == null ? '' : childId).trim()
2386
+ if (!id) return { ok: false, code: 'VIBE_MATH_INVALID_ARGUMENT', message: 'interruptChild 失败:childId 为空——无法确定要中断哪个子代理。', next: { kind: 'reason', tool: 'vibe_math_list_agents', hint: '用 vibe_math_list_agents 列出本会话在册子代理的 id,再带 childId 调用。' } }
2387
+ if (!Object.prototype.hasOwnProperty.call(agentRegistry, id)) return { ok: false, code: 'VIBE_MATH_CHILD_NOT_FOUND', message: 'interruptChild 失败:' + id + ' 不在本会话的在册子代理里(在册 ' + Object.keys(agentRegistry).length + ' 个)——可能已经结束,或不是本会话的子代理。', next: { kind: 'reason', tool: 'vibe_math_list_agents', hint: '用 vibe_math_list_agents 取当前在册 id;要停整个会话用 vibe_math_abort。' } }
2388
+ try {
2389
+ await subagents.interrupt(id, { kind: 'ancestor', agent: rootAgent })
2390
+ return { ok: true, childId: id }
2391
+ } catch (e) {
2392
+ return { ok: false, code: 'VIBE_MATH_INTERRUPT_FAILED', message: 'interruptChild 失败:宿主拒绝中断 ' + id + '(' + String((e && e.message) || e) + ')。', next: { kind: 'reason', tool: 'vibe_math_abort', hint: '宿主不接受单点中断时,用 vibe_math_abort 终止整个会话的子代理。' } }
2393
+ }
2394
+ }
2169
2395
 
2170
2396
  // ================= prompts =================
2171
2397
  function solverPersonaText() { return params.solverPersona ? (String(params.solverPersona) + '\n\n') : '' }
@@ -2212,6 +2438,14 @@ export function apply(ctx) {
2212
2438
  // 数学计算可用性行(prompts.md §2+§4):与"顺手形式化"并列注入;off 档为空串。
2213
2439
  return '\n' + t + formalReplyNote() + notice + math + '\n'
2214
2440
  }
2441
+ /**
2442
+ * F7:机器回执的**键名大小写**是契约的一部分,必须显式写在提示词里。
2443
+ * v2 各回执的对象类型不同(验证回执用 `Result`/`Reason` 首字母大写;计划/求解/方法卡回执用小写
2444
+ * `directions`/`lemmas`/`status`/`solution`/`solution_probability`),历史上只在示例里体现大小写——
2445
+ * 模型猜错键名会被解析器当成"缺字段"静默兜底(`Result` 缺失 ⇒ 0.5),所以这里统一说清:
2446
+ * **解析器按大小写精确匹配、不接受别名**(`parsed.Result` / `parsed.directions` 都是原样取键)。
2447
+ */
2448
+ const REPLY_KEY_STYLE_LINE = '\nKEY CASE IS PART OF THE REPLY CONTRACT: the verifier reply uses `Result`/`Reason` (capitalised; the debate round adds `changed`), while every other reply uses lowercase keys exactly as shown (`directions`, `lemmas`, `status`, `solution`, `solution_probability`). The parser matches keys EXACTLY, case-sensitively, and accepts NO aliases (e.g. `result` is not read).\n'
2215
2449
  function capabilitiesText(role) {
2216
2450
  const maxCalls = role === 'solver' ? params.solverMaxToolCalls : params.verifierMaxToolCalls
2217
2451
  const netOn = role === 'solver' ? params.solverAllowNetwork : params.verifierAllowNetwork
@@ -2227,6 +2461,7 @@ export function apply(ctx) {
2227
2461
  t += '- You may READ any file under Verified/ as a known, trusted dependency (resolved facts).\n'
2228
2462
  t += '- You should BASE your reasoning on the existing knowledge under Propos/ (propositions with proofs/refutations and probabilities) and Reliable/ (trusted references).\n'
2229
2463
  t += '- You must NOT write files directly: return structured JSON only — the scheduler is the single writer.\n'
2464
+ t += REPLY_KEY_STYLE_LINE
2230
2465
  t += '\nHOW TO READ EXISTING KNOWLEDGE (coarse scan → fine read):\n'
2231
2466
  t += '- These are JSON files. A conclusion object carries summary-index fields (概述 / 布尔估计 / 优先级) and the full detail (证明列表 / 证伪列表 / 完整过程 / progress).\n'
2232
2467
  t += '- COARSE SCAN first: use a read/grep tool to extract ONLY the summary index (概述, 布尔估计, 优先级, titles) to locate which files / objects look relevant — do NOT load full proofs yet.\n'
@@ -2402,7 +2637,10 @@ export function apply(ctx) {
2402
2637
  if (node === 'verdict') { const overridden = resolution.action === 'override' && (resolution.verdict === 1 || resolution.verdict === 0); const v = overridden ? Number(resolution.verdict) : data.verdict; const applied = await settleVerdict(data.task, v); delete tasks[data.task.id]; return { verdict: v, overridden: overridden, applied: applied } }
2403
2638
  return {}
2404
2639
  }
2405
- async function resolveDecision(id, resolution) { const d = decisionQueue.find(function (x) { return x.id === id }); if (!d) return { ok: false, message: 'decision not found' }; if (d.status !== 'pending') return { ok: false, message: 'decision already resolved' }; d.status = 'resolved'; d.resolution = resolution; if (scheduler.gate && scheduler.gate.decisionId === id) scheduler.gate = null; logActivity('decide', id + ' resolved: ' + resolution.action + (resolution.verdict !== undefined ? ' ' + resolution.verdict : '')); await saveAll(); scheduleTick(); return { ok: true, message: 'decision resolved' } }
2640
+ async function resolveDecision(id, resolution) {
2641
+ const d = decisionQueue.find(function (x) { return x.id === id })
2642
+ if (!d) return { ok: false, code: 'VIBE_MATH_DECISION_NOT_FOUND', message: 'decision not found:' + String(id == null ? '' : id) + ' 不在人工决策队列里(队列现有 ' + decisionQueue.length + ' 个' + (decisionQueue.length ? (':' + decisionQueue.map(function (x) { return x.id }).join(', ')) : '') + ')——可能已被 vibe_math_decide 结清,或被 start/resume/abort/换项目作废(作废是终态,不会回到 pending)。', next: { kind: 'reason', tool: 'vibe_math_list_decisions', hint: '先用 vibe_math_list_decisions 取当前 pending 的 id;若期望它还在,检查是否发生过 start/resume/abort/换项目。' } }
2643
+ if (d.status !== 'pending') return { ok: false, code: 'VIBE_MATH_DECISION_ALREADY_RESOLVED', message: 'decision already resolved:' + String(id) + ' 当前状态是 ' + String(d.status) + '——每次决策只生效一次。', next: { kind: 'reason', tool: 'vibe_math_list_decisions', hint: '用 vibe_math_list_decisions 看还有哪些 pending;要重新决策请等框架发起新的决策项。' } }; d.status = 'resolved'; d.resolution = resolution; if (scheduler.gate && scheduler.gate.decisionId === id) scheduler.gate = null; logActivity('decide', id + ' resolved: ' + resolution.action + (resolution.verdict !== undefined ? ' ' + resolution.verdict : '')); await saveAll(); scheduleTick(); return { ok: true, message: 'decision resolved' } }
2406
2644
  /**
2407
2645
  * 清掉 gate 时**必须结清它指向的那个人工决策**(审计 M10)。start/resume/abort/切项目都会离开
2408
2646
  * 需要那次决策的运行,而决策若仍是 `pending`,它会永远留在 `vibe_math_list_decisions` 里反复出现——
@@ -2694,13 +2932,14 @@ export function apply(ctx) {
2694
2932
  }
2695
2933
  async function backfillVerifiers(t) {
2696
2934
  const cap = reviewerCap()
2697
- while (t.children.length < t.expectedCount && t.children.length < cap) {
2935
+ const want = voteCount(t).expected // 快照目标(不再直接读 t.expectedCount,保持单一来源)
2936
+ while (t.children.length < want && t.children.length < cap) {
2698
2937
  if (activeCount() >= params.maxParallelThreshold) break
2699
2938
  const index = t.children.length
2700
2939
  const childId = await spawnChild('verifier:' + t.rId + ':' + index, verifierReviewPrompt(t.r), { role: 'verifier', rId: t.rId, round: 1, index: index })
2701
2940
  t.children.push(childId)
2702
2941
  }
2703
- if (t.children.length >= t.expectedCount) t.status = 'debating'
2942
+ if (t.children.length >= want) t.status = 'debating'
2704
2943
  }
2705
2944
  async function reconcileVerify() {
2706
2945
  const ids = Object.keys(tasks)
@@ -2716,7 +2955,7 @@ export function apply(ctx) {
2716
2955
  } else continue
2717
2956
  }
2718
2957
  if (t.status === 'paused') {
2719
- const allReported = t.children.length > 0 && t.children.every(function (cid) { const r = t.childResults[cid]; return r && r.round === t.round })
2958
+ const allReported = voteCount(t, t.round).allParticipantsReported
2720
2959
  if (allReported) { t.status = 'debating'; await advanceVerification(t, t.round); continue }
2721
2960
  }
2722
2961
  if (t.status !== 'spawning') continue
@@ -2869,8 +3108,9 @@ export function apply(ctx) {
2869
3108
  if (!lemma || !lemma.title) return
2870
3109
  let be = clamp01(lemma.布尔估计 != null ? lemma.布尔估计 : 0.6)
2871
3110
  if (be >= 1) be = 0.99; else if (be <= 0) be = 0.01 // 写入时概率必须 <1 且 >0(待验证器验证)
3111
+ const p8Ids = (await getPropos()).map(function (x) { return x.id })
2872
3112
  const p = {
2873
- id: 'p-' + shortId(), 概述: lemma.statement || lemma.title,
3113
+ id: 'p-' + shortIdUnique(function (x) { return p8Ids.indexOf(x) !== -1 }), 概述: lemma.statement || lemma.title,
2874
3114
  布尔估计: be,
2875
3115
  细类型: (lemma.细类型 && typeof lemma.细类型 === 'object') ? lemma.细类型 : { 未分类: {} },
2876
3116
  证明列表: [{ 完整过程: lemma.proof || '', 正确概率: clamp01(0.7), '支持信息/依据': '' }],
@@ -2940,9 +3180,55 @@ export function apply(ctx) {
2940
3180
  }
2941
3181
 
2942
3182
  // ================= verification (验证器) =================
3183
+ /**
3184
+ * 验证任务的**票数/参与集单一来源**(跨预设审计 D4,与 v3 的 voteCount 同型)。
3185
+ *
3186
+ * 定义(本文件里"参与集"只有一个含义):一个 verify 任务的参与集 = **本轮被派出的评审**,
3187
+ * 也就是 `t.children`(当前 round 的 `childResults` 条目是它们的票);"需要几票"= **任务创建时的
3188
+ * 快照** `t.expectedCount`(processVerify 建任务时写入),**绝不**读实时 `params.verifierCount`
3189
+ * 或 `params.maxParallelThreshold`。旧实现把这个答案分散在三处——`consensus()` 只看
3190
+ * `Object.keys(t.childResults)`(已报票)且用常量 MIN_REVIEWERS 当下限、`finalizeVerification()`
3191
+ * 又自己数一遍、两处内联 `allReported` 用 `t.children` + round——同一个计数在不同视图里可以不同
3192
+ * (中途增/减评审、某个评审掉线后,辩论视图与裁决视图可能对"这一轮算不算完"给出不同答案)。
3193
+ * 现在所有视图都读这一个函数。
3194
+ */
3195
+ // 契约(P2-v2 修复轮,必须保持):一致/票数下限仍按 **MIN_REVIEWERS** 判定,一致性的对象是
3196
+ // **所有已报结果**(reportedIds)。`voteCount` 只是把这些读法收拢到一个来源 + 提供审计投影;
3197
+ // 它**不**改变"一票不算共识"的既有语义(v2-fix-probes 的 M11 探针钉着这条)。
3198
+ function voteCount(t, round) {
3199
+ const tt = t || {}
3200
+ const expected = Math.max(MIN_REVIEWERS, Number(tt.expectedCount) || 0)
3201
+ const r = Number((round === undefined || round === null) ? (tt.round || 1) : round) || 1
3202
+ const participants = Array.isArray(tt.children) ? tt.children.slice() : []
3203
+ const results = tt.childResults || {}
3204
+ const reportedIds = Object.keys(results)
3205
+ const thisRoundIds = participants.filter(function (cid) { const x = results[cid]; return x && Number(x.round) === r })
3206
+ return {
3207
+ round: r, expected: expected, participants: participants,
3208
+ dispatched: participants.length, reported: reportedIds.length, reportedIds: reportedIds,
3209
+ reportedThisRound: thisRoundIds.length,
3210
+ // 旧语义的「本轮都报了吗」:只看**已派出的评审**,不额外要求达到快照票数(那一条属于裁决下限)。
3211
+ allParticipantsReported: participants.length > 0 && thisRoundIds.length === participants.length,
3212
+ quorum: participants.length > 0 && participants.length >= expected && thisRoundIds.length === participants.length,
3213
+ }
3214
+ }
3215
+ /**
3216
+ * 待验证任务视图(v2 唯一的"在飞验证任务清单"入口)。**单一来源**:`status` 与 `report` 两条
3217
+ * 路径都必须调它——审计 6.5 之前这段投影被内联复制了两份,任何一边改字段就会让两个视图分叉
3218
+ * (与 D4 的教训同型)。字段语义:expected = 创建时快照票数,dispatched = 本轮派出的评审数,
3219
+ * reported = 本轮已报票数,quorum = 本轮是否齐票。
3220
+ */
3221
+ function verifyTasksView(tasks) {
3222
+ return Object.keys(tasks || {}).filter(function (k) { return tasks[k] && tasks[k].type === 'verify' })
3223
+ .map(function (k) {
3224
+ const t = tasks[k]
3225
+ const v = voteCount(t)
3226
+ return { rId: t.rId, status: t.status, round: v.round, expected: v.expected, dispatched: v.dispatched, reported: v.reportedThisRound, quorum: v.quorum }
3227
+ })
3228
+ }
2943
3229
  // 一票不算共识:≥MIN_REVIEWERS 份独立评审才可能达成/否决共识(审计 M11;与 v3 的最小票数同型)。
2944
- function consensus(t) { const cids = Object.keys(t.childResults); if (cids.length < MIN_REVIEWERS) return false; const vs = cids.map(function (cid) { return t.childResults[cid].Result }); return vs.every(function (v) { return v === 1 }) || vs.every(function (v) { return v === 0 }) }
2945
- function buildTranscript(t) { const parts = []; const cids = Object.keys(t.childResults); for (let i = 0; i < cids.length; i++) { const r = t.childResults[cids[i]]; parts.push('Reviewer ' + i + ': Result=' + r.Result + ' Reason=' + r.Reason) } return parts.join('\n') }
3230
+ function consensus(t) { const v = voteCount(t); if (v.reportedIds.length < MIN_REVIEWERS) return false; const vs = v.reportedIds.map(function (cid) { return t.childResults[cid].Result }); return vs.every(function (x) { return x === 1 }) || vs.every(function (x) { return x === 0 }) }
3231
+ function buildTranscript(t) { const parts = []; const cids = Object.keys(t.childResults); for (let i = 0; i < cids.length; i++) { const r = t.childResults[cids[i]]; const changedNote = (r.changed != null && String(r.changed).trim()) ? (' [changed: ' + String(r.changed).trim() + ']') : ''; parts.push('Reviewer ' + i + ': Result=' + r.Result + ' Reason=' + r.Reason + changedNote) } return parts.join('\n') }
2946
3232
  // 说明(审计 L17):这里曾有一个 `verifierWeight(cid, rigor)`,但全仓只有它的定义、没有任何调用点,
2947
3233
  // 且其公式(clamped ±0.2 rigor 加成、按 childId 取准确率)与 finalVerdict 里真正在用的
2948
3234
  // 稳定身份键 + 0.1 自信加成**并不相同**——留着它只会让人以为 forced 模式走的是那条公式。已删除。
@@ -2952,7 +3238,11 @@ export function apply(ctx) {
2952
3238
  * 由 spawnChild 写进 agentRegistry 的 meta,再随每次投票进入 childResults。
2953
3239
  */
2954
3240
  function verifierIdentityKey() { const o = childAgentOptions(); return 'm:' + String(o.provider || '-') + '/' + String(o.model || '-') }
2955
- const PENDING_SCORE_MAX_OBJECTS = 32
3241
+ const PENDING_SCORE_MAX_OBJECTS = 32 // 全局最多保留多少个对象的待计分样本(FIFO 丢弃,见 recordReviewForScoring)
3242
+ const PENDING_SCORE_CAP_PER_OBJECT = 8 // 每个对象最多保留多少条待计分样本
3243
+ const PENDING_SCORE_WARN_INTERVAL_MS = 5 * 60 * 1000 // 全局丢弃告警的最小间隔(避免每票刷屏)
3244
+ const scoreCapWarned = new Set()
3245
+ let scoreGlobalWarnedAt = 0
2956
3246
  /** 记下本轮各评审的投票,等**这个对象后来**取得布尔定论时再回溯计分(见 scorePendingReviews)。 */
2957
3247
  function recordReviewForScoring(objectId, key, result) {
2958
3248
  const id = String(objectId || '')
@@ -2960,9 +3250,26 @@ export function apply(ctx) {
2960
3250
  let list = pendingReviewScores[id]
2961
3251
  if (!list) { list = []; pendingReviewScores[id] = list }
2962
3252
  list.push({ key: String(key || 'm:unknown'), result: clamp01(result) })
2963
- if (list.length > 8) list.shift()
3253
+ // 有界保留(**故意**的截断,已文档化):每对象最多 PENDING_SCORE_CAP_PER_OBJECT 条待计分样本、
3254
+ // 全局最多 PENDING_SCORE_MAX_OBJECTS 个对象;超限丢弃**最旧**的(FIFO)。丢弃会写活动日志,
3255
+ // 避免"verifierAccuracy 随对象数悄悄漂移"而无人知晓(已累计的准确率不受影响)。
3256
+ if (list.length > PENDING_SCORE_CAP_PER_OBJECT) {
3257
+ list.shift()
3258
+ if (!scoreCapWarned.has(id)) {
3259
+ scoreCapWarned.add(id)
3260
+ if (scoreCapWarned.size > 64) scoreCapWarned.clear()
3261
+ logActivity('verify', '评审计分样本达上限(每对象 ' + PENDING_SCORE_CAP_PER_OBJECT + ' 条):' + id + ' 丢弃最旧一条(故意保留上限;已累计 verifierAccuracy 不受影响)')
3262
+ }
3263
+ }
2964
3264
  const ids = Object.keys(pendingReviewScores)
2965
- while (ids.length > PENDING_SCORE_MAX_OBJECTS) delete pendingReviewScores[ids.shift()]
3265
+ if (ids.length > PENDING_SCORE_MAX_OBJECTS) {
3266
+ let dropped = 0
3267
+ while (ids.length > PENDING_SCORE_MAX_OBJECTS) { delete pendingReviewScores[ids.shift()]; dropped++ }
3268
+ if (now() - scoreGlobalWarnedAt > PENDING_SCORE_WARN_INTERVAL_MS) {
3269
+ scoreGlobalWarnedAt = now()
3270
+ logActivity('verify', '评审计分样本的对象数达上限(' + PENDING_SCORE_MAX_OBJECTS + '):丢弃最旧的 ' + dropped + ' 个对象的待计分样本(故意保留上限;已累计 verifierAccuracy 不受影响)')
3271
+ }
3272
+ }
2966
3273
  }
2967
3274
  /** 对象取得布尔定论(1/0)时,用它给**此前**那些还没被检验过的评审打分。绝不拿聚合值给同一批打分。 */
2968
3275
  async function scorePendingReviews(objectId, truth) {
@@ -2998,9 +3305,9 @@ export function apply(ctx) {
2998
3305
  return
2999
3306
  }
3000
3307
  if (t.children.indexOf(childId) === -1) t.children.push(childId)
3001
- t.childResults[childId] = { Result: Result, Reason: Reason, round: meta.round, key: meta.verificationKey || verifierIdentityKey() }
3308
+ t.childResults[childId] = { Result: Result, Reason: Reason, round: meta.round, key: meta.verificationKey || verifierIdentityKey(), changed: (parsed && parsed.changed != null && String(parsed.changed).trim()) ? String(parsed.changed).trim() : null }
3002
3309
  delete agentRegistry[childId]
3003
- const allReported = t.children.length > 0 && t.children.every(function (cid) { const r = t.childResults[cid]; return r && r.round === meta.round })
3310
+ const allReported = voteCount(t, meta.round).allParticipantsReported
3004
3311
  if (!allReported) { await saveAll(); return }
3005
3312
  await advanceVerification(t, meta.round)
3006
3313
  await saveAll()
@@ -3040,7 +3347,7 @@ export function apply(ctx) {
3040
3347
  * `stalled`,冷却期后重试(任务保留 ⇒ processVerify 不会为同一个 rId 另造任务,也不会每 tick 重试)。
3041
3348
  */
3042
3349
  async function finalizeVerification(t) {
3043
- const reviews = Object.keys(t.childResults || {}).length
3350
+ const reviews = voteCount(t).reportedIds.length
3044
3351
  if (reviews < MIN_REVIEWERS) {
3045
3352
  const tried = Number(t.reviewerRespawn || 0)
3046
3353
  if (scheduler.running && tried < 2 && reviewerCap() >= MIN_REVIEWERS) {
@@ -3071,7 +3378,8 @@ export function apply(ctx) {
3071
3378
  }
3072
3379
  }
3073
3380
  function finalVerdict(t) {
3074
- const rs = Object.keys(t.childResults).map(function (cid) { return t.childResults[cid] })
3381
+ const v = voteCount(t)
3382
+ const rs = v.reportedIds.map(function (cid) { return t.childResults[cid] })
3075
3383
  if (rs.length === 0) return 0.5
3076
3384
  if (rs.every(function (r) { return r.Result === 1 })) return 1
3077
3385
  if (rs.every(function (r) { return r.Result === 0 })) return 0
@@ -3146,7 +3454,7 @@ export function apply(ctx) {
3146
3454
  // 文件名来自 t.rId,而 rId 是**用户可写**的 q.id / p.id 拼出来的(buildVerifyCandidates 里
3147
3455
  // 'r-' + q.id + '-s' + j)。fs.resolve 只做规范化、并不拒绝 '..',所以 q.id = 'x/../../../../pwn'
3148
3456
  // 会让这次写落到项目树之外(审计 C1,已用 pathdemo2.cjs 复现)。路径段一律过 safeId。
3149
- await writeJson('Verification_logs/' + safeId(String(t.rId)) + '_' + Date.now() + '.json', { r: r, verdict: v, results: t.childResults, transcript: buildTranscript(t), history: t.history || [], at: now() })
3457
+ await writeJson('Verification_logs/' + safeId(String(t.rId)) + '_' + Date.now() + '_' + shortId() + '.json', { r: r, verdict: v, results: t.childResults, transcript: buildTranscript(t), history: t.history || [], at: now() })
3150
3458
 
3151
3459
  if (r.kind === 'proposition') {
3152
3460
  const p = await findProposition(r.pId)
@@ -3434,7 +3742,7 @@ export function apply(ctx) {
3434
3742
  const cid = paperInFlight
3435
3743
  paperInFlight = ''; paperInFlightAt = 0
3436
3744
  if (cid) {
3437
- try { await interruptChild(cid) } catch (e) { /* best effort */ }
3745
+ try { await interruptTraced(cid, 'paper writer re-dispatch') } catch (e) { /* best effort */ }
3438
3746
  delete agentRegistry[cid]
3439
3747
  paperAbandoned[cid] = true
3440
3748
  }
@@ -3451,13 +3759,25 @@ export function apply(ctx) {
3451
3759
  /** 证据索引(spec §3.9):只列**确实存在**的文件;绝不含 Paper/ 自己(否则输入哈希会自我污染)。
3452
3760
  * off 档必须真的是无操作(formal-verify-v2 的语料守卫会断言"off 的提示词里零 Lean 文本"):
3453
3761
  * formal 未启用时,形式化相关的路径与字段一概不写进材料。 */
3762
+ /**
3763
+ * 成员可见路径说明(跨预设审计 P0)。成员/子代理的**文件工具按会话 cwd 解析**相对路径,而下面列出的
3764
+ * 路径都是**项目根相对**的 ⇒ 必须显式说明"先拼绝对前缀";计算产物用回执里的绝对字段。一处常量、
3765
+ * 两处**成员可见**材料文本(`paperEvidenceIndex` 证据索引 + `buildPaperDigest` 摘要)各自 append(绝不替换既有行)。
3766
+ * 另三处(`paperCompile` / `paperEmitArtifacts` / `paperMissingArtifacts`)是**框架侧文件操作**,其路径从不进入
3767
+ * 成员可见文本,因此**不**追加本说明——这不是遗漏,而是按"是否需要成员照抄路径"分类的结果。
3768
+ */
3769
+ const PAPER_PATH_NOTE = '(路径说明:成员/子代理的文件工具按**会话 cwd** 解析相对路径,因此上面列出的相对路径都必须先拼上**项目根的绝对前缀**再使用;计算产物请用回执里的绝对字段 `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来。)'
3454
3770
  async function paperEvidenceIndex() {
3455
3771
  const formalNow = formalOn()
3456
- const out = ['qs/qs.json']
3772
+ const out = []
3773
+ if (await paperPathExists('qs/qs.json')) out.push('qs/qs.json')
3457
3774
  if (formalNow && await paperPathExists('VibeMath_State/formal.json')) out.push('VibeMath_State/formal.json')
3458
3775
  for (const f of await listFiles('Propos')) out.push('Propos/' + f)
3459
3776
  for (const f of await listFiles('Verified')) out.push('Verified/' + f)
3460
3777
  if (formalNow) for (const f of await listFiles('Verified/Lean')) out.push('Verified/Lean/' + f)
3778
+ // 审计 F2:提示词要求「引用 Reliable/ 里的可信来源时要给出处」,材料证据索引也必须收它,
3779
+ // 否则被要求引用的那一层永远进不了论文材料(reliableFiles() 因此从死代码变成活代码)。
3780
+ for (const f of await reliableFiles()) out.push('Reliable/' + f)
3461
3781
  const dirs = ['Verification_logs', 'Progress_Logs'].concat(formalNow ? ['Formal'] : [])
3462
3782
  for (let i = 0; i < dirs.length; i++) { const files = await listFiles(dirs[i]); for (let j = 0; j < files.length; j++) out.push(dirs[i] + '/' + files[j]) }
3463
3783
  return out.filter(function (x) { return x.indexOf('Paper/') !== 0 }).sort()
@@ -3484,6 +3804,7 @@ export function apply(ctx) {
3484
3804
  const props = await getPropos()
3485
3805
  L.push('')
3486
3806
  L.push('[PROPOSITIONS] (Propos/*.json)')
3807
+ L.push(PAPER_PATH_NOTE)
3487
3808
  if (props.length === 0) L.push('- (none)')
3488
3809
  for (let i = 0; i < props.length; i++) {
3489
3810
  const p = props[i]
@@ -3525,7 +3846,7 @@ export function apply(ctx) {
3525
3846
  PAPER_SKELETON.map(function (s, i) { return ' ' + (i + 1) + '. ' + s.key + ' — ' + s.spec }).join('\n') + '\n' +
3526
3847
  '- A section with no evidence must be exactly 「' + PAPER_NO_EVIDENCE + '」 (do not pad it).\n' +
3527
3848
  '- Markdown subset only: `#`/`##`/`###`, `- ` lists, `**bold**`, `*em*`, `` `code` ``, and inline math as `$...$`. No tables, images, footnotes or raw HTML.\n\n' +
3528
- 'OUTPUT CONTRACT — respond with ONLY one ```json code fence, no prose:\n' +
3849
+ 'OUTPUT CONTRACT — respond with ONLY one ```json code fence, no prose:\n' + REPLY_KEY_STYLE_LINE +
3529
3850
  '{"title":"<paper title>","abstract":"<original problem + main results>","sections":[{"name":"<one of the 9 headings>","body":"<markdown>"}, ...]}\n\n' +
3530
3851
  'MATERIAL (evidence only — do not add anything beyond it):\n' + digest
3531
3852
  }
@@ -3881,7 +4202,7 @@ export function apply(ctx) {
3881
4202
  const prevEpoch = await readJson('VibeMath_State/process_epoch.json')
3882
4203
  const stale = typeof prevEpoch === 'string' && prevEpoch !== processEpoch
3883
4204
  if (fresh || stale) {
3884
- if (fresh) { const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]) }
4205
+ if (fresh) { const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptTraced(ids[i], 'batch clear (start/abort)') }
3885
4206
  if (Object.keys(agentRegistry).length > 0 || Object.keys(tasks).length > 0) {
3886
4207
  logActivity(fresh ? 'start' : 'resume', 'cleared ' + Object.keys(agentRegistry).length + ' agent(s) and ' + Object.keys(tasks).length + ' task(s) (' + (fresh ? 'restart' : 'stale from previous process') + ')')
3887
4208
  agentRegistry = {}; tasks = {}
@@ -3905,7 +4226,7 @@ export function apply(ctx) {
3905
4226
  async function startScheduler() { const r = await init(true); if (!r.ok) return r; abandonGatedDecision('scheduler restarted'); scheduler.running = true; scheduler.startedAt = now(); logActivity('start', 'scheduler started for project ' + currentProject); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
3906
4227
  async function resumeScheduler() { const r = await init(false); if (!r.ok) return r; abandonGatedDecision('scheduler resumed (离开那次运行,挂起的节点不再等待)'); scheduler.running = true; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
3907
4228
  async function pauseScheduler() { scheduler.running = false; logActivity('pause', 'scheduler paused'); await saveAll(); return { ok: true, message: 'scheduler paused' } }
3908
- async function abortScheduler() { scheduler.running = false; abandonGatedDecision('scheduler aborted'); const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null; logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
4229
+ async function abortScheduler() { scheduler.running = false; abandonGatedDecision('scheduler aborted'); const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptTraced(ids[i], 'batch clear (start/abort)'); agentRegistry = {}; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null; logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
3909
4230
  // auto 模式语义 = 无人值守自动通过关键节点:切回 auto 时把仍挂起的人工决策按自动策略放行
3910
4231
  async function autoResolvePending() {
3911
4232
  const pending = decisionQueue.filter(function (d) { return d.status === 'pending' })
@@ -3926,26 +4247,42 @@ export function apply(ctx) {
3926
4247
  }
3927
4248
  if (pending.length > 0) { scheduler.gate = null; logActivity('mode', 'switched to auto — auto-resolved ' + pending.length + ' pending decision(s)'); await saveAll(); scheduleTick() }
3928
4249
  }
4250
+ /** 审计 F7:`project` 是**当前会话**的项目名,`projects` 是磁盘上已存在的 slug 列表;
4251
+ * 两者不闭合时(新会话 project='default' 而磁盘上没有该目录)语义必须显式,不能靠调用方猜。 */
4252
+ async function projectExistsOnDisk() { return (await listDirsAt(vibeRoot(), 'Projects')).indexOf(currentProject) !== -1 }
3929
4253
  async function getStatus() {
3930
4254
  const qs = await getQs(); const propos = await getPropos()
3931
4255
  return {
4256
+ at: now(),
3932
4257
  ok: true, initialized: rootAgent !== undefined, running: scheduler.running,
3933
- project: currentProject, projects: await listDirsAt(vibeRoot(), 'Projects'),
4258
+ project: currentProject, projectExists: await projectExistsOnDisk(), projects: await listDirsAt(vibeRoot(), 'Projects'),
4259
+ stateCommit: stateCommit,
4260
+ stateWriteFailures: { count: stateWriteFailures.length, last: stateWriteFailures[stateWriteFailures.length - 1] || null },
3934
4261
  mode: params.mode, activeCount: activeCount(), maxParallelThreshold: params.maxParallelThreshold,
3935
4262
  frameworkRoot: frameworkRoot(),
3936
4263
  problems: { total: qs.length, solved: qs.filter(function (q) { return q.已解决 }).length },
3937
4264
  propositions: { total: propos.length, resolved: propos.filter(function (p) { return p.布尔估计 === 1 || p.布尔估计 === 0 }).length },
3938
4265
  pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).length,
3939
4266
  registeredAgents: Object.keys(agentRegistry).length,
4267
+ // D4 审计面:每个在飞验证任务的参与集/票数都由 voteCount 投影出来(两个视图可对照)。
4268
+ verifyTasks: verifyTasksView(tasks),
3940
4269
  recentActivity: activityLog.slice(-Math.min(ACTIVITY_REPORT_MAX, Number(params.activityLogCap) || 100)), params: params,
3941
4270
  formal: {
3942
4271
  mode: formalMode(), required: formalRequired(),
3943
4272
  leanCommand: params.leanCommand, leanArgs: params.leanArgs, leanTimeoutMs: params.leanTimeoutMs,
3944
4273
  objects: Object.keys(formalRecords()).map(function (k) { const r = formalRecords()[k] || {}; return { target: k, status: r.status, file: r.file, proof: r.proof, note: r.note } }),
3945
4274
  todo: formalTodo(),
3946
- paths: { project: 'Formal/', lib: vibeRoot() + '/Formal/Lib/', proved: vibeRoot() + '/Formal/Proved/', proofs: 'Verified/Lean/' },
4275
+ // F6:四项路径基准不同 ⇒ 必须点名 base(project/proofs 相对 base,lib/proved 已绝对)。
4276
+ paths: { base: frameworkRoot() + '/', project: 'Formal/', proofs: 'Verified/Lean/', lib: vibeRoot() + '/Formal/Lib/', proved: vibeRoot() + '/Formal/Proved/', note: 'project/proofs 相对 base(项目根);lib/proved 已含绝对前缀' },
3947
4277
  },
3948
4278
  // 最终论文的可观测面(spec §2/§6):自动开关、在途/排队、定稿时间、产物与编译结果。
4279
+ // F3(对照 v5 的 leanNoticesScope/fieldScopes):本面把**会话内存**与**耐久**字段并排放在同一层,
4280
+ // 这里逐组标注来源,读者不必猜哪个重启会归零。
4281
+ fieldScopes: {
4282
+ durable: ['project', 'projects', 'problems', 'propositions', 'stateCommit', 'formal', 'paper.finalizedAt', 'paper.artifacts', 'paper.compile'],
4283
+ session: ['stateWriteFailures', 'activeCount', 'pendingDecisions', 'registeredAgents', 'verifyTasks', 'recentActivity', 'paper.inFlight', 'paper.inFlightSince', 'paper.inFlightAgeMs', 'paper.reapedThisRun', 'paper.queued'],
4284
+ note: 'session = 本进程内存,重启后归零(看着永远健康是假象);durable = 落在项目树里(State/Formal/Progress/Verified 等),重启后仍在',
4285
+ },
3949
4286
  paper: await paperStatusView(),
3950
4287
  }
3951
4288
  }
@@ -3997,12 +4334,14 @@ export function apply(ctx) {
3997
4334
  registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), async function () { await refreshParams(); return await getStatus() })
3998
4335
  registerTool('vibe_math_report', 'Return the full progress report and write it to Progress_Logs/report.json.', objParams({}), async function () { await refreshParams(); await maybeWriteReport(true); return await buildReport() })
3999
4336
  registerTool('vibe_math_set_mode', 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.', objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), async function (args) { params.mode = args.mode; await saveAll(); await saveSettings(); if (params.mode === 'auto') await autoResolvePending(); return { ok: true, mode: params.mode } })
4000
- registerTool('vibe_math_set_params', 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象的 formal.status 未达到 passed/blocked 之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式(框架会在用户 leanArgs 之后、文件名之前自动追加 `--search-path <VibeMath 根>`,用户已显式给出就不注入);leanAsync = true(默认,后台队列:入队即返回,只有作业落地 ok 才置 passed 并写归档证明)| false(同步 await 的旧语义)。leanJobsMaxParallel = 后台编译并发上限(默认 1 = 串行);leanInitiative = off|normal|eager 控制**日常的**形式化主动性(与 formalVerify 的验证要求强度是两件事:formalVerify=off 只关验证阶段,eager 仍注入日线);leanSearchPaths = 额外 Lean 搜索路径(默认空 = 只用自动注入的 VibeMath 根;非空时先注入它们、再注入自动根)。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测),产物在 Paper/<项目>/。数学计算(工具 math_computation,回执落在 Computation/<id>/):mathComputation = off|auto|on(默认 auto;off 时提示词零提及)· mathMode = typed|typed+shell(默认 typed+shell;typed 时提示词不含 shell 兜底段且 cli 返回 REFUSED{reason:policy})· mathEngines = 允许的引擎列表(默认含 cli,cli 默认开启)· mathTimeoutMs(默认 60000,最小 1000;到时主动 terminate)· mathPackages(需预检的包,缺包只报告+给安装计划)· mathInstallScope = user|system(默认 user;system 只对当次显式调用生效、永不记忆)。', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, solverAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' }, leanAsync: { type: 'boolean' }, leanJobsMaxParallel: { type: 'integer' }, leanInitiative: { type: 'string', enum: ['off', 'normal', 'eager'] }, leanSearchPaths: { type: 'array', items: { type: 'string' } }, mathComputation: { type: 'string', enum: ['off', 'auto', 'on'] }, mathMode: { type: 'string', enum: ['typed', 'typed+shell'] }, mathEngines: { type: 'array', items: { type: 'string' } }, mathTimeoutMs: { type: 'integer' }, mathPackages: { type: 'array', items: { type: 'string' } }, mathInstallScope: { type: 'string', enum: ['user', 'system'] }, finalPaper: { type: 'boolean' }, paperFormat: { type: 'string', enum: ['both', 'md', 'tex'] }, paperLanguage: { type: 'string', enum: ['zh', 'en'] }, paperCompilePdf: { type: 'boolean' }, paperLatexCommand: { type: 'string' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); if (args && MATH_PARAM_NAMES.some(function (k) { return k in args })) { try { await refreshMathProbe(true) } catch (e) { /* 探测失败不影响参数保存 */ } } return { ok: true, params: params } })
4337
+ registerTool('vibe_math_set_params', 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象的 formal.status 未达到 passed/blocked 之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式(框架会在用户 leanArgs 之后、文件名之前自动追加 `--search-path <VibeMath 根>`,用户已显式给出就不注入);leanAsync = true(默认,后台队列:入队即返回,只有作业落地 ok 才置 passed 并写归档证明)| false(同步 await 的旧语义)。leanJobsMaxParallel = 后台编译并发上限(默认 1 = 串行);leanInitiative = off|normal|eager 控制**日常的**形式化主动性(与 formalVerify 的验证要求强度是两件事:formalVerify=off 只关验证阶段,eager 仍注入日线);leanSearchPaths = 额外 Lean 搜索路径(默认空 = 只用自动注入的 VibeMath 根;非空时先注入它们、再注入自动根)。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测),产物在 Paper/<项目>/。数学计算(工具 math_computation,回执落在 Computation/<id>/):mathComputation = off|auto|on(默认 auto;off 时提示词零提及)· mathMode = typed|typed+shell(默认 typed+shell;typed 时提示词不含 shell 兜底段且 cli 返回 REFUSED{reason:policy})· mathEngines = 允许的引擎列表(默认含 cli,cli 默认开启)· mathTimeoutMs(默认 60000,最小 1000;到时主动 terminate)· mathPackages(需预检的包,缺包只报告+给安装计划)· mathInstallScope = user|system(默认 user;system 只对当次显式调用生效、永不记忆)。', objParams(paramProps()), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); if (args && MATH_PARAM_NAMES.some(function (k) { return k in args })) { try { await refreshMathProbe(true) } catch (e) { /* 探测失败不影响参数保存 */ } } return { ok: true, params: params } })
4001
4338
  registerTool('vibe_math_setup', 'Return the interactive parameter schema for guided configuration.', objParams({}), async function () { await refreshParams(); const list = PARAM_SCHEMA.map(function (p) { const out = Object.assign({}, p); out.current = params[p.name]; out.default = DEFAULT_PARAMS[p.name]; return out }); return { ok: true, parameters: list, saveTo: frameworkRoot() + '/vibe_math_setting.json' } })
4002
4339
  registerTool('vibe_math_save_settings', 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.', objParams({}), async function () { return await saveSettings() })
4003
4340
  registerTool('vibe_math_template', 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.', objParams({ where: { type: 'string', enum: ['global', 'project'] } }), async function (args) { return await createTemplate((args && args.where) || 'global') })
4004
4341
  registerTool('vibe_math_add_problem', 'Add a problem to the current project qs/qs.json.', objParams({ id: { type: 'string' }, description: { type: 'string' }, priority: { type: 'integer' } }, ['id', 'description']), async function (args) { const qs = await getQs(); if (qs.some(function (q) { return q.id === args.id })) return { ok: false, message: 'problem id already exists' }; qs.push({ id: args.id, 概述: args.description, 已解决: false, 解法列表: [], 优先级: args.priority || 0, progress: { directions: [] } }); await writeQs(qs); scheduleTick(); return { ok: true, message: 'problem added' } })
4005
- registerTool('vibe_math_add_proposition', 'Add a proposition to Propos/ (with 概述, 布尔估计, 细类型, 优先级, 价值/关键性).', objParams({ id: { type: 'string' }, 概述: { type: 'string' }, 布尔估计: { type: 'number' }, 优先级: { type: 'integer' }, '价值/关键性': { type: 'number' }, 细类型: { type: 'object' } }, ['id', '概述']), async function (args) {
4342
+ registerTool('vibe_math_add_proposition', 'Add a NEW proposition to Propos/ (with 概述, 布尔估计, 细类型, 优先级, 价值/关键性). The id must be NEW: if it already exists the call is REFUSED with code PROPOSITION_ID_EXISTS (existing cards are never overwritten — their 证明列表/证伪列表 are preserved); pick a fresh id (e.g. p-<8 hex>) or edit the card file Propos/<分类>/<id>.md directly.', objParams({ id: { type: 'string' }, 概述: { type: 'string' }, 布尔估计: { type: 'number' }, 优先级: { type: 'integer' }, '价值/关键性': { type: 'number' }, 细类型: { type: 'object' } }, ['id', '概述']), async function (args) {
4343
+ const p1Conflict = propositionIdConflict(args.id, await getPropos())
4344
+ if (p1Conflict) { logActivity('proposition', '拒绝覆盖已有命题:' + args.id); return Object.assign({ ok: false }, p1Conflict) }
4006
4345
  const p = { id: args.id, 概述: args.概述, 布尔估计: clamp01(args.布尔估计 != null ? args.布尔估计 : 0.5), 细类型: (args.细类型 && typeof args.细类型 === 'object') ? args.细类型 : { 未分类: {} }, 证明列表: [], 证伪列表: [], 优先级: (args.优先级 != null) ? args.优先级 : 1, '价值/关键性': clamp01(args['价值/关键性'] != null ? args['价值/关键性'] : 0.5), progress: { 来源: 'user', 说明: '用户手动添加。' } }
4007
4346
  await upsertProposition(p); scheduleTick(); return { ok: true, proposition: p, file: proposFile(categoryOf(p)) }
4008
4347
  })
@@ -4011,10 +4350,10 @@ export function apply(ctx) {
4011
4350
  registerTool('vibe_math_set_project', 'Switch the current math project.', objParams({ name: { type: 'string' } }, ['name']), async function (args) { const slug = slugify(args.name); return await setProject(slug, false) })
4012
4351
  registerTool('vibe_math_list_projects', 'List math projects.', objParams({}), async function () { return { ok: true, current: currentProject, projects: await listDirsAt(vibeRoot(), 'Projects') } })
4013
4352
  registerTool('vibe_math_list_decisions', 'List pending manual decisions.', objParams({}), async function () { return { ok: true, decisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }) } })
4014
- registerTool('vibe_math_decide', 'Resolve a pending manual decision (verdict override uses verdict: 1|0).', objParams({ id: { type: 'string' }, action: { type: 'string', enum: ['approve', 'reject', 'override'] }, verdict: { type: 'number' } }, ['id', 'action']), async function (args) { const d = decisionQueue.find(function (x) { return x.id === args.id }); if (!d) return { ok: false, message: 'decision not found' }; if (d.status !== 'pending') return { ok: false, message: 'decision already resolved' }; const resolution = { action: args.action, verdict: args.verdict }; const applied = await applyDecision(d.node, d.data, resolution); const r = await resolveDecision(args.id, resolution); return Object.assign({ ok: true, applied: applied }, r) })
4353
+ registerTool('vibe_math_decide', 'Resolve a pending manual decision (verdict override uses verdict: 1|0).', objParams({ id: { type: 'string' }, action: { type: 'string', enum: ['approve', 'reject', 'override'] }, verdict: { type: 'number' } }, ['id', 'action']), async function (args) { const d = decisionQueue.find(function (x) { return x.id === args.id }); const resolution = { action: args.action, verdict: args.verdict }; if (!d || d.status !== 'pending') return await resolveDecision(args.id, resolution); const applied = await applyDecision(d.node, d.data, resolution); const r = await resolveDecision(args.id, resolution); return Object.assign({ ok: true, applied: applied }, r) })
4015
4354
  registerTool('vibe_math_list_agents', 'List tracked sub-agents (child sessions).', objParams({}), async function () { const out = []; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) { const m = agentRegistry[ids[i]]; out.push({ childId: ids[i], role: m.role, qid: m.qid, direction: m.direction, round: m.round, rId: m.rId }) } return { ok: true, agents: out, count: out.length } })
4016
4355
  registerTool('vibe_math_message_agent', 'Send a message to a tracked child agent (next turn).', objParams({ childId: { type: 'string' }, message: { type: 'string' } }, ['childId', 'message']), async function (args) { if (!agentRegistry[args.childId]) return { ok: false, message: 'unknown childId' }; await followupChild(args.childId, args.message); return { ok: true, message: 'message delivered' } })
4017
- registerTool('vibe_math_interrupt_agent', 'Interrupt a tracked child agent.', objParams({ childId: { type: 'string' } }, ['childId']), async function (args) { await interruptChild(args.childId); return { ok: true, message: 'interrupt requested' } })
4356
+ registerTool('vibe_math_interrupt_agent', 'Interrupt a tracked child agent.', objParams({ childId: { type: 'string' } }, ['childId']), async function (args) { const r = await interruptChild(args.childId); return Object.assign({}, r, { ok: !!(r && r.ok) }) })
4018
4357
  // ---- Lean 形式化验证(契约 §5)----
4019
4358
  registerTool('vibe_math_lean_run', '(member) Execute the Lean toolchain on one .lean file inside the VibeMath root and report the result. Never throws: a missing toolchain returns LEAN_NOT_FOUND, a non-zero exit returns the compiler output. Pass target=<object id> to also record the run against that object.', objParams({ file: { type: 'string' }, target: { type: 'string' }, timeout_ms: { type: 'integer' } }, ['file']), async function (args, agent) { return await leanRunTool(memberIdOf(agent), args) })
4020
4359
  registerTool('vibe_math_lean_archive', '(member) Archive Lean code. kind="def": a REUSABLE definition/object/assumption → the global cross-project library (Formal/Lib). kind="lemma": a machine-checked lemma → Formal/Proved. kind="proof": the formal proof of a project object → Formal/<target>.lean, and (when the run passes) also Verified/Lean/<target>.lean, marking the object Lean-passed. kind="blocked": record an explicit, reasoned "cannot/not worth formalizing" decision (note required).', objParams({ kind: { type: 'string', enum: ['def', 'lemma', 'proof', 'blocked'] }, name: { type: 'string' }, target: { type: 'string' }, content: { type: 'string' }, from: { type: 'string' }, note: { type: 'string' }, run: { type: 'boolean' } }, ['kind']), async function (args, agent) { return await leanArchive(memberIdOf(agent), args) })
@@ -4050,7 +4389,7 @@ export function apply(ctx) {
4050
4389
  if (cmd === 'save') return await saveSettings()
4051
4390
  if (cmd === 'template') return await createTemplate(args[0] === 'project' ? 'project' : 'global')
4052
4391
  if (cmd === 'add') { const id = args[0]; const desc = args.slice(1).join(' '); if (!id || !desc) return { ok: false, message: 'usage: /vibe add <id> <description>' }; const qs = await getQs(); if (qs.some(function (q) { return q.id === id })) return { ok: false, message: 'problem id already exists' }; qs.push({ id: id, 概述: desc, 已解决: false, 解法列表: [], 优先级: 0, progress: { directions: [] } }); await writeQs(qs); scheduleTick(); return { ok: true, message: 'problem added' } }
4053
- if (cmd === 'add-proposition') { const id = args[0]; const desc = args.slice(1).join(' '); if (!id || !desc) return { ok: false, message: 'usage: /vibe add-proposition <id> <概述>' }; const p = { id: id, 概述: desc, 布尔估计: 0.5, 细类型: { 未分类: {} }, 证明列表: [], 证伪列表: [], 优先级: 1, '价值/关键性': 0.5, progress: { 来源: 'user-vibe', 说明: '用户通过 /vibe 添加。' } }; await upsertProposition(p); scheduleTick(); return { ok: true, proposition: p, file: proposFile(categoryOf(p)) } }
4392
+ if (cmd === 'add-proposition') { const id = args[0]; const desc = args.slice(1).join(' '); if (!id || !desc) return { ok: false, message: 'usage: /vibe add-proposition <id> <概述>' }; const p1c = propositionIdConflict(id, await getPropos()); if (p1c) { logActivity('proposition', '拒绝覆盖已有命题:' + id); return Object.assign({ ok: false }, p1c) } const p = { id: id, 概述: desc, 布尔估计: 0.5, 细类型: { 未分类: {} }, 证明列表: [], 证伪列表: [], 优先级: 1, '价值/关键性': 0.5, progress: { 来源: 'user-vibe', 说明: '用户通过 /vibe 添加。' } }; await upsertProposition(p); scheduleTick(); return { ok: true, proposition: p, file: proposFile(categoryOf(p)) } }
4054
4393
  if (cmd === 'list-propositions') { const all = await getPropos(); return { ok: true, count: all.length, propositions: all.map(function (p) { return { id: p.id, 概述: p.概述, 布尔估计: p.布尔估计, 优先级: p.优先级, '价值/关键性': p['价值/关键性'], category: p._category } }) } }
4055
4394
  if (cmd === 'project') {
4056
4395
  if (args.length === 0 || args[0] === 'list') return { ok: true, current: currentProject, projects: await listDirsAt(vibeRoot(), 'Projects') }
@@ -4086,10 +4425,6 @@ export function apply(ctx) {
4086
4425
  mathProbeDue: mathProbeDue,
4087
4426
  refreshMathProbe: refreshMathProbe,
4088
4427
  mathProbe: function () { return mathProbe },
4089
- // P2a:persona 两个文本块必须含的「归档→编辑→重跑」规则(文本取自共享模块常量,persona 不手抄)。
4090
- mathArchiveWorkflowLine: MATH_ARCHIVE_WORKFLOW_LINE,
4091
- // A 项:替代声明规则(文本由共享模块给出;persona 两块与每轮可用性行都必须带它)。
4092
- mathSubstitutionRuleLine: MATH_SUBSTITUTION_RULE_LINE,
4093
4428
  leanQueueSize: function () { return leanQueue.length },
4094
4429
  leanJobsPublic: function () { const out = []; for (const j of leanJobs.values()) out.push(leanJobPublic(j)); return out },
4095
4430
  // childOwner 裁剪用:这个会话当前仍"可能再发 subagent/end"的 child(在册的 + 任务正在等的)。
@@ -4137,12 +4472,12 @@ export function apply(ctx) {
4137
4472
  registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), 'vibe_math_status')
4138
4473
  registerTool('vibe_math_report', 'Return the full progress report and write it to Progress_Logs/report.json.', objParams({}), 'vibe_math_report')
4139
4474
  registerTool('vibe_math_set_mode', 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.', objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), 'vibe_math_set_mode')
4140
- registerTool('vibe_math_set_params', 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象的 formal.status 未达到 passed/blocked 之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式(框架会在用户 leanArgs 之后、文件名之前自动追加 `--search-path <VibeMath 根>`,用户已显式给出就不注入);leanAsync = true(默认,后台队列:入队即返回,只有作业落地 ok 才置 passed 并写归档证明)| false(同步 await 的旧语义)。leanJobsMaxParallel = 后台编译并发上限(默认 1 = 串行);leanInitiative = off|normal|eager 控制**日常的**形式化主动性(与 formalVerify 的验证要求强度是两件事:formalVerify=off 只关验证阶段,eager 仍注入日线);leanSearchPaths = 额外 Lean 搜索路径(默认空 = 只用自动注入的 VibeMath 根;非空时先注入它们、再注入自动根)。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测),产物在 Paper/<项目>/。数学计算(工具 math_computation,回执落在 Computation/<id>/):mathComputation = off|auto|on(默认 auto;off 时提示词零提及)· mathMode = typed|typed+shell(默认 typed+shell;typed 时提示词不含 shell 兜底段且 cli 返回 REFUSED{reason:policy})· mathEngines = 允许的引擎列表(默认含 cli,cli 默认开启)· mathTimeoutMs(默认 60000,最小 1000;到时主动 terminate)· mathPackages(需预检的包,缺包只报告+给安装计划)· mathInstallScope = user|system(默认 user;system 只对当次显式调用生效、永不记忆)。', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, solverAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' }, leanAsync: { type: 'boolean' }, leanJobsMaxParallel: { type: 'integer' }, leanInitiative: { type: 'string', enum: ['off', 'normal', 'eager'] }, leanSearchPaths: { type: 'array', items: { type: 'string' } }, mathComputation: { type: 'string', enum: ['off', 'auto', 'on'] }, mathMode: { type: 'string', enum: ['typed', 'typed+shell'] }, mathEngines: { type: 'array', items: { type: 'string' } }, mathTimeoutMs: { type: 'integer' }, mathPackages: { type: 'array', items: { type: 'string' } }, mathInstallScope: { type: 'string', enum: ['user', 'system'] }, finalPaper: { type: 'boolean' }, paperFormat: { type: 'string', enum: ['both', 'md', 'tex'] }, paperLanguage: { type: 'string', enum: ['zh', 'en'] }, paperCompilePdf: { type: 'boolean' }, paperLatexCommand: { type: 'string' } }), 'vibe_math_set_params')
4475
+ registerTool('vibe_math_set_params', 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象的 formal.status 未达到 passed/blocked 之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式(框架会在用户 leanArgs 之后、文件名之前自动追加 `--search-path <VibeMath 根>`,用户已显式给出就不注入);leanAsync = true(默认,后台队列:入队即返回,只有作业落地 ok 才置 passed 并写归档证明)| false(同步 await 的旧语义)。leanJobsMaxParallel = 后台编译并发上限(默认 1 = 串行);leanInitiative = off|normal|eager 控制**日常的**形式化主动性(与 formalVerify 的验证要求强度是两件事:formalVerify=off 只关验证阶段,eager 仍注入日线);leanSearchPaths = 额外 Lean 搜索路径(默认空 = 只用自动注入的 VibeMath 根;非空时先注入它们、再注入自动根)。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测),产物在 Paper/<项目>/。数学计算(工具 math_computation,回执落在 Computation/<id>/):mathComputation = off|auto|on(默认 auto;off 时提示词零提及)· mathMode = typed|typed+shell(默认 typed+shell;typed 时提示词不含 shell 兜底段且 cli 返回 REFUSED{reason:policy})· mathEngines = 允许的引擎列表(默认含 cli,cli 默认开启)· mathTimeoutMs(默认 60000,最小 1000;到时主动 terminate)· mathPackages(需预检的包,缺包只报告+给安装计划)· mathInstallScope = user|system(默认 user;system 只对当次显式调用生效、永不记忆)。', objParams(paramProps()), 'vibe_math_set_params')
4141
4476
  registerTool('vibe_math_setup', 'Return the interactive parameter schema for guided configuration.', objParams({}), 'vibe_math_setup')
4142
4477
  registerTool('vibe_math_save_settings', 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.', objParams({}), 'vibe_math_save_settings')
4143
4478
  registerTool('vibe_math_template', 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.', objParams({ where: { type: 'string', enum: ['global', 'project'] } }), 'vibe_math_template')
4144
4479
  registerTool('vibe_math_add_problem', 'Add a problem to the current project qs/qs.json.', objParams({ id: { type: 'string' }, description: { type: 'string' }, priority: { type: 'integer' } }, ['id', 'description']), 'vibe_math_add_problem')
4145
- registerTool('vibe_math_add_proposition', 'Add a proposition to Propos/ (with 概述, 布尔估计, 细类型, 优先级, 价值/关键性).', objParams({ id: { type: 'string' }, 概述: { type: 'string' }, 布尔估计: { type: 'number' }, 优先级: { type: 'integer' }, '价值/关键性': { type: 'number' }, 细类型: { type: 'object' } }, ['id', '概述']), 'vibe_math_add_proposition')
4480
+ registerTool('vibe_math_add_proposition', 'Add a NEW proposition to Propos/ (with 概述, 布尔估计, 细类型, 优先级, 价值/关键性). The id must be NEW: if it already exists the call is REFUSED with code PROPOSITION_ID_EXISTS (existing cards are never overwritten — their 证明列表/证伪列表 are preserved); pick a fresh id (e.g. p-<8 hex>) or edit the card file Propos/<分类>/<id>.md directly.', objParams({ id: { type: 'string' }, 概述: { type: 'string' }, 布尔估计: { type: 'number' }, 优先级: { type: 'integer' }, '价值/关键性': { type: 'number' }, 细类型: { type: 'object' } }, ['id', '概述']), 'vibe_math_add_proposition')
4146
4481
  registerTool('vibe_math_list_propositions', 'List propositions from Propos/ (summary index: id, 概述, 布尔估计, 优先级, 价值/关键性, category, 依赖假设).', objParams({}), 'vibe_math_list_propositions')
4147
4482
  registerTool('vibe_math_new_project', 'Create a new math project folder and switch to it.', objParams({ name: { type: 'string' } }, ['name']), 'vibe_math_new_project')
4148
4483
  registerTool('vibe_math_set_project', 'Switch the current math project.', objParams({ name: { type: 'string' } }, ['name']), 'vibe_math_set_project')
@@ -4567,7 +4902,6 @@ function leanJobId(key, fingerprint) {
4567
4902
  /** "已落地且通过"的**唯一**判据:state=settled 且 exit 0 且未超时未中断(内容哈希由 settle 校验)。 */
4568
4903
  function leanJobSettledOk(rec) { return !!rec && rec.state === 'settled' && rec.exitCode === 0 && !rec.timedOut && !rec.interrupted }
4569
4904
  /** 主动性档位(修订 §1):与"验证时的要求强度"(formalVerify)是两件事。 */
4570
- const LEAN_INITIATIVE_MODES = ['off', 'normal', 'eager']
4571
4905
 
4572
4906
  export const __testHelpers = {
4573
4907
  uuid,
@@ -4621,6 +4955,11 @@ function now() { return Date.now() }
4621
4955
 
4622
4956
  function uuid() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 36; i++) { if (i === 8 || i === 13 || i === 18 || i === 23) s += '-'; else s += h[Math.floor(Math.random() * 16)] } return s }
4623
4957
 
4958
+ /// P8:框架分配 id 时先查重(shortId 是 Math.random 的 8 位 hex,碰撞概率极低但后果是静默覆盖)。
4959
+ function shortIdUnique(isTaken) {
4960
+ for (let i = 0; i < 8; i++) { const id = shortId(); if (!isTaken(id)) return id }
4961
+ return shortId()
4962
+ }
4624
4963
  function shortId() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 8; i++) s += h[Math.floor(Math.random() * 16)]; return s }
4625
4964
 
4626
4965
  function clamp01(v) { const n = Number(v); if (!Number.isFinite(n)) return 0.5; return Math.max(0, Math.min(1, n)) }