dsh-vibe-math 2.2.2 → 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 (36) hide show
  1. package/AUDIT-CHECKLIST.md +28 -0
  2. package/README.md +98 -1
  3. package/RELEASE-NOTES-2.3.0.md +207 -0
  4. package/audit-formal-sensitivity.mjs +247 -0
  5. package/audit-persona-sensitivity.mjs +249 -0
  6. package/audit-persona-surface.test.mjs +349 -0
  7. package/audit-v5-integrity.mjs +40 -1
  8. package/audit-v5-sensitivity.mjs +77 -6
  9. package/docs/formal-verification.md +321 -0
  10. package/docs/generate_framework_diagram_v5.mjs +22 -16
  11. package/formal-verify-v2.test.mjs +672 -0
  12. package/formal-verify-v3.test.mjs +824 -0
  13. package/formal-verify-v4.test.mjs +603 -0
  14. package/formal-verify-v5.test.mjs +526 -0
  15. package/package.json +15 -2
  16. package/prompt-corpus-persona/persona-corpus.json +32 -0
  17. package/prompt-corpus-persona/persona-corpus.md +674 -0
  18. package/prompt-corpus-v3/formal-verify-v3.json +280 -0
  19. package/prompt-corpus-v3/formal-verify-v3.md +2826 -0
  20. package/prompt-corpus-v5/prompt-corpus-v5.json +75 -9
  21. package/prompt-corpus-v5/prompt-corpus-v5.md +384 -65
  22. package/prompt-v5-integrity.test.mjs +111 -10
  23. package/vibe-math-v2/agent.cordis.yml +40 -2
  24. package/vibe-math-v2/vibe-math-v2.js +627 -19
  25. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +145 -1
  26. package/vibe-math-v3/agent.cordis.yml +46 -2
  27. package/vibe-math-v3/vibe-math-v3.js +749 -21
  28. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +87 -2
  29. package/vibe-math-v4/agent.cordis.yml +46 -4
  30. package/vibe-math-v4/vibe-math-v4.js +652 -15
  31. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +226 -0
  32. package/vibe-math-v5/agent.cordis.yml +41 -5
  33. package/vibe-math-v5/vibe-math-v5.js +562 -9
  34. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +108 -4
  35. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +57 -0
  36. package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +51 -46
@@ -46,12 +46,26 @@ export function apply(ctx) {
46
46
  // toolFilter (scoped tools.restrict() in the child). Empty = inherit all tools.
47
47
  // CAUTION: only set one of these; an empty allow:[] would deny EVERY tool.
48
48
  toolAllow: [], toolDeny: [],
49
+ // ---- Lean formal verification (docs/formal-verification.md) ----------------
50
+ // 'off' (default, a TRUE no-op) | 'encourage' | 'require'. The MODE is dynamic: every
51
+ // mode-dependent prompt string is computed from params.formalVerify at the moment the
52
+ // prompt is built, never frozen into a brief, so switching the knob takes effect on the
53
+ // very next wake.
54
+ formalVerify: 'off',
55
+ leanCommand: 'lean',
56
+ leanArgs: [], // inserted BEFORE the file name (e.g. ['env','lean'] with lake)
57
+ leanTimeoutMs: 120000,
49
58
  }
50
59
  let params = Object.assign({}, DEFAULT_PARAMS)
51
60
  let running = false, autoDone = false, phase = 'idle'
52
61
  let residents = new Map(), mailboxes = new Map(), taskboard = [], decisions = []
53
62
  let meetings = [], reports = [], activityLog = []
54
63
  let problemText = '', problemId = 'problem', runId = 'run-' + shortId()
64
+ // Lean formalization records, keyed by object id. v4 has no session-log projection, so this
65
+ // is persisted through v4's OWN durable State/*.json mechanism (State/formal.json) and must
66
+ // survive `resume` — otherwise a require-mode object would lose the very record that decides
67
+ // whether its verdict may take effect.
68
+ let formal = {}, formalTodos = []
55
69
  let meetingState = null, verifyState = null, pendingVerify = [], pendingMeeting = null // pendingVerify: FIFO queue (several residents may independently propose different objects before any verify runs — a single slot silently DROPPED all but the last proposal)
56
70
  let busy = new Set(), wakeKind = new Map(), currentResident = ''
57
71
  let finalizeLock = null // 'meeting'|'verify' while a consensus finalize is running (reentry guard)
@@ -200,7 +214,15 @@ export function apply(ctx) {
200
214
  } catch(e){}
201
215
  return true
202
216
  }
203
- async function ensureDirs(){ const base=frameworkRoot(); const dirs=['Problems','Progress','Propos','Methods','Subproblems','Shared/meetings','Shared/debates','Verified/命题','Verified/问题','Reliable','Notes','State']; return await runShell(mkdirCmd([vibeRoot()+'/Projects'].concat(dirs.map(d=>base+'/'+d)))) }
217
+ async function ensureDirs(){
218
+ const base=frameworkRoot()
219
+ const dirs=['Problems','Progress','Propos','Methods','Subproblems','Shared/meetings','Shared/debates','Verified/命题','Verified/问题','Verified/Lean','Formal','Reliable','Notes','State']
220
+ // The GLOBAL reuse library (Formal/Lib + Formal/Proved) deliberately lives beside the
221
+ // project tree, NOT inside it: cross-project reuse is the whole point (spec §3). It is
222
+ // created here so the first `lean_archive kind='def'` never has to invent its parent.
223
+ const globalDirs=['Formal/Lib','Formal/Proved']
224
+ return await runShell(mkdirCmd([vibeRoot()+'/Projects'].concat(dirs.map(d=>base+'/'+d)).concat(globalDirs.map(d=>vibeRoot()+'/'+d))))
225
+ }
204
226
  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 } }
205
227
  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 } }
206
228
  async function readCurrentProject(){ try { const t=await readTextAbs(vibeRoot()+'/.current'); if(t) return String(t).trim() } catch(e){} return currentProject }
@@ -213,12 +235,444 @@ export function apply(ctx) {
213
235
  return obj||{}
214
236
  }
215
237
 
238
+ // ================= Lean formal verification ==============================
239
+ // Contract: docs/formal-verification.md (shared by v2/v3/v4/v5).
240
+ //
241
+ // The point of this feature is a SHIFT IN WHAT MUST BE REVIEWED, not an extra chore.
242
+ // Unanimous consensus answers "do we all believe this?"; a machine-checked Lean
243
+ // development answers "is this true?" and shrinks the open question to the one thing a
244
+ // human (or an agent) can actually audit:
245
+ //
246
+ // do the Lean definitions / objects / conditions / assumptions / conclusion
247
+ // match the proposition as originally stated?
248
+ //
249
+ // So once a Lean run passes, the voting prompt stops asking a resident to redo the
250
+ // derivation and asks for a FIDELITY review. `require` mode makes that concrete: a
251
+ // 真/假 verdict does not take effect until the object is either `passed` (a green run)
252
+ // or carries an explicit, reasoned `blocked` record — "decide by difficulty, but decide
253
+ // out loud, and never silently skip".
254
+ const FORMAL_MODES=['off','encourage','require']
255
+ // Read the mode OFF `params` every single time. Nothing mode-dependent may be cached in a
256
+ // brief/closure: the knob is switchable at runtime and members must see the NEW text on
257
+ // their very next wake.
258
+ const formalMode=()=>{ const m=String(params.formalVerify); return FORMAL_MODES.indexOf(m)!==-1?m:'off' }
259
+ const formalOn=()=>formalMode()!=='off'
260
+ const formalRoot=()=>frameworkRoot()+'/Formal'
261
+ const formalLibRoot=()=>vibeRoot()+'/Formal/Lib'
262
+ const formalProvedRoot=()=>vibeRoot()+'/Formal/Proved'
263
+ const verifiedLeanRoot=()=>frameworkRoot()+'/Verified/Lean'
264
+ const formalRecords=()=>(formal||{})
265
+ const formalTodo=()=>(formalTodos||[])
266
+ function formalOf(target){
267
+ const key=idSafe(String(target==null?'':target))
268
+ if(!key||key==='id') return {status:'none'}
269
+ const r=formalRecords()[key]
270
+ return r||{status:'none'}
271
+ }
272
+ const formalKey=(target)=>{ const k=idSafe(String(target==null?'':target)); return (k&&k!=='id')?k:'' }
273
+ /**
274
+ * Write one object's formal record (and optionally the TODO list) to v4's durable state.
275
+ * ASYNC on purpose: the write goes through v4's per-file serialized `writeJson` queue, and a
276
+ * caller that only fires-and-forgets it could have the process die (or `resume` read the file)
277
+ * before the record lands. Every caller here awaits the result.
278
+ */
279
+ async function putFormal(target,record,todo){
280
+ const key=formalKey(target); if(!key) return false
281
+ formal[key]=record
282
+ if(Array.isArray(todo)) formalTodos=todo
283
+ await saveFormal()
284
+ return true
285
+ }
286
+ function saveFormal(){ return writeJson('State/formal.json',{records:formal,todo:formalTodos}) }
287
+ // `passed` requires a GREEN RUN, not merely an archived file: a proof file that has never
288
+ // been executed proves nothing, so a hand-written file cannot buy its way past the gate.
289
+ const formalGateOk=(rec)=>!!rec&&(rec.status==='passed'||rec.status==='blocked')
290
+ // Human-readable one-liner reused by the Verified card and the index. The exact strings
291
+ // ('Lean 通过' / '阻塞(…)') are part of the card contract (docs §8).
292
+ function formalStatusLine(target){
293
+ const r=formalOf(target)
294
+ if(r.status==='passed') return 'Lean 通过('+(r.proof||r.file||'')+')'
295
+ if(r.status==='blocked') return '阻塞('+(r.note||'未说明')+')'
296
+ if(r.status==='attempted') return '已尝试未通过'
297
+ return '未尝试'
298
+ }
299
+ const runLine=(run)=>run?(run.ok?'ok(exit 0,'+((run.ms||0)/1000).toFixed(1)+'s)':'fail(exit '+String(run.exitCode)+','+((run.ms||0)/1000).toFixed(1)+'s)'):'—'
300
+ function tail(s,n){ const t=String(s==null?'':s); return t.length>n?t.slice(-n):t }
301
+
302
+ // Lexically normalise an absolute path (collapse '.', '..' and duplicate slashes) WITHOUT
303
+ // touching the filesystem. A plain `startsWith(root)` check is NOT enough:
304
+ // "…/VibeMath/Projects/../../../../etc/evil.lean" still starts with the root as a string
305
+ // while resolving outside it.
306
+ function normalizeAbsPath(p){
307
+ const parts=String(p==null?'':p).replace(/\\/g,'/').split('/')
308
+ const out=[]
309
+ for(const seg of parts){
310
+ if(seg===''){ if(out.length===0) out.push(''); continue }
311
+ if(seg==='.') continue
312
+ if(seg==='..'){ if(out.length>1) out.pop(); continue }
313
+ out.push(seg)
314
+ }
315
+ return out.join('/')
316
+ }
317
+ // Resolve a Lean path to an absolute, NORMALISED path provably inside the VibeMath root —
318
+ // or null. Note the boundary is the VibeMath root and NOT the project: the global reuse
319
+ // library deliberately lives at <VibeMath>/Formal/{Lib,Proved}, outside the project tree,
320
+ // so climbing out of the project but staying inside VibeMath is legal. Every Lean file
321
+ // access (run, archive, read) goes through this.
322
+ function leanAbsPath(rel){
323
+ const raw=String(rel==null?'':rel).trim()
324
+ if(!raw) return null
325
+ const abs=(raw.charAt(0)==='/'||/^[a-z]:/i.test(raw))?raw:frameworkRoot()+'/'+raw.replace(/^\.\//,'')
326
+ const norm=normalizeAbsPath(abs)
327
+ const root=normalizeAbsPath(vibeRoot())
328
+ if(norm!==root&&norm.indexOf(root+'/')!==0) return null
329
+ return norm
330
+ }
331
+ // <VibeMath>-relative → absolute. Used for the GLOBAL library, which sits beside the
332
+ // project tree rather than inside it.
333
+ function vibeRelAbs(rel){ return vibeRoot()+'/'+String(rel==null?'':rel).replace(/^\.\//,'') }
334
+
335
+ // Run the toolchain on one file. NEVER throws into the scheduler loop: every failure mode
336
+ // (no service, no executable, spawn failure, timeout, non-zero exit) becomes a readable
337
+ // result, because a thrown error inside an end handler would strand the whole group.
338
+ async function leanRunFile(relPath,timeoutMs){
339
+ const started=now()
340
+ const rel=String(relPath==null?'':relPath).trim()
341
+ if(!rel) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'file is required'}
342
+ // Path guard: only files inside the VibeMath tree may be executed, so a crafted path can
343
+ // never make the framework run something outside the workspace.
344
+ const abs=leanAbsPath(rel)
345
+ if(abs===null) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'Lean file must live under '+vibeRoot().replace(/\\/g,'/')+'/ (got '+rel+')',file:rel}
346
+ if(!/\.lean$/.test(abs)) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'only .lean files can be executed',file:rel}
347
+ if(await readTextAbs(abs)===undefined) return {ok:false,code:'V4_NOT_FOUND',message:'no such file: '+rel,file:rel}
348
+ const sub=subprocessOf()
349
+ if(sub===undefined||typeof sub.spawn!=='function'){
350
+ return {ok:false,code:'NO_SUBPROCESS',message:'the host exposes no subprocess service; Lean cannot be executed here',file:rel,ms:0}
351
+ }
352
+ const cap=Math.max(1000,Math.floor(Number(timeoutMs))||Math.floor(Number(params.leanTimeoutMs))||120000)
353
+ let exe
354
+ try { exe=await sub.resolveExecutable(String(params.leanCommand||'lean')) }
355
+ catch(e){
356
+ return {ok:false,code:'LEAN_NOT_FOUND',message:'cannot resolve "'+String(params.leanCommand||'lean')+'": '+String((e&&e.message)||e)+' — 仍可把形式化代码写下来归档,但无法在此宿主上执行',file:rel,ms:now()-started}
357
+ }
358
+ const argv=[exe].concat((Array.isArray(params.leanArgs)?params.leanArgs:[]).map(String)).concat([abs])
359
+ let handle
360
+ try {
361
+ handle=sub.spawn({ argv, cwd:frameworkRoot(), stdio:{stdin:'ignore',stdout:{maxBytes:64*1024},stderr:{maxBytes:64*1024}}, graceMs:cap })
362
+ } catch(e){ return {ok:false,code:'LEAN_SPAWN_FAILED',message:String((e&&e.message)||e),file:rel,ms:now()-started} }
363
+ let outcome
364
+ try { outcome=await handle.done }
365
+ catch(e){ return {ok:false,code:'LEAN_RUN_FAILED',message:String((e&&e.message)||e),file:rel,ms:now()-started} }
366
+ let out='',err=''
367
+ try { if(handle.collected&&handle.collected.stdout) out=handle.collected.stdout.readFrom(0).text } catch(e){ /* best effort */ }
368
+ try { if(handle.collected&&handle.collected.stderr) err=handle.collected.stderr.readFrom(0).text } catch(e){ /* best effort */ }
369
+ const exitCode=outcome?outcome.exitCode:null
370
+ const ms=now()-started
371
+ const ok=exitCode===0
372
+ const timedOut=!ok&&ms>=cap
373
+ return {
374
+ ok, exitCode, signal:(outcome&&outcome.signal)||null, ms,
375
+ command:argv.join(' '), file:rel,
376
+ stdout:tail(out,4000), stderr:tail(err,4000), timedOut,
377
+ code: ok?undefined:(timedOut?'LEAN_TIMEOUT':'LEAN_FAILED'),
378
+ }
379
+ }
380
+ // Record one run against an object. `passed`/`blocked` are NEVER downgraded by a later red
381
+ // run (only an explicit re-archive decides those); everything else becomes `attempted`.
382
+ async function formalSetRun(target,run){
383
+ const key=formalKey(target); if(!key) return
384
+ const prev=formalOf(key)
385
+ await putFormal(key,Object.assign({},prev,{
386
+ status:'attempted',
387
+ file:run.file||prev.file||'',
388
+ decision:prev.decision||'used',
389
+ 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)},
390
+ updatedAt:now(),
391
+ }))
392
+ }
393
+
394
+ function formalModeWord(){ return formalMode()==='require'?'强制':'鼓励' }
395
+ /**
396
+ * The verification-prompt block. Every branch is computed from the CURRENT mode and the
397
+ * object's CURRENT record at call time (spec §6.1). In particular the `passed` branch is
398
+ * what makes the feature worthwhile: it tells the voter that re-deriving is NOT the job.
399
+ */
400
+ function formalPromptBlock(target){
401
+ if(!formalOn()) return ''
402
+ const rec=target?formalOf(target):{status:'none'}
403
+ const L=[]
404
+ L.push('【Lean 形式化验证('+formalModeWord()+'模式)】')
405
+ if(rec.status==='passed'){
406
+ L.push(' · 该对象已有**通过的 Lean 形式化证明**('+(rec.proof||rec.file||'')+',最近运行 exit 0)。')
407
+ L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')
408
+ L.push(' 定义 / 对象 / 条件 / 假设 / 结论是否与命题原文**完全一致**(有偏差就指出偏差),')
409
+ L.push(' 并据此给出 verdict。')
410
+ L.push(' ▸ 因此请把 verdict 用在**忠实性**上:一致 → 1;发现任何偏离 → 0(或按不确定度给中间值并说明)。')
411
+ } else if(rec.status==='blocked'){
412
+ L.push(' · 该对象已被记录为**形式化阻塞**:'+(rec.note||'未说明')+'。')
413
+ L.push(' 请复核这个判断是否成立;若你认为其实可以形式化,请指出来并动手做。')
414
+ L.push(' ▸ 因此请把 verdict 用在"这个阻塞判断是否成立 / 是否仍有别的形式化路线"上,并给出理由。')
415
+ } else {
416
+ L.push(' · 请先判断该对象的**实现难度**:若能在可接受的工作量内形式化,优先写 Lean 代码并执行。')
417
+ L.push(' · 工具:vibe_v4_lean_run(执行)· vibe_v4_lean_archive(归档)· vibe_v4_lean_lib(查已有可复用库)')
418
+ L.push(' · 工作目录:Formal/(相对项目根 '+frameworkRoot().replace(/\\/g,'/')+'/);')
419
+ L.push(' 可复用定义放 '+formalLibRoot().replace(/\\/g,'/')+'/,已证引理放 '+formalProvedRoot().replace(/\\/g,'/')+'/;')
420
+ L.push(' 写之前先 vibe_v4_lean_lib 查重。')
421
+ L.push(' · **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义/对象/条件/假设/结论是否与')
422
+ L.push(' 命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。')
423
+ if(formalMode()==='require'){
424
+ L.push(' · **本模式要求**:**必须产出 Lean 形式化**,或**必须**给出显式的阻塞原因')
425
+ L.push(" (vibe_v4_lean_archive kind='blocked' note=… 或回执 formal.note)。若两者都没有,")
426
+ L.push(' 本次裁定不会生效,会被记为未定论(原因 formal-required)并进入「形式化待办」。')
427
+ } else {
428
+ L.push(' · 若判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断。')
429
+ }
430
+ if(rec.status==='attempted'){
431
+ L.push(' ▸ 该对象已有形式化尝试但尚未通过(最近一次 '+(rec.run?(rec.run.ok?'通过':'未通过'):'无运行记录')+')。')
432
+ L.push(" 请修复后重跑(vibe_v4_lean_run),跑通后用 kind='proof' 归档。")
433
+ } else {
434
+ L.push(" ▸ 若你在本轮把它形式化并跑通(vibe_v4_lean_archive kind='proof'),后续轮次的")
435
+ L.push(' 审查对象就会从"推导是否正确"变成"Lean 代码是否忠实于命题"。')
436
+ }
437
+ }
438
+ return L.join('\n')
439
+ }
440
+ /** The ordinary-work-round line (spec §6.2): formalize reusable objects as you go. */
441
+ function formalWorkLine(){
442
+ if(!formalOn()) return ''
443
+ return '【顺手形式化('+formalModeWord()+')】把你工作中常用或可能复用的对象、假设、新定义,'
444
+ +"用 Lean 形式化定义并归档到全局可复用库(vibe_v4_lean_archive kind='def'),已成立的引理归到 Formal/Proved/"
445
+ +"(kind='lemma');写之前先 vibe_v4_lean_lib 查重,避免重复定义。"
446
+ +(formalMode()==='require'
447
+ ? '本模式下,任何要定论为真/假的对象都必须先有 Lean 通过或显式阻塞记录。'
448
+ : '这会让后续的验证与证明省掉大量重复工作。')
449
+ }
450
+ /** The `formal` object every non-off prompt documents in its JSON reply contract. */
451
+ function formalReplyField(target){
452
+ return '{"formal":{"target":"'+String(target||'p-x')+'","decision":"used|blocked","file":"Formal/'+String(target||'p-x')+'.lean","note":"难度判断/阻塞原因"}}'
453
+ }
454
+
455
+ // ---- the three indexes (framework-maintained) ---------------------------
456
+ async function writeFormalIndex(){
457
+ const recs=formalRecords()
458
+ const L=['# Lean 形式化索引|'+currentProject+'|'+fmtTime(),'',
459
+ '> 本文件由框架维护(工具调用时更新;`vibe_v4_lean_lib` 会重建)。权威状态在 `State/formal.json`。','',
460
+ '| 对象 | 状态 | 形式化文件 | 归档证明 | 最近运行 | 难度判断 / 阻塞原因 |','|---|---|---|---|---|---|']
461
+ const keys=Object.keys(recs)
462
+ if(!keys.length) L.push('| (暂无) | | | | | |')
463
+ for(const k of keys){
464
+ const r=recs[k]||{}
465
+ L.push('| '+k+' | '+(r.status||'none')+' | '+(r.file||'—')+' | '+(r.proof||'—')+' | '+runLine(r.run)+' | '+String(r.note||'—').replace(/\|/g,'/').slice(0,120)+' |')
466
+ }
467
+ L.push('')
468
+ if(formalTodo().length){
469
+ L.push('## 形式化待办(require 模式:定论被搁置)')
470
+ for(const t of formalTodo()) L.push('- '+t.id+' —— '+(t.why||'formal-required')+'('+fmtTime(t.at)+')')
471
+ L.push('')
472
+ }
473
+ await writeText('Formal/Index.md',L.join('\n'))
474
+ }
475
+ async function writeFormalTodo(){
476
+ const L=['# 形式化待办|'+currentProject+'|'+fmtTime(),'',
477
+ '> 这些对象在 `require` 模式下尚不具备「Lean 已通过」或「显式阻塞记录」,因此**定论被搁置**。',
478
+ "> 完成形式化(vibe_v4_lean_archive kind='proof')或记录阻塞原因(kind='blocked')后,重新提议验证即可。",'']
479
+ const list=formalTodo()
480
+ if(!list.length) L.push('(暂无)')
481
+ for(const t of list) L.push('- '+t.id+'|'+(t.why||'formal-required')+'|'+fmtTime(t.at))
482
+ L.push('')
483
+ await writeText('Formal/TODO.md',L.join('\n'))
484
+ }
485
+ /**
486
+ * Scan and rebuild the three indexes. Listing is deliberately CHEAP and side-effect free:
487
+ * it does NOT execute the toolchain (running Lean on every library file each time an agent
488
+ * asks "what can I reuse?" would be slow and surprising). Per-object run results live in
489
+ * the object records and are shown in Formal/Index.md.
490
+ */
491
+ async function rebuildLeanLibIndexes(){
492
+ const scan=async(dirAbs,dirRel,kindLabel)=>{
493
+ const rows=[]
494
+ try {
495
+ const t=await fs.resolve(dirAbs)
496
+ if(await fs.stat(t)===undefined) return rows
497
+ const entries=await fs.listDir(t)
498
+ for(const e of entries||[]){
499
+ if(!e||e.type!=='file'||!/\.lean$/.test(String(e.name))) continue
500
+ const rel=dirRel+'/'+e.name
501
+ const txt=(await readTextAbs(dirAbs+'/'+e.name))||''
502
+ const name=String(e.name).replace(/\.lean$/,'')
503
+ const first=(txt.split('\n').filter(l=>l.trim()&&!/^\s*(\/\/|--|import)/.test(l))[0]||'').trim().slice(0,110)
504
+ rows.push('| '+name+' | '+rel+' | '+kindLabel+' | '+first.replace(/\|/g,'/')+' |')
505
+ }
506
+ } catch(e){ /* listing is best-effort */ }
507
+ return rows
508
+ }
509
+ const libRows=await scan(formalLibRoot(),'Formal/Lib','def')
510
+ await writeTextAbs(formalLibRoot()+'/Index.md',['# 可复用 Lean 定义库(跨项目)|'+currentProject,'',
511
+ '> 写新定义之前先查这里:能复用就不要重新定义。','',
512
+ '| 名称 | 文件 | 类别 | 摘要 |','|---|---|---|---|']
513
+ .concat(libRows.length?libRows:['| (暂无) | | | |']).join('\n')+'\n')
514
+ const provedRows=await scan(formalProvedRoot(),'Formal/Proved','lemma')
515
+ await writeTextAbs(formalProvedRoot()+'/Index.md',['# 已成立的 Lean 命题 / 引理(机器已核对,可跨项目复用)|'+currentProject,'',
516
+ '> 这些文件是通过内核检查的引理,可直接 import 复用。','',
517
+ '| 名称 | 文件 | 类别 | 陈述 |','|---|---|---|---|']
518
+ .concat(provedRows.length?provedRows:['| (暂无) | | | |']).join('\n')+'\n')
519
+ await writeFormalIndex()
520
+ await writeFormalTodo()
521
+ return {lib:libRows.length,proved:provedRows.length,objects:Object.keys(formalRecords()).length}
522
+ }
523
+
524
+ /**
525
+ * Execute one Lean file through the toolchain, record the run (optionally against an
526
+ * object), refresh the index, and report the outcome verbatim. Deliberately callable even
527
+ * from `off` mode: a human debugging their toolchain may want it, and registration is
528
+ * static while the MODE only decides whether the framework TELLS members about it.
529
+ */
530
+ async function leanRunTool(memberId,o){
531
+ const args=o||{}
532
+ const run=await leanRunFile(String(args.file||''),args.timeout_ms)
533
+ try {
534
+ if(run.ok||run.file){
535
+ if(String(args.target||'').trim()) formalSetRun(String(args.target),run)
536
+ await writeFormalIndex()
537
+ }
538
+ } catch(e){ /* the index is best-effort; a run result must always come back */ }
539
+ logActivity('formal',(memberId||'host')+' lean_run '+(run.file||String(args.file||''))+' → '+(run.ok?'通过':(run.code||'未通过')))
540
+ return Object.assign({ok:!!run.ok},run,{
541
+ mode:formalMode(),
542
+ hint: run.ok
543
+ ? "通过。若是某个对象的证明,请用 vibe_v4_lean_archive kind='proof' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind='def'/'lemma' 归档到全局库。"
544
+ : '未通过。请按上面的编译器输出修复后重跑;若判断无法完成,用 vibe_v4_lean_archive kind=\'blocked\' 记录原因。',
545
+ })
546
+ }
547
+ /**
548
+ * One tool, three archives (spec §5.2).
549
+ * def/lemma → the GLOBAL cross-project library (Formal/Lib, Formal/Proved)
550
+ * proof → the project working file Formal/<target>.lean, plus Verified/Lean/<target>.lean
551
+ * when the run is GREEN (and only then is the object `passed`)
552
+ * blocked → an explicit, reasoned "we judged this infeasible" record (note REQUIRED)
553
+ */
554
+ async function leanArchive(memberId,o){
555
+ const args=o||{}
556
+ const kind=String(args.kind||'')
557
+ const content=typeof args.content==='string'?args.content:undefined
558
+ const from=args.from?String(args.from):''
559
+ const bodyFrom=async()=>{
560
+ if(content!==undefined) return {body:content}
561
+ if(from){
562
+ const srcAbs=leanAbsPath(from)
563
+ if(srcAbs===null) return {error:{ok:false,code:'V4_INVALID_ARGUMENT',message:'from must be a .lean file inside the workspace (got '+from+')'}}
564
+ const body=await readTextAbs(srcAbs)
565
+ if(body===undefined) return {error:{ok:false,code:'V4_NOT_FOUND',message:'no such file: '+from}}
566
+ return {body}
567
+ }
568
+ return {error:{ok:false,code:'V4_INVALID_ARGUMENT',message:'provide content, or from=<existing .lean file>'}}
569
+ }
570
+ if(kind==='def'||kind==='lemma'){
571
+ const name=idSafe(String(args.name||''))
572
+ if(!name||name==='id') return {ok:false,code:'V4_INVALID_ARGUMENT',message:'name is required for a reusable definition/lemma'}
573
+ const got=await bodyFrom(); if(got.error) return got.error
574
+ const rel='Formal/'+(kind==='def'?'Lib':'Proved')+'/'+name+'.lean'
575
+ if(!await writeTextAbs(vibeRelAbs(rel),got.body)) return {ok:false,code:'V4_WRITE_FAILED',message:'could not write '+rel}
576
+ // The global library sits BESIDE the project tree, so it must be executed through its
577
+ // ABSOLUTE path — the project-relative form would resolve inside frameworkRoot.
578
+ const run=args.run===false?null:await leanRunFile(vibeRelAbs(rel))
579
+ try { await rebuildLeanLibIndexes() } catch(e){ /* index rebuild is best-effort */ }
580
+ logActivity('formal',String(memberId||'host')+' 归档'+(kind==='def'?'可复用定义':'已证引理')+' '+name+' → '+rel+(run?('(运行 '+(run.ok?'通过':'未通过')+')'):''))
581
+ return {ok:true,kind,name,file:rel,run:run||undefined,note:'已并入全局可复用库,后续项目可直接 import 复用'}
582
+ }
583
+ if(kind==='proof'){
584
+ const key=formalKey(String(args.target||''))
585
+ if(!key) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'target is required for kind=proof'}
586
+ const got=await bodyFrom(); if(got.error) return got.error
587
+ const workRel='Formal/'+key+'.lean'
588
+ if(!await writeText(workRel,got.body)) return {ok:false,code:'V4_WRITE_FAILED',message:'could not write '+workRel}
589
+ const run=await leanRunFile(workRel)
590
+ const prev=formalOf(key)
591
+ const passed=!!run.ok
592
+ const rec=Object.assign({},prev,{
593
+ status:passed?'passed':'attempted',
594
+ file:workRel,
595
+ proof:passed?('Verified/Lean/'+key+'.lean'):(prev.proof||''),
596
+ decision:'used',
597
+ note:String(args.note||prev.note||''),
598
+ 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)},
599
+ updatedAt:now(),
600
+ })
601
+ // ★ `passed` requires a green run: the proof is only copied into Verified/Lean/ when the
602
+ // kernel actually accepted it. A red run still records the working file (so the agent
603
+ // can iterate) but must not mint a proof.
604
+ if(passed) await writeText('Verified/Lean/'+key+'.lean',got.body)
605
+ await putFormal(key,rec)
606
+ try { await rebuildLeanLibIndexes() } catch(e){ /* best-effort */ }
607
+ logActivity('formal',String(memberId||'host')+' 为 '+key+' 归档形式化证明 '+workRel+(passed?'(**通过**,已归档到 '+rec.proof+',验证转为忠实性审查)':'(**未通过**:'+tail(run.stderr||run.message,160)+')'))
608
+ return {ok:true,kind,target:key,file:workRel,proof:rec.proof,passed,run,status:rec.status}
609
+ }
610
+ if(kind==='blocked'){
611
+ const key=formalKey(String(args.target||''))
612
+ if(!key) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'target is required for kind=blocked'}
613
+ const note=String(args.note||'').trim()
614
+ // "决定权在代理,但决定必须显式、可审计" — an empty note would make the escape hatch
615
+ // indistinguishable from silently skipping formalization, so it is refused outright.
616
+ if(!note) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'阻塞记录必须写明原因(note)——"因难度决定不做形式化"必须显式、可审计'}
617
+ const prev=formalOf(key)
618
+ const rec=Object.assign({},prev,{status:'blocked',decision:'blocked',note,updatedAt:now()})
619
+ await putFormal(key,rec)
620
+ try { await rebuildLeanLibIndexes() } catch(e){ /* best-effort */ }
621
+ logActivity('formal',String(memberId||'host')+' 记录 '+key+' 形式化阻塞:'+note)
622
+ return {ok:true,kind,target:key,status:'blocked',note}
623
+ }
624
+ return {ok:false,code:'V4_INVALID_ARGUMENT',message:"kind must be 'def' | 'lemma' | 'proof' | 'blocked'"}
625
+ }
626
+ /**
627
+ * The per-round `formal` reply channel. This is the path that fires IN PRACTICE: a resident
628
+ * that never calls a Lean tool still has to state its difficulty judgement. Every failure is
629
+ * swallowed into the activity log — an end handler must never throw into the scheduler.
630
+ */
631
+ async function applyFormalReply(rId,formalReply){
632
+ try {
633
+ const key=formalKey(String(formalReply.target||''))
634
+ if(!key) return
635
+ const decision=String(formalReply.decision||'').trim()
636
+ if(decision==='blocked'){
637
+ const note=String(formalReply.note||'').trim()
638
+ if(!note){
639
+ logActivity('formal',(rId||'host')+" 的 formal.decision='blocked' 缺少 note(难度判断/阻塞原因)——本次未记录")
640
+ return
641
+ }
642
+ await leanArchive(rId,{kind:'blocked',target:key,note})
643
+ } else if(decision==='used'){
644
+ const file=String(formalReply.file||('Formal/'+key+'.lean'))
645
+ const prev=formalOf(key)
646
+ await putFormal(key,Object.assign({},prev,{
647
+ status:prev.status==='passed'||prev.status==='blocked'?prev.status:'attempted',
648
+ file,decision:'used',note:String(formalReply.note||prev.note||''),updatedAt:now(),
649
+ }))
650
+ try { await writeFormalIndex() } catch(e){ /* best-effort */ }
651
+ } else if(decision){
652
+ logActivity('formal',(rId||'host')+" 的 formal.decision 只能是 'used' 或 'blocked'(收到 "+decision+")")
653
+ }
654
+ } catch(e){ logActivity('formal','formal 回执处理失败:'+String((e&&e.message)||e)) }
655
+ }
656
+ /** The {mode, on, objects, todo} view the host tools report (state-storage transparency). */
657
+ function formalView(){
658
+ const recs=formalRecords()
659
+ return {
660
+ mode:formalMode(),
661
+ on:formalOn(),
662
+ objects:Object.keys(recs).map(k=>({target:k,status:(recs[k]||{}).status||'none',file:(recs[k]||{}).file||'',proof:(recs[k]||{}).proof||'',note:(recs[k]||{}).note||''})),
663
+ passed:Object.keys(recs).filter(k=>(recs[k]||{}).status==='passed'),
664
+ blocked:Object.keys(recs).filter(k=>(recs[k]||{}).status==='blocked'),
665
+ todo:formalTodo().map(t=>t.id),
666
+ }
667
+ }
668
+
216
669
  // ---- persistence ----
217
670
  async function saveAll(){
218
671
  await writeJson('State/residents.json', Object.fromEntries(residents))
219
672
  await writeJson('State/mailboxes.json', Object.fromEntries(mailboxes))
220
673
  await writeJson('State/taskboard.json', taskboard)
221
674
  await writeJson('State/decisions.json', decisions)
675
+ await writeJson('State/formal.json', {records:formal,todo:formalTodos})
222
676
  await writeJson('State/session.json', {running,autoDone,phase,problemId,problemText,runId,meetings,reports,lastActivityAt,lastProgressAt,activityLog,processEpoch,artifactCount})
223
677
  }
224
678
  async function loadAll(){
@@ -227,6 +681,13 @@ export function apply(ctx) {
227
681
  const mb=await readJson('State/mailboxes.json'); if(mb&&typeof mb==='object') mailboxes=new Map(Object.entries(mb))
228
682
  const tb=await readJson('State/taskboard.json'); if(Array.isArray(tb)) taskboard=tb
229
683
  const dc=await readJson('State/decisions.json'); if(Array.isArray(dc)) decisions=dc
684
+ // Formal records are part of the run's durable state: a `require`-mode object's gate
685
+ // decision must survive a process restart, so they are restored alongside the rest.
686
+ const fm=await readJson('State/formal.json')
687
+ if(fm&&typeof fm==='object'){
688
+ formal=(fm.records&&typeof fm.records==='object')?fm.records:{}
689
+ formalTodos=Array.isArray(fm.todo)?fm.todo:[]
690
+ }
230
691
  }
231
692
 
232
693
  // ---- resident prompts ----
@@ -296,7 +757,11 @@ export function apply(ctx) {
296
757
  // could blank them).
297
758
  function coreRulesBrief(){
298
759
  const base=frameworkRoot()
760
+ // The formalization line is appended HERE (not frozen into a brief) because the mode is
761
+ // dynamic: after a /compact the resident must re-anchor on the rules it is actually
762
+ // living under right now.
299
763
  return '[核心规则重申] 只有 Verified/(及标记"已验证·真/假")算已确立;验证须全组一致(全真或全假)才作数,否则留库附平均概率;你只写自己的库('+base+'/ 的 Progress/<你>/、Propos/<你>/、Methods/<你>/、Subproblems/<你>/),可只读任何人的库;任务分工由团队讨论决定;退出只输出一个 JSON 对象。'
764
+ +(formalOn()?('\n'+formalWorkLine()):'')
300
765
  }
301
766
  function brainstormPrompt(r){
302
767
  return (params.residentPersona?params.residentPersona+'\n':'')
@@ -313,8 +778,12 @@ export function apply(ctx) {
313
778
  +'Resident researcher '+r.rId+' — 第 '+r.rounds+' 轮。一切由你和团队讨论决定。动手前先**读别人的库**对齐事实、避免重复;把新进展/结论**直接用 fs 写进你自己的文件**;想对团队说的话放 "input"(会转给其他常驻)。\n'
314
779
  +'\n团队成员:\n'+banner()+'\n'
315
780
  +'New items:\n'+ (await inboxText(r.rId)) +'\n'
781
+ // 顺手形式化: computed from the CURRENT mode on every wake (docs §1: the mode is dynamic).
782
+ +(formalOn()?('\n'+formalWorkLine()+'\n'):'')
316
783
  +'Reply with ONLY a JSON object:\n'
317
- +'{"summary":"<what you did / decided this round, 1-3 sentences>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<task id|null>","task_done":"<task id|null>","contextPct":40}'
784
+ +'{"summary":"<what you did / decided this round, 1-3 sentences>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<task id|null>","task_done":"<task id|null>","contextPct":40'
785
+ +(formalOn()?(',"formal":{"target":"<对象 id>","decision":"used|blocked","file":"Formal/<对象 id>.lean","note":"难度判断/阻塞原因"}'):'')
786
+ +'}'
318
787
  }
319
788
  function meetingPrompt(r, st){
320
789
  const prior=Object.entries(st.inputs).filter(([k])=>k!==r.rId).map(([k,iv])=>' ['+k+'] '+String(iv.input||iv.summary||'')).join('\n')
@@ -333,15 +802,30 @@ export function apply(ctx) {
333
802
  // others' opinions exist yet. vs.verdicts only ever holds the CURRENT round's votes.
334
803
  const src = (vs.stage==='debate' && vs.history && Object.keys(vs.history).length>0) ? vs.history : (vs.stage==='debate' ? vs.verdicts : {})
335
804
  const others=Object.entries(src).map(([k,v])=>'- '+k+': 正确概率 '+String(v.prob!=null?Number(v.prob).toFixed(2):0.5)+' → '+v.reason).join('\n')
336
- return (params.residentPersona?params.residentPersona+'\n':'')
805
+ const L=[]
806
+ L.push((params.residentPersona?params.residentPersona+'\n':'')
337
807
  +'Resident '+r.rId+' — 团队验证。 The group is verifying object '+vs.targetId+'('+vs.targetType+',提出者 '+vs.targetOwner+')。\n'
338
808
  +'请给出你对「该对象为真」的**正确概率 `verdict`**,仅一个 0–1 数值:**1 = 绝对为真,0 = 绝对为假,0.5 = 完全不确定,其余为介于其间的程度**(不要给 TRUE/FALSE,就给一个数值)。\n'
339
809
  +'判定规则:仅当**全体常驻一致给 1(都认为是真)或一致给 0(都认为是假)**,才按「真/假」写入 Verified/;否则**只作为概率数值(一种程度)保留在库中**,附全组平均正确概率,不写成真/假。\n'
340
810
  +'请给出你**诚实独立的判断**'
341
811
  +(vs.stage==='debate'?',并参考他人意见:\n':'。\n')
342
- +(vs.stage==='debate'&&others?('### 他人上一轮意见(已转发给你)\n'+others+'\n'):'')
343
- +'\nReply with ONLY a JSON object:\n'
344
- +'{"vote":{"verdict":0.9,"reason":"<your logic>"}}'
812
+ +(vs.stage==='debate'&&others?('### 他人上一轮意见(已转发给你)\n'+others+'\n'):''))
813
+ // ---- Lean formalization: the block is computed from the CURRENT mode and the object's
814
+ // CURRENT record, so a mode switch and a fresh proof both show up on the next wake ----
815
+ if(formalOn()){
816
+ L.push('')
817
+ L.push(formalPromptBlock(vs.targetId))
818
+ }
819
+ L.push('')
820
+ L.push('Reply with ONLY a JSON object:')
821
+ L.push('{"vote":{"verdict":0.9,"reason":"<your logic>"}}')
822
+ if(formalOn()){
823
+ // The formal field belongs in the VOTING contract too: voters are exactly the agents who
824
+ // must either formalize the object or record why they judged it infeasible.
825
+ L.push('若你本轮做了形式化或给出难度判断,请一并加上:')
826
+ L.push(formalReplyField(vs.targetId))
827
+ }
828
+ return L.join('\n')
345
829
  }
346
830
 
347
831
  // ---- resident lifecycle ----
@@ -616,7 +1100,19 @@ export function apply(ctx) {
616
1100
  // verdict is a PURE 0-1 probability; only ALL=1 (true) or ALL=0 (false) is a binary verdict.
617
1101
  const allTrue = allVoted && vals.every(x=>Number(x.prob)===1)
618
1102
  const allFalse = allVoted && vals.every(x=>Number(x.prob)===0)
619
- if(allTrue||allFalse){ await closeVerify(vs,allTrue); doSchedule=true }
1103
+ if(allTrue||allFalse){
1104
+ // ── the `require` gate (docs §8) ───────────────────────────────────────────────
1105
+ // A unanimous verdict is a CONSENSUS, not a proof. In `require` mode the group has
1106
+ // decided that consensus alone may not be promoted to Verified/: the object must
1107
+ // also be either machine-checked (`passed`) or carry an explicit, reasoned
1108
+ // "we judged this infeasible" record (`blocked`). The gate never wedges the run — it
1109
+ // records 未定论 + a formalization TODO so the group keeps going and can formalize
1110
+ // later. This is the SINGLE choke point: every 真/假 promotion passes through here.
1111
+ const rec=formalOf(vs.targetId)
1112
+ if(formalMode()==='require' && !formalGateOk(rec)) await deferForFormal(vs,allTrue)
1113
+ else await closeVerify(vs,allTrue)
1114
+ doSchedule=true
1115
+ }
620
1116
  else if(vs.round+1<params.verdictMaxRounds){
621
1117
  // Move to a REAL debate round: snapshot the current votes into history (so the next round's
622
1118
  // prompt can show others' previous stances), then CLEAR verdicts so every resident is asked to
@@ -640,11 +1136,44 @@ export function apply(ctx) {
640
1136
  await writeVerifiedCard(vs,isTrue)
641
1137
  await rewriteSource(target,isTrue,vs.targetOwner)
642
1138
  verifiedRecently.set(target, now()) // dedup: block an immediate re-proposal of the same object
643
- logActivity('verify',target+' → Verified ('+(isTrue?'真':'假')+') by unanimous consensus')
1139
+ // A formalization TODO that has just been satisfied must not linger in the todo file.
1140
+ if(formalOn() && formalTodos.some(t=>t.id===target)){
1141
+ formalTodos=formalTodos.filter(t=>t.id!==target)
1142
+ await saveFormal()
1143
+ try { await writeFormalTodo(); await writeFormalIndex() } catch(e){ /* best-effort */ }
1144
+ }
1145
+ logActivity('verify',target+' → Verified ('+(isTrue?'真':'假')+') by unanimous consensus'+(formalOn()?('|形式化: '+formalStatusLine(target)):''))
644
1146
  verifyState=null; wakeKind.clear(); await saveAll()
645
1147
  // scheduling is done by finalizeVerify AFTER it releases finalizeLock (so a chained verify is
646
1148
  // never swallowed by the still-held reentry lock)
647
1149
  }
1150
+ /**
1151
+ * `require` mode withheld the verdict: record it as 未定论 with a machine-readable reason, put
1152
+ * the object on the formalization TODO, and say so in the activity log. The object keeps its
1153
+ * mean probability (留库附概率, exactly like a non-unanimous round) and stays where it was, so
1154
+ * nothing is lost and the group can carry on and formalize later. Deliberately NOT routed
1155
+ * through closeVerify: no Verified card may be written, and the source card's status must not
1156
+ * become 已验证·真/假.
1157
+ */
1158
+ async function deferForFormal(vs,isTrue){
1159
+ const target=vs.targetId
1160
+ const rec=formalOf(target)
1161
+ const why='formal-required:尚未取得 Lean 形式化通过,也没有显式阻塞记录(当前状态 '+(rec.status||'none')+')'
1162
+ await writeDebateDoc(vs,false,isTrue?1:0) // the 真/假 tally is recorded as the debate outcome
1163
+ const vals=Object.values(vs.verdicts)
1164
+ const mean=vals.length?vals.reduce((a,x)=>a+(x.prob!=null?x.prob:0.5),0)/vals.length:(isTrue?1:0)
1165
+ // The card stays in the library with the group's mean UNCHANGED relative to a normal
1166
+ // non-unanimous round: this is not a probability revision, it is a withheld conclusion.
1167
+ await rewriteSourceProb(target,mean,vs.targetOwner)
1168
+ if(!formalTodos.some(t=>t.id===target)) formalTodos.push({id:target,at:now(),why,verdict:isTrue?1:0})
1169
+ await putFormal(target,rec,formalTodos) // single durable write of records + todo together
1170
+ try { await writeFormalTodo(); await writeFormalIndex() } catch(e){ /* best-effort */ }
1171
+ logActivity('verify',target+' 的表决结果为 '+(isTrue?'真':'假')+',但 **require 模式**要求先有 Lean 通过或显式阻塞记录,因此本轮**不定论**(已记入 Formal/TODO.md;原因 formal-required)')
1172
+ verifyState=null; wakeKind.clear(); await saveAll()
1173
+ // Scheduling is done by finalizeVerify (doSchedule=true) AFTER it releases finalizeLock:
1174
+ // a withheld verdict must return the group to normal work immediately, exactly like a
1175
+ // normal 未定论 round. Without it the run would sit idle until the heartbeat fired.
1176
+ }
648
1177
  // Queue a verify proposal UNLESS the same object was just verified (closed as 真/假). In parallel
649
1178
  // self-organization several residents may independently propose targets while a verify is already
650
1179
  // settling — sometimes the SAME object (test9: p-r3-04 was Verified twice back-to-back), sometimes
@@ -673,7 +1202,12 @@ export function apply(ctx) {
673
1202
  const isSub=vs.targetType==='subproblem'
674
1203
  const dir= isSub?'问题':'命题'
675
1204
  const type= isSub?'问题': vs.targetType==='method'?'方法':'命题'
676
- const text='# 已验证|'+vs.targetId+'\n- ID: '+vs.targetId+'\n- 类型: '+type+'\n- 结论: '+(isTrue?'真':'假')+'\n- 概率: '+(isTrue?1:0)+'\n- 来源: 全体常驻一致\n## 陈述\n参见来源卡。\n'
1205
+ // In a non-off formal mode the card must state HOW STRONG this conclusion actually is:
1206
+ // 'Lean 通过(Verified/Lean/<id>.lean)' means the kernel checked a formalization (whose
1207
+ // fidelity the m votes then reviewed); '阻塞(…)' means the group explicitly decided not
1208
+ // to formalize and said why. `off` mode is untouched — no formal line at all.
1209
+ const formalLine=formalOn()?('\n- 形式化: '+formalStatusLine(vs.targetId)):''
1210
+ const text='# 已验证|'+vs.targetId+'\n- ID: '+vs.targetId+'\n- 类型: '+type+'\n- 结论: '+(isTrue?'真':'假')+'\n- 概率: '+(isTrue?1:0)+'\n- 来源: 全体常驻一致'+formalLine+'\n## 陈述\n参见来源卡。\n'
677
1211
  await writeText('Verified/'+dir+'/'+vs.targetId+'.md', text)
678
1212
  }
679
1213
  // Does `content` declare the target as its card ID? Accept both the exact `- ID: <id>` and the
@@ -753,8 +1287,11 @@ export function apply(ctx) {
753
1287
  function heartbeatPrompt(r){
754
1288
  return (params.residentPersona?params.residentPersona+'\n':'')
755
1289
  +'Resident researcher '+r.rId+' — CHECKPOINT(团队空闲,请由你们继续自主推进)。当前项目尚未解决(除非你已确认)。团队在等待有人继续:请**继续解决这个问题**——读他人的库对齐、推进某个子问题/引理/方法、尝试一条路线;或向团队发消息(input)、提议任务(propose_task)让大家分工。若你确实认为问题已解决、或已彻底无路可走,才提议开会(propose_meeting)让团队表决/商量、或声明 solved=true。默认立场是:**请推进,而不是停在原地。**\n'
1290
+ +(formalOn()?(formalWorkLine()+'\n'):'')
756
1291
  +'Reply with ONLY a JSON object:\n'
757
- +'{"summary":"<what you will do / what you advanced this round>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<id|null>","contextPct":40}'
1292
+ +'{"summary":"<what you will do / what you advanced this round>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<id|null>","contextPct":40'
1293
+ +(formalOn()?(',"formal":{"target":"<对象 id>","decision":"used|blocked","file":"Formal/<对象 id>.lean","note":"难度判断/阻塞原因"}'):'')
1294
+ +'}'
758
1295
  }
759
1296
  function clearHeartbeat(){ if(heartbeatDisposer!==null){ try{ heartbeatDisposer() }catch(e){} heartbeatDisposer=null } }
760
1297
  function armHeartbeat(){
@@ -948,6 +1485,10 @@ export function apply(ctx) {
948
1485
  }
949
1486
  if((kind==='verif-ind'||kind==='verif-deb') && verifyState){
950
1487
  const v=(parsed&&parsed.vote)||{}
1488
+ // Lean difficulty judgement carried on the SAME reply. Handled BEFORE the vote is stored
1489
+ // so a voter that says "I formalized it" / "I judge this infeasible" has that recorded
1490
+ // together with its verdict. Never allowed to throw into the scheduler.
1491
+ if(parsed.formal && typeof parsed.formal==='object') await applyFormalReply(r.rId, parsed.formal)
951
1492
  // verdict = 0-1 probability the object is TRUE (1=绝对真, 0=绝对假, 0.5=不确定);
952
1493
  // also accept legacy 'TRUE'/'FALSE' strings AND quoted numeric strings ("0.9"), which LLMs
953
1494
  // occasionally emit — without this a confident "0.9" was silently misread as 0.5 (uncertainty).
@@ -965,6 +1506,10 @@ export function apply(ctx) {
965
1506
  await continueVerifyRound(); return
966
1507
  }
967
1508
  // normal turn
1509
+ // The `formal` reply field is honoured on EVERY turn kind (docs §4: 回执里 formal:{target,
1510
+ // decision:'blocked'|'used'}), because the ordinary work round is where reusable objects are
1511
+ // formalized and where a difficulty judgement is most often stated.
1512
+ if(parsed.formal && typeof parsed.formal==='object') await applyFormalReply(r.rId, parsed.formal)
968
1513
  if(r.status==='brainstorm'){ r.insight=parsed.summary||output; r.status='active' }
969
1514
  if(typeof parsed.solved==='boolean'){ reports.push({rId:r.rId,solved:parsed.solved,summary:parsed.summary||'',at:now()}); if(reports.length>100) reports.shift() } // solved-signal ring (not surfaced in status/report)
970
1515
  if(parsed.propose_verify) maybeQueueVerify(parsed.propose_verify, r.rId)
@@ -1002,6 +1547,9 @@ export function apply(ctx) {
1002
1547
  // still-running turns keep writing into the same per-resident files the new run is about to use.
1003
1548
  for(const [,or] of residents){ if(or.childId){ try{ subagents.interrupt(or.childId,{kind:'ancestor',agent:rootAgent}) }catch(e){} } }
1004
1549
  residents=new Map(); mailboxes=new Map(); taskboard=[]; decisions=[]; meetings=[]; reports=[]; verifyState=null; meetingState=null; pendingVerify=[]; residentSeq=0; artifactCount=0; clearHeartbeat()
1550
+ // A fresh run starts with a clean formal slate: the ids r-1.. and p-* are reused, so
1551
+ // carrying a previous run's records over would let a stale `passed` open the new gate.
1552
+ formal={}; formalTodos=[]
1005
1553
  busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; lastSyncMeetingAt=0; finalizeLock=null; verifiedRecently.clear() // fresh run must NOT inherit stale concurrency/coordination state (busy/wakeKind/currentResident/pendingMeeting) from a previous run on the same reused session
1006
1554
  lastActivityAt=now(); lastProgressAt=now() // fresh stall/activity clock for the new run (else B could fire immediately on a reused session)
1007
1555
  const dirs=Array.isArray(seedDirections)?seedDirections.slice(0,params.residentCount):[]
@@ -1056,14 +1604,29 @@ export function apply(ctx) {
1056
1604
  residents:listResidents(), busy:[...busy], taskboard:taskboard.length,
1057
1605
  meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null, pendingVerifyCount: pendingVerify.length,
1058
1606
  parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
1059
- params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','stallAutoMeetingMs','provider','model','residentPersona','toolAllow','toolDeny'].map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
1607
+ // The Lean knobs and the per-object formal records are part of the readable status: without
1608
+ // them a `require`-mode run that keeps returning 未定论 would be undiagnosable from outside.
1609
+ formal: formalView(),
1610
+ params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','stallAutoMeetingMs','provider','model','residentPersona','toolAllow','toolDeny','formalVerify','leanCommand','leanArgs','leanTimeoutMs'].map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
1611
+ function formalReportText(){
1612
+ if(!formalOn()) return '- 未启用(`formalVerify` = off;可用 vibe_v4_set 切到 encourage / require)'
1613
+ const v=formalView()
1614
+ return ['- 模式:'+formalMode()+'('+(formalMode()==='require'?'强制:定论前必须有 Lean 通过或显式阻塞记录':'鼓励:按实现难度自行决定')+')',
1615
+ '- 已通过:'+(v.passed.join('、')||'(无)'),
1616
+ '- 已记录阻塞:'+(v.blocked.join('、')||'(无)'),
1617
+ '- 形式化待办:'+(v.todo.join('、')||'(无)'),
1618
+ '- 可复用库:'+formalLibRoot().replace(/\\/g,'/')+'/ 与 '+formalProvedRoot().replace(/\\/g,'/')+'/(跨项目)|本项目形式化:Formal/|归档证明:Verified/Lean/'].join('\n')
1619
+ }
1060
1620
  function report(){ return { ok:true, running, phase, autoDone, project:currentProject, problem:problemText,
1061
1621
  residents:listResidents(), taskboard:taskboard.filter(t=>t.status!=='done'),
1062
1622
  meeting: meetingState?{id:meetingState.id, agenda:meetingState.agenda, spoke:Object.keys(meetingState.inputs).length+'/'+residents.size}:null,
1063
1623
  verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage, voted:Object.keys(verifyState.verdicts).length+'/'+residents.size}:null,
1064
1624
  pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null,
1065
1625
  parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
1626
+ formal: formalView(),
1066
1627
  meetings:meetings.length, recentActivity: activityLog.slice(-8) } }
1628
+ /** Human-readable mirror of the formal state (kept OUT of the JSON report shape). */
1629
+ function formalReport(){ return {ok:true, formalMode:formalMode(), formalReport:'## Lean 形式化\n'+formalReportText(), formal:formalView()} }
1067
1630
  async function addMember(direction){
1068
1631
  // Adding a member starts a REAL resident turn (spawnResident → brainstorm) — refuse unless the
1069
1632
  // run is live: on a concluded (autoDone) or never-started/paused run the new member would work
@@ -1100,7 +1663,16 @@ export function apply(ctx) {
1100
1663
  function normalizeParam(k, v){
1101
1664
  const INT_KEYS=['residentCount','compactThreshold','compactAfterRounds','maxParallel','activityTimeoutMs','verdictMaxRounds','meetingKeepEvery','stallAutoMeetingMs']
1102
1665
  if(INT_KEYS.includes(k)){ const n=Number(v); if(!Number.isFinite(n)) return v; return Math.floor(n) }
1103
- if(k==='toolAllow'||k==='toolDeny'){ if(Array.isArray(v)) return v.map(x=>String(x).trim()).filter(Boolean); if(typeof v==='string') return v.split(',').map(x=>x.trim()).filter(Boolean); return [] }
1666
+ if(k==='toolAllow'||k==='toolDeny'||k==='leanArgs'){ if(Array.isArray(v)) return v.map(x=>String(x).trim()).filter(Boolean); if(typeof v==='string') return v.split(',').map(x=>x.trim()).filter(Boolean); return [] }
1667
+ // ---- Lean formal verification (docs §1) ------------------------------------------
1668
+ // `formalVerify` is a three-way enum and MUST degrade to the no-op 'off' on anything else.
1669
+ // Degrading to a STRONGER mode would let a typo silently gate every conclusion — the exact
1670
+ // failure mode `require` is supposed to avoid.
1671
+ if(k==='formalVerify') return FORMAL_MODES.indexOf(String(v))!==-1?String(v):'off'
1672
+ // A blank command would make resolveExecutable('') fail confusingly; fall back to the default.
1673
+ if(k==='leanCommand'){ const s=String(v==null?'':v).trim(); return s||'lean' }
1674
+ // A non-positive timeout is meaningless (the run would be killed instantly) → default.
1675
+ if(k==='leanTimeoutMs'){ const n=Number(v); if(!Number.isFinite(n)||n<=0) return DEFAULT_PARAMS.leanTimeoutMs; return Math.floor(n) }
1104
1676
  return v
1105
1677
  }
1106
1678
  /**
@@ -1169,6 +1741,41 @@ export function apply(ctx) {
1169
1741
  frameworkRoot:frameworkRoot, currentProject:()=>currentProject, problemText:()=>problemText,
1170
1742
  residentCount:()=>residents.size,
1171
1743
  busyCount:()=>busy.size,
1744
+ // Lean formal verification (docs/formal-verification.md) — exposed to the tool layer and to
1745
+ // the test suites exactly as v5 exposes its own helpers.
1746
+ formalMode, formalOn, formalRecords, formalTodo, formalOf, formalView, formalReport,
1747
+ rebuildLeanLibIndexes, leanArchive, leanRunTool, writeFormalIndex, writeFormalTodo,
1748
+ leanRunToolApi: async (relPath,timeoutMs)=>await leanRunFile(relPath,timeoutMs),
1749
+ /**
1750
+ * The prompt builders, addressed BY RESIDENT ID. "成员读到的文字就是产品"
1751
+ * (AUDIT-CHECKLIST §0.1): a suite that can only observe tool return values is blind to a
1752
+ * prompt defect, so the exact strings a resident would receive must be directly readable.
1753
+ * These are pure builders — calling one has no side effects on the run.
1754
+ */
1755
+ promptApi:{
1756
+ normal:(rId)=>{ const r=residents.get(String(rId)); return r?normalPrompt(r):'' },
1757
+ heartbeat:(rId)=>{ const r=residents.get(String(rId)); return r?heartbeatPrompt(r):'' },
1758
+ brainstorm:(rId)=>{ const r=residents.get(String(rId)); return r?brainstormPrompt(r):'' },
1759
+ coreRules:()=>coreRulesBrief(),
1760
+ // `vs` mirrors the live verification state ({targetId,targetType,targetOwner,stage,history,verdicts});
1761
+ // pass one explicitly to ask "what WOULD the voters read for this object right now?".
1762
+ verify:(rId,vs)=>{ const r=residents.get(String(rId)); return r?verifyPrompt(r, vs||verifyState||{targetId:'',targetType:'proposition',targetOwner:'',stage:'independent',verdicts:{}}):'' },
1763
+ formalBlock:(target)=>formalPromptBlock(target),
1764
+ formalWorkLine:()=>formalWorkLine(),
1765
+ },
1766
+ /** Dispatcher behind vibe_v4_prompts (kept here so the tool layer never re-implements it). */
1767
+ promptFor:async (which,rId,arg)=>{
1768
+ const r=residents.get(String(rId))
1769
+ if(!r) return ''
1770
+ if(which==='brainstorm') return brainstormPrompt(r)
1771
+ if(which==='heartbeat') return heartbeatPrompt(r)
1772
+ if(which==='coreRules') return coreRulesBrief()
1773
+ if(which==='verify'){
1774
+ const target=idSafe(String((arg&&arg.target)||''))
1775
+ return verifyPrompt(r,{targetId:target,targetType:guessTargetType(target),targetOwner:'',stage:String((arg&&arg.stage)||'independent'),history:{},verdicts:{}})
1776
+ }
1777
+ return normalPrompt(r)
1778
+ },
1172
1779
  }
1173
1780
  } // end makeSession
1174
1781
 
@@ -1195,6 +1802,16 @@ export function apply(ctx) {
1195
1802
  registerTool('vibe_v4_abort','Abort V4 and interrupt residents.',objParams({}),(s)=>s.initAbort())
1196
1803
  registerTool('vibe_v4_status','Show V4 status.',objParams({}),(s)=>s.status())
1197
1804
  registerTool('vibe_v4_report','Return the V4 progress report.',objParams({}),(s)=>s.report())
1805
+ registerTool('vibe_v4_formal_report','Human-readable Lean formal-verification mirror (mode, Lean-passed objects, recorded blockers, formalization TODO, library paths).',objParams({}),(s)=>s.formalReport())
1806
+ // The exact text a resident would receive. "成员读到的文字就是产品" (AUDIT-CHECKLIST §0.1): a host
1807
+ // (or an audit) must be able to READ the prompt, not just the tool return values, or a prompt
1808
+ // defect stays invisible. Pure builder calls — no side effects on the run.
1809
+ registerTool('vibe_v4_prompts','Read the exact prompt text a resident would receive (which: brainstorm|normal|heartbeat|verify|coreRules). member = resident id; target/stage describe the object for `verify`. Prompt text is the product — this makes it auditable.',objParams({which:{type:'string',enum:['brainstorm','normal','heartbeat','verify','coreRules']},member:{type:'string'},target:{type:'string'},stage:{type:'string'}},['which']),async (s,a)=>{
1810
+ const which=String(a.which||'normal')
1811
+ if(which==='coreRules') return {ok:true,which,text:await s.promptApi.coreRules()}
1812
+ const text=await s.promptFor(which,String(a.member||'r-1'),a)
1813
+ return {ok:true,which,member:String(a.member||'r-1'),text:typeof text==='string'?text:String(text||'')}
1814
+ })
1198
1815
  registerTool('vibe_v4_message','Inject a message to a resident (or all).',objParams({to:{type:'string'},content:{type:'string'}},['to','content']),(s,a)=>{ const to=a.to||'all'; if(to==='all') return s.broadcast(a.content); return s.postMessage('facilitator',to,a.content) })
1199
1816
  registerTool('vibe_v4_meeting','Start a meeting (coordinate / allocate / propose verification).',objParams({agenda:{type:'string'}},['agenda']),(s,a)=>s.startMeeting(a.agenda))
1200
1817
  registerTool('vibe_v4_list_members','List residents.',objParams({}),(s)=>({ok:true,residents:s.listResidents()}))
@@ -1203,7 +1820,11 @@ export function apply(ctx) {
1203
1820
  // model/provider inheritance: set model/provider to override the residents' LLM route (''=inherit
1204
1821
  // the main assistant's route). toolAllow/toolDeny are per-resident tool permissions (scoped
1205
1822
  // restrict). residentPersona prepends a persona line to every resident prompt.
1206
- registerTool('vibe_v4_set','Set V4 parameters. model/provider override resident LLM route (empty=inherit main); toolAllow/toolDeny restrict resident tools (arrays of tool names); residentPersona adds a persona line.',objParams({residentCount:{type:'integer'},compactAfterRounds:{type:'integer'},compactThreshold:{type:'integer'},meetingKeepEvery:{type:'integer'},maxParallel:{type:'integer'},activityTimeoutMs:{type:'integer'},verdictMaxRounds:{type:'integer'},stallAutoMeetingMs:{type:'integer'},provider:{type:'string'},model:{type:'string'},residentPersona:{type:'string'},toolAllow:{type:'array',items:{type:'string'}},toolDeny:{type:'array',items:{type:'string'}}}),(s,a)=>{ s.setParams(a); return {ok:true} })
1823
+ // Lean knobs (docs/formal-verification.md §1): formalVerify is the three-way mode switch (the
1824
+ // MODE is dynamic — switching it changes the very next prompt), leanCommand/leanArgs select the
1825
+ // executable, leanTimeoutMs bounds one run. Invalid values fall back to the defaults and an
1826
+ // unknown mode degrades to 'off' (never to a STRONGER mode).
1827
+ registerTool('vibe_v4_set','Set V4 parameters. model/provider override resident LLM route (empty=inherit main); toolAllow/toolDeny restrict resident tools (arrays of tool names); residentPersona adds a persona line; formalVerify: "off" (default, a true no-op) | "encourage" (residents 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 unanimous true/false verdict is withheld as undecided until the object is Lean-passed or has an explicit reasoned blocker record); leanCommand/leanArgs/leanTimeoutMs configure the toolchain.',objParams({residentCount:{type:'integer'},compactAfterRounds:{type:'integer'},compactThreshold:{type:'integer'},meetingKeepEvery:{type:'integer'},maxParallel:{type:'integer'},activityTimeoutMs:{type:'integer'},verdictMaxRounds:{type:'integer'},stallAutoMeetingMs:{type:'integer'},provider:{type:'string'},model:{type:'string'},residentPersona:{type:'string'},toolAllow:{type:'array',items:{type:'string'}},toolDeny:{type:'array',items:{type:'string'}},formalVerify:{type:'string',enum:['off','encourage','require']},leanCommand:{type:'string'},leanArgs:{type:'array',items:{type:'string'}},leanTimeoutMs:{type:'integer'}}),(s,a)=>{ s.setParams(a); return s.status() })
1207
1828
  // resident-facing tools: route to the CALLING resident (exec.agent.id === childId);
1208
1829
  // fall back to the last-woken resident when called by the host/assistant.
1209
1830
  registerTool('vibe_v4_send_message','(resident) Send a message to another resident (to=all broadcasts to the whole team).',objParams({to:{type:'string'},content:{type:'string'}},['to','content']),(s,a,x)=>{ const from=s.residentIdOf(x); if(!from) return {ok:false,message:'no such resident'}; if(String(a.to)==='all') return s.broadcast(a.content, from); return s.postMessage(from,a.to,a.content) })
@@ -1223,11 +1844,26 @@ export function apply(ctx) {
1223
1844
  registerTool('vibe_v4_claim_write','Reserved: shared-file write lock (framework-managed).',objParams({target:{type:'string'}},['target']),(s,a)=>({ok:true,key:a.target}))
1224
1845
  registerTool('vibe_v4_release_write','Reserved: shared-file write lock release.',objParams({target:{type:'string'}},['target']),(s,a)=>({ok:true,key:a.target}))
1225
1846
 
1847
+ // ── Lean formal verification (docs/formal-verification.md §5) ─────────────
1848
+ // These three tools are registered UNCONDITIONALLY. Registration is STATIC (a mode-dependent
1849
+ // registration would be a dynamic effect and break the ctx.effect discipline), while the MODE
1850
+ // only decides whether the framework TELLS residents about them: in 'off' mode they still work
1851
+ // if a human or an agent calls them deliberately, but no prompt mentions them.
1852
+ registerTool('vibe_v4_lean_run','(resident) 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 verbatim, a timeout returns LEAN_TIMEOUT. Pass target=<object id> to also record the run against that object.',objParams({file:{type:'string'},target:{type:'string'},timeout_ms:{type:'integer'}},['file']),(s,a,x)=>s.leanRunTool(s.residentIdOf(x),a))
1853
+ registerTool('vibe_v4_lean_archive',"(resident) Archive Lean code. kind=\"def\": a REUSABLE definition/object/assumption → the global cross-project library (Formal/Lib). kind=\"lemma\": a machine-checked lemma → Formal/Proved. kind=\"proof\": the formal proof of a project object → Formal/<target>.lean, and (when the run passes) also Verified/Lean/<target>.lean, marking the object Lean-passed. kind=\"blocked\": record an explicit, reasoned \"cannot/not worth formalizing\" decision (note required).",objParams({kind:{type:'string',enum:['def','lemma','proof','blocked']},name:{type:'string'},target:{type:'string'},content:{type:'string'},from:{type:'string'},note:{type:'string'},run:{type:'boolean'}},['kind']),(s,a,x)=>s.leanArchive(s.residentIdOf(x),a))
1854
+ registerTool('vibe_v4_lean_lib','(resident) List (and by default rebuild) the Lean reuse library: this project\'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:{type:'boolean'}}),async (s,a)=>{
1855
+ const r=(a&&a.refresh===false)?{lib:null,proved:null,objects:Object.keys(s.formalRecords()).length}:await s.rebuildLeanLibIndexes()
1856
+ return { ok:true, mode:s.formalMode(), rebuilt:!(a&&a.refresh===false), counts:r, todo:s.formalTodo(),
1857
+ objects:s.formalView().objects,
1858
+ paths:{project:'Formal/(相对项目根)',lib:'VibeMath/Formal/Lib/',proved:'VibeMath/Formal/Proved/',proofs:'Verified/Lean/'},
1859
+ hint:"复用优先:先在 Lib/ 里找现成定义;新定义用 vibe_v4_lean_archive kind='def' 归档,已证引理用 kind='lemma'。" }
1860
+ })
1861
+
1226
1862
  // Same lifecycle rule as registerTool: commands.register() returns a disposer, so the
1227
1863
  // registration belongs to this fiber and must be unwound with it.
1228
1864
  ctx.effect(() => commands.register({
1229
1865
  name:'v4', description:'control the Vibe Math V4 framework',
1230
- input:{hint:'[configure|start|resume|pause|abort|status|report|meeting|members|add|remove|set]'},
1866
+ input:{hint:'[configure|start|resume|pause|abort|status|report|message <to|all> <content>|meeting|members|add|remove|set]'},
1231
1867
  handler: async function(inv){
1232
1868
  const s=getSession(inv&&inv.agent); if(!s) return {kind:'success',text:JSON.stringify({ok:false,error:'no session'})}
1233
1869
  const line=String(inv&&inv.rawInput?inv.rawInput:'').trim(); const parts=line.split(/\s+/); const cmd=parts[0]||''; const rest=parts.slice(1)
@@ -1240,11 +1876,12 @@ export function apply(ctx) {
1240
1876
  else if(cmd==='status') r=s.status()
1241
1877
  else if(cmd==='report') r=s.report()
1242
1878
  else if(cmd==='meeting') r=await s.startMeeting(rest.join(' '))
1879
+ else if(cmd==='message'){ const to=rest[0]||'all'; const content=rest.slice(1).join(' '); r=!content?{ok:false,usage:'message <to|all> <content>'}:((to==='all')?await s.broadcast(content):await s.postMessage('facilitator',to,content)) }
1243
1880
  else if(cmd==='members') r={ok:true,residents:s.listResidents()}
1244
1881
  else if(cmd==='add') r=await s.addMember(rest.join(' '))
1245
1882
  else if(cmd==='remove') r=await s.removeMember(rest[0]||'')
1246
1883
  else if(cmd==='set'){ const upd={}; for(const tok of rest){ const eq=tok.indexOf('='); if(eq>0){ const k=tok.slice(0,eq); const rv=tok.slice(eq+1); const n=Number(rv); upd[k]=Number.isFinite(n)?n:rv } } r=s.setParams(upd) }
1247
- else r={ok:false,usage:'configure|start|resume|pause|abort|status|report|message|meeting|members|add|remove|set'}
1884
+ else r={ok:false,usage:'configure|start|resume|pause|abort|status|report|message <to|all> <content>|meeting|members|add|remove|set'}
1248
1885
  return {kind:'success',text:JSON.stringify(r,null,2)}
1249
1886
  },
1250
1887
  }))