dsh-vibe-math 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/AUDIT-CHECKLIST.md +48 -2
  2. package/README.md +368 -81
  3. package/RELEASE-NOTES-2.2.2.md +88 -0
  4. package/RELEASE-NOTES-2.3.0.md +207 -0
  5. package/audit-formal-sensitivity.mjs +247 -0
  6. package/audit-persona-sensitivity.mjs +249 -0
  7. package/audit-persona-surface.test.mjs +349 -0
  8. package/audit-v5-integrity.mjs +40 -1
  9. package/audit-v5-sensitivity.mjs +84 -6
  10. package/docs/formal-verification.md +321 -0
  11. package/docs/generate_framework_diagram_v5.mjs +290 -0
  12. package/docs//346/236/266/346/236/204/345/233/276.md +75 -0
  13. package/formal-verify-v2.test.mjs +672 -0
  14. package/formal-verify-v3.test.mjs +824 -0
  15. package/formal-verify-v4.test.mjs +603 -0
  16. package/formal-verify-v5.test.mjs +526 -0
  17. package/package.json +33 -15
  18. package/prompt-corpus-persona/persona-corpus.json +32 -0
  19. package/prompt-corpus-persona/persona-corpus.md +674 -0
  20. package/prompt-corpus-v3/formal-verify-v3.json +280 -0
  21. package/prompt-corpus-v3/formal-verify-v3.md +2826 -0
  22. package/prompt-corpus-v5/prompt-corpus-v5.json +131 -713
  23. package/prompt-corpus-v5/prompt-corpus-v5.md +1828 -5225
  24. package/prompt-v5-integrity.test.mjs +154 -11
  25. package/vibe-math-v2/agent.cordis.yml +40 -2
  26. package/vibe-math-v2/vibe-math-v2.js +627 -19
  27. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +145 -1
  28. package/vibe-math-v3/agent.cordis.yml +46 -2
  29. package/vibe-math-v3/vibe-math-v3.js +749 -21
  30. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +87 -2
  31. package/vibe-math-v4/agent.cordis.yml +46 -4
  32. package/vibe-math-v4/vibe-math-v4.js +652 -15
  33. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +226 -0
  34. package/vibe-math-v5/agent.cordis.yml +41 -5
  35. package/vibe-math-v5/vibe-math-v5.js +572 -9
  36. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +122 -4
  37. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +426 -0
  38. package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +173 -0
@@ -51,6 +51,7 @@ const EV = {
51
51
  queue: 'vibe5/queue',
52
52
  counters: 'vibe5/counters',
53
53
  progress: 'vibe5/progress',
54
+ formal: 'vibe5/formal',
54
55
  }
55
56
 
56
57
  // Stable error codes (ported from DSH agent-teams' typed-error discipline).
@@ -135,6 +136,10 @@ export function apply(ctx) {
135
136
  meetings: [],
136
137
  debates: [],
137
138
  verdicts: {},
139
+ // Per-object Lean formalization record (docs/formal-verification.md). Kept in the
140
+ // projection so it survives restore with zero context cost, exactly like verdicts.
141
+ formal: {},
142
+ todo: [],
138
143
  queue: [],
139
144
  counters: { academician: 0, researcher: 0, temp: 0, task: 0, meeting: 0, message: 0, verify: 0 },
140
145
  runId: '',
@@ -252,6 +257,16 @@ export function apply(ctx) {
252
257
  return Object.assign({}, inst, { verdicts })
253
258
  })
254
259
  }
260
+ if (t === EV.formal) {
261
+ return withInstitute(state, key, (inst) => {
262
+ if (!d.target || typeof d.target !== 'string') return inst
263
+ const formal = Object.assign({}, inst.formal)
264
+ if (d.record === null) delete formal[d.target]
265
+ else formal[d.target] = d.record
266
+ const todo = d.todo === undefined ? (inst.todo || []) : (Array.isArray(d.todo) ? d.todo : (inst.todo || []))
267
+ return Object.assign({}, inst, { formal, todo })
268
+ })
269
+ }
255
270
  if (t === EV.queue) {
256
271
  return withInstitute(state, key, (inst) => Object.assign({}, inst, { queue: Array.isArray(d.queue) ? d.queue : [] }))
257
272
  }
@@ -396,6 +411,14 @@ export function apply(ctx) {
396
411
  chatDigestMs: 45000,
397
412
  chatDigestMax: 12,
398
413
  meetingKeepEvery: 5,
414
+ // ── Lean formal verification (§ docs/formal-verification.md) ─────────
415
+ // 'off' — 不额外进行任何要求(默认)
416
+ // 'encourage' — 鼓励:验证时按实现难度决定是否用 Lean 形式化;平时顺手形式化可复用对象
417
+ // 'require' — 强制:真/假结论必须已有「形式化已通过」或「显式阻塞原因」,否则记为未定论
418
+ formalVerify: 'off',
419
+ leanCommand: 'lean',
420
+ leanArgs: [],
421
+ leanTimeoutMs: 120000,
399
422
  // ── model / tools ────────────────────────────────────────────────────
400
423
  provider: '',
401
424
  model: '',
@@ -898,6 +921,15 @@ export function apply(ctx) {
898
921
  // failed provision invisible to the whole institute.
899
922
  const absent = inst().members.filter((m) => m.phase !== 'active' && m.phase !== 'dismissed')
900
923
  if (absent.length) b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))
924
+ // Lean mode is a RUNTIME knob, so its line is computed here (per round) rather than
925
+ // frozen into the charter — changing the mode must reach members immediately.
926
+ if (formalOn()) {
927
+ const recs = formalRecords()
928
+ const passed = Object.keys(recs).filter((k) => recs[k] && recs[k].status === 'passed').length
929
+ const blocked = Object.keys(recs).filter((k) => recs[k] && recs[k].status === 'blocked').length
930
+ b.push('[形式化] ' + (formalMode() === 'require' ? '强制' : '鼓励') + ' Lean|已通过 ' + passed
931
+ + '|已记录阻塞 ' + blocked + (formalTodo().length ? '|形式化待办 ' + formalTodo().length + ' 项(见 Formal/TODO.md)' : ''))
932
+ }
901
933
  const tasks = inst().tasks.filter((t) => t.status !== 'deleted')
902
934
  const mine = tasks.filter((t) => t.ownerId === member.id && t.status === 'in_progress')
903
935
  const ready = tasks.filter((t) => t.status === 'pending' && taskReady(t))
@@ -983,13 +1015,18 @@ export function apply(ctx) {
983
1015
  }
984
1016
  async function mkdirs() {
985
1017
  const base = instRoot()
986
- const dirs = ['Shared/Chat', 'Shared/Meetings', 'Shared/Debates', 'State', 'Problems']
1018
+ const dirs = ['Shared/Chat', 'Shared/Meetings', 'Shared/Debates', 'State', 'Problems', 'Formal', 'Verified/Lean']
1019
+ // The REUSABLE Lean library is global (cross-project), so it hangs off the VibeMath
1020
+ // root rather than the institute root — creating it under instRoot would scatter a
1021
+ // second, invisible copy per institute.
1022
+ const globalDirs = ['Formal/Lib', 'Formal/Proved']
987
1023
  for (const m of activeMembers()) {
988
1024
  for (const d of ['Progress', 'Propos', 'Methods', 'Subproblems']) dirs.push('Members/' + m.id + '/' + d)
989
1025
  }
1026
+ const paths = dirs.map((d) => base + '/' + d).concat(globalDirs.map((d) => vibeRoot() + '/' + d))
990
1027
  const script = isWindows()
991
- ? 'New-Item -Force -ItemType Directory -Path ' + dirs.map((d) => psQuote(base + '/' + d)).join(',') + ' | Out-Null'
992
- : 'mkdir -p ' + dirs.map((d) => shQuote(base + '/' + d)).join(' ')
1028
+ ? 'New-Item -Force -ItemType Directory -Path ' + paths.map((p) => psQuote(p)).join(',') + ' | Out-Null'
1029
+ : 'mkdir -p ' + paths.map((p) => shQuote(p)).join(' ')
993
1030
  return await runShell(script)
994
1031
  }
995
1032
 
@@ -1344,6 +1381,12 @@ export function apply(ctx) {
1344
1381
  L.push(' "task_done": "t-3",')
1345
1382
  L.push(' "task_update": {"task_id":"t-3","expected_revision":2,"action":"complete|release|reopen|edit|set_dependencies|delete"},')
1346
1383
  L.push(' "input": "本轮会议/辩论的发言正文(会议轮用;也可直接用 say)",')
1384
+ if (formalOn()) {
1385
+ L.push(' "formal": {"target":"p-x","decision":"used|blocked","file":"Formal/p-x.lean","note":"难度判断/阻塞原因"}')
1386
+ L.push(' ← Lean 形式化:' + (formalMode() === 'require'
1387
+ ? '**强制**:定论前必须有「Lean 已通过」或显式阻塞原因(note 必填),否则本轮裁定记为未定论,'
1388
+ : '**鼓励**:按实现难度自行决定;做了就归档,没做就写明难度判断,') + '详见提示词里的【Lean 形式化验证】段')
1389
+ }
1347
1390
  L.push(' "reject_assign": {"task_id":"t-3","why":"你对这项分派的异议理由"} ← 有异议时填;理由会被广播给')
1348
1391
  L.push(' 全体表决者(任务仍会执行,但你的理由不会被埋掉),')
1349
1392
  if (kind !== 'temp') {
@@ -1399,6 +1442,7 @@ export function apply(ctx) {
1399
1442
  L.push('作为院士,除了做研究,你还要**统筹全所**:用 vibe_v5_overview 看清谁在做什么、')
1400
1443
  L.push('哪里是瓶颈;把工作拆成任务并用 vibe_v5_assign 分派;必要时用 vibe_v5_nudge 督办。')
1401
1444
  }
1445
+ if (formalOn()) { L.push(''); L.push(formalWorkLine()) }
1402
1446
  L.push('')
1403
1447
  L.push('------------')
1404
1448
  L.push(stateBlock(member))
@@ -1414,6 +1458,7 @@ export function apply(ctx) {
1414
1458
  L.push('读一读同事的库、推进你的子问题/引理/方法、尝试一条新路线;')
1415
1459
  L.push('或者向团队发消息(say)、开一个议题(propose_meeting)、给某个方向开任务(task_create)。')
1416
1460
  L.push('如果你确实已无路可走或认为原问题接近解决,请说明你的判断与理由。')
1461
+ if (formalOn()) { L.push(''); L.push(formalWorkLine()) }
1417
1462
  L.push('')
1418
1463
  L.push('------------')
1419
1464
  L.push(stateBlock(member))
@@ -1461,6 +1506,21 @@ export function apply(ctx) {
1461
1506
  L.push(' 介于 0 与 1 之间(例如 0.9)表示你不确定——这会被记为**弃权/存疑**,')
1462
1507
  L.push(' 不计入法定票数 m,但会计入全组平均概率。')
1463
1508
  L.push('')
1509
+ if (formalOn()) {
1510
+ const rec = formalOf(vs.target)
1511
+ L.push(formalPromptBlock(vs.target))
1512
+ // Make the SHIFT explicit: with a machine-checked proof in hand, re-deriving is
1513
+ // wasted effort and the real risk is a statement that does not say what we meant.
1514
+ if (rec.status === 'passed') {
1515
+ L.push(' ▸ 因此请把 verdict 用在**忠实性**上:一致 → 1;发现任何偏离 → 0(或按不确定度给中间值并说明)。')
1516
+ } else if (rec.status === 'blocked') {
1517
+ L.push(' ▸ 因此请把 verdict 用在"这个阻塞判断是否成立 / 是否仍有别的形式化路线"上,并给出理由。')
1518
+ } else {
1519
+ L.push(' ▸ 若你在本轮把它形式化并跑通(vibe_v5_lean_archive kind=\'proof\'),后续轮次的')
1520
+ L.push(' 审查对象就会从"推导是否正确"变成"Lean 代码是否忠实于命题"。')
1521
+ }
1522
+ L.push('')
1523
+ }
1464
1524
  if (vs.stage === 'debate' && vs.history) {
1465
1525
  L.push('### 上一轮各成员的意见(框架已公开给你,请参考后重新判断)')
1466
1526
  for (const [k, v] of Object.entries(vs.history)) {
@@ -1477,6 +1537,12 @@ export function apply(ctx) {
1477
1537
  L.push('------------')
1478
1538
  L.push('结束时请**只**输出一个 JSON 对象(```json 围栏内):')
1479
1539
  L.push('{"verdict":{"target":"' + vs.target + '","verdict":<0-1 数值>,"reason":"<你的理由>"}, "contextPct": 40}')
1540
+ if (formalOn()) {
1541
+ // The formal field belongs in the VOTING contract too: voters are exactly the agents
1542
+ // who must either formalize the object or record why they judged it infeasible.
1543
+ L.push('若你本轮做了形式化或给出难度判断,请一并加上:')
1544
+ L.push('{"formal":{"target":"' + vs.target + '","decision":"used|blocked","file":"Formal/' + vs.target + '.lean","note":"难度判断/阻塞原因"}}')
1545
+ }
1480
1546
  return L.join('\n')
1481
1547
  }
1482
1548
  function kindLabel2(k) { return k === 'method' ? '方法/理论' : k === 'subproblem' ? '子问题' : '命题' }
@@ -1542,6 +1608,354 @@ export function apply(ctx) {
1542
1608
  return t
1543
1609
  }
1544
1610
 
1611
+ // ================= Lean formal verification ==============================
1612
+ // Contract: docs/formal-verification.md.
1613
+ //
1614
+ // The point of this feature is a SHIFT IN WHAT MUST BE REVIEWED, not an extra chore.
1615
+ // Consensus verification answers "do we all believe this?"; a machine-checked Lean
1616
+ // development answers "is this true?" and shrinks the open question to one a human can
1617
+ // actually audit:
1618
+ //
1619
+ // do the Lean definitions / objects / conditions / assumptions / conclusion
1620
+ // match the proposition as originally stated?
1621
+ //
1622
+ // So once a Lean run passes, the voting prompt stops asking voters to redo the
1623
+ // derivation and asks them to do a FIDELITY review. `require` mode makes that concrete:
1624
+ // a 真/假 verdict does not take effect until the object is either `passed` or has an
1625
+ // explicit, reasoned `blocked` record — "decide by difficulty, but decide out loud".
1626
+ const FORMAL_MODES = ['off', 'encourage', 'require']
1627
+ const formalMode = () => (FORMAL_MODES.indexOf(String(params.formalVerify)) !== -1 ? String(params.formalVerify) : 'off')
1628
+ const formalOn = () => formalMode() !== 'off'
1629
+ const formalRoot = () => instRoot() + '/Formal'
1630
+ const formalLibRoot = () => vibeRoot() + '/Formal/Lib'
1631
+ const formalProvedRoot = () => vibeRoot() + '/Formal/Proved'
1632
+ const verifiedLeanRoot = () => instRoot() + '/Verified/Lean'
1633
+ const formalRecords = () => (inst().formal || {})
1634
+ const formalTodo = () => (inst().todo || [])
1635
+ function formalOf(target) {
1636
+ const r = formalRecords()[idSafe(String(target || ''))]
1637
+ return r || { status: 'none' }
1638
+ }
1639
+ async function putFormal(target, record, todo) {
1640
+ await commit(EV.formal, { target: idSafe(String(target)), record: record || null, todo })
1641
+ }
1642
+ // `passed` requires a GREEN RUN, not merely an archived file: a proof file that has
1643
+ // never been executed proves nothing.
1644
+ const formalGateOk = (rec) => !!rec && (rec.status === 'passed' || rec.status === 'blocked')
1645
+ function formalStatusLine(target) {
1646
+ const r = formalOf(target)
1647
+ if (r.status === 'passed') return 'Lean 通过(' + (r.proof || r.file || '') + ')'
1648
+ if (r.status === 'blocked') return '阻塞(' + (r.note || '未说明') + ')'
1649
+ if (r.status === 'attempted') return '已尝试未通过'
1650
+ return '未尝试'
1651
+ }
1652
+ const tail = (s, n) => { const t = String(s == null ? '' : s); return t.length > n ? t.slice(-n) : t }
1653
+
1654
+ // Lexically normalise an absolute path (collapse '.', '..' and duplicate slashes)
1655
+ // WITHOUT touching the filesystem. A plain `startsWith(root)` check is not enough:
1656
+ // "…/VibeMath/Projects/../../../../etc/evil.lean" still starts with the root as a
1657
+ // string while resolving outside it.
1658
+ function normalizeAbsPath(p) {
1659
+ const parts = String(p == null ? '' : p).replace(/\\/g, '/').split('/')
1660
+ const out = []
1661
+ for (const seg of parts) {
1662
+ if (seg === '' ) { if (out.length === 0) out.push(''); continue }
1663
+ if (seg === '.') continue
1664
+ if (seg === '..') { if (out.length > 1) out.pop(); continue }
1665
+ out.push(seg)
1666
+ }
1667
+ return out.join('/')
1668
+ }
1669
+ // Resolve a Lean path to an absolute, NORMALISED path that is provably inside the
1670
+ // VibeMath root — or null. Every Lean file access (run, archive, read) goes through it.
1671
+ function leanAbsPath(rel) {
1672
+ const raw = String(rel == null ? '' : rel).trim()
1673
+ if (!raw) return null
1674
+ const abs = (raw.charAt(0) === '/' || /^[a-z]:/i.test(raw)) ? raw : instRoot() + '/' + raw.replace(/^\.\//, '')
1675
+ const norm = normalizeAbsPath(abs)
1676
+ const root = normalizeAbsPath(vibeRoot())
1677
+ if (norm !== root && norm.indexOf(root + '/') !== 0) return null
1678
+ return norm
1679
+ }
1680
+
1681
+ // Run the toolchain on one file. NEVER throws into the scheduler: every failure mode
1682
+ // (no service, no executable, timeout, non-zero exit) becomes a readable result.
1683
+ async function leanRunFile(relPath, timeoutMs) {
1684
+ const started = now()
1685
+ const rel = String(relPath || '').trim()
1686
+ if (!rel) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'file is required' }
1687
+ // Path guard: only files inside the VibeMath tree may be executed, so a crafted path
1688
+ // can never make the framework run something outside the workspace.
1689
+ const abs = leanAbsPath(rel)
1690
+ if (abs === null) {
1691
+ return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'Lean file must live under ' + vibeRoot().replace(/\\/g, '/') + '/ (got ' + rel + ')' }
1692
+ }
1693
+ if (!/\.lean$/.test(abs)) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'only .lean files can be executed' }
1694
+ if (await readTextAbs(abs) === undefined) return { ok: false, code: 'V5_NOT_FOUND', message: 'no such file: ' + rel }
1695
+ const sub = subprocessOf()
1696
+ if (sub === undefined || typeof sub.spawn !== 'function') {
1697
+ return { ok: false, code: 'NO_SUBPROCESS', message: 'the host exposes no subprocess service; Lean cannot be executed here', file: rel, ms: 0 }
1698
+ }
1699
+ const cap = Math.max(1000, Number(timeoutMs) || Number(params.leanTimeoutMs) || 120000)
1700
+ let exe
1701
+ try {
1702
+ exe = await sub.resolveExecutable(String(params.leanCommand || 'lean'))
1703
+ } catch (e) {
1704
+ return { ok: false, code: 'LEAN_NOT_FOUND', message: 'cannot resolve "' + String(params.leanCommand || 'lean') + '": ' + String((e && e.message) || e) + ' — 仍可把形式化代码写下来归档,但无法在此宿主上执行', file: rel, ms: now() - started }
1705
+ }
1706
+ const argv = [exe].concat((Array.isArray(params.leanArgs) ? params.leanArgs : []).map(String)).concat([abs])
1707
+ let handle
1708
+ try {
1709
+ handle = sub.spawn({
1710
+ argv,
1711
+ cwd: instRoot(),
1712
+ stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } },
1713
+ graceMs: cap,
1714
+ })
1715
+ } catch (e) {
1716
+ return { ok: false, code: 'LEAN_SPAWN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
1717
+ }
1718
+ let outcome
1719
+ try {
1720
+ outcome = await handle.done
1721
+ } catch (e) {
1722
+ return { ok: false, code: 'LEAN_RUN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
1723
+ }
1724
+ let out = '', err = ''
1725
+ try { if (handle.collected && handle.collected.stdout) out = handle.collected.stdout.readFrom(0).text } catch (e) { /* best effort */ }
1726
+ try { if (handle.collected && handle.collected.stderr) err = handle.collected.stderr.readFrom(0).text } catch (e) { /* best effort */ }
1727
+ const exitCode = outcome ? outcome.exitCode : null
1728
+ const ms = now() - started
1729
+ const ok = exitCode === 0
1730
+ return {
1731
+ ok, exitCode, signal: (outcome && outcome.signal) || null, ms,
1732
+ command: argv.join(' '), file: rel,
1733
+ stdout: tail(out, 4000), stderr: tail(err, 4000),
1734
+ timedOut: ms >= cap,
1735
+ code: ok ? undefined : (ms >= cap ? 'LEAN_TIMEOUT' : 'LEAN_FAILED'),
1736
+ }
1737
+ }
1738
+ async function formalSetRun(target, run) {
1739
+ const t = idSafe(String(target || ''))
1740
+ if (!t) return
1741
+ const prev = formalOf(t)
1742
+ // Running a file NEVER changes an already-decided status: a green run does not by
1743
+ // itself make an object `passed` (only archiving a proof does), and a failing scratch
1744
+ // run must not silently erase a recorded `passed`/`blocked` decision. Everything else
1745
+ // becomes `attempted`, which is the honest "we tried, see the compiler output" state.
1746
+ const keep = (prev.status === 'passed' || prev.status === 'blocked') ? prev.status : 'attempted'
1747
+ await putFormal(t, Object.assign({}, prev, {
1748
+ status: keep,
1749
+ file: run.file || prev.file || '',
1750
+ run: { at: now(), ok: !!run.ok, exitCode: run.exitCode === undefined ? null : run.exitCode, ms: run.ms || 0, stdoutTail: tail(run.stdout, 800), stderrTail: tail(run.stderr, 800) },
1751
+ updatedAt: now(),
1752
+ }))
1753
+ }
1754
+ function formalPromptBlock(target) {
1755
+ if (!formalOn()) return ''
1756
+ const mode = formalMode()
1757
+ const rec = target ? formalOf(target) : { status: 'none' }
1758
+ const L = []
1759
+ L.push('【Lean 形式化验证(' + (mode === 'require' ? '强制' : '鼓励') + '模式)】')
1760
+ if (rec.status === 'passed') {
1761
+ // The whole point of the feature: the review subject CHANGES.
1762
+ L.push(' · 该对象已有**通过的 Lean 形式化证明**(' + (rec.proof || rec.file) + ',最近运行 exit 0)。')
1763
+ L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')
1764
+ L.push(' 定义 / 对象 / 条件 / 假设 / 结论是否与命题原文**完全一致**(有偏差就指出偏差),')
1765
+ L.push(' 并据此给出 verdict。')
1766
+ } else if (rec.status === 'blocked') {
1767
+ L.push(' · 该对象已被记录为**形式化阻塞**:' + (rec.note || '未说明') + '。')
1768
+ L.push(' 请复核这个判断是否成立;若你认为其实可以形式化,请指出来并动手做。')
1769
+ } else {
1770
+ L.push(' · 请先判断该对象的**实现难度**:若能在可接受的工作量内形式化,优先写 Lean 代码并执行。')
1771
+ L.push(' · 工具:vibe_v5_lean_run(执行)· vibe_v5_lean_archive(归档)· vibe_v5_lean_lib(查已有可复用库)')
1772
+ L.push(' · 工作目录:Formal/(相对研究所根);可复用定义放 ' + formalLibRoot().replace(/\\/g, '/') + '/,')
1773
+ L.push(' 已证引理放 ' + formalProvedRoot().replace(/\\/g, '/') + '/;写之前先 lean_lib 查重。')
1774
+ L.push(' · **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义/对象/条件/假设/结论是否与')
1775
+ L.push(' 命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。')
1776
+ if (mode === 'require') {
1777
+ L.push(' · **本模式要求**:必须产出 Lean 形式化,或**必须**给出显式的阻塞原因(lean_archive')
1778
+ L.push(' kind=\'blocked\' note=… 或回执 formal.note)。若两者都没有,本次裁定不会生效,')
1779
+ L.push(' 会被记为未定论(原因 formal-required)并进入「形式化待办」。')
1780
+ } else {
1781
+ L.push(' · 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断。')
1782
+ }
1783
+ }
1784
+ return L.join('\n')
1785
+ }
1786
+ function formalWorkLine() {
1787
+ if (!formalOn()) return ''
1788
+ return '【顺手形式化(' + (formalMode() === 'require' ? '强制' : '鼓励') + ')】把你工作中常用或可能复用的对象、假设、'
1789
+ + '新定义用 Lean 形式化定义并归档到全局可复用库(vibe_v5_lean_archive kind=\'def\'),已成立的引理归到 Proved/'
1790
+ + '(kind=\'lemma\');写之前先 vibe_v5_lean_lib 查重,避免重复定义。'
1791
+ + (formalMode() === 'require'
1792
+ ? '本模式下,任何要定论为真/假的对象都必须先有 Lean 通过或显式阻塞记录。'
1793
+ : '这会让后续的验证与证明省掉大量重复工作。')
1794
+ }
1795
+ // ---- the three indexes (framework-maintained) ---------------------------
1796
+ async function writeFormalIndex() {
1797
+ const recs = formalRecords()
1798
+ const L = ['# Lean 形式化索引|' + instituteName + '|' + fmtTime(), '',
1799
+ '> 本文件由框架维护(工具调用时更新;`vibe_v5_lean_lib` 会重建)。权威状态在会话日志投影里。', '',
1800
+ '| 对象 | 状态 | 形式化文件 | 归档证明 | 最近运行 | 难度判断 / 阻塞原因 |', '|---|---|---|---|---|---|']
1801
+ const keys = Object.keys(recs)
1802
+ if (!keys.length) L.push('| (暂无) | | | | | |')
1803
+ for (const k of keys) {
1804
+ const r = recs[k] || {}
1805
+ const run = r.run ? (r.run.ok ? 'ok(exit 0,' + ((r.run.ms || 0) / 1000).toFixed(1) + 's)' : 'fail(exit ' + r.run.exitCode + ',' + ((r.run.ms || 0) / 1000).toFixed(1) + 's)') : '—'
1806
+ L.push('| ' + k + ' | ' + (r.status || 'none') + ' | ' + (r.file || '—') + ' | ' + (r.proof || '—') + ' | ' + run + ' | ' + String(r.note || '—').replace(/\|/g, '/').slice(0, 120) + ' |')
1807
+ }
1808
+ L.push('')
1809
+ if (formalTodo().length) {
1810
+ L.push('## 形式化待办(require 模式:定论被搁置)')
1811
+ for (const t of formalTodo()) L.push('- ' + t.id + ' —— ' + (t.why || 'formal-required') + '(' + fmtTime(t.at) + ')')
1812
+ L.push('')
1813
+ }
1814
+ await writeTextRel('Formal/Index.md', L.join('\n'))
1815
+ }
1816
+ async function writeFormalTodo() {
1817
+ const list = formalTodo()
1818
+ const L = ['# 形式化待办|' + instituteName + '|' + fmtTime(), '',
1819
+ '> 这些对象在 `require` 模式下尚不具备「Lean 已通过」或「显式阻塞记录」,因此**定论被搁置**。',
1820
+ '> 完成形式化(vibe_v5_lean_archive kind=\'proof\')或记录阻塞原因(kind=\'blocked\')后,重新提议验证即可。', '']
1821
+ if (!list.length) L.push('(暂无)')
1822
+ for (const t of list) L.push('- ' + t.id + '|' + (t.why || 'formal-required') + '|' + fmtTime(t.at))
1823
+ L.push('')
1824
+ await writeTextRel('Formal/TODO.md', L.join('\n'))
1825
+ }
1826
+ async function rebuildLeanLibIndexes() {
1827
+ // Listing must be CHEAP and side-effect free: it does NOT execute the toolchain
1828
+ // (running `lean` on every library file each time an agent asked "what can I reuse?"
1829
+ // would be slow and surprising). Per-object run results live in the object records
1830
+ // and are shown in Formal/Index.md.
1831
+ const scan = async (dirAbs, dirRel, kindLabel) => {
1832
+ const rows = []
1833
+ try {
1834
+ const t = await fs.resolve(dirAbs)
1835
+ if (await fs.stat(t) === undefined) return rows
1836
+ const entries = await fs.listDir(t)
1837
+ for (const e of entries || []) {
1838
+ if (!e || e.type !== 'file' || !/\.lean$/.test(String(e.name))) continue
1839
+ const rel = dirRel + '/' + e.name
1840
+ const txt = (await readTextAbs(dirAbs + '/' + e.name)) || ''
1841
+ const name = String(e.name).replace(/\.lean$/, '')
1842
+ const first = (txt.split('\n').filter((l) => l.trim() && !/^\s*(\/\/|--|import)/.test(l))[0] || '').trim().slice(0, 110)
1843
+ rows.push('| ' + name + ' | ' + rel + ' | ' + kindLabel + ' | ' + first.replace(/\|/g, '/') + ' |')
1844
+ }
1845
+ } catch (e) { /* listing is best-effort */ }
1846
+ return rows
1847
+ }
1848
+ const libRows = await scan(formalLibRoot(), 'Formal/Lib', 'def')
1849
+ await writeTextAbs(vibeRoot() + '/Formal/Lib/Index.md', ['# 可复用 Lean 定义库(跨项目)|' + instituteName, '',
1850
+ '> 写新定义之前先查这里:能复用就不要重新定义。', '',
1851
+ '| 名称 | 文件 | 类别 | 摘要 |', '|---|---|---|---|']
1852
+ .concat(libRows.length ? libRows : ['| (暂无) | | | |']).join('\n') + '\n')
1853
+ const provedRows = await scan(formalProvedRoot(), 'Formal/Proved', 'lemma')
1854
+ await writeTextAbs(vibeRoot() + '/Formal/Proved/Index.md', ['# 已成立的 Lean 命题 / 引理(机器已核对,可跨项目复用)|' + instituteName, '',
1855
+ '> 这些文件是通过内核检查的引理,可直接 import 复用。', '',
1856
+ '| 名称 | 文件 | 类别 | 陈述 |', '|---|---|---|---|']
1857
+ .concat(provedRows.length ? provedRows : ['| (暂无) | | | |']).join('\n') + '\n')
1858
+ await writeFormalIndex()
1859
+ await writeFormalTodo()
1860
+ return { lib: libRows.length, proved: provedRows.length, objects: Object.keys(formalRecords()).length }
1861
+ }
1862
+ // A vibe-root-relative path → absolute. Used for the GLOBAL library, which sits beside
1863
+ // the project tree rather than inside the current institute.
1864
+ function instRootless(rel) { return vibeRoot() + '/' + String(rel).replace(/^\.\//, '') }
1865
+
1866
+ // Execute one Lean file through the toolchain, record the run (optionally against an
1867
+ // object), refresh the indexes, and report the outcome verbatim. Deliberately called
1868
+ // even from `off` mode: a human debugging their toolchain may want it.
1869
+ async function leanRunTool(memberId, o) {
1870
+ const args = o || {}
1871
+ const run = await leanRunFile(String(args.file || ''), args.timeout_ms)
1872
+ if (run.ok || run.file) {
1873
+ if (String(args.target || '').trim()) await formalSetRun(String(args.target), run)
1874
+ await writeFormalIndex()
1875
+ }
1876
+ if (run.ok) {
1877
+ await saveChatLine('【形式化】' + (memberId || 'office') + ' 运行 Lean 通过:' + run.file
1878
+ + '(' + (run.ms / 1000).toFixed(1) + 's)' + (args.target ? '|对象 ' + args.target + ' 记为已尝试/已通过' : ''))
1879
+ }
1880
+ return Object.assign({ ok: !!run.ok }, run, {
1881
+ hint: run.ok
1882
+ ? '通过。若是某个对象的证明,请用 lean_archive kind=\'proof\' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind=\'def\'/\'lemma\' 归档到全局库。'
1883
+ : '未通过。请按上面的编译器输出修复后重跑;若判断无法完成,用 lean_archive kind=\'blocked\' 记录原因。',
1884
+ })
1885
+ }
1886
+ async function leanArchive(memberId, o) {
1887
+ const args = o || {}
1888
+ const kind = String(args.kind || '')
1889
+ const content = typeof args.content === 'string' ? args.content : undefined
1890
+ const from = args.from ? String(args.from) : ''
1891
+ if (kind === 'def' || kind === 'lemma') {
1892
+ const name = idSafe(String(args.name || ''))
1893
+ if (!name) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'name is required for a reusable definition/lemma' }
1894
+ let body = content
1895
+ if (body === undefined && from) {
1896
+ const srcAbs = leanAbsPath(from)
1897
+ if (srcAbs === null) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'from must be a .lean file inside the workspace (got ' + from + ')' }
1898
+ body = await readTextAbs(srcAbs)
1899
+ }
1900
+ if (body === undefined) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'provide content, or from=<existing .lean file>' }
1901
+ const rel = 'Formal/' + (kind === 'def' ? 'Lib' : 'Proved') + '/' + name + '.lean'
1902
+ const okWrite = await writeTextAbs(instRootless(rel), body)
1903
+ if (!okWrite) return { ok: false, code: 'V5_WRITE_FAILED', message: 'could not write ' + rel }
1904
+ // The global library sits beside the project tree, so it must be executed through
1905
+ // its ABSOLUTE path (the relative form would resolve inside the institute root).
1906
+ const run = args.run === false ? null : await leanRunFile(instRootless(rel))
1907
+ await rebuildLeanLibIndexes()
1908
+ await saveChatLine('【形式化】' + memberId + ' 归档了' + (kind === 'def' ? '可复用定义' : '已证引理') + ' `' + name + '` → ' + rel
1909
+ + (run ? '(运行 ' + (run.ok ? '通过' : '未通过') + ')' : ''))
1910
+ return { ok: true, kind, name, file: rel, run: run || undefined, note: '已并入全局可复用库,后续项目可直接 import 复用' }
1911
+ }
1912
+ if (kind === 'proof') {
1913
+ const target = idSafe(String(args.target || ''))
1914
+ if (!target) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'target is required for kind=proof' }
1915
+ let body = content
1916
+ if (body === undefined && from) {
1917
+ const srcAbs = leanAbsPath(from)
1918
+ if (srcAbs === null) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'from must be a .lean file inside the workspace (got ' + from + ')' }
1919
+ body = await readTextAbs(srcAbs)
1920
+ }
1921
+ if (body === undefined) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'provide content, or from=<existing .lean file>' }
1922
+ const workRel = 'Formal/' + target + '.lean'
1923
+ if (!await writeTextRel(workRel, body)) return { ok: false, code: 'V5_WRITE_FAILED', message: 'could not write ' + workRel }
1924
+ const run = await leanRunFile(workRel)
1925
+ const prev = formalOf(target)
1926
+ const passed = !!run.ok
1927
+ const rec = Object.assign({}, prev, {
1928
+ status: passed ? 'passed' : 'attempted',
1929
+ file: workRel,
1930
+ proof: passed ? 'Verified/Lean/' + target + '.lean' : (prev.proof || ''),
1931
+ decision: 'used',
1932
+ note: String(args.note || prev.note || ''),
1933
+ run: { at: now(), ok: !!run.ok, exitCode: run.exitCode === undefined ? null : run.exitCode, ms: run.ms || 0, stdoutTail: tail(run.stdout, 800), stderrTail: tail(run.stderr, 800) },
1934
+ updatedAt: now(),
1935
+ })
1936
+ if (passed) await writeTextRel('Verified/Lean/' + target + '.lean', body)
1937
+ await putFormal(target, rec)
1938
+ await rebuildLeanLibIndexes()
1939
+ await saveChatLine('【形式化】' + memberId + ' 为 ' + target + ' 归档形式化证明 ' + workRel
1940
+ + '(运行 ' + (passed ? '**通过**,已归档到 ' + rec.proof + ',验证转为忠实性审查' : '**未通过**:' + tail(run.stderr || run.message, 160)) + ')')
1941
+ return { ok: true, kind, target, file: workRel, proof: rec.proof, passed, run, status: rec.status }
1942
+ }
1943
+ if (kind === 'blocked') {
1944
+ const target = idSafe(String(args.target || ''))
1945
+ if (!target) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'target is required for kind=blocked' }
1946
+ const note = String(args.note || '').trim()
1947
+ if (!note) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)——"因难度决定不做形式化"必须显式、可审计' }
1948
+ const prev = formalOf(target)
1949
+ const rec = Object.assign({}, prev, { status: 'blocked', decision: 'blocked', note, updatedAt: now() })
1950
+ await putFormal(target, rec)
1951
+ await rebuildLeanLibIndexes()
1952
+ await saveChatLine('【形式化】' + memberId + ' 记录 ' + target + ' 形式化阻塞:' + note)
1953
+ return { ok: true, kind, target, status: 'blocked', note }
1954
+ }
1955
+ return { ok: false, code: 'V5_INVALID_ARGUMENT', message: "kind must be 'def' | 'lemma' | 'proof' | 'blocked'" }
1956
+ }
1957
+
1958
+
1545
1959
  // ---- context / compaction accounting ---------------------------------
1546
1960
  // A SHORT core-rules recap, injected ONLY (a) right after a REAL compaction, or
1547
1961
  // (b) in the same wake as a soft-compact directive — never on every round. The
@@ -2193,7 +2607,19 @@ export function apply(ctx) {
2193
2607
  try {
2194
2608
  const j = judgeVerdict(vs)
2195
2609
  if (j.outcome === 'true' || j.outcome === 'false') {
2196
- await closeVerify(vs, j.outcome === 'true', j)
2610
+ // ── the `require` gate (§8 of docs/formal-verification.md) ────────────────
2611
+ // A unanimous boolean verdict is a CONSENSUS, not a proof. In `require` mode the
2612
+ // institute has decided that consensus alone may not be promoted to Verified/:
2613
+ // the object must also be either machine-checked (`passed`) or carry an explicit,
2614
+ // reasoned "we judged this infeasible" record (`blocked`). The gate never wedges
2615
+ // the run — it records 未定论 + a formalization TODO so the institute can keep
2616
+ // going and formalize later.
2617
+ const rec = formalOf(vs.target)
2618
+ if (formalMode() === 'require' && !formalGateOk(rec)) {
2619
+ await deferForFormal(vs, j, isTrueVote(j))
2620
+ } else {
2621
+ await closeVerify(vs, j.outcome === 'true', j)
2622
+ }
2197
2623
  } else if (vs.round >= Math.max(1, Math.floor(Number(params.verdictMaxRounds) || 3))) {
2198
2624
  await finalizeUndecided(vs, j)
2199
2625
  } else {
@@ -2218,6 +2644,30 @@ export function apply(ctx) {
2218
2644
  }
2219
2645
  await scheduleNext()
2220
2646
  }
2647
+ const isTrueVote = (j) => j && j.outcome === 'true'
2648
+ // `require` mode withheld the verdict: record it as 未定论 with a machine-readable
2649
+ // reason, put the object on the formalization TODO, and say so in the group chat. The
2650
+ // object keeps its mean probability and its debate record, so nothing is lost.
2651
+ async function deferForFormal(vs, j, isTrue) {
2652
+ const rec = formalOf(vs.target)
2653
+ const why = 'formal-required:尚未取得 Lean 形式化通过,也没有显式阻塞记录(当前状态 ' + (rec.status || 'none') + ')'
2654
+ await writeDebateDoc(vs, false, j)
2655
+ await putVerdict(vs.target, Object.assign({}, vs, {
2656
+ closed: true, outcome: 'undecided', reason: why, mean: j.mean, formalDeferred: true,
2657
+ m: j.m, P: j.P, bTrue: j.bTrue, bFalse: j.bFalse, abstain: j.abstain, closedAt: now(),
2658
+ }))
2659
+ const list = formalTodo().filter((t) => t.id !== vs.target)
2660
+ list.push({ id: vs.target, at: now(), why, verdict: isTrue ? 1 : 0 })
2661
+ await putFormal(vs.target, rec.status === 'none' ? { status: 'none', deferredAt: now() } : rec, list)
2662
+ await writeFormalTodo()
2663
+ await writeFormalIndex()
2664
+ await saveChatLine('【形式化】' + vs.target + ' 的表决结果为 ' + (isTrue ? '真' : '假')
2665
+ + ',但 **require 模式**要求先有 Lean 通过或显式阻塞记录,因此本轮**不定论**(已记入 Formal/TODO.md)。'
2666
+ + '请完成形式化(vibe_v5_lean_archive kind=\'proof\')或记录阻塞原因(kind=\'blocked\')后重新提议验证。')
2667
+ await markProgress()
2668
+ await armNextVerify()
2669
+ await scheduleNext()
2670
+ }
2221
2671
  async function finalizeUndecided(vs, j) {
2222
2672
  await writeDebateDoc(vs, false, j)
2223
2673
  // Keep it in the library with the group's MEAN probability — the design's
@@ -2254,6 +2704,16 @@ export function apply(ctx) {
2254
2704
  let beginLock = false
2255
2705
  async function armNextVerify() {
2256
2706
  dbg.arm += 1
2707
+ // A MEETING and a VERIFICATION never run at the same time. `startMeeting` already
2708
+ // parks a meeting while a verification is in flight; this is the missing mirror for
2709
+ // the other direction. Without it, a member replying `propose_verify` while a
2710
+ // meeting was live started a second consensus process immediately, because
2711
+ // `maybeQueueVerify` calls this directly (bypassing schedulePass, whose meeting
2712
+ // check is what used to hide the asymmetry). The meeting's watchdog clock would
2713
+ // then be starved while two coordination processes competed for the same members.
2714
+ // The proposal stays in the queue; schedulePass reaches this again once the
2715
+ // meeting is over.
2716
+ if (meeting) return
2257
2717
  // Only one begin may be in flight. Without this, two callers (a scheduling pass
2258
2718
  // and a fresh proposal) could both pass the `currentVerify()` check before either
2259
2719
  // has published its verdict record and would start the SAME object twice.
@@ -2313,6 +2773,9 @@ export function apply(ctx) {
2313
2773
  '- 来源: ' + (isTrue ? j.bTrue : j.bFalse) + ' 名有表决权者一致判' + (isTrue ? '真' : '假') + '(m=' + j.m + ')',
2314
2774
  '- 表决者: ' + j.voters.join('、'),
2315
2775
  '- 弃权: ' + j.abstain + '|全组平均概率: ' + Number(j.mean).toFixed(2),
2776
+ // The formal record travels WITH the conclusion: a reader of the card must be able
2777
+ // to see how strong the result really is (machine-checked vs consensus-only).
2778
+ ...(formalOn() ? ['- 形式化: ' + formalStatusLine(vs.target) + (formalOf(vs.target).proof ? '(证明:' + formalOf(vs.target).proof + ')' : '')] : []),
2316
2779
  '- 时间: ' + fmtTime(),
2317
2780
  '',
2318
2781
  '## 陈述',
@@ -2933,6 +3396,34 @@ export function apply(ctx) {
2933
3396
  } else await notice(member.id, '标记完成失败:没有任务 ' + String(p.task_done))
2934
3397
  }
2935
3398
  // (5) verification
3399
+ // (4b) Lean formalization signal — the mandatory difficulty judgement. This is the
3400
+ // path that matters in practice: a member that never calls a Lean tool still has to
3401
+ // say "used / blocked", and `require` mode refuses to conclude without it.
3402
+ if (p.formal && typeof p.formal === 'object') {
3403
+ const f = p.formal
3404
+ const target = idSafe(String(f.target || ''))
3405
+ if (target) {
3406
+ const decision = String(f.decision || '').trim()
3407
+ if (decision === 'blocked') {
3408
+ const note = String(f.note || '').trim()
3409
+ if (!note) await notice(member.id, 'formal.decision=\'blocked\' 必须写明 note(难度判断/阻塞原因)——本次未记录。')
3410
+ else {
3411
+ const r = await leanArchive(member.id, { kind: 'blocked', target, note })
3412
+ if (r.ok === false) await notice(member.id, '记录形式化阻塞失败(' + (r.code || '') + '):' + (r.message || ''))
3413
+ }
3414
+ } else if (decision === 'used') {
3415
+ const file = String(f.file || ('Formal/' + target + '.lean'))
3416
+ const prev = formalOf(target)
3417
+ await putFormal(target, Object.assign({}, prev, {
3418
+ status: prev.status === 'passed' || prev.status === 'blocked' ? prev.status : 'attempted',
3419
+ file, decision: 'used', note: String(f.note || prev.note || ''), updatedAt: now(),
3420
+ }))
3421
+ await writeFormalIndex()
3422
+ } else if (decision) {
3423
+ await notice(member.id, 'formal.decision 只能是 \'used\' 或 \'blocked\'(收到 ' + decision + ')')
3424
+ }
3425
+ }
3426
+ }
2936
3427
  if (p.propose_verify) {
2937
3428
  const pv = typeof p.propose_verify === 'string' ? { target: p.propose_verify } : p.propose_verify
2938
3429
  if (pv && pv.target) {
@@ -3065,10 +3556,10 @@ export function apply(ctx) {
3065
3556
  const out = {}
3066
3557
  const ints = ['researcherCount', 'quorumCap', 'verdictMaxRounds', 'maxTempPerMember', 'maxTempTotal',
3067
3558
  'compactThreshold', 'compactAfterRounds', 'maxParallel', 'activityTimeoutMs', 'stallAutoMeetingMs',
3068
- 'chatDigestMs', 'chatDigestMax', 'meetingKeepEvery']
3559
+ 'chatDigestMs', 'chatDigestMax', 'meetingKeepEvery', 'leanTimeoutMs']
3069
3560
  const bools = ['academician', 'academicianLeads', 'memberMayRejectAssign']
3070
- const strs = ['quorumMode', 'provider', 'model', 'staffPersona']
3071
- const arrs = ['toolAllow', 'toolDeny', 'tempToolAllow', 'tempToolDeny']
3561
+ const strs = ['quorumMode', 'provider', 'model', 'staffPersona', 'formalVerify', 'leanCommand']
3562
+ const arrs = ['toolAllow', 'toolDeny', 'tempToolAllow', 'tempToolDeny', 'leanArgs']
3072
3563
  for (const k of ints) if (input[k] !== undefined) { const n = Math.floor(Number(input[k])); if (Number.isFinite(n)) out[k] = n }
3073
3564
  for (const k of bools) if (input[k] !== undefined) out[k] = (input[k] === true || input[k] === 'true')
3074
3565
  for (const k of strs) if (input[k] !== undefined) out[k] = String(input[k])
@@ -3078,11 +3569,18 @@ export function apply(ctx) {
3078
3569
  out[k] = Array.isArray(v) ? v.map(String).filter((x) => x.trim()) : String(v).split(',').map((x) => x.trim()).filter(Boolean)
3079
3570
  }
3080
3571
  if (out.quorumMode !== undefined && out.quorumMode !== 'm-unanimous' && out.quorumMode !== 'all-unanimous') out.quorumMode = 'm-unanimous'
3572
+ // An unknown Lean mode must degrade to 'off' (the no-op), never to a stronger mode:
3573
+ // a typo silently forcing formal verification would block every conclusion.
3574
+ if (out.formalVerify !== undefined) {
3575
+ out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'off'
3576
+ }
3577
+ if (out.leanCommand !== undefined && !String(out.leanCommand).trim()) out.leanCommand = 'lean'
3081
3578
  // Guard every duration against a negative/NaN value: such a value would make a
3082
3579
  // watchdog fire instantly and abandon all consensus (v4 §30-T41).
3083
- for (const k of ['activityTimeoutMs', 'stallAutoMeetingMs', 'chatDigestMs']) {
3580
+ for (const k of ['activityTimeoutMs', 'stallAutoMeetingMs', 'chatDigestMs', 'leanTimeoutMs']) {
3084
3581
  if (out[k] !== undefined && !(out[k] > 0)) delete out[k]
3085
3582
  }
3583
+ if (out.leanTimeoutMs !== undefined) out.leanTimeoutMs = Math.max(1000, out.leanTimeoutMs)
3086
3584
  if (out.researcherCount !== undefined && out.researcherCount < 0) out.researcherCount = 0
3087
3585
  if (out.quorumCap !== undefined && out.quorumCap < 1) out.quorumCap = 1
3088
3586
  return out
@@ -3111,6 +3609,8 @@ export function apply(ctx) {
3111
3609
  maxParallel: params.maxParallel, activityTimeoutMs: params.activityTimeoutMs,
3112
3610
  stallAutoMeetingMs: params.stallAutoMeetingMs, chatDigestMs: params.chatDigestMs,
3113
3611
  chatDigestMax: params.chatDigestMax, meetingKeepEvery: params.meetingKeepEvery,
3612
+ formalVerify: params.formalVerify, leanCommand: params.leanCommand,
3613
+ leanArgs: params.leanArgs, leanTimeoutMs: params.leanTimeoutMs,
3114
3614
  provider: params.provider, model: params.model,
3115
3615
  toolAllow: params.toolAllow, toolDeny: params.toolDeny,
3116
3616
  }
@@ -3375,6 +3875,18 @@ export function apply(ctx) {
3375
3875
  verified: Object.keys(s.verdicts).filter((k) => s.verdicts[k] && s.verdicts[k].closed && s.verdicts[k].outcome !== 'undecided'),
3376
3876
  undecided: Object.keys(s.verdicts).filter((k) => { const v = s.verdicts[k]; return v && v.closed && v.outcome === 'undecided' }),
3377
3877
  solveVotes: Array.from(solveVotes.entries()).map(([k, v]) => k + '=' + v),
3878
+ // Lean formal verification: mode + per-object status + the formalization TODO. This
3879
+ // is how an office/human audits "did we really get strict proofs, or only consensus?"
3880
+ formal: {
3881
+ mode: formalMode(),
3882
+ objects: Object.keys(formalRecords()).map((k) => {
3883
+ const r = formalRecords()[k] || {}
3884
+ return { target: k, status: r.status, file: r.file || '', proof: r.proof || '', note: r.note || '', run: r.run ? { ok: r.run.ok, exitCode: r.run.exitCode, ms: r.run.ms } : null }
3885
+ }),
3886
+ passed: Object.keys(formalRecords()).filter((k) => (formalRecords()[k] || {}).status === 'passed'),
3887
+ blocked: Object.keys(formalRecords()).filter((k) => (formalRecords()[k] || {}).status === 'blocked'),
3888
+ todo: formalTodo(),
3889
+ },
3378
3890
  lastProgressAt, params: visibleParams(),
3379
3891
  }
3380
3892
  }
@@ -3426,6 +3938,16 @@ export function apply(ctx) {
3426
3938
  L.push('- 历史会议:' + s.meetings.length + ' 次|辩论录:' + s.debates.length + ' 份')
3427
3939
  L.push('- 解决票:' + (solveVotes.size ? Array.from(solveVotes.entries()).map(([k, v]) => k + '=' + v).join('、') : '(无)'))
3428
3940
  L.push('')
3941
+ L.push('## Lean 形式化')
3942
+ if (!formalOn()) L.push('- 未启用(`formalVerify` = off;可用 vibe_v5_set 切到 encourage / require)')
3943
+ else {
3944
+ L.push('- 模式:' + formalMode() + '(' + (formalMode() === 'require' ? '强制:定论前必须有 Lean 通过或显式阻塞记录' : '鼓励:按实现难度自行决定') + ')')
3945
+ L.push('- 已通过:' + (Object.keys(s.formal || {}).filter((k) => (s.formal || {})[k].status === 'passed').join('、') || '(无)'))
3946
+ L.push('- 已记录阻塞:' + (Object.keys(s.formal || {}).filter((k) => (s.formal || {})[k].status === 'blocked').join('、') || '(无)'))
3947
+ L.push('- 形式化待办:' + ((s.todo || []).map((t) => t.id).join('、') || '(无)'))
3948
+ L.push('- 可复用库:VibeMath/Formal/{Lib,Proved}/(跨项目)|本所形式化:Formal/|归档证明:Verified/Lean/')
3949
+ }
3950
+ L.push('')
3429
3951
  L.push('## 文件位置')
3430
3952
  L.push('- 根目录:' + instRoot())
3431
3953
  L.push('- 已确立:Verified/|成员库:Members/<id>/|群聊:Shared/Chat/|会议:Shared/Meetings/|辩论:Shared/Debates/')
@@ -3485,6 +4007,10 @@ export function apply(ctx) {
3485
4007
  taskCreate, taskList: listTasks, getTask, taskUpdate, taskAssign, taskPrioritize,
3486
4008
  // libraries
3487
4009
  publishProgress, recordCard, readLibrary,
4010
+ // Lean formal verification (docs/formal-verification.md)
4011
+ formalMode, formalOn, formalRecords, formalTodo, formalOf, rebuildLeanLibIndexes,
4012
+ leanArchive, leanRunTool, writeFormalIndex, writeFormalTodo,
4013
+ leanRunToolApi: async (relPath, timeoutMs) => await leanRunFile(relPath, timeoutMs),
3488
4014
  // consensus / meetings
3489
4015
  maybeQueueVerify, castVerdict, currentVerify, hasVerifyInFlight, startMeeting, quorumM, voterCount,
3490
4016
  // authorization helpers (used by tool handlers)
@@ -3525,12 +4051,14 @@ export function apply(ctx) {
3525
4051
  registerTool('vibe_v5_stop', 'Stop the institute: interrupt every member, clear coordination state, and release their child sessions.', objParams({}), (s) => s.initStop())
3526
4052
  registerTool('vibe_v5_status', 'Machine-readable institute status (members, tasks, quorum, meetings, verification, mail).', objParams({}), (s) => s.status())
3527
4053
  registerTool('vibe_v5_report', 'Human-readable institute report (staffing, tasks, consensus, meetings, file locations).', objParams({}), (s) => s.report())
3528
- registerTool('vibe_v5_set', 'Tune institute parameters (persisted in the session-log projection). provider/model override staff LLM routes (empty = inherit the office route). toolAllow/toolDeny restrict PERMANENT staff tools; tempToolAllow/tempToolDeny restrict temp workers. quorumCap sets m = min(quorumCap, voters); quorumMode "m-unanimous" (v5) or "all-unanimous" (v4 legacy).', objParams({
4054
+ registerTool('vibe_v5_set', 'Tune institute parameters (persisted in the session-log projection). provider/model override staff LLM routes (empty = inherit the office route). toolAllow/toolDeny restrict PERMANENT staff tools; tempToolAllow/tempToolDeny restrict temp workers. quorumCap sets m = min(quorumCap, voters); quorumMode "m-unanimous" (v5) or "all-unanimous" (v4 legacy). formalVerify: "off" (default, no extra requirement) | "encourage" (agents decide by implementation difficulty whether to formalize in Lean; a passing Lean run turns the vote into a FIDELITY review of the Lean statements) | "require" (same, plus a gate: a true/false verdict is withheld as undecided until the object is Lean-passed or has an explicit reasoned blocker record).', objParams({
3529
4055
  academician: B, academicianLeads: B, memberMayRejectAssign: B, researcherCount: I,
3530
4056
  quorumCap: I, quorumMode: S, verdictMaxRounds: I,
3531
4057
  maxTempPerMember: I, maxTempTotal: I,
3532
4058
  compactThreshold: I, compactAfterRounds: I, maxParallel: I,
3533
4059
  activityTimeoutMs: I, stallAutoMeetingMs: I, chatDigestMs: I, chatDigestMax: I, meetingKeepEvery: I,
4060
+ formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] },
4061
+ leanCommand: S, leanArgs: SA, leanTimeoutMs: I,
3534
4062
  provider: S, model: S, staffPersona: S, toolAllow: SA, toolDeny: SA, tempToolAllow: SA, tempToolDeny: SA,
3535
4063
  }), (s, a) => s.setParams(a))
3536
4064
  registerTool('vibe_v5_message', 'Relay a message from the office/human into the institute (to a member id, to "all", or to "voters").', objParams({ to: S, content: S }, ['to', 'content']), (s, a) => {
@@ -3596,6 +4124,21 @@ export function apply(ctx) {
3596
4124
  parts.push('(见 Shared/Chat/ 目录;未读消息 ' + st.chat.pending + ' 条)')
3597
4125
  if (st.verify) parts.push('## 进行中表决\n' + JSON.stringify(st.verify))
3598
4126
  if (st.meeting) parts.push('## 进行中会议\n' + JSON.stringify(st.meeting))
4127
+ // The academician supervises progress; in a formalization mode "which objects still lack
4128
+ // a Lean result" is exactly the kind of bottleneck it must see to organise the work.
4129
+ if (s.formalOn()) {
4130
+ parts.push('## Lean 形式化(' + s.formalMode() + ')')
4131
+ const recs = s.formalRecords()
4132
+ const keys = Object.keys(recs)
4133
+ if (!keys.length) parts.push('- 尚无对象被形式化')
4134
+ for (const k of keys) {
4135
+ const r = recs[k] || {}
4136
+ parts.push('- ' + k + '|' + (r.status || 'none') + (r.proof ? '|' + r.proof : '') + (r.note ? '|' + r.note : ''))
4137
+ }
4138
+ const todo = s.formalTodo()
4139
+ if (todo.length) parts.push('- 形式化待办(定论被搁置):' + todo.map((t) => t.id).join('、') + '(见 Formal/TODO.md)')
4140
+ parts.push('- 可复用库:vibe_v5_lean_lib 可列出 Formal/Lib 与 Formal/Proved')
4141
+ }
3599
4142
  parts.push('## 停滞提示')
3600
4143
  const idleFor = Date.now() - st.lastProgressAt
3601
4144
  parts.push('- 距上次实质进展:' + Math.round(idleFor / 1000) + ' 秒')
@@ -3605,6 +4148,26 @@ export function apply(ctx) {
3605
4148
  registerTool('vibe_v5_prioritize', '(academician) Set institute-wide priorities: an ordered list of {task_id, priority} plus WHY. This orders work only — it never changes what is true.', objParams({ order: { type: 'array', items: { type: 'object' } }, why: S }), (s, a, x) => s.taskPrioritize(s.memberIdOfAgent(x), a))
3606
4149
  registerTool('vibe_v5_nudge', '(academician) Supervise: wake one member with a stated reason and a concrete suggested next step. Prefer a specific next step over a bare "hurry up".', objParams({ to: S, why: S, next_step: S }, ['to', 'why']), (s, a, x) => s.nudge(s.memberIdOfAgent(x), a))
3607
4150
 
4151
+ // ── Lean formal verification (docs/formal-verification.md) ────────────────
4152
+ // These three tools are registered UNCONDITIONALLY: tool registration is static (a
4153
+ // dynamic registration would depend on a runtime knob and break the effect discipline),
4154
+ // while the MODE only decides whether the framework TELLS members about them. In 'off'
4155
+ // mode they still work if a human or agent calls them deliberately.
4156
+ registerTool('vibe_v5_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.', objParams({ file: S, target: S, timeout_ms: I }, ['file']), (s, a, x) => s.leanRunTool(s.memberIdOfAgent(x), a))
4157
+ registerTool('vibe_v5_lean_archive', '(member) Archive Lean code. kind="def": a REUSABLE definition/object/assumption → the global cross-project library (Formal/Lib). kind="lemma": a machine-checked lemma → Formal/Proved. kind="proof": the formal proof of a project object → Formal/<target>.lean, and (when the run passes) also Verified/Lean/<target>.lean, marking the object Lean-passed. kind="blocked": record an explicit, reasoned "cannot/not worth formalizing" decision (note required).', objParams({ kind: { type: 'string', enum: ['def', 'lemma', 'proof', 'blocked'] }, name: S, target: S, content: S, from: S, note: S, run: B }, ['kind']), (s, a, x) => s.leanArchive(s.memberIdOfAgent(x), a))
4158
+ registerTool('vibe_v5_lean_lib', '(member) List (and by default rebuild) the Lean reuse library: your institute\'s Formal/Index.md, plus the global cross-project Formal/Lib and Formal/Proved indexes. Look here BEFORE writing a new definition so you reuse instead of redefining.', objParams({ refresh: B }), async (s, a) => {
4159
+ const r = a && a.refresh === false ? { lib: null, proved: null, objects: Object.keys(s.formalRecords()).length } : await s.rebuildLeanLibIndexes()
4160
+ const st = s.status()
4161
+ return {
4162
+ ok: true, mode: s.formalMode(), rebuilt: !(a && a.refresh === false),
4163
+ counts: r, todo: s.formalTodo(),
4164
+ objects: Object.keys(s.formalRecords()).map((k) => ({ target: k, status: (s.formalRecords()[k] || {}).status, file: (s.formalRecords()[k] || {}).file, proof: (s.formalRecords()[k] || {}).proof, note: (s.formalRecords()[k] || {}).note })),
4165
+ paths: { project: 'Formal/(相对研究所根)', lib: 'VibeMath/Formal/Lib/', proved: 'VibeMath/Formal/Proved/', proofs: 'Verified/Lean/' },
4166
+ hint: '复用优先:先在 Lib/ 里找现成定义;新定义用 lean_archive kind=\'def\' 归档,已证引理用 kind=\'lemma\'。',
4167
+ verify: st.verify ? st.verify.target : null,
4168
+ }
4169
+ })
4170
+
3608
4171
  // ── /v5 slash command ────────────────────────────────────────────────────
3609
4172
  ctx.effect(() => commands.register({
3610
4173
  name: 'v5', description: 'control the Vibe Math V5 research institute',