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.
- package/AUDIT-CHECKLIST.md +48 -2
- package/README.md +368 -81
- package/RELEASE-NOTES-2.2.2.md +88 -0
- package/RELEASE-NOTES-2.3.0.md +207 -0
- package/audit-formal-sensitivity.mjs +247 -0
- package/audit-persona-sensitivity.mjs +249 -0
- package/audit-persona-surface.test.mjs +349 -0
- package/audit-v5-integrity.mjs +40 -1
- package/audit-v5-sensitivity.mjs +84 -6
- package/docs/formal-verification.md +321 -0
- package/docs/generate_framework_diagram_v5.mjs +290 -0
- package/docs//346/236/266/346/236/204/345/233/276.md +75 -0
- package/formal-verify-v2.test.mjs +672 -0
- package/formal-verify-v3.test.mjs +824 -0
- package/formal-verify-v4.test.mjs +603 -0
- package/formal-verify-v5.test.mjs +526 -0
- package/package.json +33 -15
- package/prompt-corpus-persona/persona-corpus.json +32 -0
- package/prompt-corpus-persona/persona-corpus.md +674 -0
- package/prompt-corpus-v3/formal-verify-v3.json +280 -0
- package/prompt-corpus-v3/formal-verify-v3.md +2826 -0
- package/prompt-corpus-v5/prompt-corpus-v5.json +131 -713
- package/prompt-corpus-v5/prompt-corpus-v5.md +1828 -5225
- package/prompt-v5-integrity.test.mjs +154 -11
- package/vibe-math-v2/agent.cordis.yml +40 -2
- package/vibe-math-v2/vibe-math-v2.js +627 -19
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +145 -1
- package/vibe-math-v3/agent.cordis.yml +46 -2
- package/vibe-math-v3/vibe-math-v3.js +749 -21
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +87 -2
- package/vibe-math-v4/agent.cordis.yml +46 -4
- package/vibe-math-v4/vibe-math-v4.js +652 -15
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +226 -0
- package/vibe-math-v5/agent.cordis.yml +41 -5
- package/vibe-math-v5/vibe-math-v5.js +572 -9
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +122 -4
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +426 -0
- 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 ' +
|
|
992
|
-
: 'mkdir -p ' +
|
|
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
|
-
|
|
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',
|