dsh-vibe-math 2.7.2 → 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 (112) hide show
  1. package/README.en.md +11 -10
  2. package/README.md +13 -12
  3. package/cordis.patch.yml +112 -16
  4. package/docs/AUDIT-CHECKLIST.md +229 -3
  5. package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
  6. package/docs/final-paper.md +2 -2
  7. package/docs/math-computation.md +13 -6
  8. package/docs/parameter-schema.md +37 -0
  9. package/docs/release-notes/README.en.md +1 -0
  10. package/docs/release-notes/README.md +1 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
  12. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
  13. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  14. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  15. package/docs/release-notes/TEMPLATE.md +6 -0
  16. package/docs/status-report-fields.md +167 -0
  17. package/docs/test-timing.md +8 -5
  18. package/installer.js +27 -17
  19. package/package.json +250 -203
  20. package/prompt-corpus-persona/persona-corpus.json +9 -9
  21. package/prompt-corpus-persona/persona-corpus.md +225 -33
  22. package/prompt-corpus-v2/formal-verify-v2.json +295 -115
  23. package/prompt-corpus-v2/formal-verify-v2.md +2287 -283
  24. package/prompt-corpus-v3/formal-verify-v3.json +105 -105
  25. package/prompt-corpus-v3/formal-verify-v3.md +213 -143
  26. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  27. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  28. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  29. package/prompt-corpus-v5/prompt-corpus-v5.md +451 -207
  30. package/scripts/update-doc-counts.mjs +71 -0
  31. package/tests/audit-artifact-docs.mjs +72 -0
  32. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  33. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  34. package/tests/audit-installer-compat.test.mjs +52 -4
  35. package/tests/audit-installer-policy.test.mjs +41 -8
  36. package/tests/audit-math-computation-contract.mjs +64 -8
  37. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  38. package/tests/audit-math-computation-parity.mjs +164 -3
  39. package/tests/audit-math-computation-sensitivity.mjs +4 -2
  40. package/tests/audit-package-membership.mjs +57 -0
  41. package/tests/audit-package-membership.mutants.mjs +28 -0
  42. package/tests/audit-participant-set-parity.mjs +160 -0
  43. package/tests/audit-path-discipline.mjs +83 -0
  44. package/tests/audit-path-discipline.mutants.mjs +51 -0
  45. package/tests/audit-persona-sensitivity.mjs +14 -0
  46. package/tests/audit-persona-surface.mutants.mjs +69 -0
  47. package/tests/audit-persona-surface.test.mjs +63 -22
  48. package/tests/audit-prompt-duplication.mjs +143 -0
  49. package/tests/audit-prompt-duplication.mutants.mjs +50 -0
  50. package/tests/audit-prompt-invariants.mjs +43 -5
  51. package/tests/audit-readme-bilingual.test.mjs +152 -0
  52. package/tests/audit-readme-counts.mjs +95 -0
  53. package/tests/audit-readme-counts.mutants.mjs +52 -0
  54. package/tests/audit-registration.mjs +78 -0
  55. package/tests/audit-status-report-fields.mjs +172 -0
  56. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  57. package/tests/audit-v3-registration-parity.mjs +156 -0
  58. package/tests/audit-v5-integrity.mjs +300 -13
  59. package/tests/audit-v5-lean-abstention.mjs +392 -0
  60. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  61. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  62. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  63. package/tests/audit-v5-sensitivity.mjs +12 -7
  64. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  65. package/tests/e2e-v5-round2.test.mjs +458 -13
  66. package/tests/formal-verify-v2.test.mjs +91 -43
  67. package/tests/formal-verify-v3.mutants.mjs +117 -0
  68. package/tests/formal-verify-v3.test.mjs +85 -39
  69. package/tests/formal-verify-v4.mutants.mjs +152 -0
  70. package/tests/formal-verify-v4.test.mjs +541 -9
  71. package/tests/formal-verify-v5.test.mjs +68 -9
  72. package/tests/helpers/doc-counts-format.mjs +41 -0
  73. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  74. package/tests/math-computation-a1.mutants.mjs +72 -0
  75. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  76. package/tests/math-computation-discovery.mutants.mjs +76 -0
  77. package/tests/math-computation-shared.test.mjs +274 -5
  78. package/tests/math-computation-v2.test.mjs +767 -0
  79. package/tests/math-computation-v3.test.mjs +759 -0
  80. package/tests/math-computation-v4.mutants.mjs +133 -0
  81. package/tests/math-computation-v4.test.mjs +739 -0
  82. package/tests/prompt-v5-integrity.test.mjs +13 -2
  83. package/tests/run-tests.mjs +139 -9
  84. package/tests/run-tests.mutants.mjs +70 -0
  85. package/tests/selfdrive-v5.mjs +53 -1
  86. package/tests/selfdrive-v5.mutants.mjs +132 -0
  87. package/tests/v2-fix-probes.mutants.mjs +100 -0
  88. package/tests/v2-fix-probes.test.mjs +1039 -0
  89. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  90. package/tests/v3-fix-probes.mutants.mjs +95 -0
  91. package/tests/v3-fix-probes.test.mjs +891 -0
  92. package/tests/v5-institute-fixes.mutants.mjs +190 -0
  93. package/vibe-math-v2/agent.cordis.yml +62 -6
  94. package/vibe-math-v2/math-computation.js +130 -41
  95. package/vibe-math-v2/math-engines.js +12 -12
  96. package/vibe-math-v2/vibe-math-v2.js +388 -113
  97. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
  98. package/vibe-math-v3/agent.cordis.yml +72 -6
  99. package/vibe-math-v3/math-computation.js +130 -41
  100. package/vibe-math-v3/math-engines.js +12 -12
  101. package/vibe-math-v3/vibe-math-v3.js +422 -126
  102. package/vibe-math-v4/agent.cordis.yml +44 -8
  103. package/vibe-math-v4/math-computation.js +130 -41
  104. package/vibe-math-v4/math-engines.js +12 -12
  105. package/vibe-math-v4/vibe-math-v4.js +196 -43
  106. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
  107. package/vibe-math-v5/agent.cordis.yml +46 -12
  108. package/vibe-math-v5/math-computation.js +130 -41
  109. package/vibe-math-v5/math-engines.js +12 -12
  110. package/vibe-math-v5/vibe-math-v5.js +1173 -218
  111. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +80 -12
  112. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +2 -2
@@ -1,3 +1,118 @@
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: '每个求解方向的最大迭代轮数', suggestion: 3 },
16
+ { name: 'directionsPerSolver', type: 'integer', description: '每个 solver 提示词附带的其他活跃方向摘要数量:1 = 只看自己方向', suggestion: 1 },
17
+ { name: 'verifierCount', type: 'integer', description: '每个验证对象的独立验证器数量(实际下界为 2:少于 2 份独立评审一律不定论,见 minVotes)', suggestion: 3 },
18
+ { name: 'debateMaxRounds', type: 'integer', description: '验证辩论(交流群)最大轮数', suggestion: 5 },
19
+ { name: 'verdictMode', type: 'enum', options: ['flat', 'forced'], description: '裁决模式:flat = 均衡机制(分歧时 0.5);forced = 强制裁决(对每票等权取均值;严格 1/0 是绝对投票,其影响通过数值本身拉向端点。**不按"历史准确率"加权**——该统计量统计的是与本批裁决的一致度,没有后续真值可纠正);两者都先做近共识判定(同侧且均值≥0.85/≤0.15取均值,修复 v2 flat 误判)', suggestion: 'forced' },
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: 'plannerPersona', type: 'string', description: '注入规划代理提示词开头的人格/要求', suggestion: '' },
26
+ { name: 'methodKeeperPersona', type: 'string', description: '注入方法整理代理提示词开头的人格/要求', suggestion: '' },
27
+ { name: 'knowledgeContext', type: 'string', description: '共享知识/数据模型说明(空 = 内置完整版;非空 = 覆盖)', suggestion: '' },
28
+ { name: 'solverToolAllow', type: 'string[]', description: '求解器允许的工具名列表(空 = 继承全部工具)', suggestion: [] },
29
+ { name: 'solverToolDeny', type: 'string[]', description: '求解器禁止的工具名列表', suggestion: [] },
30
+ { name: 'verifierToolAllow', type: 'string[]', description: '验证器允许的工具名列表', suggestion: [] },
31
+ { name: 'verifierToolDeny', type: 'string[]', description: '验证器禁止的工具名列表', suggestion: [] },
32
+ { name: 'solverAllowNetwork', type: 'boolean', description: '求解器网络工具开关:空=继承;true=允许;false=禁止', suggestion: '' },
33
+ { name: 'verifierAllowNetwork', type: 'boolean', description: '验证器网络工具开关', suggestion: '' },
34
+ { name: 'solverAllowScripts', type: 'boolean', description: '求解器脚本工具开关', suggestion: '' },
35
+ { name: 'verifierAllowScripts', type: 'boolean', description: '验证器脚本工具开关', suggestion: '' },
36
+ { name: 'solverMaxToolCalls', type: 'integer', description: '求解器每轮外部工具调用上限(0 = 不限)', suggestion: 0 },
37
+ { name: 'verifierMaxToolCalls', type: 'integer', description: '验证器每轮外部工具调用上限(0 = 不限)', suggestion: 0 },
38
+ { name: 'reportIntervalMs', type: 'integer', description: '进度汇报间隔(毫秒):0 = 仅事件驱动', suggestion: 0 },
39
+ { name: 'reportMode', type: 'enum', options: ['file', 'push', 'both'], description: 'file = 写报告文件;push = 推送消息;both = 两者', suggestion: 'file' },
40
+ { name: 'promoteValueThreshold', type: 'number', description: '命题「价值/关键性」≥ 该值且未决(0,1) 时自动晋升为问题', suggestion: 0.7 },
41
+ { name: 'priorityAdjust', type: 'enum', options: ['none', 'deadend-deprioritize', 'survival-map'], description: '问题优先级动态调整策略', suggestion: 'none' },
42
+ { name: 'proposPriorityAdjust', type: 'enum', options: ['none', 'progress-graded'], description: '命题优先级动态调整策略', suggestion: 'none' },
43
+ { name: 'tickIntervalMs', type: 'integer', description: '调度器心跳间隔(毫秒)', suggestion: 2000 },
44
+ { name: 'activityLogCap', type: 'integer', description: '活动日志保留条数(影响 status/report 里 recentActivity 的细节量,两者最多显示 ' + ACTIVITY_REPORT_MAX + ' 条)', suggestion: 100 },
45
+ { name: 'maxExplorerRetries', type: 'integer', description: 'explorer 拆方向失败的重派生上限', suggestion: 3 },
46
+ { name: 'planningHorizon', type: 'integer', description: '规划代理一次计划的最多动作数("接下来 n 次")', suggestion: 3 },
47
+ { name: 'plannerEnabled', type: 'boolean', description: 'false = 完全走内置启发式调度(规划代理禁用)', suggestion: true },
48
+ { name: 'plannerProvider', type: 'string', description: '规划代理模型 provider(空 = 继承根代理)', suggestion: '' },
49
+ { name: 'plannerModel', type: 'string', description: '规划代理模型 id(空 = 继承根代理)', suggestion: '' },
50
+ { name: 'planMinIntervalMs', type: 'integer', description: '每一次规划调用的最小间隔(毫秒);对**每一次**规划调用都生效(含空计划、含「空闲但仍有工作」,不再有空闲绕过)', suggestion: 30000 },
51
+ { name: 'plannerMaxFails', type: 'integer', description: '规划代理连续失败达此值 → 自动降级启发式', suggestion: 3 },
52
+ { name: 'methodKeepIntervalMs', type: 'integer', description: 'Method Keeper 定时整理间隔(0 = 事件驱动)', suggestion: 0 },
53
+ { name: 'methodKeepEvery', type: 'integer', description: '每积累 N 个待沉淀发明/新命题触发一次整理', suggestion: 5 },
54
+ { name: 'methodAutoPromote', type: 'boolean', description: '项目级方法自动晋升全局库(false = 人工门)', suggestion: false },
55
+ { name: 'indexAutoRebuild', type: 'boolean', description: '每次写盘后自动重建索引(false = 手动 vibe_math_index)', suggestion: true },
56
+ { name: 'projectLockTimeoutMs', type: 'integer', description: '项目锁等待超时(毫秒)', suggestion: 60000 },
57
+ { name: 'fileLockTimeoutMs', type: 'integer', description: '文件写锁租约时长(毫秒,默认 60000,最小 1000):持有代理存活期间自动续租,持有者消失后最多这么久失效,避免长文写入被别人抢锁(审计 D2)', suggestion: 60000 },
58
+ { name: 'formalVerify', type: 'enum', options: ['off', 'encourage', 'require'], description: 'Lean 形式化验证:off = 不额外要求(默认,提示词里不出现任何 Lean 内容);encourage = 鼓励按实现难度形式化,Lean 通过后验证转为忠实性审查;require = 同上并加门禁:真/假定论必须先达到 Lean 已通过 或 已记录显式阻塞原因,否则记为未定论(formal-required)并进「形式化待办」', suggestion: 'off' },
59
+ { name: 'leanCommand', type: 'string', description: 'Lean 可执行文件(例:lean / lake;配合 leanArgs=[env,lean] 用 lake)', suggestion: 'lean' },
60
+ { name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加参数', suggestion: [] },
61
+ { name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,非正数回退默认);异步档下它同时是**每个后台作业**的预算(到时主动 terminate,作业记 timeout,对象留在 attempted)', suggestion: 120000 },
62
+ { name: 'leanAsync', type: 'boolean', description: 'Lean 编译模式:true(默认)= 后台队列,vibe_math_lean_run / vibe_math_lean_archive{run:true} 立即返回 async.jobId 入队,成员不阻塞,结果由下一轮提示的【形式化结果】行与 vibe_math_lean_job 公告(**只有作业落地为 ok 才会置 passed 并写归档证明**);false = 完全同步 await(与旧行为逐字一致)', suggestion: true },
63
+ { name: 'leanJobsMaxParallel', type: 'integer', description: '后台 Lean 编译的并发上限(默认 1 = 串行,保持可预测的资源占用;调大可并行编译多个作业)', suggestion: 1 },
64
+ { name: 'leanInitiative', type: 'enum', options: ['off', 'normal', 'eager'], description: '日常流程中的形式化主动性:off(不主动,只在验证提示词按 formalVerify 的要求做)| normal(默认:顺手把有价值且可能复用的东西形式化)| eager(更主动:日常就主动把有价值的小引理/命题/定义形式化)。注意它与 formalVerify(验证时的要求强度:off|encourage|require)是**两件事**', suggestion: 'normal' },
65
+ { name: 'leanSearchPaths', type: 'string[]', description: '额外 Lean 搜索路径(默认空数组 = 只用框架自动注入的 VibeMath 根)。非空时按顺序先注入这里给的路径、再注入自动根(去重);若 leanArgs 里已显式给了 --search-path/-R/--root,则完全尊重用户配置、不注入任何东西', suggestion: [] },
66
+ { name: 'finalPaper', type: 'boolean', description: '收口(checkTermination 的完整收口分支:无未解决问题、无待验证对象、无任务/计划/门)时自动撰写最终论文:派遣一名专职「论文撰写」子代理,把已定论命题/解法/方法整理成 Paper/<项目>/{paper.md,paper.tex,paper.meta.json,paper.log.md}。false = 只关自动触发,/vibe paper 手动命令仍可用', suggestion: true },
67
+ { name: 'paperFormat', type: 'enum', options: ['both', 'md', 'tex'], description: '论文产出格式:both = markdown + latex;md = 只写 paper.md(跳过编译);tex = 只写 paper.tex(tex 才会尝试编译 pdf)', suggestion: 'both' },
68
+ { name: 'paperLanguage', type: 'enum', options: ['zh', 'en'], description: '论文语言:zh = 中文(LaTeX 用 ctexart,引擎优先 xelatex);en = 英文(article,引擎优先 pdflatex/latexmk)', suggestion: 'zh' },
69
+ { name: 'paperCompilePdf', type: 'boolean', description: '检测到 LaTeX 时是否编译 paper.pdf(-interaction=nonstopmode 跑两遍;失败先尝试修复:换引擎/去不支持宏包/最小模板)。false 或无 LaTeX 时只保留 tex+md 并记日志(不阻塞定稿)', suggestion: true },
70
+ { name: 'paperLatexCommand', type: 'string', description: '指定 LaTeX 引擎可执行文件(空 = 按语言探测:中文 xelatex > latexmk > pdflatex > lualatex > tectonic;英文 pdflatex 优先)。解析不到时按"未检测到"降级', suggestion: '' },
71
+ // ---- 数学计算 math_computation(六参数冻结;描述与 prompts.md §1/§6 口径一致)----
72
+ { name: 'mathComputation', type: 'enum', options: ['off', 'auto', 'on'], description: '数学计算总开关:off = 真无操作(提示词零提及);auto = 探测到 mathEngines 里任一允许引擎才工作;on = 同上(探测失败时工具仍返回可执行的安装指引,而不是假装可用)', suggestion: 'auto' },
73
+ { name: 'mathMode', type: 'enum', options: ['typed', 'typed+shell'], description: '计算策略:typed+shell(默认)= 工具不可用时允许宿主 shell 兜底,但结论必须标注"未经工具归档(shell 路径)";typed = 只用工具路径(提示词里不出现 shell 兜底段,engine:"cli" 返回 REFUSED{reason:policy})', suggestion: 'typed+shell' },
74
+ { name: 'mathEngines', type: 'string[]', description: '允许的引擎列表(默认 python|r|octave|julia|matlab|maple|wolfram|cli)。cli 默认开启且走同一套超时/输出上限/回执;从列表里移除某引擎即禁用(商业引擎只探测+许可,永不安装)', suggestion: MATH_PARAM_DEFAULTS.mathEngines.slice() },
75
+ { name: 'mathTimeoutMs', type: 'integer', description: '单次数学计算的超时上限(毫秒,默认 60000,最小 1000);到时主动 terminate 并把回执标为 MATH_TIMEOUT', suggestion: 60000 },
76
+ { name: 'mathPackages', type: 'string[]', description: '需要预检的包(默认空)。缺包只报告 + 给"用户自装指引"或"代理代装计划",不会执行脚本,也永不自动安装', suggestion: [] },
77
+ { name: 'mathInstallScope', type: 'enum', options: ['user', 'system'], description: '安装作用域:user(默认,用户级目录);system 只对当次显式调用生效、永不记忆(不会写进状态文件)', suggestion: 'user' },
78
+ ]
79
+
80
+ // 钉住"机读键集合"(现状 v2=49 / v3=63 个,不随 PARAM_SCHEMA 增删而漂移)。
81
+ const PARAM_PROPS_KEYS = ['mode', 'maxParallelThreshold', 'solverMaxRounds', 'verifierCount', 'debateMaxRounds', 'verdictMode', 'reportMode', 'promoteValueThreshold', 'priorityAdjust', 'proposPriorityAdjust', 'provider', 'model', 'solverPersona', 'verifierPersona', 'explorerPersona', 'plannerPersona', 'methodKeeperPersona', 'knowledgeContext', 'solverToolAllow', 'solverToolDeny', 'verifierToolAllow', 'verifierToolDeny', 'solverAllowNetwork', 'verifierAllowNetwork', 'solverAllowScripts', 'verifierAllowScripts', 'solverMaxToolCalls', 'verifierMaxToolCalls', 'reportIntervalMs', 'tickIntervalMs', 'activityLogCap', 'maxExplorerRetries', 'directionsPerSolver', 'planningHorizon', 'plannerEnabled', 'plannerProvider', 'plannerModel', 'planMinIntervalMs', 'plannerMaxFails', 'methodKeepIntervalMs', 'methodKeepEvery', 'methodAutoPromote', 'indexAutoRebuild', 'projectLockTimeoutMs', 'formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs', 'leanAsync', 'leanJobsMaxParallel', 'leanInitiative', 'leanSearchPaths', 'mathComputation', 'mathMode', 'mathEngines', 'mathTimeoutMs', 'mathPackages', 'mathInstallScope', 'finalPaper', 'paperFormat', 'paperLanguage', 'paperCompilePdf', 'paperLatexCommand']
82
+ // 源无法表达的条目:数组 items / oneOf / mode 的现状枚举顺序 —— 逐字沿用,并由
83
+ // "例外集合 == docs/parameter-schema.md" 断言钉住。
84
+ const PARAM_PROPS_EXTRA = {
85
+ solverToolAllow: { type: 'array', items: { type: 'string' } },
86
+ solverToolDeny: { type: 'array', items: { type: 'string' } },
87
+ verifierToolAllow: { type: 'array', items: { type: 'string' } },
88
+ verifierToolDeny: { type: 'array', items: { type: 'string' } },
89
+ leanArgs: { type: 'array', items: { type: 'string' } },
90
+ mathEngines: { type: 'array', items: { type: 'string' } },
91
+ mathPackages: { type: 'array', items: { type: 'string' } },
92
+ leanSearchPaths: { type: 'array', items: { type: 'string' } },
93
+ mode: { type: 'string', enum: ['manual', 'auto'] },
94
+ }
95
+ function paramProps() {
96
+ const props = {}
97
+ for (const name of PARAM_PROPS_KEYS) {
98
+ const e = PARAM_SCHEMA.find(function (x) { return x && x.name === name })
99
+ if (!e) continue
100
+ const enumVals = (Array.isArray(e.enum) && e.enum.length) ? e.enum : ((Array.isArray(e.options) && e.options.length) ? e.options : null)
101
+ const isEnum = e.type === 'enum' || !!enumVals
102
+ const p = isEnum ? { type: 'string', enum: (enumVals || []).slice() } : { type: e.type }
103
+ if (e.description) p.description = e.description
104
+ props[name] = p
105
+ }
106
+ // 例外表覆盖"源无法表达"的字段,但**保留源里的描述**(覆盖度 v2 49/49、v3 63/63)
107
+ for (const k of Object.keys(PARAM_PROPS_EXTRA)) {
108
+ const base = props[k] || {}
109
+ props[k] = Object.assign({}, PARAM_PROPS_EXTRA[k])
110
+ if (base.description) props[k].description = base.description
111
+ }
112
+ return props
113
+ }
114
+ // ================= end F-B =================
115
+
1
116
  // Vibe Math V3 — host plugin implementing the THIRD-generation architecture
2
117
  // ("vibe-math-v3/实现方案.md"): paper-style Markdown knowledge base + agent
3
118
  // self-organizing scheduling + universal theory/method invention library.
@@ -112,6 +227,7 @@ export function apply(ctx) {
112
227
  // 判据时最坏年龄仍有 2467ms,几乎顶到 2500ms 的租约上限。250ms 轮询下最坏租约年龄 ≈
113
228
  // max(250ms, projectLockTimeoutMs/4),即上限的 1/4 再加一次轮询的抖动。
114
229
  const LOCK_POLL_MS = 250
230
+ const ACTIVITY_PERSIST_MAX = 200 // P5:活动日志落盘上限(恢复时保留更多线索)
115
231
 
116
232
  function sessionIdOf(agent) { try { return (agent && agent.id) ? String(agent.id) : undefined } catch (e) { return undefined } }
117
233
  // Walk up the durable session lineage to the top-level (root) agent of this session,
@@ -191,6 +307,7 @@ export function apply(ctx) {
191
307
  methodAutoPromote: false, // 项目级方法自动晋升全局库(false = 人工门)
192
308
  indexAutoRebuild: true, // 每次写盘后自动重建索引(false = 手动 vibe_math_index)
193
309
  projectLockTimeoutMs: 60000, // 项目锁等待超时
310
+ fileLockTimeoutMs: 60000, // 文件写锁租约时长(审计 D2;持有者活着则每 LOCK_POLL_MS 续租)
194
311
  // ---- Lean 形式化验证(契约:docs/formal-verification.md,四架构同名同语义)----
195
312
  formalVerify: 'off', // off | encourage | require(三档开关,见 实现方案.md §10.1)
196
313
  leanCommand: 'lean', // 要执行的 Lean 可执行文件(例:'lake')
@@ -224,6 +341,8 @@ export function apply(ctx) {
224
341
  // 长跑进程里无界增长并整表落盘。它已经不再参与任何加权(见 finalVerdict 的 M8 说明),
225
342
  // 只是审计轨迹,所以按插入顺序保留最近 N 条即可。
226
343
  const VERIFIER_ACC_KEEP = 200
344
+ // F2(对照 v2:221):status 与 report 的 recentActivity **共用**这一上限;参数 activityLogCap
345
+ // 只能把它调小、不能调大(旧实现 status 硬编码 10、report 硬编码 30,读者无从知道)。
227
346
  function pruneVerifierAccuracy() {
228
347
  const keys = Object.keys(verifierAccuracy)
229
348
  for (let i = 0; i < keys.length - VERIFIER_ACC_KEEP; i++) delete verifierAccuracy[keys[i]]
@@ -276,87 +395,53 @@ export function apply(ctx) {
276
395
  * 保证结果永不为 `.` / `..`、也不含分隔符。
277
396
  */
278
397
  let warnedNoPolicy = false
398
+ // F-5:围栏漂移检测(只告警不拒写)。边界:若解析结果**不暴露**任何 root 字段,本比对无法运行。
399
+ function policyRootOf(p) {
400
+ if (!p || typeof p !== 'object') return ''
401
+ const cand = p.workspaceRoot || p.root || p.cwd || (p.config && p.config.workspaceRoot) || ''
402
+ return String(cand || '').replace(/\\/g, '/').replace(/\/+$/, '')
403
+ }
404
+ let warnedFenceDrift = false
405
+ function warnFenceDriftOnce(actual, expected) {
406
+ if (warnedFenceDrift) return
407
+ warnedFenceDrift = true
408
+ console.error('vibe-math-v3' + ': 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')
409
+ }
410
+ function checkFenceRoot(p) {
411
+ const actual = policyRootOf(p)
412
+ const expected = String(workspaceRoot() || '').replace(/\\/g, '/').replace(/\/+$/, '')
413
+ if (actual && expected && actual !== expected) warnFenceDriftOnce(actual, expected)
414
+ return p
415
+ }
279
416
  function warnNoPolicyOnce() { if (!warnedNoPolicy) { warnedNoPolicy = true; console.error('vibe-math-v3: sandboxPolicy unavailable; writes go out with no explicit policy') } }
280
417
  // Sandbox fence for our own writes. The `resolve({})` fallback is a last resort and is
281
418
  // deliberately reported (once): with no session it resolves the policy's CONFIGURED root
282
419
  // (dsh-sandbox-policy: resolveWorkspaceRoot(config.workspaceRoot ?? process.cwd())),
283
420
  // which is not necessarily this session's workspace — a silently different fence.
284
- 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-v3: 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 }
421
+ 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-v3: 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 }
285
422
  function makeSignal(ms) { return AbortSignal.timeout(ms || 30000) }
286
423
 
287
424
  // ================= 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: '每个求解方向的最大迭代轮数', suggestion: 3 },
292
- { name: 'directionsPerSolver', type: 'integer', description: '每个 solver 提示词附带的其他活跃方向摘要数量:1 = 只看自己方向', suggestion: 1 },
293
- { name: 'verifierCount', type: 'integer', description: '每个验证对象的独立验证器数量(实际下界为 2:少于 2 份独立评审一律不定论,见 minVotes)', suggestion: 3 },
294
- { name: 'debateMaxRounds', type: 'integer', description: '验证辩论(交流群)最大轮数', suggestion: 5 },
295
- { name: 'verdictMode', type: 'enum', options: ['flat', 'forced'], description: '裁决模式:flat = 均衡机制(分歧时 0.5);forced = 强制裁决(对每票等权取均值;严格 1/0 是绝对投票,其影响通过数值本身拉向端点。**不按"历史准确率"加权**——该统计量统计的是与本批裁决的一致度,没有后续真值可纠正);两者都先做近共识判定(同侧且均值≥0.85/≤0.15取均值,修复 v2 flat 误判)', suggestion: 'forced' },
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: 'plannerPersona', type: 'string', description: '注入规划代理提示词开头的人格/要求', suggestion: '' },
302
- { name: 'methodKeeperPersona', type: 'string', description: '注入方法整理代理提示词开头的人格/要求', suggestion: '' },
303
- { name: 'knowledgeContext', type: 'string', description: '共享知识/数据模型说明(空 = 内置完整版;非空 = 覆盖)', suggestion: '' },
304
- { name: 'solverToolAllow', type: 'string[]', description: '求解器允许的工具名列表(空 = 继承全部工具)', suggestion: [] },
305
- { name: 'solverToolDeny', type: 'string[]', description: '求解器禁止的工具名列表', suggestion: [] },
306
- { name: 'verifierToolAllow', type: 'string[]', description: '验证器允许的工具名列表', suggestion: [] },
307
- { name: 'verifierToolDeny', type: 'string[]', description: '验证器禁止的工具名列表', suggestion: [] },
308
- { name: 'solverAllowNetwork', type: 'boolean', description: '求解器网络工具开关:空=继承;true=允许;false=禁止', suggestion: '' },
309
- { name: 'verifierAllowNetwork', type: 'boolean', description: '验证器网络工具开关', suggestion: '' },
310
- { name: 'solverAllowScripts', type: 'boolean', description: '求解器脚本工具开关', suggestion: '' },
311
- { name: 'verifierAllowScripts', type: 'boolean', description: '验证器脚本工具开关', suggestion: '' },
312
- { name: 'solverMaxToolCalls', type: 'integer', description: '求解器每轮外部工具调用上限(0 = 不限)', suggestion: 0 },
313
- { name: 'verifierMaxToolCalls', type: 'integer', description: '验证器每轮外部工具调用上限(0 = 不限)', suggestion: 0 },
314
- { name: 'reportIntervalMs', type: 'integer', description: '进度汇报间隔(毫秒):0 = 仅事件驱动', suggestion: 0 },
315
- { name: 'reportMode', type: 'enum', options: ['file', 'push', 'both'], description: 'file = 写报告文件;push = 推送消息;both = 两者', suggestion: 'file' },
316
- { name: 'promoteValueThreshold', type: 'number', description: '命题「价值/关键性」≥ 该值且未决(0,1) 时自动晋升为问题', suggestion: 0.7 },
317
- { name: 'priorityAdjust', type: 'enum', options: ['none', 'deadend-deprioritize', 'survival-map'], description: '问题优先级动态调整策略', suggestion: 'none' },
318
- { name: 'proposPriorityAdjust', type: 'enum', options: ['none', 'progress-graded'], description: '命题优先级动态调整策略', suggestion: 'none' },
319
- { name: 'tickIntervalMs', type: 'integer', description: '调度器心跳间隔(毫秒)', suggestion: 2000 },
320
- { name: 'activityLogCap', type: 'integer', description: '活动日志保留条数', suggestion: 100 },
321
- { name: 'maxExplorerRetries', type: 'integer', description: 'explorer 拆方向失败的重派生上限', suggestion: 3 },
322
- { name: 'planningHorizon', type: 'integer', description: '规划代理一次计划的最多动作数("接下来 n 次")', suggestion: 3 },
323
- { name: 'plannerEnabled', type: 'boolean', description: 'false = 完全走内置启发式调度(规划代理禁用)', suggestion: true },
324
- { name: 'plannerProvider', type: 'string', description: '规划代理模型 provider(空 = 继承根代理)', suggestion: '' },
325
- { name: 'plannerModel', type: 'string', description: '规划代理模型 id(空 = 继承根代理)', suggestion: '' },
326
- { name: 'planMinIntervalMs', type: 'integer', description: '每一次规划调用的最小间隔(毫秒);对**每一次**规划调用都生效(含空计划、含「空闲但仍有工作」,不再有空闲绕过)', suggestion: 30000 },
327
- { name: 'plannerMaxFails', type: 'integer', description: '规划代理连续失败达此值 → 自动降级启发式', suggestion: 3 },
328
- { name: 'methodKeepIntervalMs', type: 'integer', description: 'Method Keeper 定时整理间隔(0 = 事件驱动)', suggestion: 0 },
329
- { name: 'methodKeepEvery', type: 'integer', description: '每积累 N 个待沉淀发明/新命题触发一次整理', suggestion: 5 },
330
- { name: 'methodAutoPromote', type: 'boolean', description: '项目级方法自动晋升全局库(false = 人工门)', suggestion: false },
331
- { name: 'indexAutoRebuild', type: 'boolean', description: '每次写盘后自动重建索引(false = 手动 vibe_math_index)', suggestion: true },
332
- { name: 'projectLockTimeoutMs', type: 'integer', description: '项目锁等待超时(毫秒)', suggestion: 60000 },
333
- { name: 'formalVerify', type: 'enum', options: ['off', 'encourage', 'require'], description: 'Lean 形式化验证:off = 不额外要求(默认,提示词里不出现任何 Lean 内容);encourage = 鼓励按实现难度形式化,Lean 通过后验证转为忠实性审查;require = 同上并加门禁:真/假定论必须先达到 Lean 已通过 或 已记录显式阻塞原因,否则记为未定论(formal-required)并进「形式化待办」', suggestion: 'off' },
334
- { name: 'leanCommand', type: 'string', description: 'Lean 可执行文件(例:lean / lake;配合 leanArgs=[env,lean] 用 lake)', suggestion: 'lean' },
335
- { name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加参数', suggestion: [] },
336
- { name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,非正数回退默认);异步档下它同时是**每个后台作业**的预算(到时主动 terminate,作业记 timeout,对象留在 attempted)', suggestion: 120000 },
337
- { name: 'leanAsync', type: 'boolean', description: 'Lean 编译模式:true(默认)= 后台队列,vibe_math_lean_run / vibe_math_lean_archive{run:true} 立即返回 async.jobId 入队,成员不阻塞,结果由下一轮提示的【形式化结果】行与 vibe_math_lean_job 公告(**只有作业落地为 ok 才会置 passed 并写归档证明**);false = 完全同步 await(与旧行为逐字一致)', suggestion: true },
338
- { name: 'leanJobsMaxParallel', type: 'integer', description: '后台 Lean 编译的并发上限(默认 1 = 串行,保持可预测的资源占用;调大可并行编译多个作业)', suggestion: 1 },
339
- { name: 'leanInitiative', type: 'enum', options: ['off', 'normal', 'eager'], description: '日常流程中的形式化主动性:off(不主动,只在验证提示词按 formalVerify 的要求做)| normal(默认:顺手把有价值且可能复用的东西形式化)| eager(更主动:日常就主动把有价值的小引理/命题/定义形式化)。注意它与 formalVerify(验证时的要求强度:off|encourage|require)是**两件事**', suggestion: 'normal' },
340
- { name: 'leanSearchPaths', type: 'string[]', description: '额外 Lean 搜索路径(默认空数组 = 只用框架自动注入的 VibeMath 根)。非空时按顺序先注入这里给的路径、再注入自动根(去重);若 leanArgs 里已显式给了 --search-path/-R/--root,则完全尊重用户配置、不注入任何东西', suggestion: [] },
341
- { name: 'finalPaper', type: 'boolean', description: '收口(checkTermination 的完整收口分支:无未解决问题、无待验证对象、无任务/计划/门)时自动撰写最终论文:派遣一名专职「论文撰写」子代理,把已定论命题/解法/方法整理成 Paper/<项目>/{paper.md,paper.tex,paper.meta.json,paper.log.md}。false = 只关自动触发,/vibe paper 手动命令仍可用', suggestion: true },
342
- { name: 'paperFormat', type: 'enum', options: ['both', 'md', 'tex'], description: '论文产出格式:both = markdown + latex;md = 只写 paper.md(跳过编译);tex = 只写 paper.tex(tex 才会尝试编译 pdf)', suggestion: 'both' },
343
- { name: 'paperLanguage', type: 'enum', options: ['zh', 'en'], description: '论文语言:zh = 中文(LaTeX 用 ctexart,引擎优先 xelatex);en = 英文(article,引擎优先 pdflatex/latexmk)', suggestion: 'zh' },
344
- { name: 'paperCompilePdf', type: 'boolean', description: '检测到 LaTeX 时是否编译 paper.pdf(-interaction=nonstopmode 跑两遍;失败先尝试修复:换引擎/去不支持宏包/最小模板)。false 或无 LaTeX 时只保留 tex+md 并记日志(不阻塞定稿)', suggestion: true },
345
- { name: 'paperLatexCommand', type: 'string', description: '指定 LaTeX 引擎可执行文件(空 = 按语言探测:中文 xelatex > latexmk > pdflatex > lualatex > tectonic;英文 pdflatex 优先)。解析不到时按"未检测到"降级', suggestion: '' },
346
- // ---- 数学计算 math_computation(六参数冻结;描述与 prompts.md §1/§6 口径一致)----
347
- { name: 'mathComputation', type: 'enum', options: ['off', 'auto', 'on'], description: '数学计算总开关:off = 真无操作(提示词零提及);auto = 探测到 mathEngines 里任一允许引擎才工作;on = 同上(探测失败时工具仍返回可执行的安装指引,而不是假装可用)', suggestion: 'auto' },
348
- { name: 'mathMode', type: 'enum', options: ['typed', 'typed+shell'], description: '计算策略:typed+shell(默认)= 工具不可用时允许宿主 shell 兜底,但结论必须标注"未经工具归档(shell 路径)";typed = 只用工具路径(提示词里不出现 shell 兜底段,engine:"cli" 返回 REFUSED{reason:policy})', suggestion: 'typed+shell' },
349
- { name: 'mathEngines', type: 'string[]', description: '允许的引擎列表(默认 python|r|octave|julia|matlab|maple|wolfram|cli)。cli 默认开启且走同一套超时/输出上限/回执;从列表里移除某引擎即禁用(商业引擎只探测+许可,永不安装)', suggestion: MATH_PARAM_DEFAULTS.mathEngines.slice() },
350
- { name: 'mathTimeoutMs', type: 'integer', description: '单次数学计算的超时上限(毫秒,默认 60000,最小 1000);到时主动 terminate 并把回执标为 MATH_TIMEOUT', suggestion: 60000 },
351
- { name: 'mathPackages', type: 'string[]', description: '需要预检的包(默认空)。缺包只报告 + 给"用户自装指引"或"代理代装计划",不会执行脚本,也永不自动安装', suggestion: [] },
352
- { name: 'mathInstallScope', type: 'enum', options: ['user', 'system'], description: '安装作用域:user(默认,用户级目录);system 只对当次显式调用生效、永不记忆(不会写进状态文件)', suggestion: 'user' },
353
- ]
354
425
 
355
426
  // ================= fs (adapted to DSH 0.1.1: resolve returns {targetKey, displayPath}) =================
356
427
  async function fsTarget(rel) { return await fs.resolve(rel, { cwd: frameworkRoot() }) }
357
428
  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 } }
358
- async function writeText(rel, content) { const t = await fsTarget(rel); await fs.writeText(t, content, undefined, undefined, getPolicy()); return true }
359
- async function writeJson(rel, obj) { if (!assertWritable(rel)) return false; return await writeText(rel, JSON.stringify(obj, null, 2)) }
429
+ async function writeText(rel, content) {
430
+ const t = await fsTarget(rel)
431
+ const r = await fs.writeText(t, content, undefined, undefined, getPolicy())
432
+ // P4:宿主若用返回值(而非抛异常)报告写失败,也要留痕——旧实现无条件 return true 会把它吞掉。
433
+ if (r && r.ok === false) noteStateWriteFailure(rel, 'host reported write failure' + (r.error ? (': ' + r.error) : ''))
434
+ return true
435
+ }
436
+ async function writeJson(rel, obj) {
437
+ if (!assertWritable(rel)) {
438
+ // P4:损坏守卫拒绝写入过去是**完全静默**的(返回 false 没人看)⇒ 会话以为已提交、磁盘还是旧内容。
439
+ noteStateWriteFailure(rel, 'corruption guard refused write')
440
+ logActivity('state', '拒绝写入(该 JSON 已存在但无法解析,先修/删它):' + rel)
441
+ return false
442
+ }
443
+ return await writeText(rel, JSON.stringify(obj, null, 2))
444
+ }
360
445
  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 } }
361
446
  /**
362
447
  * Corruption guard. `readJson` cannot tell "no file yet" from "file present but
@@ -385,11 +470,11 @@ export function apply(ctx) {
385
470
  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 } }
386
471
  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 } }
387
472
  async function readCurrentProject() {
388
- 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) {}
389
- 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' }
473
+ 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)) }
474
+ 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' }
390
475
  }
391
476
  async function writeCurrentProject() {
392
- 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) {}
477
+ 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: 同上
393
478
  }
394
479
 
395
480
  // ================= subprocess =================
@@ -443,8 +528,27 @@ export function apply(ctx) {
443
528
  // Progress_Logs/ 必须和别的骨架目录**一起**建出来(审计 L8):vibe_math_report 会写
444
529
  // `Progress_Logs/report.json`,README 也把它列为布局的一部分,但此前它只靠 writeText 的隐式
445
530
  // mkdir 兜底——在宿主不支持删除/创建的路径上(runShell 失败)报告目录就时有时无。
446
- async function ensureDirs() { const base = frameworkRoot(); const dirs = ['Problems', 'Progress', 'Progress_Logs', 'Propos', 'Methods', 'Verified/命题', 'Verified/问题', 'Verified/Lean', 'Formal', 'Formal/Jobs', 'Reliable', 'Notes', 'Logs/Verification', 'Logs/Plans', 'State', 'Computation']; const paths = [vibeRoot() + '/Projects', vibeRoot() + '/Methods', vibeRoot() + '/Formal/Lib', vibeRoot() + '/Formal/Proved'].concat(dirs.map(function (d) { return base + '/' + d })); return await runShell(mkdirCmd(paths)) }
447
- async function removeFile(rel) { const base = frameworkRoot(); return await runShell(rmCmd(base + '/' + rel)) }
531
+ // F-4c:shell 兜底失败不能沉默(v2 有 warnShellOnce,v3 此前丢了 ⇒ mkdir/rm 失败无人知晓)。
532
+ const shellWarned = {}
533
+ function warnShellOnce(where, r) {
534
+ const why = (r && (r.error || (r.exitCode === undefined ? undefined : ('exit code ' + r.exitCode)))) || 'unknown failure'
535
+ const key = String(where) + '|' + why
536
+ if (shellWarned[key]) return
537
+ shellWarned[key] = true
538
+ console.error('vibe-math-v3: ' + where + ' failed (' + why + ')')
539
+ }
540
+ async function ensureDirs() {
541
+ const base = frameworkRoot(); const dirs = ['Problems', 'Progress', 'Progress_Logs', 'Propos', 'Methods', 'Verified/命题', 'Verified/问题', 'Verified/Lean', 'Formal', 'Formal/Jobs', 'Reliable', 'Notes', 'Logs/Verification', 'Logs/Plans', 'State', 'Computation']; const paths = [vibeRoot() + '/Projects', vibeRoot() + '/Methods', vibeRoot() + '/Formal/Lib', vibeRoot() + '/Formal/Proved'].concat(dirs.map(function (d) { return base + '/' + d })); return await runShell(mkdirCmd(paths))
542
+ const r = await runShell(mkdirCmd(paths))
543
+ if (!r || !r.ok) warnShellOnce('ensureDirs (mkdir ' + base + ')', r)
544
+ return r
545
+ }
546
+ async function removeFile(rel) {
547
+ const base = frameworkRoot()
548
+ const r = await runShell(rmCmd(base + '/' + rel))
549
+ if (!r || !r.ok) warnShellOnce('removeFile(' + rel + ')', r)
550
+ return r
551
+ }
448
552
 
449
553
  // ================= 数学计算 math_computation:会话侧接线(共享模块,FREEZE §4/§5) =================
450
554
  // 探测量(是否装了 python/r/…)是异步的,而提示词是同步构造的 ⇒ 会话级缓存 + TTL 刷新,
@@ -577,7 +681,7 @@ export function apply(ctx) {
577
681
  // ================= settings =================
578
682
  function sanitizeParams(obj) {
579
683
  const out = {}
580
- const intFields = ['maxParallelThreshold', 'solverMaxRounds', 'directionsPerSolver', 'verifierCount', 'debateMaxRounds', 'solverMaxToolCalls', 'verifierMaxToolCalls', 'reportIntervalMs', 'tickIntervalMs', 'activityLogCap', 'maxExplorerRetries', 'planningHorizon', 'planMinIntervalMs', 'plannerMaxFails', 'methodKeepIntervalMs', 'methodKeepEvery', 'projectLockTimeoutMs']
684
+ const intFields = ['maxParallelThreshold', 'solverMaxRounds', 'directionsPerSolver', 'verifierCount', 'debateMaxRounds', 'solverMaxToolCalls', 'verifierMaxToolCalls', 'reportIntervalMs', 'tickIntervalMs', 'activityLogCap', 'maxExplorerRetries', 'planningHorizon', 'planMinIntervalMs', 'plannerMaxFails', 'methodKeepIntervalMs', 'methodKeepEvery', 'projectLockTimeoutMs', 'fileLockTimeoutMs']
581
685
  const numFields = ['promoteValueThreshold']
582
686
  const arrayFields = ['solverToolAllow', 'solverToolDeny', 'verifierToolAllow', 'verifierToolDeny', 'leanArgs']
583
687
  const boolFields = ['plannerEnabled', 'methodAutoPromote', 'indexAutoRebuild', 'finalPaper', 'paperCompilePdf', 'leanAsync']
@@ -927,6 +1031,31 @@ export function apply(ctx) {
927
1031
  lines.push('')
928
1032
  }
929
1033
  await writeText('Progress/' + qid + '.md', lines.join('\n').trimEnd() + '\n')
1034
+ // 兜底(审计 F1):聚合索引在上面用 `**完整叙述**:见 Progress/<qid>/<d.id>.md` 指向**逐方向**
1035
+ // 研究日志。设计上这些文件由**代理**写(见 applyAgentWrites 的 Progress/<qid>/ 分支),但代理没写
1036
+ // 时索引就会指向不存在的文件。这里补一份**调度器维护**的版本:`ensureProgressDir` 建目录、
1037
+ // `directionMdText` 生成内容;带 scheduler-managed 标记的文件每轮刷新,**代理写的文件绝不动**。
1038
+ // D6:只在**确实要写**时建目录(旧实现在每轮 writeJournal 开头无条件跑一次 shell mkdir,
1039
+ // 即使所有方向文件都已存在且归代理所有);宿主没有 subprocess 时直接跳过(writeText 仍会落盘),
1040
+ // 这样无 shell 宿主的每一轮日志也不会留下一条 mkdir 失败记录。
1041
+ let journalDirReady = false
1042
+ for (const d of dirs) {
1043
+ const rel = 'Progress/' + qid + '/' + d.id + '.md'
1044
+ // 归属判定:带 `<!-- scheduler-managed -->` 首行的文件由**调度器**维护(每轮刷新);
1045
+ // 其它文件视为**代理**所有、绝不触碰(契约:聚合索引不覆盖各方向文件)。
1046
+ let exists = false, owned = false
1047
+ try {
1048
+ const txt = await readText(rel)
1049
+ exists = txt !== undefined
1050
+ owned = exists && String(txt).indexOf('<!-- scheduler-managed -->') === 0
1051
+ } catch (e) { exists = false; owned = false }
1052
+ if (exists && !owned) continue
1053
+ if (!journalDirReady) {
1054
+ journalDirReady = true
1055
+ try { if (subprocessOf()) await ensureProgressDir(qid) } catch (e) { /* 无 shell:不打扰这一轮 */ }
1056
+ }
1057
+ await writeText(rel, '<!-- scheduler-managed -->\n' + directionMdText(qid, d, true))
1058
+ }
930
1059
  }
931
1060
  // 代理直接写内容:模拟/落盘代理声称写的文件(__writes),真实环境代理用 write 工具自己写,此处为调度器兜底落盘
932
1061
  async function applyAgentWrites(writes) {
@@ -1056,10 +1185,87 @@ export function apply(ctx) {
1056
1185
  * 仍为 true、status 照常响应。计数还会写进 scheduler_state.json 一路带下去。
1057
1186
  */
1058
1187
  function activeCount() { return Object.keys(agentRegistry).length }
1188
+ /** P2:状态提交的原子性 + 完整性标记。
1189
+ *
1190
+ * 可用能力只有 fs.resolve/stat/readText/writeText(**宿主没有 rename**)。所以提交协议是
1191
+ * 「**暂存 → 回读校验 → 提交 → 标记**」:
1192
+ * 1) 每个状态文件先写 `<rel>.tmp`,再回读确认**非空**(截断/失败写在这里就被抓住);
1193
+ * 2) 暂存成功才把它写进最终路径(`writeText(rel, …)`),失败则记入 `stateWriteFailures`;
1194
+ * 3) 全部落地后**最后**写 `State/commit.json`:`{seq, at, checkpoint, files, missing, mode}`
1195
+ * —— 它是"这次提交完整"的唯一标记,恢复时用它判断上一次提交是否撕裂。
1196
+ *
1197
+ * **不用 shell move**:宿主可能只提供受控/模拟的 subprocess(本轮实测:假 subprocess 返回 exit 0
1198
+ * 但不会真的移动文件 ⇒ "搬"成功、文件却不存在,状态直接丢失)。有鉴于此,这里坚持只走宿主 fs API;
1199
+ * 代价是没有 rename 级原子性,收益是任何宿主上都不会静默丢文件。
1200
+ */
1201
+ let stateCommitSeq = 0
1202
+ let stateCommit = { seq: 0, at: 0, complete: false, mode: '', files: [], missing: [] }
1203
+ const stateWriteFailures = [] // P4:被拒绝/失败的落盘(status/report 可见)
1204
+ function noteStateWriteFailure(rel, error) {
1205
+ stateWriteFailures.push({ rel: String(rel), error: String(error || 'unknown'), at: now() })
1206
+ if (stateWriteFailures.length > 32) stateWriteFailures.shift()
1207
+ }
1208
+ async function relNonEmpty(rel) {
1209
+ try { const t = await fsTarget(rel); const st = await fs.stat(t); return !!(st && (st.size === undefined || st.size > 0)) } catch (e) { return false }
1210
+ }
1211
+ async function writeJsonAtomic(rel, obj) {
1212
+ // P4:原子路径也必须过损坏守卫(否则「存在但解析失败」的状态文件会被静默覆写)
1213
+ if (!assertWritable(rel)) {
1214
+ noteStateWriteFailure(rel, 'corruption guard refused write')
1215
+ logActivity('state', '拒绝写入(该 JSON 已存在但无法解析,先修/删它):' + rel)
1216
+ return 'refused'
1217
+ }
1218
+ const text = JSON.stringify(obj, null, 2)
1219
+ // 没有 shell 就没法清理暂存文件(宿主 fs 无 delete/rename)⇒ 直接写,避免留下 .tmp 垃圾与告警
1220
+ if (!subprocessOf()) {
1221
+ try { await writeText(rel, text) } catch (e) { noteStateWriteFailure(rel, (e && e.message) || e) }
1222
+ if (!(await relNonEmpty(rel))) noteStateWriteFailure(rel, 'written but empty/missing')
1223
+ return 'direct'
1224
+ }
1225
+ const tmp = rel + '.tmp'
1226
+ let staged = false
1227
+ try { await writeText(tmp, text); staged = await relNonEmpty(tmp) } catch (e) { staged = false }
1228
+ let mode = 'direct'
1229
+ if (staged) {
1230
+ try { await writeText(rel, text); mode = 'stage-commit' } catch (e) { noteStateWriteFailure(rel, (e && e.message) || e) }
1231
+ try { await removeFile(tmp) } catch (e) { /* 清理暂存文件失败不影响提交内容 */ }
1232
+ } else {
1233
+ try { await writeText(rel, text) } catch (e) { noteStateWriteFailure(rel, (e && e.message) || e) }
1234
+ }
1235
+ if (!(await relNonEmpty(rel))) noteStateWriteFailure(rel, 'written but empty/missing')
1236
+ return mode
1237
+ }
1238
+ async function commitState(files) {
1239
+ stateCommitSeq += 1
1240
+ const modes = []
1241
+ const missing = []
1242
+ for (let i = 0; i < files.length; i++) {
1243
+ const mode = await writeJsonAtomic(files[i].rel, files[i].obj)
1244
+ modes.push(mode)
1245
+ if (!(await relNonEmpty(files[i].rel))) missing.push(files[i].rel)
1246
+ }
1247
+ const commit = {
1248
+ seq: stateCommitSeq, at: now(), epoch: processEpoch,
1249
+ checkpoint: Number(scheduler.lastCheckpoint) || now(),
1250
+ files: files.map(function (x) { return x.rel }), missing: missing,
1251
+ mode: modes.indexOf('stage-commit') === -1 ? 'direct' : 'stage-commit',
1252
+ }
1253
+ await writeJsonAtomic('State/commit.json', commit)
1254
+ stateCommit = Object.assign({ complete: missing.length === 0 }, commit)
1255
+ return commit
1256
+ }
1059
1257
  async function loadState() {
1060
1258
  const s = await readJson('State/scheduler_state.json')
1061
1259
  // 丢弃历史持久化的 activeCount:旧值可能已漂移,绝不能覆盖推导值。
1062
1260
  if (s) { const restored = Object.assign({}, s); delete restored.activeCount; scheduler = Object.assign({}, scheduler, restored) }
1261
+ await loadCommitMarker()
1262
+ // P5:活动日志回读(旧实现只落 report 快照且从不回读)
1263
+ const al = await readJson('State/activity_log.json')
1264
+ 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) }
1265
+ // P6:论文排队/回收计数恢复;在途撰写者跨进程不可恢复 ⇒ 清掉留痕
1266
+ const pj = await readJson('State/paper.json')
1267
+ if (pj && typeof pj === 'object') { if (pj.pending) paperPending = pj.pending; paperReaps = Number(pj.reaps) || 0 }
1268
+ if (paperInFlight) { logActivity('paper', '上一进程的在途撰写者 ' + paperInFlight + ' 不可恢复 ⇒ 交由 paper 锁/回收逻辑重新触发'); paperInFlight = ''; paperInFlightAt = 0 }
1063
1269
  const r = await readJson('State/agents.json'); if (r) agentRegistry = r
1064
1270
  const dq = await readJson('State/decision_queue.json'); if (dq) decisionQueue = dq
1065
1271
  const va = await readJson('State/verifier_accuracy.json'); if (va) verifierAccuracy = va
@@ -1086,22 +1292,43 @@ export function apply(ctx) {
1086
1292
  formalState = { records: clean, todo: Array.isArray(fm.todo) ? fm.todo.filter(function (t) { return t && t.id }) : [], libRuns: (fm.libRuns && typeof fm.libRuns === 'object') ? fm.libRuns : {} }
1087
1293
  }
1088
1294
  }
1295
+ /** P2:恢复时校验上一次提交的完整性(标记列出但缺失/为空的状态文件 = 撕裂提交)。 */
1296
+ async function loadCommitMarker() {
1297
+ const cm = await readJson('State/commit.json')
1298
+ if (!cm || typeof cm !== 'object') { stateCommit = { seq: 0, at: 0, complete: false, mode: '', files: [], missing: [] }; return stateCommit }
1299
+ const missing = []
1300
+ const files = Array.isArray(cm.files) ? cm.files : []
1301
+ for (let i = 0; i < files.length; i++) { if (!(await relNonEmpty(files[i]))) missing.push(files[i]) }
1302
+ stateCommitSeq = Number(cm.seq) || 0
1303
+ stateCommit = Object.assign({}, cm, { complete: missing.length === 0, missing: missing })
1304
+ if (missing.length && files.length) logActivity('state', '上一次状态提交不完整(缺失/为空:' + missing.join(', ') + ')⇒ 按逐文件容错读取,最后一次提交的部分字段可能丢失')
1305
+ return stateCommit
1306
+ }
1089
1307
  async function saveAll() {
1090
- await writeJson('State/scheduler_state.json', scheduler)
1091
- await writeJson('State/agents.json', agentRegistry)
1092
- await writeJson('State/decision_queue.json', decisionQueue)
1093
- await writeJson('State/verifier_accuracy.json', verifierAccuracy)
1094
- await writeJson('State/tasks.json', tasks)
1095
- await writeJson('State/explorer_retries.json', explorerRetries)
1096
- await writeJson('State/plans.json', { queued: planQueue, epoch: processEpoch })
1097
- await writeJson('State/method_log.json', methodLog)
1098
- await writeJson('State/project_lock.json', projectLock)
1099
- await writeJson('State/archived_journals.json', archivedJ)
1100
- // 未启用且从未产生任何形式化记录时不落这份状态文件(off 保持真正的无操作)。
1101
- if (formalOn() || Object.keys(formalState.records).length || formalState.todo.length || Object.keys(formalState.libRuns).length) await writeJson('State/formal.json', formalState)
1102
- if (lastPlanSummary) await writeJson('State/last_plan.json', lastPlanSummary)
1308
+ // P2:checkpoint 先落内存再提交(旧顺序 ⇒ 磁盘 checkpoint 永远滞后一次)
1103
1309
  scheduler.lastCheckpoint = now()
1104
- }
1310
+ const files = [
1311
+ // P5:活动日志(有界)落盘;P6:论文排队/回收计数落盘
1312
+ // 注:这里**没有** v2 的 pending_review_scores.json——v3 的"历史准确率"按稳定身份键直接累计、
1313
+ // 没有"等对象后来取得布尔定论再回溯计分"的待计分表,因此它的缺席是**设计如此**,不是漏掉的产物。
1314
+ { rel: 'State/activity_log.json', obj: activityLog.slice(-ACTIVITY_PERSIST_MAX) },
1315
+ { rel: 'State/paper.json', obj: { pending: paperPending, reaps: paperReaps } },
1316
+ { rel: 'State/scheduler_state.json', obj: scheduler },
1317
+ { rel: 'State/agents.json', obj: agentRegistry },
1318
+ { rel: 'State/decision_queue.json', obj: decisionQueue },
1319
+ { rel: 'State/verifier_accuracy.json', obj: verifierAccuracy },
1320
+ { rel: 'State/tasks.json', obj: tasks },
1321
+ { rel: 'State/explorer_retries.json', obj: explorerRetries },
1322
+ { rel: 'State/plans.json', obj: { queued: planQueue, epoch: processEpoch } },
1323
+ { rel: 'State/method_log.json', obj: methodLog },
1324
+ { rel: 'State/project_lock.json', obj: projectLock },
1325
+ { rel: 'State/archived_journals.json', obj: archivedJ },
1326
+ ]
1327
+ if (formalOn() || Object.keys(formalState.records).length || formalState.todo.length || Object.keys(formalState.libRuns).length) files.push({ rel: 'State/formal.json', obj: formalState })
1328
+ if (lastPlanSummary) files.push({ rel: 'State/last_plan.json', obj: lastPlanSummary })
1329
+ await commitState(files)
1330
+ }
1331
+ // 未启用且从未产生任何形式化记录时不落这份状态文件(off 保持真正的无操作)。
1105
1332
  async function refreshParams() {
1106
1333
  params = Object.assign({}, DEFAULT_PARAMS)
1107
1334
  await loadSettings()
@@ -1123,20 +1350,24 @@ export function apply(ctx) {
1123
1350
  function logActivity(event, detail) { activityLog.push({ at: now(), event: event, detail: String(detail || '') }); const cap = Number(params.activityLogCap) || 100; if (activityLog.length > cap) activityLog.shift(); reportDirty = true }
1124
1351
  async function buildReport() {
1125
1352
  return {
1126
- ok: true, at: now(), project: currentProject, frameworkRoot: frameworkRoot(),
1353
+ ok: true, at: now(), project: currentProject, projectExists: await projectExistsOnDisk(), frameworkRoot: frameworkRoot(),
1127
1354
  running: scheduler.running, mode: params.mode,
1128
1355
  activeCount: activeCount(), maxParallelThreshold: params.maxParallelThreshold,
1129
1356
  problems: { total: problems.size, solved: allProblems().filter(function (q) { return q.状态 === '已解决' }).length },
1130
1357
  propositions: { total: propos.size, resolved: allPropos().filter(function (p) { return p.概率 === 1 || p.概率 === 0 }).length },
1131
1358
  verifyPending: (await buildVerifyCandidates()).length,
1132
1359
  methods: { project: methods.size, global: globalMethods.size, pendingInventions: methodLog.pendingInventions.length },
1133
- pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }),
1360
+ // F-A(有意拆分):pendingDecisions 是**合同**(两面同形的数字);pendingDecisionItems 是**诊断**(明细),只出现在 report——status 面向代理自省、report 面向文件快照。改动此处需同步 docs/status-report-fields.md(该文档租约在共享层 owner)。
1361
+ pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).length,
1362
+ pendingDecisionItems: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }),
1134
1363
  registeredAgents: Object.keys(agentRegistry).length,
1135
1364
  queuedPlanActions: planQueue.length,
1136
- recentActivity: activityLog.slice(-Math.min(30, Number(params.activityLogCap) || 100)),
1365
+ recentActivity: activityLog.slice(-Math.min(ACTIVITY_REPORT_MAX, Number(params.activityLogCap) || 100)),
1137
1366
  // Lean 形式化:模式 + 每个对象的状态 + 形式化待办(可读参数表在 params 里)。
1138
1367
  formal: formalSummary(),
1139
1368
  params: params,
1369
+ // F5:与 status 对齐,周期快照也能看到论文/编译状态。
1370
+ paper: await paperStatusView(),
1140
1371
  }
1141
1372
  }
1142
1373
  async function maybeWriteReport(force) {
@@ -1153,7 +1384,7 @@ export function apply(ctx) {
1153
1384
  lines.push('# 项目进展报告|' + currentProject + '|' + fmtTime())
1154
1385
  lines.push('')
1155
1386
  lines.push('## 总览')
1156
- lines.push('- 运行中:' + scheduler.running + ';活跃子代理:' + activeCount() + '/' + params.maxParallelThreshold)
1387
+ lines.push('- 运行中:' + (scheduler.running ? '是' : '否') + ';活跃子代理:' + activeCount() + '/' + params.maxParallelThreshold)
1157
1388
  lines.push('- 问题:' + allProblems().filter(function (q) { return q.状态 === '已解决' }).length + '/' + problems.size + ' 已解决;命题:' + allPropos().filter(function (p) { return p.概率 === 1 || p.概率 === 0 }).length + '/' + propos.size + ' 已定论')
1158
1389
  lines.push('- 方法库:项目 ' + methods.size + ' 条,全局 ' + globalMethods.size + ' 条,待沉淀发明 ' + methodLog.pendingInventions.length + ' 条')
1159
1390
  lines.push('- 待执行计划动作:' + planQueue.length + ' 条;待人工决策:' + decisionQueue.filter(function (d) { return d.status === 'pending' }).length + ' 条')
@@ -1161,7 +1392,9 @@ export function apply(ctx) {
1161
1392
  lines.push('## 问题状态')
1162
1393
  for (const q of allProblems()) {
1163
1394
  const dirs = getDirState(q.id)
1164
- const dirInfo = dirs.length ? '(方向:' + dirs.map(function (d) { return d.id + ':' + d.status }).join(', ') + ')' : ''
1395
+ // F7:人读报告不把裸代码状态拼进中文;中文化后把代码放括号里备查。
1396
+ const DIR_STATUS_ZH = { active: '进行中', 'dead-end': '死路', success: '已成功', queued: '排队中' }
1397
+ const dirInfo = dirs.length ? '(方向:' + dirs.map(function (d) { return d.id + ':' + (DIR_STATUS_ZH[d.status] || d.status) + '(' + d.status + ')' }).join(', ') + ')' : ''
1165
1398
  lines.push('- **' + q.id + '**:' + q.状态 + ',优先级 ' + q.优先级 + ',解法 ' + (q.solutions || []).length + ' 条' + dirInfo)
1166
1399
  }
1167
1400
  lines.push('')
@@ -1191,10 +1424,10 @@ export function apply(ctx) {
1191
1424
  if (!rootAgent || typeof rootAgent.followup !== 'function') return
1192
1425
  try {
1193
1426
  const report = await buildReport()
1194
- const text = '[Vibe Math V3] 进度更新:项目 "' + currentProject + '" 运行中=' + report.running +
1427
+ const text = '[Vibe Math V3] 进度更新:项目 "' + currentProject + '" 运行中=' + (report.running ? '是' : '否') +
1195
1428
  ',问题 ' + report.problems.solved + '/' + report.problems.total + ' 已解决,命题 ' + report.propositions.resolved + '/' + report.propositions.total + ' 已定论,' +
1196
- '活跃代理轮数=' + report.activeCount + ',待人工决策=' + report.pendingDecisions.length + ',待执行计划=' + report.queuedPlanActions + '。' +
1197
- '请调用 vibe_math_report 汇总当前进展及各代理状态,并用人话简要汇报(不打断用户,简短即可)。'
1429
+ '活跃代理轮数=' + report.activeCount + ',待人工决策=' + report.pendingDecisions + ',待执行计划=' + report.queuedPlanActions + '。' +
1430
+ '请调用 vibe_math_report 汇总当前进展,并用 vibe_math_list_agents 取各代理(id/角色/目标)状态,再用人话简要汇报(不打断用户,简短即可)。'
1198
1431
  // 来源 kind 必须是**已声明**的:`MessageSourceMap` 是 merge-extensible 联合,但没有共享的
1199
1432
  // catch-all `plugin` kind(审计 L5/dsh-llm message.d.ts),{kind:'plugin'} 是契约外形状。
1200
1433
  // role 本来就是 'user',正文自带 "[Vibe Math V3] 进度更新" 的真署名,故用核心声明的 {kind:'user'}。
@@ -1206,7 +1439,14 @@ export function apply(ctx) {
1206
1439
  }
1207
1440
 
1208
1441
  // ================= child spawn / followup =================
1209
- 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' }
1442
+ // F-6a:宿主 list() 失败(或既无 spawn 也无 fork)时回退到假定值必须**留痕**,不能静默猜。
1443
+ let providerFallbackWarned = false
1444
+ function warnProviderFallback(why) {
1445
+ if (providerFallbackWarned) return
1446
+ providerFallbackWarned = true
1447
+ console.error('vibe-math-v3: pickProvider() falling back to \'spawn\': ' + why)
1448
+ }
1449
+ 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' }
1210
1450
  function childAgentOptions(role) {
1211
1451
  const o = {}
1212
1452
  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) {}
@@ -1226,11 +1466,8 @@ export function apply(ctx) {
1226
1466
  // host applies the filter when establishing a continuable child
1227
1467
  // (dsh-subagent: childCtx.tools.restrict(...)), so a stale name meant the child
1228
1468
  // was never created at all.
1229
- const IS_WINDOWS = process.platform === 'win32'
1230
- const SCRIPT_TOOLS = IS_WINDOWS ? ['pwsh'] : ['bash']
1231
1469
  // 'web_fetch' is only registered when the composition enables fetch (the v4
1232
1470
  // preset sets `fetch: false`), so it is a candidate that sanitizeToolFilter drops.
1233
- const NETWORK_TOOLS = ['web_search', 'web_fetch']
1234
1471
  /**
1235
1472
  * Drop filter names this host does not register. `known` comes from the host's
1236
1473
  * own rejection message, which lists every registered global tool, so this
@@ -1315,6 +1552,12 @@ export function apply(ctx) {
1315
1552
  * 都吞成静默成功(调用方拿到 undefined,还以为已经中断)。现在:空/未知 id ⇒ `code` +
1316
1553
  * `next{tool,hint}`,宿主抛错 ⇒ 保留错误文本并给替代出口;只有真的发出中断才 `ok:true`。
1317
1554
  */
1555
+ /** F-6b:中断失败必须留痕(调用点此前丢掉 {ok:false},界面看起来一切正常)。 */
1556
+ async function interruptTraced(cid, why) {
1557
+ const r = await interruptChild(cid)
1558
+ if (r && r.ok === false) logActivity('interrupt', '中断失败(' + why + '):' + String(cid) + ' — ' + String(r.message || r.code || ''))
1559
+ return r
1560
+ }
1318
1561
  async function interruptChild(childId) {
1319
1562
  const id = String(childId == null ? '' : childId).trim()
1320
1563
  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 调用。' } }
@@ -1540,14 +1783,16 @@ export function apply(ctx) {
1540
1783
  function plannerPrompt(brief) {
1541
1784
  return personaText('plannerPersona') + 'You are the SCHEDULING PLANNER of a multi-agent mathematical research system. Your job: autonomously choose the OPTIMAL schedule — you may lay out the NEXT ' + params.planningHorizon + ' agent-task calls in one plan (they will be executed in order, beyond-capacity ones queued for later ticks).\n\n' +
1542
1785
  'CURRENT STATE BRIEF (JSON):\n' + JSON.stringify(brief, null, 2) + '\n\n' +
1543
- 'ACTION VOCABULARY (code validates every action against hard invariants; invalid actions are dropped):\n' +
1786
+ 'ACTION VOCABULARY — the code ACCEPTS exactly these 6 actions: spawn / interrupt / promote are HARD-VALIDATED (invalid ones are dropped), while wait / continue / stop are ADVISORY ONLY (they are logged and have NO scheduling effect):\n' +
1544
1787
  '- {"action":"spawn","role":"explorer","target":"<qid>","reason":"..."} — problem has no directions yet or all dead (re-derive).\n' +
1545
1788
  '- {"action":"spawn","role":"solver","target":"<qid>","direction":"<dirId>","reason":"..."} — active direction, needs a solving round.\n' +
1546
1789
  '- {"action":"spawn","role":"verifier","target":"<rId>","reason":"..."} — verify candidate (from verify_candidates); keep solving AND verifying balanced.\n' +
1547
1790
  '- {"action":"spawn","role":"method-keeper","reason":"..."} — distill pending inventions / maintain the theory library.\n' +
1548
1791
  '- {"action":"interrupt","childId":"<childId>","reason":"..."} — stop a running child (direction dead, superseded...).\n' +
1549
1792
  '- {"action":"promote","target":"<pId>","reason":"..."} — high-value unresolved proposition → judge problem.\n' +
1550
- '- {"action":"wait","target":"<id>","reason":"..."} — advisory: wait for a dependency.\n' +
1793
+ '- {"action":"wait","target":"<id>","reason":"..."} — advisory only (logged; no scheduling effect): you are waiting for a dependency.\n' +
1794
+ '- {"action":"continue","childId":"<childId>","reason":"..."} — advisory only (logged): continuation of an in-flight child is code-driven; this never re-dispatches anything.\n' +
1795
+ '- {"action":"stop","childId":"<childId>","reason":"..."} — advisory only (logged; it does NOT stop anyone). To actually stop a child use `interrupt` with a live childId.\n' +
1551
1796
  '\nHARD RULES: never re-schedule verified objects; problems with 依赖未就绪 (依赖就绪=false) should wait unless you explicitly accept a temporary assumption; respect capacity (brief.free_slots); PREFER problems whose dependencies are ready and whose directions have the highest survival; DO NOT forget verification — unresolved solutions/proofs/refutations (verify_candidates) will never be checked unless you schedule a verifier; DO NOT assume a direction is already being worked just because it is shown "active" in a problem — check brief.problems[].running_solver_dirs and brief.active_agents: schedule a solver for a direction ONLY if that direction is NOT in running_solver_dirs (an "active" direction absent from running_solver_dirs is WAITING to be dispatched, not being worked); schedule at most ' + params.planningHorizon + ' actions.\n' +
1552
1797
  'Respond with ONLY a single JSON object wrapped in a ```json code fence — no prose:\n' +
1553
1798
  '{"summary":"one-line plan rationale","plan":[{"action":"...","role":"...","target":"...","direction":"...","childId":"...","reason":"..."}]}'
@@ -1562,7 +1807,7 @@ export function apply(ctx) {
1562
1807
  mathWorkLine() +
1563
1808
  (formalWorkLine() + (formalOn() ? '\n【方法沉淀 × Lean 形式化】除了方法卡,你沉淀的每个可复用对象 / 定义 / 假设都应当归档到全局 Lean 库(vibe_math_lean_archive kind=\'def\'),已成立的引理归档到 Proved/(kind=\'lemma\');归档时**连同定义与陈述一起写清**,方便后续直接 import。\n' : '')) +
1564
1809
  'OUTPUT CONTRACT — pick ONE channel. Write method cards into Markdown; only the created IDs, which cards were used, and improvements cross the machine reply.\n' +
1565
- 'CHANNEL A (recommended, you can write files): write each method card into `Methods/<m-id>.md` (`# 方法|标题` + `- 标题/ID/类型/状态/可信断言/适用场景` + `## 核心内容`/`## 应用记录`/`## 改进历史`), then reply ONLY this metadata:\n' +
1810
+ 'CHANNEL A (recommended, you can write files): write each method card into `Methods/<m-id>.md` — the `<m-id>` in the FILE NAME must be EXACTLY the id you list in `created`(调度器按 `created` 里的 id 去 `Methods/<id>.md` 找卡;不一致会被当成"已沉淀"而实际没有卡)(`# 方法|标题` + `- 标题/ID/类型/状态/可信断言/适用场景` + `## 核心内容`/`## 应用记录`/`## 改进历史`), then reply ONLY this metadata:\n' +
1566
1811
  '{"meta":{"kind":"methods","used":[{"id":"m-...","效果":"...","建议":"..."}],"created":["m-xxx"],"improvements":[{"id":"m-...","改进内容":"...","原因":"..."}]}}\n' +
1567
1812
  'CHANNEL B (your file tools are unavailable): put the method-card content into __writes and carry the same meta:\n' +
1568
1813
  '{"__writes":[{"path":"Methods/<m-id>.md","content":"<# 方法|标题 + 锚点 + ## 核心内容... 完整卡面>"}],"meta":{"kind":"methods","used":[...],"created":["m-xxx"],"improvements":[...]}}'
@@ -2353,6 +2598,21 @@ export function apply(ctx) {
2353
2598
  await saveDirState(); await writeJournal(qid)
2354
2599
  }
2355
2600
  function statusFromStop(stopReason) { return (stopReason === 'completed' || stopReason === 'max-tokens') ? 'continue' : 'dead-end' }
2601
+ /**
2602
+ * P1:`add_proposition`/`/vibe add-proposition` 只用于**新建**。id 已存在时**拒绝**,绝不覆盖——
2603
+ * 旧实现 `propos.set(id, p)` + `saveProposition(p)` 会把已有卡的 陈述/证明列表/证伪列表 整条换掉
2604
+ * (静默丢数据)。自动路径(sync_meta 的引理)本来就有 `propos.has` + 文件存在双守卫,这里拉齐语义。
2605
+ */
2606
+ function propositionIdConflict(id) {
2607
+ const hit = propos.get(id)
2608
+ if (!hit) return null
2609
+ return {
2610
+ code: 'PROPOSITION_ID_EXISTS',
2611
+ message: '命题 id "' + id + '" 已存在:add_proposition 只用于**新建**,不会覆盖已有卡(其 陈述/证明尝试/证伪尝试 一律保留)。',
2612
+ next: '换一个新 id 新建(例如 p-' + shortId() + ');要修改已有卡请写进它自己的 Propos/<分类>/<id>.md(代理直接写卡是允许的)。',
2613
+ existing: { id: hit.id, 标题: hit.标题, 概率: hit.概率, 状态: hit.状态 },
2614
+ }
2615
+ }
2356
2616
  async function addLemmaAsProposition(qid, lemma) {
2357
2617
  if (!lemma || !lemma.title) return
2358
2618
  // M11:`prob`(提示词名)与 `布尔估计`(遗留扁平名)都接受;`分类` 与 `细类型` 同理。
@@ -2360,7 +2620,7 @@ export function apply(ctx) {
2360
2620
  let be = clamp01(rawProb != null ? rawProb : 0.6)
2361
2621
  if (be >= 1) be = 0.99; else if (be <= 0) be = 0.01
2362
2622
  const p = {
2363
- id: 'p-' + shortId(), 标题: lemma.title, 状态: '未定论', 概率: be,
2623
+ id: 'p-' + shortIdUnique(function (x) { return propos.has(x) }), 标题: lemma.title, 状态: '未定论', 概率: be,
2364
2624
  优先级: (lemma.优先级 != null) ? lemma.优先级 : 1, 依赖: [], 价值关键性: clamp01(lemma['价值/关键性'] != null ? lemma['价值/关键性'] : 0.5),
2365
2625
  分类: categoryOf({ 分类: lemmaCategory(lemma) || '未分类' }),
2366
2626
  陈述: lemma.statement || lemma.title,
@@ -3804,7 +4064,7 @@ export function apply(ctx) {
3804
4064
  return v.dispatched > 0 && v.dispatched >= v.expected && v.quorum
3805
4065
  }
3806
4066
  function consensus(t) { const vs = voteCount(t).reportedIds.map(function (cid) { return t.childResults[cid].Result }); if (vs.length === 0) return false; return vs.every(function (v) { return v === 1 }) || vs.every(function (v) { return v === 0 }) }
3807
- 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') }
4067
+ 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') }
3808
4068
  async function handleVerifier(childId, meta, output, stopReason) {
3809
4069
  const rId = meta.rId
3810
4070
  const parsed = parseJson(output)
@@ -3823,7 +4083,7 @@ export function apply(ctx) {
3823
4083
  return
3824
4084
  }
3825
4085
  if (t.children.indexOf(childId) === -1) t.children.push(childId)
3826
- t.childResults[childId] = { Result: Result, Reason: Reason, round: meta.round }
4086
+ t.childResults[childId] = { Result: Result, Reason: Reason, round: meta.round, changed: (parsed && parsed.changed != null && String(parsed.changed).trim()) ? String(parsed.changed).trim() : null }
3827
4087
  delete agentRegistry[childId]
3828
4088
  // H5:推进/裁决的前置条件是"已凑齐下限票数且本轮都已回报",不是"目前存在的那几个孩子都报了"。
3829
4089
  // 未凑齐时保持 spawning,由 reconcileVerify 在后续 tick 里补派验证器(不裁决、不定论)。
@@ -3916,7 +4176,7 @@ export function apply(ctx) {
3916
4176
  verifierAccuracy[cids[i]] = acc
3917
4177
  }
3918
4178
  pruneVerifierAccuracy()
3919
- await writeJson('Logs/Verification/' + t.rId + '_' + Date.now() + '.json', { r: r, verdict: v, results: t.childResults, transcript: buildTranscript(t), history: t.history || [], at: now() })
4179
+ await writeJson('Logs/Verification/' + t.rId + '_' + Date.now() + '_' + shortId() + '.json', { r: r, verdict: v, results: t.childResults, transcript: buildTranscript(t), history: t.history || [], at: now() })
3920
4180
  // ── require 门禁(契约 §8)────────────────────────────────────────────────
3921
4181
  // 判定为真(严格证明)或假(严格反驳)之前必须先有 `passed` 或 `blocked`。门禁放在**改
3922
4182
  // 对象之前**:不这样就改写不出"不改变对象的既有权重/概率字段"(一旦先把 概率 写成 1,
@@ -4075,7 +4335,7 @@ export function apply(ctx) {
4075
4335
  const cid = paperInFlight
4076
4336
  paperInFlight = ''; paperInFlightAt = 0
4077
4337
  if (cid) {
4078
- try { await interruptChild(cid) } catch (e) { /* best effort */ }
4338
+ try { await interruptTraced(cid, 'paper writer re-dispatch') } catch (e) { /* best effort */ }
4079
4339
  delete agentRegistry[cid]
4080
4340
  paperAbandoned[cid] = true
4081
4341
  }
@@ -4106,6 +4366,9 @@ export function apply(ctx) {
4106
4366
  for (const p of allProblems()) out.push(problemRel(p))
4107
4367
  for (const p of allPropos()) out.push(propositionRel(p))
4108
4368
  for (const m of methods.values()) out.push(methodRel(m, false))
4369
+ // 审计 F2(v3 同型):Reliable/ 是用户提供的可信参考层,材料证据索引必须收它,
4370
+ // 否则被引用/被要求给出处的来源进不了论文材料。
4371
+ for (const f of await listFiles('Reliable')) out.push('Reliable/' + f)
4109
4372
  const dirs = ['Verified/命题', 'Verified/问题', 'Logs/Verification', 'Logs/Plans', 'Progress'].concat(formalNow ? ['Verified/Lean', 'Formal'] : [])
4110
4373
  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]) }
4111
4374
  const seen = {}, uniq = []
@@ -4531,7 +4794,7 @@ export function apply(ctx) {
4531
4794
  const prevEpoch = await readJson('State/process_epoch.json')
4532
4795
  const stale = typeof prevEpoch === 'string' && prevEpoch !== processEpoch
4533
4796
  if (fresh || stale) {
4534
- if (fresh) { const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]) }
4797
+ if (fresh) { const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptTraced(ids[i], 'batch clear (start/abort)') }
4535
4798
  if (Object.keys(agentRegistry).length > 0 || Object.keys(tasks).length > 0) {
4536
4799
  logActivity(fresh ? 'start' : 'resume', 'cleared ' + Object.keys(agentRegistry).length + ' agent(s) and ' + Object.keys(tasks).length + ' task(s) (' + (fresh ? 'restart' : 'stale from previous process') + ')')
4537
4800
  agentRegistry = {}; tasks = {}
@@ -4598,7 +4861,7 @@ export function apply(ctx) {
4598
4861
  async function startScheduler(override) { const r = await init(true); if (!r.ok) return r; const lock = await acquireProjectLock(override === true); if (!lock.ok) return lock; scheduler.running = true; scheduler.startedAt = now(); scheduler.gate = null; logActivity('start', 'scheduler started for project ' + currentProject + '(v3:md 知识库 + 规划代理调度 + 方法库)'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
4599
4862
  async function resumeScheduler(override) { const r = await init(false); if (!r.ok) return r; const lock = await acquireProjectLock(override === true); if (!lock.ok) return lock; scheduler.running = true; scheduler.gate = null; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
4600
4863
  async function pauseScheduler() { scheduler.running = false; await releaseProjectLock(); logActivity('pause', 'scheduler paused'); await saveAll(); return { ok: true, message: 'scheduler paused' } }
4601
- async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; planQueue = []; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null; await releaseProjectLock(); logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
4864
+ async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptTraced(ids[i], 'batch clear (start/abort)'); agentRegistry = {}; planQueue = []; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null; await releaseProjectLock(); logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
4602
4865
  async function autoResolvePending() {
4603
4866
  const pending = decisionQueue.filter(function (d) { return d.status === 'pending' })
4604
4867
  for (let i = 0; i < pending.length; i++) {
@@ -4619,10 +4882,16 @@ export function apply(ctx) {
4619
4882
  }
4620
4883
  if (pending.length > 0) { scheduler.gate = null; logActivity('mode', 'switched to auto — auto-resolved ' + pending.length + ' pending decision(s)'); await saveAll(); scheduleTick() }
4621
4884
  }
4885
+ /** 审计 F7(v3 同型):`project` 是**当前会话**的项目名,`projects` 是磁盘上已存在的 slug 列表;
4886
+ * 两者不闭合时(新会话 project='default' 而磁盘上没有该目录)语义必须显式,不能靠调用方猜。 */
4887
+ async function projectExistsOnDisk() { return (await listDirsAt(vibeRoot(), 'Projects')).indexOf(currentProject) !== -1 }
4622
4888
  async function getStatus() {
4623
4889
  return {
4890
+ at: now(),
4624
4891
  ok: true, initialized: rootAgent !== undefined, running: scheduler.running,
4625
- project: currentProject, projects: await listDirsAt(vibeRoot(), 'Projects'),
4892
+ project: currentProject, projectExists: await projectExistsOnDisk(), projects: await listDirsAt(vibeRoot(), 'Projects'),
4893
+ stateCommit: stateCommit,
4894
+ stateWriteFailures: { count: stateWriteFailures.length, last: stateWriteFailures[stateWriteFailures.length - 1] || null },
4626
4895
  mode: params.mode, activeCount: activeCount(), maxParallelThreshold: params.maxParallelThreshold,
4627
4896
  frameworkRoot: frameworkRoot(),
4628
4897
  problems: { total: problems.size, solved: allProblems().filter(function (q) { return q.状态 === '已解决' }).length },
@@ -4634,8 +4903,14 @@ export function apply(ctx) {
4634
4903
  queuedPlanActions: planQueue.length,
4635
4904
  plannerEnabled: params.plannerEnabled, plannerFails: plannerFails,
4636
4905
  formal: formalSummary(),
4637
- recentActivity: activityLog.slice(-Math.min(10, Number(params.activityLogCap) || 100)), params: params,
4906
+ recentActivity: activityLog.slice(-Math.min(ACTIVITY_REPORT_MAX, Number(params.activityLogCap) || 100)), params: params,
4638
4907
  // 最终论文的可观测面(spec §2/§6)。
4908
+ // F3(对照 v5 的 leanNoticesScope/fieldScopes):逐组标注会话内存 vs 耐久来源。
4909
+ fieldScopes: {
4910
+ durable: ['project', 'projects', 'problems', 'propositions', 'stateCommit', 'formal', 'methods.project', 'methods.global', 'paper.finalizedAt', 'paper.artifacts', 'paper.compile'],
4911
+ session: ['stateWriteFailures', 'activeCount', 'pendingDecisions', 'registeredAgents', 'verifyPending', 'queuedPlanActions', 'plannerFails', 'methods.pendingInventions', 'recentActivity', 'paper.inFlight', 'paper.inFlightSince', 'paper.inFlightAgeMs', 'paper.reapedThisRun', 'paper.queued'],
4912
+ note: 'session = 本进程内存,重启后归零(看着永远健康是假象);durable = 落在项目树里(State/Formal/Progress/Verified 等),重启后仍在',
4913
+ },
4639
4914
  paper: await paperStatusView(),
4640
4915
  }
4641
4916
  }
@@ -4725,7 +5000,7 @@ export function apply(ctx) {
4725
5000
  registerTool('vibe_math_status', TOOL_DESC.vibe_math_status, objParams({}), async function () { await refreshParams(); return await getStatus() })
4726
5001
  registerTool('vibe_math_report', TOOL_DESC.vibe_math_report, objParams({}), async function () { await refreshParams(); await maybeWriteReport(true); return await buildReport() })
4727
5002
  registerTool('vibe_math_set_mode', TOOL_DESC.vibe_math_set_mode, 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 } })
4728
- registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, 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' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { 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: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { 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 } })
5003
+ registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, 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 } })
4729
5004
  registerTool('vibe_math_setup', TOOL_DESC.vibe_math_setup, 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' } })
4730
5005
  registerTool('vibe_math_save_settings', TOOL_DESC.vibe_math_save_settings, objParams({}), async function () { return await saveSettings() })
4731
5006
  registerTool('vibe_math_template', TOOL_DESC.vibe_math_template, objParams({ where: { type: 'string', enum: ['global', 'project'] } }), async function (args) { return await createTemplate((args && args.where) || 'global') })
@@ -4734,7 +5009,7 @@ export function apply(ctx) {
4734
5009
  // M9:内存键、对象 id、文件名必须是**同一个**归一化 id。此前内存键用原始 id、文件名用
4735
5010
  // idSafe(id)、重载键又来自文件名 ⇒ 同一个对象分裂成两个身份(`p-1/2` 与 `p-1-2`)。
4736
5011
  const id = idSafe(args.id)
4737
- if (propos.has(id)) return { ok: false, message: 'proposition id already exists' }
5012
+ if (propos.has(id)) { const cf = propositionIdConflict(id); logActivity('proposition', '拒绝覆盖已有命题:' + id); return Object.assign({ ok: false }, cf) }
4738
5013
  const p = { id: id, 标题: id, 状态: '未定论', 概率: clamp01(args.概率 != null ? args.概率 : 0.5), 优先级: (args.优先级 != null) ? args.优先级 : 1, 依赖: [], 价值关键性: clamp01(args['价值/关键性'] != null ? args['价值/关键性'] : 0.5), 分类: args.分类 || '未分类', 陈述: String(args.概述 == null ? '' : args.概述), proofs: [], refutes: [], 来源问题: String(args.来源问题 || ''), 来源方向: String(args.来源方向 || ''), 在问题清单: false }
4739
5014
  propos.set(p.id, p); await saveProposition(p); await rebuildIndex(); scheduleTick(); return { ok: true, proposition: p, file: propositionRel(p) }
4740
5015
  })
@@ -4798,13 +5073,34 @@ export function apply(ctx) {
4798
5073
  function fileLockKey(target) {
4799
5074
  return String(target || '').replace(/\\/g, '/')
4800
5075
  }
5076
+ /** 文件写锁的租约时长(审计 D2):与项目锁同型——**持有者活着就续租**,持有者消失后最多这么久失效。
5077
+ * 旧实现只在 claim 时写一次 `at`、判定窗口固定 60s 且全仓没有续租点:成员写一份大文件超过 60s 后,
5078
+ * 另一个成员可以合法抢锁(锁恰好在它最该起作用的场景失效)。 */
5079
+ function fileLockTtl() { return Math.max(1000, Number(params.fileLockTimeoutMs) || 60000) }
5080
+ function fileLockDue() { return Math.max(250, fileLockTtl() / 4) }
5081
+ function fileLockHolderAlive(id) {
5082
+ const cid = String(id || '')
5083
+ if (!cid || cid === 'scheduler') return true
5084
+ if (agentRegistry[cid]) return true
5085
+ try { return String((rootAgent && rootAgent.id) || '') === cid } catch (e) { return false }
5086
+ }
5087
+ /** 每次锁轮询(LOCK_POLL_MS)里与 renewProjectLock 一同执行:只续租**活着**的持有者。 */
5088
+ async function renewFileLocks() {
5089
+ const ids = Object.keys(fileOwner)
5090
+ for (let i = 0; i < ids.length; i++) {
5091
+ const o = fileOwner[ids[i]]
5092
+ if (!o) continue
5093
+ if (!fileLockHolderAlive(o.childId)) continue // 持有者已消失 ⇒ 让租约自然过期(有界接管)
5094
+ if ((now() - (Number(o.at) || 0)) >= fileLockDue()) o.at = now()
5095
+ }
5096
+ }
4801
5097
  async function claimWrite(target, agent) {
4802
5098
  const childId = (agent && agent.id) ? String(agent.id) : 'scheduler'
4803
5099
  const key = String(target || '').replace(/\\/g, '/')
4804
5100
  if (!key) return { ok: false, message: 'target required' }
4805
5101
  const lockKey = fileLockKey(key)
4806
5102
  const owner = fileOwner[lockKey]
4807
- if (owner && owner.childId !== childId && (now() - (owner.at || 0)) < 60000) {
5103
+ if (owner && owner.childId !== childId && (now() - (owner.at || 0)) < fileLockTtl()) {
4808
5104
  return { ok: false, busy: owner.childId, message: '文件 "' + key + '" 正被其他代理写入,请稍后(写锁)' }
4809
5105
  }
4810
5106
  // 确保目标父目录存在(如 Progress/<qid>/ 供方向文件写入)
@@ -4978,7 +5274,7 @@ export function apply(ctx) {
4978
5274
  if (cmd === 'save') return await saveSettings()
4979
5275
  if (cmd === 'template') return await createTemplate(args[0] === 'project' ? 'project' : 'global')
4980
5276
  if (cmd === 'add') { const raw = args[0]; const desc = args.slice(1).join(' '); if (!raw || !desc) return { ok: false, message: 'usage: /vibe add <id> <description>' }; const id = idSafe(raw); if (problems.has(id)) return { ok: false, message: 'problem id already exists' }; problems.set(id, { id: id, 标题: id, 状态: '求解中', 优先级: 0, 依赖: [], 被依赖: [], 来源: '原始', 计划: '待调度', 陈述: desc, 来源与动机: '', solutions: [], 判断命题: '', 来源命题: '' }); await saveProblem(problems.get(id)); await rebuildIndex(); scheduleTick(); return { ok: true, message: 'problem added', file: problemRel(problems.get(id)) } }
4981
- if (cmd === 'add-proposition') { const raw = args[0]; const desc = args.slice(1).join(' '); if (!raw || !desc) return { ok: false, message: 'usage: /vibe add-proposition <id> <概述>' }; const id = idSafe(raw); const p = { id: id, 标题: id, 状态: '未定论', 概率: 0.5, 优先级: 1, 依赖: [], 价值关键性: 0.5, 分类: '未分类', 陈述: desc, proofs: [], refutes: [], 来源问题: '', 在问题清单: false }; propos.set(p.id, p); await saveProposition(p); await rebuildIndex(); scheduleTick(); return { ok: true, proposition: p, file: propositionRel(p) } }
5277
+ if (cmd === 'add-proposition') { const raw = args[0]; const desc = args.slice(1).join(' '); if (!raw || !desc) return { ok: false, message: 'usage: /vibe add-proposition <id> <概述>' }; const id = idSafe(raw); const p1c = propositionIdConflict(id); if (p1c) { logActivity('proposition', '拒绝覆盖已有命题:' + id); return Object.assign({ ok: false }, p1c) } const p = { id: id, 标题: id, 状态: '未定论', 概率: 0.5, 优先级: 1, 依赖: [], 价值关键性: 0.5, 分类: '未分类', 陈述: desc, proofs: [], refutes: [], 来源问题: '', 在问题清单: false }; propos.set(p.id, p); await saveProposition(p); await rebuildIndex(); scheduleTick(); return { ok: true, proposition: p, file: propositionRel(p) } }
4982
5278
  if (cmd === 'list-propositions') { const all = allPropos(); return { ok: true, count: all.length, propositions: all.map(function (p) { return { id: p.id, 标题: p.标题, 概率: p.概率, 状态: p.状态, 优先级: p.优先级, 分类: categoryOf(p) } }) } }
4983
5279
  if (cmd === 'methods') { const all = Array.from(methods.values()); return { ok: true, count: all.length, methods: all.map(function (m) { return { id: m.id, 标题: m.标题, 类型: m.类型, 状态: m.状态 } }) } }
4984
5280
  if (cmd === 'index') { await loadKnowledgeBase(); return await rebuildIndex() }
@@ -5007,7 +5303,7 @@ export function apply(ctx) {
5007
5303
  scheduleTick: scheduleTick,
5008
5304
  // Lease renewal for the apply-scope interval (see the ctx.effect below). Kept on the session so
5009
5305
  // the timer needs no per-session context, and so a mock host WITHOUT ctx.timeout still renews.
5010
- renewLockIfDue: renewProjectLock,
5306
+ renewLockIfDue: async function () { await renewProjectLock(); await renewFileLocks() },
5011
5307
  onChildEnd: onChildEnd,
5012
5308
  dispatchVibeCommand: dispatchVibeCommand,
5013
5309
  handlers: handlers,
@@ -5026,10 +5322,6 @@ export function apply(ctx) {
5026
5322
  mathProbeDue: mathProbeDue,
5027
5323
  refreshMathProbe: refreshMathProbe,
5028
5324
  mathProbe: function () { return mathProbe },
5029
- // P2a:persona 两个文本块必须含的「归档→编辑→重跑」规则(文本取自共享模块常量,persona 不手抄)。
5030
- mathArchiveWorkflowLine: MATH_ARCHIVE_WORKFLOW_LINE,
5031
- // A 项:替代声明规则(文本由共享模块给出;persona 两块与每轮可用性行都必须带它)。
5032
- mathSubstitutionRuleLine: MATH_SUBSTITUTION_RULE_LINE,
5033
5325
  // childOwner 裁剪用(审计 L1):这个会话当前仍"可能再发 subagent/end"的 child
5034
5326
  // = 在册子代理 + 任何任务正在等的那几个。
5035
5327
  referencedChildIds: function () {
@@ -5068,7 +5360,7 @@ export function apply(ctx) {
5068
5360
  registerTool('vibe_math_status', TOOL_DESC.vibe_math_status, objParams({}), 'vibe_math_status')
5069
5361
  registerTool('vibe_math_report', TOOL_DESC.vibe_math_report, objParams({}), 'vibe_math_report')
5070
5362
  registerTool('vibe_math_set_mode', TOOL_DESC.vibe_math_set_mode, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), 'vibe_math_set_mode')
5071
- registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, 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' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { 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: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { 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')
5363
+ registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams(paramProps()), 'vibe_math_set_params')
5072
5364
  registerTool('vibe_math_setup', TOOL_DESC.vibe_math_setup, objParams({}), 'vibe_math_setup')
5073
5365
  registerTool('vibe_math_save_settings', TOOL_DESC.vibe_math_save_settings, objParams({}), 'vibe_math_save_settings')
5074
5366
  registerTool('vibe_math_template', TOOL_DESC.vibe_math_template, objParams({ where: { type: 'string', enum: ['global', 'project'] } }), 'vibe_math_template')
@@ -5517,7 +5809,6 @@ function leanJobId(key, fingerprint) {
5517
5809
  /** "已落地且通过"的**唯一**判据:state=settled 且 exit 0 且未超时未中断(内容哈希由 settle 校验)。 */
5518
5810
  function leanJobSettledOk(rec) { return !!rec && rec.state === 'settled' && rec.exitCode === 0 && !rec.timedOut && !rec.interrupted }
5519
5811
  /** 主动性档位(修订 §1):与"验证时的要求强度"(formalVerify)是两件事。 */
5520
- const LEAN_INITIATIVE_MODES = ['off', 'normal', 'eager']
5521
5812
 
5522
5813
  export const __testHelpers = {
5523
5814
  uuid,
@@ -5602,7 +5893,7 @@ const TOOL_DESC = {
5602
5893
  vibe_math_save_settings: 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.',
5603
5894
  vibe_math_template: 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.',
5604
5895
  vibe_math_add_problem: 'Add a problem to the current project (creates Problems/<id>.md).',
5605
- vibe_math_add_proposition: 'Add a proposition to Propos/ (creates Propos/<分类>/<id>.md).',
5896
+ vibe_math_add_proposition: 'Add a NEW proposition to Propos/ (creates Propos/<分类>/<id>.md). The id must be NEW: an existing id is REFUSED with code PROPOSITION_ID_EXISTS (cards are never overwritten — 陈述/证明尝试/证伪尝试 are preserved); pick a fresh id (e.g. p-<8 hex>) or write into that card file directly.',
5606
5897
  vibe_math_list_propositions: 'List propositions from Propos/ (summary index: id, 标题, 概率, 状态, 优先级, 分类).',
5607
5898
  vibe_math_new_project: 'Create a new math project folder and switch to it.',
5608
5899
  vibe_math_set_project: 'Switch the current math project.',
@@ -5617,7 +5908,7 @@ const TOOL_DESC = {
5617
5908
  vibe_math_method_add: 'Manually add a method card to Methods/ (creates Methods/<id>.md).',
5618
5909
  vibe_math_method_list: 'List methods from Methods/ (+ global VibeMath/Methods/): id, 标题, 类型, 状态, 可信断言, applications count.',
5619
5910
  vibe_math_lock_status: 'Show the project lock occupancy.',
5620
- vibe_math_claim_write: '(member) Acquire the write lock for one target file (relative to the project root). Call before writing a Markdown file directly; a file may only be written by ONE agent at a time. Returns the display path you may write (VibeMath/Projects/<project>/<target>) and a hint.',
5911
+ vibe_math_claim_write: '(member) Acquire the write lock for one target file (relative to the project root). Call before writing a Markdown file directly; a file may only be written by ONE agent at a time. The lock is a LEASE: it is renewed automatically while you are still alive, so a long write keeps it; if your process disappears the lease expires after fileLockTimeoutMs (default 60000) and another agent may take it. Returns the display path you may write (VibeMath/Projects/<project>/<target>) and a hint.',
5621
5912
  vibe_math_release_write: '(member) Release the write lock for one target file (relative to the project root). Call after you finished writing it.',
5622
5913
  vibe_math_sync_meta: '(member) After you write content into Markdown files, report ONLY lightweight scheduling metadata to keep the scheduler state in sync (content stays in the md files). meta.kind must be one of:\n- "directions": {qid, directions:[{id,title,method,core_assumption,feasibility}], methods_used:[{id,效果,建议}], new_inventions:[{类型,标题,内容描述,是否已入库}]}\n- "solver": {qid, dirId, round, survival, status:"continue|success|dead-end", dead_end_reason, lemmas:[{id,title,statement,proof,prob,价值/关键性,分类,优先级}], methods_used, new_inventions, solution_prob, solution_text, sub_questions:[{q_sub_title,q_sub_statement,assumption_title,assumption_statement}]}\n- "methods": {used:[{id,效果,建议}], created:[ids], improvements:[{id,改进内容,原因}]}',
5623
5914
  vibe_math_lean_run: "(member) Execute the Lean toolchain on one .lean file inside the workspace 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.",
@@ -5631,6 +5922,11 @@ const TOOL_DESC = {
5631
5922
 
5632
5923
  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 }
5633
5924
 
5925
+ /// P8:框架分配 id 时先查重(shortId 是 Math.random 的 8 位 hex,碰撞概率极低但后果是静默覆盖)。
5926
+ function shortIdUnique(isTaken) {
5927
+ for (let i = 0; i < 8; i++) { const id = shortId(); if (!isTaken(id)) return id }
5928
+ return shortId()
5929
+ }
5634
5930
  function shortId() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 8; i++) s += h[Math.floor(Math.random() * 16)]; return s }
5635
5931
 
5636
5932
  function clamp01(v) { const n = Number(v); if (!Number.isFinite(n)) return 0.5; return Math.max(0, Math.min(1, n)) }