dsh-vibe-math 2.5.0 → 2.6.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/README.en.md +41 -2
- package/README.md +41 -2
- package/cordis.patch.yml +22 -8
- package/docs/AUDIT-CHECKLIST.md +2 -2
- package/docs/final-paper.md +171 -0
- package/docs/release-notes/RELEASE-NOTES-2.6.0.md +268 -0
- package/docs/test-timing.md +8 -4
- package/package.json +5 -3
- package/prompt-corpus-persona/persona-corpus.json +13 -13
- package/prompt-corpus-persona/persona-corpus.md +49 -21
- package/prompt-corpus-v2/formal-verify-v2.json +12 -0
- package/prompt-corpus-v2/formal-verify-v2.md +144 -53
- package/prompt-corpus-v3/formal-verify-v3.json +36 -0
- package/prompt-corpus-v3/formal-verify-v3.md +405 -78
- package/tests/audit-persona-sensitivity.mjs +60 -7
- package/tests/audit-persona-surface.test.mjs +16 -7
- package/tests/e2e-v5-round2.test.mjs +471 -1
- package/tests/formal-verify-v2.test.mjs +3 -0
- package/tests/formal-verify-v3.test.mjs +4 -0
- package/tests/prompt-v5-integrity.test.mjs +9 -3
- package/tests/selfdrive-v5.mjs +55 -4
- package/vibe-math-v2/agent.cordis.yml +14 -4
- package/vibe-math-v2/vibe-math-v2.js +819 -11
- package/vibe-math-v3/agent.cordis.yml +10 -4
- package/vibe-math-v3/vibe-math-v3.js +800 -11
- package/vibe-math-v4/agent.cordis.yml +12 -4
- package/vibe-math-v4/vibe-math-v4.js +793 -8
- package/vibe-math-v5/agent.cordis.yml +8 -4
- package/vibe-math-v5/vibe-math-v5.js +1116 -8
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +70 -1
|
@@ -90,6 +90,18 @@ export function apply(ctx) {
|
|
|
90
90
|
leanCommand: 'lean',
|
|
91
91
|
leanArgs: [], // inserted BEFORE the file name (e.g. ['env','lean'] with lake)
|
|
92
92
|
leanTimeoutMs: 120000,
|
|
93
|
+
// ---- final paper (docs/final-paper.md §1) ----------------------------------
|
|
94
|
+
// The run's conclusion is a PAPER the whole team co-authors once consensus has converged.
|
|
95
|
+
// finalPaper=true triggers it automatically at that moment (idempotent); the other four knobs
|
|
96
|
+
// decide what is produced, in which language, whether PDFs are attempted, and WHO finalises.
|
|
97
|
+
finalPaper: true,
|
|
98
|
+
paperFormat: 'both', // 'both' | 'md' | 'tex'
|
|
99
|
+
paperLanguage: 'zh', // 'zh' | 'en'
|
|
100
|
+
paperCompilePdf: true, // compile a PDF when a LaTeX engine is actually detected
|
|
101
|
+
// v2 §A5: the editor is the SESSION ROOT / human side ("office") by default — v4 has no
|
|
102
|
+
// `facilitator` roster member with an LLM — or one named resident (`resident:<id>`).
|
|
103
|
+
paperEditor: 'office',
|
|
104
|
+
paperLatexCommand: '', // preferred LaTeX executable ('' = the standard detection order)
|
|
93
105
|
}
|
|
94
106
|
let params = Object.assign({}, DEFAULT_PARAMS)
|
|
95
107
|
let running = false, autoDone = false, phase = 'idle'
|
|
@@ -102,6 +114,11 @@ export function apply(ctx) {
|
|
|
102
114
|
// whether its verdict may take effect.
|
|
103
115
|
let formal = {}, formalTodos = [], formalPersisted = false
|
|
104
116
|
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)
|
|
117
|
+
// The final-paper flow (docs/final-paper.md, v1 + v2 amendments). In-memory only: the DURABLE
|
|
118
|
+
// idempotency record is `Paper/<run id>/paper.meta.json` on disk (finalizedAt + per-artifact
|
|
119
|
+
// existence, v2 §C), so a re-trigger after a restart is still recognised even though this object
|
|
120
|
+
// did not survive. While a paper is in flight this holds the whole team-authoring state machine.
|
|
121
|
+
let paperState = null
|
|
105
122
|
let busy = new Set(), wakeKind = new Map(), currentResident = ''
|
|
106
123
|
let finalizeLock = null // 'meeting'|'verify' while a consensus finalize is running (reentry guard)
|
|
107
124
|
const verifiedRecently = new Map() // targetId -> timestamp when it was closed as Verified (dedup re-propose)
|
|
@@ -1472,10 +1489,24 @@ export function apply(ctx) {
|
|
|
1472
1489
|
const allSolved = allSpoke && speakers.length>0 && speakers.every(iv=>iv.voteSolved===true)
|
|
1473
1490
|
logActivity('meeting', 'concluded'+(allSolved?' → ALL agree solved':' (no unanimous solved vote)'))
|
|
1474
1491
|
if(allSolved){
|
|
1492
|
+
// ── FINAL PAPER ENTRY (docs/final-paper.md §A1) ─────────────────────────────────────────────────
|
|
1493
|
+
// The paper phase MUST start HERE, BEFORE `running=false; autoDone=true` and before the
|
|
1494
|
+
// coordination state is released: afterwards `startMeeting` refuses (autoDone), this
|
|
1495
|
+
// handler's meeting/verify branches return early on `!running||autoDone`, and
|
|
1496
|
+
// `scheduleNext` does nothing — co-writing/merge/cross-review could never run. The run
|
|
1497
|
+
// therefore stays `running` while the team writes; `paperFinalize()` performs the
|
|
1498
|
+
// completion flip once the paper exists (or has degraded safely).
|
|
1499
|
+
meetingState=null; pendingMeeting=null; verifyState=null; pendingVerify=[]; wakeKind.clear()
|
|
1500
|
+
await saveAll()
|
|
1501
|
+
if(params.finalPaper){
|
|
1502
|
+
const paper=await paperStart('auto',{deferCompletion:true})
|
|
1503
|
+
if(paper&&paper.ok){
|
|
1504
|
+
logActivity('paper','收口:一致性停止票通过('+PAPER_CLOSURE_SIGNAL+')→ 进入最终论文阶段,论文完成后才标记完成')
|
|
1505
|
+
return
|
|
1506
|
+
}
|
|
1507
|
+
logActivity('paper','最终论文未能启动('+((paper&&paper.message)||'unknown')+')→ 按原路径收口')
|
|
1508
|
+
} else logActivity('paper','finalPaper=false:跳过最终论文,直接收口')
|
|
1475
1509
|
running=false; autoDone=true; phase='done'; clearHeartbeat()
|
|
1476
|
-
// symmetric with initAbort: the STOP path must also release the coordination state, else
|
|
1477
|
-
// status keeps reporting a phantom in-progress meeting (and stale wake kinds) forever.
|
|
1478
|
-
meetingState=null; wakeKind.clear(); pendingMeeting=null; verifyState=null; pendingVerify=[]
|
|
1479
1510
|
logActivity('stop','all residents agree: problem solved'); await saveAll(); return
|
|
1480
1511
|
}
|
|
1481
1512
|
meetingState=null; wakeKind.clear(); await saveAll()
|
|
@@ -1849,7 +1880,19 @@ export function apply(ctx) {
|
|
|
1849
1880
|
|
|
1850
1881
|
// ---- liveness / scheduling ----
|
|
1851
1882
|
async function scheduleNext(){
|
|
1852
|
-
if(!running||autoDone){
|
|
1883
|
+
if(!running||autoDone){
|
|
1884
|
+
// A MANUALLY triggered final paper may run on an already-concluded run; it is the one thing
|
|
1885
|
+
// that still needs the timer, so it keeps its own pacing here (paperAdvance re-arms the
|
|
1886
|
+
// heartbeat while it waits and clears it once the paper is done).
|
|
1887
|
+
if(paperState&&paperState.status!=='done'&&paperState.status!=='failed'){ try{ await paperAdvance('scheduler-concluded') }catch(e){} return }
|
|
1888
|
+
clearHeartbeat(); return
|
|
1889
|
+
}
|
|
1890
|
+
// The run is CONCLUDING (its closing meeting voted unanimously to stop): the ONLY remaining
|
|
1891
|
+
// work is the final paper. No new research round may be dispatched behind it (docs/final-paper.md §A1).
|
|
1892
|
+
if(paperState&&paperState.status!=='done'&&paperState.status!=='failed'){
|
|
1893
|
+
try{ await paperAdvance('scheduler-paper') }catch(e){ console.error('vibe-math-v4: paper pass failed: '+String((e&&e.message)||e)) }
|
|
1894
|
+
return
|
|
1895
|
+
}
|
|
1853
1896
|
// Residents the host's live-child cap refused are retried here — the single choke point every
|
|
1854
1897
|
// scheduling pass goes through — so a refused spawn is deferred by a whole round (and paced by
|
|
1855
1898
|
// the heartbeat) instead of being retried inside the loop that discovered the refusal.
|
|
@@ -1991,6 +2034,10 @@ export function apply(ctx) {
|
|
|
1991
2034
|
const parsed=parseReply(output)
|
|
1992
2035
|
const kind=wakeKind.get(r.rId)||'normal'
|
|
1993
2036
|
postmark(r, parsed, kind) // context/compact bookkeeping — a mid-consensus turn may NOT ack a compaction it never received
|
|
2037
|
+
// The final-paper flow (docs/final-paper.md §A1) has its own wake kinds and its own state machine. It is
|
|
2038
|
+
// handled BEFORE the meeting/verify branches and before their `!running||autoDone` guards,
|
|
2039
|
+
// because a manually triggered paper legitimately runs on an already-concluded run.
|
|
2040
|
+
if(kind.indexOf('paper:')===0){ await onPaperWakeEnd(r,parsed); return }
|
|
1994
2041
|
if(kind==='meeting' && meetingState){
|
|
1995
2042
|
meetingState.inputs[r.rId]={input:parsed.input||parsed.summary||'',voteSolved:typeof parsed.voteSolved==='boolean'?parsed.voteSolved:null,propose_verify:parsed.propose_verify||null,propose_task:parsed.propose_task||null,task_desc:parsed.task_desc||'',claim_task:parsed.claim_task||null}
|
|
1996
2043
|
meetingState.lastInputAt=now()
|
|
@@ -2048,6 +2095,714 @@ export function apply(ctx) {
|
|
|
2048
2095
|
await saveAll(); await scheduleNext()
|
|
2049
2096
|
}
|
|
2050
2097
|
|
|
2098
|
+
// ================= final paper (docs/final-paper.md §3-§5) =================
|
|
2099
|
+
// When v4's consensus has converged (§2: the unanimous "solved" meeting closed, no ballot in
|
|
2100
|
+
// flight, nothing queued) the run must produce a PAPER, not stop at a chat log. The team
|
|
2101
|
+
// co-authors it: every resident writes the part it owns, the facilitator (the framework)
|
|
2102
|
+
// MERGES the parts, every participant CROSS-REVIEWS at least one other part, the `paperEditor`
|
|
2103
|
+
// finalises the merged draft, and the paper is finalised only when every participant has
|
|
2104
|
+
// explicitly stated it is deliverable. Dissent iterates a bounded number of rounds and, at the
|
|
2105
|
+
// cap, the disagreement is recorded in the appendix and the run is warned about.
|
|
2106
|
+
//
|
|
2107
|
+
// Everything the paper states comes from EVIDENCE ALREADY ON DISK (Verified/ cards, Propos/
|
|
2108
|
+
// cards, Methods/ cards, Shared/meetings|debates/, the Lean records) or from the team's own
|
|
2109
|
+
// written parts. Nothing is invented; unresolved/refuted items are flagged as such.
|
|
2110
|
+
const PAPER_FORMATS=['both','md','tex'], PAPER_LANGS=['zh','en']
|
|
2111
|
+
// v2 §A5: v4's `facilitator` is NOT a roster member and has no LLM (it is the framework / the
|
|
2112
|
+
// human-side messenger), so "let the facilitator unify the notation" has no author. The editor is
|
|
2113
|
+
// therefore the SESSION ROOT (the office = the human side) by default, or one named resident.
|
|
2114
|
+
const PAPER_EDITOR_DEFAULT='office'
|
|
2115
|
+
function paperEditorOk(v){ const s=String(v==null?'':v).trim(); return s==='office'||/^resident:[^\s]+$/.test(s) }
|
|
2116
|
+
// v2 §A4: v4's REAL closure signal is the closing meeting's UNANIMOUS stop vote — the branch
|
|
2117
|
+
// below in `finalizeMeeting` (allSolved). The v1 wording ("every verify target decided / mailbox
|
|
2118
|
+
// empty") is stricter than the code and would never fire, so the paper phase is entered from
|
|
2119
|
+
// inside that exact branch and any still-未决 item is flagged in the paper instead.
|
|
2120
|
+
const PAPER_CLOSURE_SIGNAL='finalizeMeeting/allSolved(一致性会议的一致停止票)'
|
|
2121
|
+
const PAPER_MAX_ROUNDS=2 // bounded iterations after dissent (docs/final-paper.md §4: "有上限")
|
|
2122
|
+
const PAPER_NUDGE_TRIES=2 // paced re-asks before the step deadline declares a timeout
|
|
2123
|
+
const PAPER_WAIT_FLOOR_MS=2000 // a step's deadline is at least this (tests run with tiny timeouts)
|
|
2124
|
+
const PAPER_KINDS={part:'paper:part',review:'paper:review',final:'paper:final',vote:'paper:vote',revise:'paper:revise'}
|
|
2125
|
+
const PAPER_MARK={part:'[PAPER PART]',review:'[PAPER REVIEW]',final:'[PAPER FINAL]',vote:'[PAPER DELIVERABLE]',revise:'[PAPER REVISE]'}
|
|
2126
|
+
const PAPER_HEAD_ZH=['标题、作者、日期与摘要','引言与问题背景','原问题的完整解法','已检验通过的命题','已解决的子问题与中间成果','创造或发现的有价值之物','规律总结','讨论、局限与展望','附录:证据与文件索引']
|
|
2127
|
+
const PAPER_HEAD_EN=['Title, Authors, Date and Abstract','Introduction and Problem Background','Complete Solution of the Original Problem','Verified Propositions','Solved Sub-problems and Intermediate Results','Valuable Creations and Discoveries','Patterns and Reusable Principles','Discussion, Limitations and Outlook','Appendix: Evidence and File Index']
|
|
2128
|
+
|
|
2129
|
+
function paperTitles(lang){ return lang==='en'?PAPER_HEAD_EN:PAPER_HEAD_ZH }
|
|
2130
|
+
/** The run id doubles as the v4 "institute id": Paper/<run id>/ (docs/final-paper.md §5). Sanitised through
|
|
2131
|
+
* `idSafe`+`slugify`, so a hostile problem text/title can never produce `../`. */
|
|
2132
|
+
function paperIdFor(){ return idSafe(slugify('v4-'+problemId))+'-'+idSafe(runId) }
|
|
2133
|
+
function paperDirFor(){ return 'Paper/'+paperIdFor() }
|
|
2134
|
+
/** Containment (docs/final-paper.md §6): every paper write must stay inside `Paper/<id>/`. */
|
|
2135
|
+
async function paperWriteRel(rel,content){
|
|
2136
|
+
const dir=paperState?paperState.dir:paperDirFor()
|
|
2137
|
+
const norm=String(rel).replace(/\\/g,'/')
|
|
2138
|
+
if(norm.indexOf(dir+'/')!==0 || norm.indexOf('..')!==-1) return false
|
|
2139
|
+
return await writeText(norm,content)
|
|
2140
|
+
}
|
|
2141
|
+
function paperLog(line){ const ps=paperState; if(ps) ps.log.push(fmtTime()+'|'+String(line)) }
|
|
2142
|
+
function paperStepLimit(){ return Math.max(PAPER_WAIT_FLOOR_MS, recoverStallMs()*4) }
|
|
2143
|
+
function paperCfg(ov){
|
|
2144
|
+
const o=ov||{}
|
|
2145
|
+
const ed=String(o.editor==null?'':o.editor).trim()
|
|
2146
|
+
return {
|
|
2147
|
+
language:PAPER_LANGS.indexOf(String(o.lang))!==-1?String(o.lang):params.paperLanguage,
|
|
2148
|
+
format:PAPER_FORMATS.indexOf(String(o.format))!==-1?String(o.format):params.paperFormat,
|
|
2149
|
+
editor:paperEditorOk(ed)?ed:(paperEditorOk(params.paperEditor)?String(params.paperEditor):PAPER_EDITOR_DEFAULT),
|
|
2150
|
+
compilePdf:params.paperCompilePdf!==false,
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
/** `resident:<id>` -> the id, `office`/absent -> ''. */
|
|
2154
|
+
function paperEditorResidentId(editor){ const m=/^resident:([^\s]+)$/.exec(String(editor||'')); return m?m[1]:'' }
|
|
2155
|
+
// ---- evidence (every claim in §4-§9 of the skeleton must be traceable to one of these) ----
|
|
2156
|
+
async function paperListDirFiles(base){
|
|
2157
|
+
const out=[]
|
|
2158
|
+
try {
|
|
2159
|
+
const t=await fs.resolve(base,{cwd:frameworkRoot()}); if(await fs.stat(t)===undefined) return out
|
|
2160
|
+
for(const e of (await fs.listDir(t))||[]){ if(!e||e.type!=='file'||!/\.md$/.test(String(e.name))) continue
|
|
2161
|
+
const rel=base+'/'+e.name; out.push({rel,text:(await readText(rel))||''}) }
|
|
2162
|
+
} catch(e){ /* best effort: an absent directory is an empty evidence set */ }
|
|
2163
|
+
out.sort((a,b)=>a.rel<b.rel?-1:a.rel>b.rel?1:0); return out
|
|
2164
|
+
}
|
|
2165
|
+
async function paperListCards(base){
|
|
2166
|
+
const out=[]
|
|
2167
|
+
try {
|
|
2168
|
+
const root=await fs.resolve(base,{cwd:frameworkRoot()}); if(await fs.stat(root)===undefined) return out
|
|
2169
|
+
for(const d of (await fs.listDir(root))||[]){ if(!d||d.type!=='directory') continue
|
|
2170
|
+
const dir=base+'/'+d.name
|
|
2171
|
+
try {
|
|
2172
|
+
const t=await fs.resolve(dir,{cwd:frameworkRoot()}); if(await fs.stat(t)===undefined) continue
|
|
2173
|
+
for(const e of (await fs.listDir(t))||[]){ if(!e||e.type!=='file'||!/\.md$/.test(String(e.name))) continue
|
|
2174
|
+
const rel=dir+'/'+e.name; out.push({rel,text:(await readText(rel))||''}) }
|
|
2175
|
+
} catch(e2){ /* one unreadable library must not stop the scan */ }
|
|
2176
|
+
}
|
|
2177
|
+
} catch(e){ /* best effort */ }
|
|
2178
|
+
out.sort((a,b)=>a.rel<b.rel?-1:a.rel>b.rel?1:0); return out
|
|
2179
|
+
}
|
|
2180
|
+
function paperField(text,name){
|
|
2181
|
+
const esc=String(name).replace(/[.*+?^${}()|[\]\\]/g,'\\$&')
|
|
2182
|
+
const re=new RegExp('(?:^|\\n|;\\s*)-\\s*'+esc+':\\s*([^;\\n]*)')
|
|
2183
|
+
const m=re.exec(String(text||'')); return m?m[1].trim():''
|
|
2184
|
+
}
|
|
2185
|
+
async function paperEvidence(){
|
|
2186
|
+
const [verifiedFiles,problemFiles,propos,methods,subproblems,meetings,debates]=await Promise.all([
|
|
2187
|
+
paperListDirFiles('Verified/命题'), paperListDirFiles('Verified/问题'), paperListCards('Propos'),
|
|
2188
|
+
paperListCards('Methods'), paperListCards('Subproblems'), paperListDirFiles('Shared/meetings'), paperListDirFiles('Shared/debates'),
|
|
2189
|
+
])
|
|
2190
|
+
const parse=f=>({rel:f.rel,id:paperField(f.text,'ID'),结论:paperField(f.text,'结论'),概率:paperField(f.text,'概率'),
|
|
2191
|
+
类型:paperField(f.text,'类型'),状态:paperField(f.text,'状态')||paperField(f.text,'状态'),标题:paperField(f.text,'标题'),
|
|
2192
|
+
可信断言:paperField(f.text,'可信断言'),text:f.text})
|
|
2193
|
+
return {
|
|
2194
|
+
problem:problemText, problemId:problemId, runId:runId, project:currentProject, converged:!!autoDone,
|
|
2195
|
+
verified:verifiedFiles.map(parse), verifiedProblems:problemFiles.map(parse),
|
|
2196
|
+
propos:propos.map(parse), methods:methods.map(parse), subproblems:subproblems.map(parse),
|
|
2197
|
+
meetings:meetings.map(f=>f.rel), debates:debates.map(f=>f.rel),
|
|
2198
|
+
formal:formalView().objects.map(o=>({target:o.target,status:o.status,file:o.file,proof:o.proof})),
|
|
2199
|
+
index:[...verifiedFiles,...problemFiles,...propos,...methods,...subproblems,...meetings,...debates].map(f=>f.rel).sort(),
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
function paperHash(s){ let h=5381; const t=String(s==null?'':s); for(let i=0;i<t.length;i++) h=((h*33)^t.charCodeAt(i))>>>0; return h.toString(16).padStart(8,'0') }
|
|
2203
|
+
/** Idempotency is per (run id, finalizedAt) plus per-artifact existence — docs/final-paper.md §C explicitly
|
|
2204
|
+
* drops v1's multi-file inputsHash. This only fingerprints one string for the log, never a
|
|
2205
|
+
* decision. */
|
|
2206
|
+
function paperFingerprint(ev){ return paperHash([ev.problemId,ev.runId,ev.problem,ev.index.join('|')].join('\n')) }
|
|
2207
|
+
// ---- prompts (stable markers so the flow is observable/driveable) ----
|
|
2208
|
+
function paperPrompt(kind,rid,again){
|
|
2209
|
+
const ps=paperState, T=paperTitles(ps.cfg.language), zh=ps.cfg.language!=='en'
|
|
2210
|
+
const head=zh
|
|
2211
|
+
? '【最终论文·团队合写】你('+rid+')是本次运行的参与常驻。'
|
|
2212
|
+
: '[FINAL PAPER — TEAM CO-AUTHORING] You ('+rid+') are a participating resident of this run.'
|
|
2213
|
+
const common=(zh?'\n论文目录:`'+ps.dir+'/`(一切写入必须在这里面)。只整理**已有证据**:你自己的库、Verified/、Shared/meetings|debates/ 与既有卡片;**不得编造**;未决/被否证的条目必须显式标注。'
|
|
2214
|
+
:'\nPaper directory: `'+ps.dir+'/` (everything you write stays inside it). Only organise EXISTING evidence (your own library, Verified/, Shared/meetings|debates/, the recorded cards); never invent; flag unresolved/refuted items explicitly.')
|
|
2215
|
+
if(kind==='part') return PAPER_MARK.part+'\n'+head+'\n'+(again?(zh?'(上一次没有收到你的部分,请重发。)\n':'(no part was received from you last time — please resend.)\n'):'')
|
|
2216
|
+
+(zh?'请撰写**你负责的那一部分**(你库里的命题/方法/子问题/进展,以及你亲自参与的会议与争论中确立的内容),成文即可,不必追求篇幅。\n'
|
|
2217
|
+
:'Write the part YOU own (the propositions/methods/sub-problems/progress in your library and what you personally established in meetings/debates).\n')
|
|
2218
|
+
+common+'\n'+(zh?'回复一个 JSON:`{"paperPart":{"title":"<部分标题>","markdown":"<正文 markdown>"}}`(也可以直接用 fs 写到 `'+ps.dir+'/parts/'+rid+'.md`)。'
|
|
2219
|
+
:'Reply with one JSON object: `{"paperPart":{"title":"<title>","markdown":"<body markdown>"}}` (you may also write `'+ps.dir+'/parts/'+rid+'.md` with fs).')
|
|
2220
|
+
+'\n'+(zh?'九节骨架:':"The paper's nine sections: ")+T.join(' / ')
|
|
2221
|
+
if(kind==='review') return PAPER_MARK.review+'\n'+head+'\n'+(again?(zh?'(上一次的互审没有指名**其他**参与者,请重发。)\n':'(your last review did not name ANOTHER participant — please resend.)\n'):'')
|
|
2222
|
+
+(zh?'合并稿见 `'+ps.dir+'/draft.md`。请**审阅至少一位其他参与者的部分**(不能审自己),指出具体问题并给出可落实的改进。\n'
|
|
2223
|
+
:'The merged draft is at `'+ps.dir+'/draft.md`. Review AT LEAST ONE OTHER participant\'s part (never your own), name concrete problems and give actionable improvements.\n')
|
|
2224
|
+
+common+'\n'+(zh?'回复一个 JSON:`{"paperReview":{"reviewed":"<被审的参与者 id>","points":["<问题>","..."],"improvements":"<你建议的改进>"}}`'
|
|
2225
|
+
:'Reply with one JSON object: `{"paperReview":{"reviewed":"<other participant id>","points":["..."],"improvements":"..."}}`')
|
|
2226
|
+
if(kind==='final') return PAPER_MARK.final+'\n'+head+'\n'
|
|
2227
|
+
+(zh?'你是本次的**定稿代表**(paperEditor)。请把合并稿 `'+ps.dir+'/draft.md` 与互审意见梳理成**最终稿**:去重、统一术语与记号,保留全部证据与未决标注。\n'
|
|
2228
|
+
:'You are the run\'s EDITOR (paperEditor). Turn the merged draft `'+ps.dir+'/draft.md` plus the review points into the FINAL draft: deduplicate, unify terms and notation, keep every evidence pointer and unresolved flag.\n')
|
|
2229
|
+
+common+'\n'+(zh?'回复一个 JSON:`{"paperFinal":{"markdown":"<最终稿正文 markdown>"}}`(或直接写 `'+ps.dir+'/final.md`)。'
|
|
2230
|
+
:'Reply with one JSON object: `{"paperFinal":{"markdown":"<final body markdown>"}}` (or write `'+ps.dir+'/final.md`).')
|
|
2231
|
+
// vote / revise
|
|
2232
|
+
return PAPER_MARK[kind]+'\n'+head+'\n'
|
|
2233
|
+
+(zh?'请阅读最终稿 `'+ps.dir+'/final.md`,然后**明确表态它是否可交付**。有反对必须说明理由(框架会迭代,超限会把分歧写进附录)。\n'
|
|
2234
|
+
:'Read the final draft at `'+ps.dir+'/final.md` and state EXPLICITLY whether it is deliverable. Any objection must state its reason (the framework iterates; at the cap the disagreement goes into the appendix).\n')
|
|
2235
|
+
+common+'\n'+(zh?'回复一个 JSON:`{"paperDeliverable":true|false,"reason":"<理由>"}`'
|
|
2236
|
+
:'Reply with one JSON object: `{"paperDeliverable":true|false,"reason":"<why>"}`')
|
|
2237
|
+
}
|
|
2238
|
+
// ---- state machine ----
|
|
2239
|
+
function paperHas(step,rid){
|
|
2240
|
+
const ps=paperState; if(!ps) return false
|
|
2241
|
+
if(step==='parts') return !!ps.parts[rid]
|
|
2242
|
+
if(step==='review') return !!ps.reviews[rid]
|
|
2243
|
+
if(step==='final') return !!ps.finalDraft
|
|
2244
|
+
if(step==='vote') return typeof ps.deliverable[rid]==='boolean'
|
|
2245
|
+
return false
|
|
2246
|
+
}
|
|
2247
|
+
function paperMarkMissing(step,rid){
|
|
2248
|
+
const ps=paperState
|
|
2249
|
+
if(step==='parts') ps.parts[rid]={text:'',missing:true,source:'missing'}
|
|
2250
|
+
else if(step==='review') ps.reviews[rid]={missing:true}
|
|
2251
|
+
else if(step==='final'){ if(!ps.finalDraft) ps.finalDraft=paperFacilitatorFinal() }
|
|
2252
|
+
else if(step==='vote') ps.deliverable[rid]=false
|
|
2253
|
+
paperLog('超时:'+rid+' 在 '+step+' 阶段未回应,已按"缺少该部分/未表态"记录')
|
|
2254
|
+
}
|
|
2255
|
+
async function paperWakeSome(ids,kind){
|
|
2256
|
+
const ps=paperState
|
|
2257
|
+
for(const rid of ids){
|
|
2258
|
+
const r=residents.get(rid)
|
|
2259
|
+
if(!r||!r.childId){ paperLog('警告:'+rid+' 已不在场,跳过 '+kind); paperMarkMissing(kind==='part'?'parts':kind==='review'?'review':kind==='final'?'final':'vote',rid); continue }
|
|
2260
|
+
if(busy.has(rid)) continue
|
|
2261
|
+
const ok=await wakeResident(r,paperPrompt(kind,rid),PAPER_KINDS[kind])
|
|
2262
|
+
if(!ok) paperLog('警告:无法唤醒 '+rid+' 执行 '+kind)
|
|
2263
|
+
}
|
|
2264
|
+
await paperWriteLog()
|
|
2265
|
+
}
|
|
2266
|
+
async function paperWait(step){
|
|
2267
|
+
const ps=paperState; if(!ps) return
|
|
2268
|
+
const miss=ps.participants.filter(x=>!paperHas(step,x))
|
|
2269
|
+
if(!miss.length) return await paperAdvance('wait-recheck')
|
|
2270
|
+
const nowTs=now()
|
|
2271
|
+
if(nowTs-ps.stepAt>=paperStepLimit()){
|
|
2272
|
+
for(const rid of miss) paperMarkMissing(step,rid)
|
|
2273
|
+
ps.warning=(ps.warning?ps.warning+';':'')+'有参与者在 '+step+' 阶段超时未回应('+miss.join('、')+'),已按"缺少该部分"记录并继续'
|
|
2274
|
+
paperLog('警告:'+ps.warning); ps.stepAt=nowTs
|
|
2275
|
+
await paperWriteLog(); return await paperAdvance('timeout')
|
|
2276
|
+
}
|
|
2277
|
+
// Paced re-ask: at most one nudge per stall window, and only PAPER_NUDGE_TRIES in total, so an
|
|
2278
|
+
// unresponsive host cannot produce a wake storm while the deadline is still open.
|
|
2279
|
+
if(nowTs-(ps.lastNudgeAt||0)>=posMs(params.activityTimeoutMs,120000)){
|
|
2280
|
+
ps.lastNudgeAt=nowTs
|
|
2281
|
+
for(const rid of miss){
|
|
2282
|
+
const key=rid+':'+step
|
|
2283
|
+
ps.attempts[key]=(ps.attempts[key]||0)+1
|
|
2284
|
+
if(ps.attempts[key]>PAPER_NUDGE_TRIES) continue
|
|
2285
|
+
const r=residents.get(rid)
|
|
2286
|
+
if(!r||!r.childId||busy.has(rid)) continue
|
|
2287
|
+
const ok=await wakeResident(r,paperPrompt(step,rid,true),PAPER_KINDS[step])
|
|
2288
|
+
if(!ok) paperLog('警告:无法唤醒 '+rid+'('+step+' 重试)')
|
|
2289
|
+
}
|
|
2290
|
+
await paperWriteLog()
|
|
2291
|
+
}
|
|
2292
|
+
armHeartbeat() // keep the clock running so the step deadline is actually reachable
|
|
2293
|
+
}
|
|
2294
|
+
async function paperAdvance(why){
|
|
2295
|
+
const ps=paperState
|
|
2296
|
+
if(!ps||ps.status==='done'||ps.status==='failed') return
|
|
2297
|
+
try {
|
|
2298
|
+
if(ps.status==='parts'){
|
|
2299
|
+
if(ps.participants.every(x=>paperHas('parts',x))) return await paperMerge()
|
|
2300
|
+
return await paperWait('parts')
|
|
2301
|
+
}
|
|
2302
|
+
if(ps.status==='review'){
|
|
2303
|
+
if(ps.participants.every(x=>paperHas('review',x))) return await paperFinalStep()
|
|
2304
|
+
return await paperWait('review')
|
|
2305
|
+
}
|
|
2306
|
+
if(ps.status==='final'){
|
|
2307
|
+
if(ps.finalDraft) return await paperVoteStep()
|
|
2308
|
+
return await paperWait('final')
|
|
2309
|
+
}
|
|
2310
|
+
if(ps.status==='vote'){
|
|
2311
|
+
if(!ps.participants.every(x=>paperHas('vote',x))) return await paperWait('vote')
|
|
2312
|
+
if(ps.participants.every(x=>ps.deliverable[x]===true)) return await paperFinalize()
|
|
2313
|
+
return await paperIterate()
|
|
2314
|
+
}
|
|
2315
|
+
} catch(e){ console.error('vibe-math-v4: paper step ('+(why||'')+') failed: '+String((e&&e.stack)||e)) }
|
|
2316
|
+
}
|
|
2317
|
+
async function paperMerge(){
|
|
2318
|
+
const ps=paperState
|
|
2319
|
+
const parts=ps.participants.map(rid=>({rid,text:String((ps.parts[rid]&&ps.parts[rid].text)||'')})).filter(p=>p.text.trim())
|
|
2320
|
+
const seen=new Map(), dups=[], terms=new Map(), conflicts=[], body=[]
|
|
2321
|
+
for(const p of parts){
|
|
2322
|
+
const paras=p.text.replace(/\r\n/g,'\n').split(/\n{2,}/)
|
|
2323
|
+
for(const raw of paras){
|
|
2324
|
+
const para=raw.trim(); if(!para) continue
|
|
2325
|
+
const norm=para.replace(/\s+/g,' ').toLowerCase()
|
|
2326
|
+
if(norm.length>=40 && seen.has(norm)){ dups.push({owner:seen.get(norm),dup:p.rid,head:para.slice(0,60)}); continue }
|
|
2327
|
+
if(norm.length>=40) seen.set(norm,p.rid)
|
|
2328
|
+
body.push(para)
|
|
2329
|
+
// Notation/term unification: the FIRST definition of a `- 术语: 定义` / `**术语**:…` line
|
|
2330
|
+
// wins; a different definition of the same term in another part is recorded as a conflict.
|
|
2331
|
+
const m=/^(?:[-*]\s*)?(?:\*\*)?([\u4e00-\u9fa5A-Za-z][^::*]{0,24})(?:\*\*)?\s*[::]\s*(\S.*)$/.exec(para)
|
|
2332
|
+
if(m){ const term=m[1].trim()
|
|
2333
|
+
if(!terms.has(term)) terms.set(term,{owner:p.rid,def:para})
|
|
2334
|
+
else if(terms.get(term).owner!==p.rid && terms.get(term).def!==para) conflicts.push({term,kept:terms.get(term).owner,conflict:p.rid}) }
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
ps.draft=body.join('\n\n')
|
|
2338
|
+
const L=['# 合并稿(facilitator 合并)','','- 论文 id:'+ps.id,'- 参与成员:'+(ps.participants.join('、')||'(无常驻)'),
|
|
2339
|
+
'- 合并规则:跨部分**完全重复**的段落只保留首次出现;同一术语的**首个定义**为准,冲突记入下方。','']
|
|
2340
|
+
for(const p of parts) L.push('## 部分|'+p.rid,'',p.text.trim(),'')
|
|
2341
|
+
L.push('## 合并说明','')
|
|
2342
|
+
L.push('- 去重:'+(dups.length?dups.map(d=>'「'+d.head+'…」('+d.dup+' 重复了 '+d.owner+')').join(';'):'无跨部分重复段落'))
|
|
2343
|
+
L.push('- 术语/记号统一:'+(terms.size?[...terms.entries()].map(([k,v])=>k+'(取 '+v.owner+' 的定义)').join(';'):'未识别到形如「术语:定义」的行'))
|
|
2344
|
+
L.push('- 术语冲突:'+(conflicts.length?conflicts.map(c=>c.term+'(保留 '+c.kept+','+c.conflict+' 的定义不同)').join(';'):'无'))
|
|
2345
|
+
ps.mergeNotes={dups:dups.length,terms:terms.size,conflicts:conflicts.length}
|
|
2346
|
+
paperLog('facilitator 合并完成:'+parts.length+' 个部分,去重 '+dups.length+' 段,统一术语 '+terms.size+' 个,冲突 '+conflicts.length+' 处')
|
|
2347
|
+
await paperWriteRel(ps.dir+'/draft.md',L.join('\n')+'\n')
|
|
2348
|
+
ps.status='review'; ps.stepAt=now(); ps.lastNudgeAt=0
|
|
2349
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
2350
|
+
if(!ps.participants.length){ return await paperFinalStep() }
|
|
2351
|
+
await paperWakeSome(ps.participants.slice(),'review')
|
|
2352
|
+
await paperAdvance('review-started')
|
|
2353
|
+
}
|
|
2354
|
+
/** The framework's own finalisation (paperEditor='facilitator'): the merged body plus the review
|
|
2355
|
+
* round APPLIED as an attributed revision block. No new claims are introduced. */
|
|
2356
|
+
function paperFacilitatorFinal(){
|
|
2357
|
+
const ps=paperState
|
|
2358
|
+
const L=[String(ps.draft||'').trim()]
|
|
2359
|
+
const rows=ps.participants.map(rid=>ps.reviews[rid]).filter(Boolean)
|
|
2360
|
+
if(rows.length){
|
|
2361
|
+
L.push('','## 互审改进(已采纳)')
|
|
2362
|
+
for(const rid of ps.participants){
|
|
2363
|
+
const rv=ps.reviews[rid]; if(!rv) continue
|
|
2364
|
+
if(rv.missing){ L.push('- '+rid+':互审超时未提交(已记入附录)'); continue }
|
|
2365
|
+
const pts=(rv.points||[]).map(x=>'「'+x+'」').join('、')
|
|
2366
|
+
L.push('- '+rid+' → 审阅 '+rv.reviewed+':'+(pts||'(未列出具体问题)')+(rv.improvements?(';改进:'+rv.improvements):''))
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return L.join('\n').trim()
|
|
2370
|
+
}
|
|
2371
|
+
async function paperFinalStep(){
|
|
2372
|
+
const ps=paperState
|
|
2373
|
+
ps.status='final'; ps.stepAt=now(); ps.lastNudgeAt=0
|
|
2374
|
+
const wantId=paperEditorResidentId(ps.cfg.editor)
|
|
2375
|
+
const r=wantId?residents.get(wantId):undefined
|
|
2376
|
+
if(wantId && r && r.childId){
|
|
2377
|
+
ps.editorId=wantId
|
|
2378
|
+
paperLog('定稿代表:resident:'+wantId+'(paperEditor 指定)')
|
|
2379
|
+
const ok=await wakeResident(r,paperPrompt('final',wantId),PAPER_KINDS.final)
|
|
2380
|
+
if(ok){ await paperWriteMeta(); await paperWriteLog(); armHeartbeat(); return }
|
|
2381
|
+
ps.warning='无法唤醒定稿代表 resident:'+wantId+',降级到 office'
|
|
2382
|
+
ps.editorDowngraded=true; paperLog('警告:'+ps.warning)
|
|
2383
|
+
} else if(wantId){
|
|
2384
|
+
// v2 §A5: the named editor left the roster (e.g. removeMember) -> downgrade to office and
|
|
2385
|
+
// RECORD the downgrade in meta + paper.log.md (never silently substitute an author).
|
|
2386
|
+
ps.warning='定稿代表 resident:'+wantId+' 已不在编制(可能已被解雇),降级到 office'
|
|
2387
|
+
ps.editorDowngraded=true; paperLog('警告:'+ps.warning)
|
|
2388
|
+
}
|
|
2389
|
+
ps.editorId='office'
|
|
2390
|
+
paperLog('定稿代表:office(会话根/人类侧)—— 框架依 office 之责把合并稿与互审记录整理成最终稿')
|
|
2391
|
+
if(!ps.finalDraft) ps.finalDraft=paperFacilitatorFinal()
|
|
2392
|
+
await paperWriteRel(ps.dir+'/final.md',ps.finalDraft)
|
|
2393
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
2394
|
+
return await paperVoteStep()
|
|
2395
|
+
}
|
|
2396
|
+
async function paperVoteStep(){
|
|
2397
|
+
const ps=paperState
|
|
2398
|
+
if(!ps.finalDraft) ps.finalDraft=paperFacilitatorFinal()
|
|
2399
|
+
await paperWriteRel(ps.dir+'/final.md',ps.finalDraft)
|
|
2400
|
+
ps.status='vote'; ps.stepAt=now(); ps.lastNudgeAt=0
|
|
2401
|
+
paperLog('交付表决:请全体参与成员明确表态"可交付"')
|
|
2402
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
2403
|
+
if(!ps.participants.length) return await paperFinalize()
|
|
2404
|
+
await paperWakeSome(ps.participants.slice(),'vote')
|
|
2405
|
+
await paperAdvance('vote-started')
|
|
2406
|
+
}
|
|
2407
|
+
async function paperIterate(){
|
|
2408
|
+
const ps=paperState
|
|
2409
|
+
const against=ps.participants.filter(x=>ps.deliverable[x]!==true)
|
|
2410
|
+
ps.dissent=against.slice()
|
|
2411
|
+
ps.round+=1
|
|
2412
|
+
paperLog('第 '+ps.round+' 轮:'+against.join('、')+' 未表示可交付('+(against.map(x=>ps.deliverable[x]===false?'明确反对':'未表态').join(';'))+')')
|
|
2413
|
+
if(ps.round>ps.maxRounds){
|
|
2414
|
+
ps.warning=(ps.warning?ps.warning+';':'')+'互审/交付迭代达到上限('+ps.maxRounds+'):'+against.join('、')+' 的分歧已写入附录'
|
|
2415
|
+
paperLog('警告:'+ps.warning)
|
|
2416
|
+
return await paperFinalize()
|
|
2417
|
+
}
|
|
2418
|
+
for(const rid of against) ps.deliverable[rid]=undefined
|
|
2419
|
+
ps.status='vote'; ps.stepAt=now(); ps.lastNudgeAt=0
|
|
2420
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
2421
|
+
await paperWakeSome(against,'revise')
|
|
2422
|
+
await paperAdvance('revise-started')
|
|
2423
|
+
}
|
|
2424
|
+
// ---- content: the nine-section skeleton, built ONLY from evidence + the team's own text ----
|
|
2425
|
+
function paperExtractSection(draft,words){
|
|
2426
|
+
const lines=String(draft||'').split(/\r?\n/)
|
|
2427
|
+
let start=-1,level=0
|
|
2428
|
+
for(let i=0;i<lines.length;i++){
|
|
2429
|
+
const m=/^(#{1,6})\s*(.+)$/.exec(lines[i])
|
|
2430
|
+
if(m&&new RegExp(words,'i').test(m[2])){ start=i+1; level=m[1].length; break }
|
|
2431
|
+
}
|
|
2432
|
+
if(start<0) return ''
|
|
2433
|
+
const out=[]
|
|
2434
|
+
for(let i=start;i<lines.length;i++){
|
|
2435
|
+
const m=/^(#{1,6})\s*/.exec(lines[i])
|
|
2436
|
+
if(m&&m[1].length<=level) break
|
|
2437
|
+
out.push(lines[i])
|
|
2438
|
+
}
|
|
2439
|
+
return out.join('\n').trim()
|
|
2440
|
+
}
|
|
2441
|
+
function paperBullets(items,empty){ return items.length?items.map(x=>'- '+x).join('\n'):('- '+empty) }
|
|
2442
|
+
function paperCompose(ps,ev,cfg){
|
|
2443
|
+
const zh=cfg.language!=='en', T=paperTitles(cfg.language)
|
|
2444
|
+
const r=(a,b)=>zh?a:b
|
|
2445
|
+
const body=String(ps.finalDraft||ps.draft||'').trim()
|
|
2446
|
+
const truth=ev.verified.filter(v=>v.结论!=='假'), refuted=ev.verified.filter(v=>v.结论==='假')
|
|
2447
|
+
const pending=ev.propos.filter(p=>!['已验证·真','已验证·假'].includes(p.状态))
|
|
2448
|
+
const firstLine=String(ev.problem||'').split(/\r?\n/)[0].trim()
|
|
2449
|
+
const conclusion=r(
|
|
2450
|
+
(truth.length?('全体一致检验通过的命题 '+truth.length+' 条'+(refuted.length?(',被否证 '+refuted.length+' 条'):'')):'本轮没有取得全体一致的检验结论')+';未决条目 '+pending.length+' 条。',
|
|
2451
|
+
(truth.length?('Verified propositions: '+truth.length+(refuted.length?('; refuted: '+refuted.length):'')):'No unanimously verified proposition this run')+'; unresolved items: '+pending.length+'.')
|
|
2452
|
+
const sec=[];
|
|
2453
|
+
sec.push({title:T[0],body:[r('**摘要**:原问题——',' **Abstract** — problem: ')+firstLine,
|
|
2454
|
+
r('主要结论:','Main conclusion: ')+conclusion].join('\n')});
|
|
2455
|
+
sec.push({title:T[1],body:[r('原问题(完整陈述):','Original problem (full statement):'),'',String(ev.problem||''),
|
|
2456
|
+
'','- '+r('项目','Project')+':'+ev.project,'- '+r('运行','Run')+':'+ev.runId,'- '+r('状态','Status')+':'+(ev.converged?r('已收敛','converged'):r('未收敛','not converged'))].join('\n')});
|
|
2457
|
+
sec.push({title:T[2],body:[body||r('(团队本轮没有产出正文;以下仅为已有证据的索引。)','(The team authored no body this run; only the evidence index below.)'),
|
|
2458
|
+
'','### '+r('最终答案与结论','Final answer and conclusions'),paperBullets(
|
|
2459
|
+
truth.map(v=>v.id+(v.概率?('('+r('判定为真,估计值','true, estimate')+' '+v.概率+')'):'')+' ← `'+v.rel+'`'),
|
|
2460
|
+
r('本轮没有被全体一致判定为真的命题','No unanimously true proposition this run'))].join('\n')});
|
|
2461
|
+
sec.push({title:T[3],body:[paperBullets(
|
|
2462
|
+
ev.verified.map(v=>{ const verdict=v.结论==='假'?r('已否证','refuted'):r('真','true')
|
|
2463
|
+
return '['+verdict+'] '+v.id+(v.概率?('|'+r('概率','probability')+' '+v.概率):'')+'|'+r('证据','evidence')+' `'+v.rel+'`'+(v.类型?('|'+v.类型):'') }),
|
|
2464
|
+
r('本轮没有被检验通过的命题','No verified proposition this run')),
|
|
2465
|
+
'','### '+r('未决 / 未通过','Unresolved / not passed'),
|
|
2466
|
+
paperBullets(pending.map(p=>'[未决] '+p.id+'('+r('状态','status')+':'+(p.状态||'未定论')+')|`'+p.rel+'`')
|
|
2467
|
+
.concat(ev.formal.filter(f=>f.status!=='passed').map(f=>'[未决·形式化] '+f.target+'('+f.status+')'+(f.file?('|`'+f.file+'`'):''))),
|
|
2468
|
+
r('无','none'))].join('\n')});
|
|
2469
|
+
sec.push({title:T[4],body:[paperBullets(
|
|
2470
|
+
ev.verifiedProblems.map(v=>'[已解决] '+v.id+'|`'+v.rel+'`').concat(ev.subproblems.map(s=>'['+(['已验证·真','已验证·假'].includes(s.状态)?r('已解决','solved'):r('未决','unresolved'))+'] '+s.id+(s.标题?('「'+s.标题+'」'):'')+'|`'+s.rel+'`')),
|
|
2471
|
+
r('本轮没有记录子问题','No sub-problem was recorded this run'))].join('\n')});
|
|
2472
|
+
sec.push({title:T[5],body:[paperBullets(
|
|
2473
|
+
ev.methods.map(m=>m.id+(m.标题?('「'+m.标题+'」'):'')+'('+(m.类型||r('方法','method'))+','+(m.状态||r('经验','experiential'))+')'+(m.可信断言?('|'+r('可信断言','trusted claims')+':'+m.可信断言):'')+'|`'+m.rel+'`'),
|
|
2474
|
+
r('本轮没有记录方法/理论/工具','No method/theory/tool was recorded this run')),
|
|
2475
|
+
'',r('已归档的形式化产物:','Archived formal artifacts: ')+paperBullets(ev.formal.filter(f=>f.status==='passed').map(f=>f.target+(f.proof?('|`'+f.proof+'`'):'')+(f.file?('|`'+f.file+'`'):'')),r('无','none'))].join('\n')});
|
|
2476
|
+
const pattern=paperExtractSection(body,zh?'规律|总结|原理':'pattern|principle')
|
|
2477
|
+
sec.push({title:T[6],body:[pattern||[r('团队本轮没有单独撰写"规律总结";以下为**有证据支撑**的可复用条目(取自方法卡与已验证命题),未作外推:',
|
|
2478
|
+
'The team did not author a separate "patterns" section; below are the EVIDENCE-BACKED reusable items (method cards and verified propositions), with no extrapolation:'),
|
|
2479
|
+
paperBullets(ev.methods.filter(m=>m.可信断言).map(m=>m.可信断言+'(`'+m.rel+'`)')
|
|
2480
|
+
.concat(truth.map(v=>v.id+'(`'+v.rel+'`)')),r('无','none'))].join('\n')].join('\n')});
|
|
2481
|
+
const disc=paperExtractSection(body,zh?'讨论|局限|展望':'discussion|limitation|outlook')
|
|
2482
|
+
sec.push({title:T[7],body:[disc||r('(团队未单独撰写讨论;以下为框架按证据整理的局限。)','(No separate discussion was authored; the framework lists the evidenced limitations.)'),
|
|
2483
|
+
'','### '+r('局限与未决','Limitations and open items'),
|
|
2484
|
+
paperBullets([
|
|
2485
|
+
r('未决/未通过条目 '+pending.length+' 条(见第 4 节,均已标注)','Unresolved items: '+pending.length+' (flagged in §4)'),
|
|
2486
|
+
r('被否证的命题 '+refuted.length+' 条(见第 4 节)','Refuted propositions: '+refuted.length+' (see §4)'),
|
|
2487
|
+
(ps.compile&&ps.compile.result==='failed')?r('PDF 编译失败:已保留 paper.tex 与 paper.md 并上报','PDF compilation failed: paper.tex and paper.md were kept and reported'):'',
|
|
2488
|
+
(ps.compile&&ps.compile.result==='not-detected')?r('本机未检测到 LaTeX 引擎:只产出 tex+md','No LaTeX engine detected: tex+md only'):'',
|
|
2489
|
+
ps.dissent.length?r('交付分歧('+ps.dissent.join('、')+')已写入附录','Deliverability dissent ('+ps.dissent.join(', ')+') is recorded in the appendix'):'',
|
|
2490
|
+
ps.warning?r('过程警告:'+ps.warning,'Process warning: '+ps.warning):'',
|
|
2491
|
+
].filter(Boolean),r('无','none'))].join('\n')});
|
|
2492
|
+
sec.push({title:T[8],body:[r('证据与文件索引(全部相对于项目根):','Evidence and file index (all relative to the project root):'),
|
|
2493
|
+
'',paperBullets(ev.index.map(p=>'`'+p+'`'),r('无','none')),
|
|
2494
|
+
'','- '+r('过程记录','Process log')+':`'+ps.dir+'/paper.log.md`',
|
|
2495
|
+
'- '+r('元数据','Metadata')+':`'+ps.dir+'/paper.meta.json`',
|
|
2496
|
+
'- '+r('各成员撰写部分','Per-member parts')+':`'+ps.dir+'/parts/`',
|
|
2497
|
+
'- '+r('互审记录','Cross-review records')+':`'+ps.dir+'/reviews/`',
|
|
2498
|
+
'- '+r('合并稿/最终稿','Merged / final draft')+':`'+ps.dir+'/draft.md`、`'+ps.dir+'/final.md`',
|
|
2499
|
+
(ps.dissent.length||ps.round>ps.maxRounds)?['','### '+r('附录:交付分歧','Appendix: deliverability dissent'),paperBullets(ps.participants.filter(x=>ps.deliverable[x]!==true).map(x=>x+':'+(ps.deliverable[x]===false?r('反对交付','objected'):r('未表态','no statement'))+'|'+r('迭代轮次','rounds')+' '+ps.round+'/'+ps.maxRounds),r('无','none'))].join('\n'):'']
|
|
2500
|
+
.filter(Boolean).join('\n')});
|
|
2501
|
+
const title=r('Vibe-Mathematics v4 研究报告','Vibe-Mathematics v4 Research Report')+'|'+firstLine.slice(0,60)
|
|
2502
|
+
const md=L=>L
|
|
2503
|
+
const mdText=['# '+title,'','- '+r('作者','Authors')+':Vibe-Mathematics v4('+r('运行','run')+' '+ps.runId+')',
|
|
2504
|
+
'- '+r('日期','Date')+':'+fmtTime(ps.finalizedAt||now()).slice(0,10),
|
|
2505
|
+
'- '+r('参与成员','Participants')+':'+(ps.participants.join('、')||r('(无常驻)','(no residents)')),
|
|
2506
|
+
'- '+r('定稿代表','Editor')+':'+(ps.editorId||cfg.editor),'',
|
|
2507
|
+
...sec.map((s,i)=>['## '+(i+1)+'. '+s.title,'',s.body,''].join('\n'))].join('\n')
|
|
2508
|
+
const texBody=sec.map((s,i)=>'\\section{'+texEsc(s.title)+'}\n'+texEsc(s.body)).join('\n\n')
|
|
2509
|
+
const tex=paperTex(cfg,title,texBody)
|
|
2510
|
+
return {title, sections:sec, md:mdText, tex, texBody}
|
|
2511
|
+
}
|
|
2512
|
+
function paperTex(cfg,title,texBody){
|
|
2513
|
+
const zh=cfg.language!=='en'
|
|
2514
|
+
const cls=zh?'ctexart':'article'
|
|
2515
|
+
const pkgs=['amsmath','amssymb','amsthm','geometry','hyperref','longtable','booktabs']
|
|
2516
|
+
return ['% generated by vibe-math-v4 (final paper)','\\documentclass[11pt]{'+cls+'}',
|
|
2517
|
+
...pkgs.map(p=>'\\usepackage{'+p+'}'),
|
|
2518
|
+
'\\title{'+texEsc(title)+'}','\\author{Vibe-Mathematics v4}','\\date{'+texEsc(fmtTime(now()).slice(0,10))+'}',
|
|
2519
|
+
'\\begin{document}','\\maketitle','',texBody,'','\\end{document}',''].join('\n')
|
|
2520
|
+
}
|
|
2521
|
+
/** tex escaping (docs/final-paper.md §5): every `_ % & # $ { } ~ ^ \\` becomes a safe command/escape. */
|
|
2522
|
+
function texEsc(s){
|
|
2523
|
+
const M={'\\':'\\textbackslash{}','{':'\\{','}':'\\}','$':'\\$','&':'\\&','#':'\\#','^':'\\textasciicircum{}','_':'\\_','%':'\\%','~':'\\textasciitilde{}'}
|
|
2524
|
+
let out=''; for(const ch of String(s==null?'':s)) out+=(M[ch]||ch); return out
|
|
2525
|
+
}
|
|
2526
|
+
// ---- compilation (detect → compile twice → capped repair → degrade; docs/final-paper.md §D/§E) ----
|
|
2527
|
+
// The host fs is TEXT-ONLY (dsh-fs has no writeBytes), so `paper.pdf` can only ever be produced
|
|
2528
|
+
// by the compiler subprocess inside `Paper/<id>/` — the plugin just `stat`s it. The Lean seam
|
|
2529
|
+
// (`sub.resolveExecutable` + `sub.spawn`) is reused verbatim so a fake compiler is injectable.
|
|
2530
|
+
async function paperDetectEngine(lang){
|
|
2531
|
+
const sub=subprocessOf(); if(sub===undefined) return {ok:false,tried:[],reason:'NO_SUBPROCESS'}
|
|
2532
|
+
const prefer=String(params.paperLatexCommand||'').trim()
|
|
2533
|
+
const base=lang==='en'?['pdflatex','latexmk','xelatex','lualatex','tectonic']:['xelatex','latexmk','pdflatex','lualatex','tectonic']
|
|
2534
|
+
const order=(prefer?[prefer]:[]).concat(base.filter(x=>x!==prefer))
|
|
2535
|
+
const tried=[]
|
|
2536
|
+
for(let i=0;i<order.length;i++){
|
|
2537
|
+
const exe=order[i]
|
|
2538
|
+
try { const r=await sub.resolveExecutable(exe); if(r) return {ok:true,exe:String(r),tried,alts:order.slice(i+1)} }
|
|
2539
|
+
catch(e){ tried.push(exe) }
|
|
2540
|
+
}
|
|
2541
|
+
return {ok:false,tried,reason:'LATEX_NOT_FOUND'}
|
|
2542
|
+
}
|
|
2543
|
+
function paperEngineArgv(exe,lang){
|
|
2544
|
+
if(/latexmk$/.test(exe)) return [exe,'-interaction=nonstopmode',lang==='en'?'-pdf':'-xelatex','paper.tex']
|
|
2545
|
+
return [exe,'-interaction=nonstopmode','paper.tex']
|
|
2546
|
+
}
|
|
2547
|
+
async function paperRunEngine(exe,argv,dir,cap){
|
|
2548
|
+
const sub=subprocessOf(); if(sub===undefined) return {ok:false,code:'NO_SUBPROCESS'}
|
|
2549
|
+
let handle
|
|
2550
|
+
try { handle=sub.spawn({argv,cwd:dir,stdio:{stdin:'ignore',stdout:{maxBytes:64*1024},stderr:{maxBytes:64*1024}},graceMs:cap}) }
|
|
2551
|
+
catch(e){ return {ok:false,code:'LATEX_SPAWN_FAILED',message:String((e&&e.message)||e)} }
|
|
2552
|
+
let killed=false,timerDispose=null,outcome
|
|
2553
|
+
try {
|
|
2554
|
+
outcome=await Promise.race([handle.done,new Promise(function(resolve){
|
|
2555
|
+
if(typeof ctx.timeout!=='function') return
|
|
2556
|
+
timerDispose=ctx.timeout(function(){ killed=true; try{ if(typeof handle.terminate==='function') handle.terminate() }catch(e){} resolve({exitCode:null,signal:'SIGTERM'}) },cap)
|
|
2557
|
+
})])
|
|
2558
|
+
} catch(e){ if(timerDispose){ try{ timerDispose() }catch(_e){} } return {ok:false,code:'LATEX_RUN_FAILED',message:String((e&&e.message)||e)} }
|
|
2559
|
+
if(timerDispose){ try{ timerDispose() }catch(e){} }
|
|
2560
|
+
let out='',err=''
|
|
2561
|
+
try { if(handle.collected&&handle.collected.stdout) out=handle.collected.stdout.readFrom(0).text } catch(e){}
|
|
2562
|
+
try { if(handle.collected&&handle.collected.stderr) err=handle.collected.stderr.readFrom(0).text } catch(e){}
|
|
2563
|
+
const exitCode=outcome?outcome.exitCode:null
|
|
2564
|
+
return {ok:exitCode===0,exitCode,timedOut:killed,stdout:tail(out,2000),stderr:tail(err,2000)}
|
|
2565
|
+
}
|
|
2566
|
+
async function paperPdfExists(dir){
|
|
2567
|
+
try { const t=await fs.resolve(dir+'/paper.pdf',{cwd:frameworkRoot()}); return !!(await fs.stat(t)) } catch(e){ return false }
|
|
2568
|
+
}
|
|
2569
|
+
/** v2 §E: at most ONE engine swap, then ONE minimal-template retry, then report + degrade. */
|
|
2570
|
+
async function paperCompile(ps,composed,ev){
|
|
2571
|
+
const dir=ps.dir
|
|
2572
|
+
// v2 §E: `paperFormat=md` produces no tex, so "cannot compile without tex" must NOT be warned.
|
|
2573
|
+
if(ps.cfg.format==='md') return {result:'skipped',reason:'paperFormat=md(未产出 tex,跳过编译)',attempts:[]}
|
|
2574
|
+
if(!ps.cfg.compilePdf) return {result:'skipped',reason:'paperCompilePdf=false',attempts:[]}
|
|
2575
|
+
// Never clobber an existing pdf (v2 §D): a previously compiled artifact is kept as-is.
|
|
2576
|
+
if(await paperPdfExists(dir)) return {result:'ok',engine:'existing',note:'已存在 paper.pdf,未覆盖',attempts:[]}
|
|
2577
|
+
const det=await paperDetectEngine(ps.cfg.language)
|
|
2578
|
+
if(!det.ok) return {result:'not-detected',reason:'未检测到 LaTeX 引擎('+(det.tried.join('/')||'subprocess 不可用')+'):只产出 tex+md,不编译',tried:det.tried,attempts:[]}
|
|
2579
|
+
const cap=posMs(params.activityTimeoutMs,120000)
|
|
2580
|
+
const attempts=[]
|
|
2581
|
+
const runTwice=async(exe,label,passes)=>{
|
|
2582
|
+
const argv=paperEngineArgv(exe,ps.cfg.language)
|
|
2583
|
+
let ok=true,first=null
|
|
2584
|
+
for(let i=0;i<(passes||2);i++){ const a=await paperRunEngine(exe,argv,dir,cap); if(i===0) first=a; if(!a.ok){ ok=false; break } }
|
|
2585
|
+
const pdf=await paperPdfExists(dir)
|
|
2586
|
+
attempts.push({stage:label,engine:exe,ok:!!(ok&&pdf),exitCode:first?first.exitCode:null,pdf,stderr:String((first&&first.stderr)||'').slice(0,300)})
|
|
2587
|
+
return {ok:!!(ok&&pdf),last:first}
|
|
2588
|
+
}
|
|
2589
|
+
let r=await runTwice(det.exe,'initial',2)
|
|
2590
|
+
if(r.ok) return {result:'ok',engine:det.exe,attempts}
|
|
2591
|
+
// repair 1: swap to another RESOLVABLE engine (same tex, nonstopmode, twice) — capped at one swap
|
|
2592
|
+
const sub=subprocessOf()
|
|
2593
|
+
for(const alt of det.alts||[]){
|
|
2594
|
+
let altExe=null
|
|
2595
|
+
try { if(sub) altExe=String(await sub.resolveExecutable(alt)) } catch(e){ continue }
|
|
2596
|
+
if(!altExe||altExe===det.exe) continue
|
|
2597
|
+
const rr=await runTwice(altExe,'engine-switch',2)
|
|
2598
|
+
if(rr.ok) return {result:'ok',engine:altExe,repaired:'engine-switch',attempts}
|
|
2599
|
+
break
|
|
2600
|
+
}
|
|
2601
|
+
// repair 2: ONE minimal-template retry (drops every optional package / custom preamble)
|
|
2602
|
+
await paperWriteRel(dir+'/paper.tex',paperMinimalTemplate(ps.cfg,composed.title,composed.texBody))
|
|
2603
|
+
const rr=await runTwice(det.exe,'minimal-template',1)
|
|
2604
|
+
if(rr.ok) return {result:'ok',engine:det.exe,repaired:'minimal-template',attempts}
|
|
2605
|
+
const last=attempts[attempts.length-1]||{}
|
|
2606
|
+
return {result:'failed',engine:det.exe,attempts,error:'编译失败(已按上限尝试换引擎/最小模板):保留 paper.tex 与 paper.md,不阻塞定稿',tail:String(last.stderr||'')}
|
|
2607
|
+
}
|
|
2608
|
+
function paperMinimalTemplate(cfg,title,texBody){
|
|
2609
|
+
const cls=cfg.language==='en'?'article':'ctexart'
|
|
2610
|
+
return ['% generated by vibe-math-v4 (minimal fallback template)','\\documentclass[11pt]{'+cls+'}',
|
|
2611
|
+
'\\begin{document}','\\section*{'+texEsc(title)+'}','',String(texBody||''),'','\\end{document}',''].join('\n')
|
|
2612
|
+
}
|
|
2613
|
+
async function paperEnsureArtifacts(meta,cfg){
|
|
2614
|
+
const filled=[]
|
|
2615
|
+
const dir=String(meta.dir||('Paper/'+meta.id))
|
|
2616
|
+
const saved=paperState
|
|
2617
|
+
try {
|
|
2618
|
+
if(!paperState||paperState.id!==meta.id){
|
|
2619
|
+
paperState={ id:meta.id, dir, trigger:'recheck', deferCompletion:false, startedAt:meta.startedAt||now(), finalizedAt:meta.finalizedAt||0,
|
|
2620
|
+
status:'done', round:meta.rounds||0, maxRounds:PAPER_MAX_ROUNDS, cfg, fingerprint:meta.fingerprint||'',
|
|
2621
|
+
participants:Array.isArray(meta.participants)?meta.participants:[], parts:{}, reviews:{}, deliverable:{}, dissent:meta.dissent||[],
|
|
2622
|
+
log:[], warning:meta.warning||'', compile:meta.compile||null, draft:'', finalDraft:'', files:meta.files||[], stepAt:now(), lastNudgeAt:0, attempts:{}, editorId:meta.editorId||'' }
|
|
2623
|
+
}
|
|
2624
|
+
const ev=await paperEvidence()
|
|
2625
|
+
const body=await readText(dir+'/final.md')
|
|
2626
|
+
if(body!==undefined&&body!=='') paperState.finalDraft=body
|
|
2627
|
+
const composed=paperCompose(paperState,ev,cfg)
|
|
2628
|
+
const wantMd=cfg.format!=='tex', wantTex=cfg.format!=='md'
|
|
2629
|
+
const mdRaw=await readText(dir+'/paper.md'), texRaw=await readText(dir+'/paper.tex')
|
|
2630
|
+
if(wantMd&&mdRaw===undefined){ if(await paperWriteRel(dir+'/paper.md',composed.md)) filled.push('paper.md') }
|
|
2631
|
+
if(wantTex&&texRaw===undefined){ if(await paperWriteRel(dir+'/paper.tex',composed.tex)) filled.push('paper.tex') }
|
|
2632
|
+
if(cfg.compilePdf&&wantTex&&!(await paperPdfExists(dir))){
|
|
2633
|
+
const c=await paperCompile(paperState,composed,ev)
|
|
2634
|
+
paperState.compile=c
|
|
2635
|
+
if(c.result==='ok') filled.push('paper.pdf')
|
|
2636
|
+
}
|
|
2637
|
+
if(filled.length) await paperWriteMeta()
|
|
2638
|
+
return filled
|
|
2639
|
+
} finally { paperState=saved||null }
|
|
2640
|
+
}
|
|
2641
|
+
async function paperWriteMeta(){
|
|
2642
|
+
const ps=paperState; if(!ps) return false
|
|
2643
|
+
const meta={ id:ps.id, dir:ps.dir, generatedBy:'vibe-math-v4', preset:'v4', trigger:ps.trigger,
|
|
2644
|
+
runId, closureSignal:PAPER_CLOSURE_SIGNAL, startedAt:ps.startedAt, finalizedAt:ps.finalizedAt||0,
|
|
2645
|
+
params:{finalPaper:params.finalPaper,paperFormat:ps.cfg.format,paperLanguage:ps.cfg.language,paperCompilePdf:ps.cfg.compilePdf,paperEditor:ps.cfg.editor,paperLatexCommand:params.paperLatexCommand},
|
|
2646
|
+
editor:ps.cfg.editor, editorId:ps.editorId||'', editorDowngraded:!!ps.editorDowngraded, participants:ps.participants, rounds:ps.round, maxRounds:ps.maxRounds,
|
|
2647
|
+
fingerprint:ps.fingerprint||'', warning:ps.warning||'', files:ps.files||[], compile:ps.compile||null, mergeNotes:ps.mergeNotes||null,
|
|
2648
|
+
parts:Object.keys(ps.parts).sort().map(k=>({id:k,chars:String((ps.parts[k]||{}).text||'').length,missing:!!(ps.parts[k]||{}).missing,source:(ps.parts[k]||{}).source||''})),
|
|
2649
|
+
reviews:Object.keys(ps.reviews).sort().map(k=>({id:k,reviewed:(ps.reviews[k]||{}).reviewed||'',points:((ps.reviews[k]||{}).points||[]).length,improvements:(ps.reviews[k]||{}).improvements||'',missing:!!(ps.reviews[k]||{}).missing})),
|
|
2650
|
+
deliverable:Object.keys(ps.deliverable).sort().map(k=>({id:k,deliverable:ps.deliverable[k]})),
|
|
2651
|
+
dissent:ps.dissent||[] }
|
|
2652
|
+
return await paperWriteRel(ps.dir+'/paper.meta.json',JSON.stringify(meta,null,2))
|
|
2653
|
+
}
|
|
2654
|
+
async function paperWriteLog(){
|
|
2655
|
+
const ps=paperState; if(!ps) return false
|
|
2656
|
+
const L=['# 最终论文过程记录(paper.log.md)','','- 论文 id:'+ps.id,'- 触发:'+ps.trigger,
|
|
2657
|
+
'- 语言 / 格式 / 定稿代表:'+ps.cfg.language+' / '+ps.cfg.format+' / '+ps.cfg.editor,'- 参与成员:'+(ps.participants.join('、')||'(无常驻)'),
|
|
2658
|
+
'- 状态:'+ps.status,'']
|
|
2659
|
+
L.push('## 过程(谁写了什么)'); for(const l of ps.log) L.push('- '+l); L.push('')
|
|
2660
|
+
L.push('## 归属与处理(谁提了什么意见、如何采纳)')
|
|
2661
|
+
for(const rid of ps.participants){ const p=ps.parts[rid]
|
|
2662
|
+
L.push('- 撰写:'+rid+(p?(p.missing?'(超时未提交)':'('+String(p.text||'').length+' 字,来源 '+(p.source||'reply')+')'):'(未提交)')) }
|
|
2663
|
+
for(const rid of ps.participants){ const rv=ps.reviews[rid]
|
|
2664
|
+
if(!rv||rv.missing){ L.push('- 互审:'+rid+'('+(rv?'超时未提交,已记入附录':'未完成')+')'); continue }
|
|
2665
|
+
L.push('- 互审:'+rid+' → '+rv.reviewed+',意见 '+((rv.points||[]).length)+' 条,处理:已采纳并记入合并稿审阅记录'+(rv.improvements?('(改进:'+String(rv.improvements).slice(0,200)+')'):'')) }
|
|
2666
|
+
L.push('- 定稿:'+(ps.editorId||ps.cfg.editor))
|
|
2667
|
+
for(const rid of ps.participants) if(typeof ps.deliverable[rid]==='boolean') L.push('- 交付表态:'+rid+' = '+(ps.deliverable[rid]?'可交付':'不可交付'))
|
|
2668
|
+
if(ps.compile) L.push('- 编译:'+JSON.stringify({result:ps.compile.result,engine:ps.compile.engine||'',repaired:ps.compile.repaired||'',attempts:(ps.compile.attempts||[]).length,error:ps.compile.error||''}))
|
|
2669
|
+
if(ps.warning) L.push('- 警告:'+ps.warning)
|
|
2670
|
+
return await paperWriteRel(ps.dir+'/paper.log.md',L.join('\n')+'\n')
|
|
2671
|
+
}
|
|
2672
|
+
async function paperFinalize(){
|
|
2673
|
+
const ps=paperState
|
|
2674
|
+
const ev=await paperEvidence()
|
|
2675
|
+
ps.fingerprint=paperFingerprint(ev)
|
|
2676
|
+
// final.md / draft.md must exist before composing so a re-run is reproducible
|
|
2677
|
+
if(!ps.finalDraft) ps.finalDraft=paperFacilitatorFinal()
|
|
2678
|
+
await paperWriteRel(ps.dir+'/final.md',ps.finalDraft)
|
|
2679
|
+
const composed=paperCompose(ps,ev,ps.cfg)
|
|
2680
|
+
ps.files=[]
|
|
2681
|
+
if(ps.cfg.format!=='tex'){ if(await paperWriteRel(ps.dir+'/paper.md',composed.md)) ps.files.push('paper.md') }
|
|
2682
|
+
if(ps.cfg.format!=='md'){ if(await paperWriteRel(ps.dir+'/paper.tex',composed.tex)) ps.files.push('paper.tex') }
|
|
2683
|
+
ps.compile=await paperCompile(ps,composed,ev)
|
|
2684
|
+
if(ps.compile&&ps.compile.result==='ok') ps.files.push('paper.pdf')
|
|
2685
|
+
ps.finalizedAt=now(); ps.status='done'
|
|
2686
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
2687
|
+
// v2 §A1: the paper phase was entered from INSIDE the closing branch, BEFORE the completion
|
|
2688
|
+
// flags — so the run only becomes "done" here, once the paper exists (or has degraded safely).
|
|
2689
|
+
if(ps.deferCompletion && autoDone!==true){
|
|
2690
|
+
running=false; autoDone=true; phase='done'; clearHeartbeat()
|
|
2691
|
+
wakeKind.clear(); meetingState=null; verifyState=null; pendingMeeting=null; pendingVerify=[]
|
|
2692
|
+
logActivity('stop','all residents agree: problem solved(一致性停止票;最终论文已产出)')
|
|
2693
|
+
await saveAll()
|
|
2694
|
+
}
|
|
2695
|
+
const warn=[ps.warning,ps.compile&&ps.compile.result==='failed'?ps.compile.error:''].filter(Boolean).join(';')
|
|
2696
|
+
if(warn) logActivity('paper','最终论文完成(有警告):'+ps.dir+'|'+warn)
|
|
2697
|
+
else logActivity('paper','最终论文完成:'+ps.dir+'('+ps.files.join('、')+(ps.compile?('|编译 '+(ps.compile.result||'')):'')+')')
|
|
2698
|
+
return {ok:true,id:ps.id,dir:ps.dir,files:ps.files,compile:ps.compile,rounds:ps.round,dissent:ps.dissent,warning:ps.warning}
|
|
2699
|
+
}
|
|
2700
|
+
function paperStatusView(){
|
|
2701
|
+
const ps=paperState
|
|
2702
|
+
return {
|
|
2703
|
+
params:{finalPaper:params.finalPaper,paperFormat:params.paperFormat,paperLanguage:params.paperLanguage,paperCompilePdf:params.paperCompilePdf,paperEditor:params.paperEditor,paperLatexCommand:params.paperLatexCommand},
|
|
2704
|
+
id:ps?ps.id:paperIdFor(), dir:ps?ps.dir:paperDirFor(), status:ps?ps.status:'idle',
|
|
2705
|
+
running:!!(ps&&ps.status!=='done'&&ps.status!=='failed'), trigger:ps?ps.trigger:'',
|
|
2706
|
+
rounds:ps?ps.round:0, maxRounds:ps?ps.maxRounds:PAPER_MAX_ROUNDS, participants:ps?ps.participants:[],
|
|
2707
|
+
editor:ps?(ps.editorId||ps.cfg.editor):params.paperEditor, editorDowngraded:!!(ps&&ps.editorDowngraded),
|
|
2708
|
+
parts:ps?Object.keys(ps.parts).length:0, reviews:ps?Object.keys(ps.reviews).length:0,
|
|
2709
|
+
finalizedAt:ps?ps.finalizedAt:0, warning:ps?ps.warning:'', files:ps?(ps.files||[]):[], compile:ps?ps.compile:null,
|
|
2710
|
+
closureSignal:PAPER_CLOSURE_SIGNAL,
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
function paperStatusSummary(){
|
|
2714
|
+
const v=paperStatusView()
|
|
2715
|
+
return {ok:true, id:v.id, dir:v.dir, status:v.status, running:v.running, trigger:v.trigger, lang:v.params.paperLanguage,
|
|
2716
|
+
format:v.params.paperFormat, editor:v.editor, editorDowngraded:v.editorDowngraded, participants:v.participants,
|
|
2717
|
+
parts:v.parts, reviews:v.reviews, rounds:v.rounds+'/'+v.maxRounds, finalizedAt:v.finalizedAt, files:v.files,
|
|
2718
|
+
params:v.params,
|
|
2719
|
+
compile:v.compile?{result:v.compile.result,engine:v.compile.engine||'',repaired:v.compile.repaired||''}:null,
|
|
2720
|
+
warning:v.warning, closureSignal:v.closureSignal}
|
|
2721
|
+
}
|
|
2722
|
+
async function paperStart(trigger,opts){
|
|
2723
|
+
const o=opts||{}
|
|
2724
|
+
const force=!!o.force
|
|
2725
|
+
const cfg=paperCfg(o)
|
|
2726
|
+
const id=paperIdFor(), dir='Paper/'+id
|
|
2727
|
+
if(paperState && paperState.id===id && paperState.status!=='done' && paperState.status!=='failed' && !force)
|
|
2728
|
+
return {ok:true,id,dir,status:paperState.status,alreadyRunning:true,message:'最终论文正在进行('+paperState.status+')'}
|
|
2729
|
+
if(paperState && paperState.id!==id && paperState.status!=='done' && paperState.status!=='failed' && !force)
|
|
2730
|
+
return {ok:false,code:'V4_PAPER_BUSY',message:'另一篇论文正在进行('+paperState.id+'),完成或 force 后再试'}
|
|
2731
|
+
// Idempotency (docs/final-paper.md §C): the run id IS the directory, so a finalizedAt in THAT directory
|
|
2732
|
+
// means this run already has its paper; only per-artifact existence is re-checked.
|
|
2733
|
+
const meta=await readJson(dir+'/paper.meta.json')
|
|
2734
|
+
if(!force && meta && meta.finalizedAt){
|
|
2735
|
+
const filled=await paperEnsureArtifacts(meta,cfg)
|
|
2736
|
+
return {ok:true,id,dir,idempotent:true,finalizedAt:meta.finalizedAt,filled,
|
|
2737
|
+
message:'论文已定稿(幂等:只补写缺失产物'+(filled.length?(':'+filled.join('、')):':无缺失')+')'+(params.finalPaper===false?'(finalPaper=false:自动触发已关闭,本次为手动命令)':'')}
|
|
2738
|
+
}
|
|
2739
|
+
const prevDeliver={}
|
|
2740
|
+
paperState={ id, dir, trigger, deferCompletion:!!o.deferCompletion, startedAt:now(), finalizedAt:0, status:'parts', round:0, maxRounds:PAPER_MAX_ROUNDS,
|
|
2741
|
+
cfg, fingerprint:'', participants:Array.from(residents.keys()).sort(), parts:{}, reviews:{}, deliverable:prevDeliver,
|
|
2742
|
+
dissent:[], log:[], warning:'', compile:null, draft:'', finalDraft:'', files:[], stepAt:now(), lastNudgeAt:0, attempts:{}, editorId:'', editorDowngraded:false }
|
|
2743
|
+
paperLog('触发('+trigger+'):'+id+'|收敛信号='+PAPER_CLOSURE_SIGNAL+'|语言='+cfg.language+'|格式='+cfg.format+'|定稿='+cfg.editor+'|参与='+(paperState.participants.join('、')||'(无常驻)'))
|
|
2744
|
+
if(params.finalPaper===false) paperLog('注意:finalPaper=false —— 自动触发已关闭;本次为手动命令')
|
|
2745
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
2746
|
+
if(!paperState.participants.length){
|
|
2747
|
+
paperState.warning='本轮没有在场常驻:正文由框架(office 侧)从既有证据整理'
|
|
2748
|
+
paperLog('警告:'+paperState.warning)
|
|
2749
|
+
await paperMerge() // -> paperFinalStep -> vote(no participants) -> finalize
|
|
2750
|
+
return {ok:true,id,dir,status:paperState.status,message:'最终论文已启动(无常驻成员,由框架从既有证据整理)'+(params.finalPaper===false?'(finalPaper=false:自动触发已关闭)':'')}
|
|
2751
|
+
}
|
|
2752
|
+
await paperWakeSome(paperState.participants.slice(),'part')
|
|
2753
|
+
await paperAdvance('parts-started')
|
|
2754
|
+
return {ok:true,id,dir,status:paperState.status,participants:paperState.participants,files:paperState.files,
|
|
2755
|
+
message:'最终论文已启动('+trigger+')'+(params.finalPaper===false?'(finalPaper=false:自动触发已关闭)':'')}
|
|
2756
|
+
}
|
|
2757
|
+
async function runPaperCommand(ov,force){
|
|
2758
|
+
const known=['lang','format','editor']
|
|
2759
|
+
for(const k of Object.keys(ov||{})) if(known.indexOf(k)===-1) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'未知选项 '+k+'(支持 lang=zh|en / format=md|tex|both / editor=office|resident:<id> / force)'}
|
|
2760
|
+
if(ov&&ov.lang&&PAPER_LANGS.indexOf(String(ov.lang))===-1) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'paperLanguage 只能是 zh|en'}
|
|
2761
|
+
if(ov&&ov.format&&PAPER_FORMATS.indexOf(String(ov.format))===-1) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'paperFormat 只能是 both|md|tex'}
|
|
2762
|
+
if(ov&&ov.editor&&!paperEditorOk(ov.editor)) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'paperEditor 只能是 office 或 resident:<id>'}
|
|
2763
|
+
return await paperStart('manual',{force:!!force,lang:ov&&ov.lang,format:ov&&ov.format,editor:ov&&ov.editor})
|
|
2764
|
+
}
|
|
2765
|
+
/** A paper wake's reply. `kind` is read from wakeKind (set by wakeResident, not cleared here). */
|
|
2766
|
+
async function onPaperWakeEnd(r,parsed){
|
|
2767
|
+
const ps=paperState; if(!ps) return
|
|
2768
|
+
const kind=String(wakeKind.get(r.rId)||'')
|
|
2769
|
+
// A paper contribution may arrive as a plain string or as `{markdown:"…"}` — anything else is
|
|
2770
|
+
// NOT accepted (a stray object must never become "[object Object]" in the final draft).
|
|
2771
|
+
const pick=v=>{ if(typeof v==='string') return v; if(v&&typeof v==='object'&&typeof v.markdown==='string') return v.markdown; return '' }
|
|
2772
|
+
if(kind===PAPER_KINDS.part){
|
|
2773
|
+
let text=pick(parsed&&parsed.paperPart).trim()
|
|
2774
|
+
if(!text) text=String((await readText(ps.dir+'/parts/'+r.rId+'.md'))||'').trim()
|
|
2775
|
+
if(text){ ps.parts[r.rId]={text,at:now(),source:'reply'}; await paperWriteRel(ps.dir+'/parts/'+r.rId+'.md',text)
|
|
2776
|
+
paperLog(r.rId+' 提交了自己负责的部分('+text.length+' 字)') }
|
|
2777
|
+
else paperLog('警告:'+r.rId+' 的部分为空(将按超时/重试处理)')
|
|
2778
|
+
} else if(kind===PAPER_KINDS.review){
|
|
2779
|
+
const rv=(parsed&&parsed.paperReview&&typeof parsed.paperReview==='object')?parsed.paperReview:{}
|
|
2780
|
+
const reviewed=String(rv.reviewed||'')
|
|
2781
|
+
const others=ps.participants.filter(x=>x!==r.rId)
|
|
2782
|
+
if(others.indexOf(reviewed)===-1){
|
|
2783
|
+
paperLog('拒绝 '+r.rId+' 的互审:未指名其他参与者(reviewed='+JSON.stringify(rv.reviewed===undefined?null:rv.reviewed)+')——必须审阅至少一位他人')
|
|
2784
|
+
} else {
|
|
2785
|
+
const points=Array.isArray(rv.points)?rv.points.map(String).filter(Boolean):[]
|
|
2786
|
+
ps.reviews[r.rId]={reviewed,points,improvements:String(rv.improvements||''),at:now(),accepted:true}
|
|
2787
|
+
paperLog(r.rId+' 审阅了 '+reviewed+' 的部分:'+points.length+' 条意见'+(rv.improvements?',并给出改进建议':'') )
|
|
2788
|
+
await paperWriteRel(ps.dir+'/reviews/'+r.rId+'.md',['# 互审记录|'+r.rId,'','- 审阅对象:'+reviewed,'- 意见:','',
|
|
2789
|
+
...points.map(p=>'- '+p),'','## 改进建议','',String(rv.improvements||'(无)')].join('\n')+'\n')
|
|
2790
|
+
}
|
|
2791
|
+
} else if(kind===PAPER_KINDS.final){
|
|
2792
|
+
let text=pick(parsed&&parsed.paperFinal).trim()
|
|
2793
|
+
if(!text) text=String((await readText(ps.dir+'/final.md'))||'').trim()
|
|
2794
|
+
if(text&&!/^#\s*互审记录/.test(text)){ ps.finalDraft=text; await paperWriteRel(ps.dir+'/final.md',text); paperLog('定稿代表 '+(ps.editorId||ps.cfg.editor)+' 提交了最终稿('+text.length+' 字)') }
|
|
2795
|
+
else paperLog('警告:定稿代表返回空稿,改由框架(office)定稿')
|
|
2796
|
+
} else if(kind===PAPER_KINDS.vote||kind===PAPER_KINDS.revise){
|
|
2797
|
+
const d=parsed?parsed.paperDeliverable:undefined
|
|
2798
|
+
if(typeof d!=='boolean'){ paperLog('警告:'+r.rId+' 未明确表态"可交付/不可交付"(将按未表态处理)'); }
|
|
2799
|
+
else { ps.deliverable[r.rId]=d
|
|
2800
|
+
paperLog(r.rId+(d?' 表示:可交付':' 表示:不可交付')+(parsed&&parsed.reason?('|理由:'+String(parsed.reason).slice(0,160)):'')) }
|
|
2801
|
+
}
|
|
2802
|
+
await paperWriteLog()
|
|
2803
|
+
await paperAdvance('resident-end:'+kind)
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2051
2806
|
// ---- controls ----
|
|
2052
2807
|
async function start({problem,residentCount,seedDirections}){
|
|
2053
2808
|
await loadSettings()
|
|
@@ -2069,6 +2824,7 @@ export function apply(ctx) {
|
|
|
2069
2824
|
// carrying a previous run's records over would let a stale `passed` open the new gate.
|
|
2070
2825
|
formal={}; formalTodos=[]
|
|
2071
2826
|
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
|
|
2827
|
+
paperState=null // a fresh run gets a fresh paper flow (its durable record lives in Paper/<run id>/)
|
|
2072
2828
|
// Re-base the artifact counter on the cards already on disk, so the auto-sync meeting counts
|
|
2073
2829
|
// artifacts written by THIS run (the project tree may already hold cards from an earlier run).
|
|
2074
2830
|
artifactBaseline=null; artifactCount=0; await syncArtifactCount()
|
|
@@ -2156,13 +2912,14 @@ export function apply(ctx) {
|
|
|
2156
2912
|
consensus: meetingState?{kind:'meeting',id:meetingState.id,agenda:meetingState.agenda,round:meetingState.round,spoke:Object.keys(meetingState.inputs).length,expected:residents.size,solvedVotes:Object.values(meetingState.inputs).filter(iv=>iv.voteSolved===true).length}
|
|
2157
2913
|
:(verifyState?{kind:'verify',target:verifyState.targetId,targetType:verifyState.targetType,stage:verifyState.stage,round:verifyState.round,voted:Object.keys(verifyState.verdicts).length,expected:residents.size}:null),
|
|
2158
2914
|
artifactCount: artifactCount, artifactBaseline: artifactBaseline,
|
|
2915
|
+
paper: paperStatusSummary(),
|
|
2159
2916
|
// The Lean knobs and the per-object formal records are part of the readable status: without
|
|
2160
2917
|
// them a `require`-mode run that keeps returning 未定论 would be undiagnosable from outside.
|
|
2161
2918
|
formal: formalView(),
|
|
2162
2919
|
// The parameter NAMES (not just the rendered string) so a caller — the `/v4 set` handler in
|
|
2163
2920
|
// particular — can reject an unknown key instead of silently dropping it.
|
|
2164
2921
|
paramsKeys: Object.keys(params),
|
|
2165
|
-
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(', ') } }
|
|
2922
|
+
params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','stallAutoMeetingMs','provider','model','residentPersona','toolAllow','toolDeny','formalVerify','leanCommand','leanArgs','leanTimeoutMs','finalPaper','paperFormat','paperLanguage','paperCompilePdf','paperEditor','paperLatexCommand'].map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
|
|
2166
2923
|
function formalReportText(){
|
|
2167
2924
|
if(!formalOn()) return '- 未启用(`formalVerify` = off;可用 vibe_v4_set 切到 encourage / require)'
|
|
2168
2925
|
const v=formalView()
|
|
@@ -2179,6 +2936,7 @@ export function apply(ctx) {
|
|
|
2179
2936
|
pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null,
|
|
2180
2937
|
parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
|
|
2181
2938
|
formal: formalView(),
|
|
2939
|
+
paper: paperStatusSummary(),
|
|
2182
2940
|
meetings:meetings.length, recentActivity: activityLog.slice(-8) } }
|
|
2183
2941
|
/** Human-readable mirror of the formal state (kept OUT of the JSON report shape). */
|
|
2184
2942
|
function formalReport(){ return {ok:true, formalMode:formalMode(), formalReport:'## Lean 形式化\n'+formalReportText(), formal:formalView()} }
|
|
@@ -2226,6 +2984,21 @@ export function apply(ctx) {
|
|
|
2226
2984
|
// Degrading to a STRONGER mode would let a typo silently gate every conclusion — the exact
|
|
2227
2985
|
// failure mode `require` is supposed to avoid.
|
|
2228
2986
|
if(k==='formalVerify') return FORMAL_MODES.indexOf(String(v))!==-1?String(v):'off'
|
|
2987
|
+
// ---- final paper (docs/final-paper.md §B): booleans and enums are coerced HERE, because `/v4 set` hands
|
|
2988
|
+
// the parameter layer RAW STRINGS ('false' must become false, not stay truthy) and an unknown
|
|
2989
|
+
// value must fall back to the default instead of silently keeping a bogus strong setting. ----
|
|
2990
|
+
if(k==='finalPaper'||k==='paperCompilePdf'){
|
|
2991
|
+
if(typeof v==='boolean') return v
|
|
2992
|
+
const s=String(v==null?'':v).trim().toLowerCase()
|
|
2993
|
+
if(s==='true'||s==='1'||s==='yes'||s==='on') return true
|
|
2994
|
+
if(s==='false'||s==='0'||s==='no'||s==='off') return false
|
|
2995
|
+
return DEFAULT_PARAMS[k]
|
|
2996
|
+
}
|
|
2997
|
+
if(k==='paperFormat') return PAPER_FORMATS.indexOf(String(v))!==-1?String(v):DEFAULT_PARAMS.paperFormat
|
|
2998
|
+
if(k==='paperLanguage') return PAPER_LANGS.indexOf(String(v))!==-1?String(v):DEFAULT_PARAMS.paperLanguage
|
|
2999
|
+
// v2 §A5: `office` (the session root / human side) by default, or `resident:<id>`.
|
|
3000
|
+
if(k==='paperEditor'){ const s=String(v==null?'':v).trim(); return paperEditorOk(s)?s:DEFAULT_PARAMS.paperEditor }
|
|
3001
|
+
if(k==='paperLatexCommand'){ const s=String(v==null?'':v).trim(); return s }
|
|
2229
3002
|
// A blank command would make resolveExecutable('') fail confusingly; fall back to the default.
|
|
2230
3003
|
if(k==='leanCommand'){ const s=String(v==null?'':v).trim(); return s||'lean' }
|
|
2231
3004
|
// A non-positive timeout is meaningless (the run would be killed instantly) → default.
|
|
@@ -2297,6 +3070,7 @@ export function apply(ctx) {
|
|
|
2297
3070
|
// can ever clear those marks). resume()/start() re-initialize anyway; this keeps status truthful
|
|
2298
3071
|
// between abort and the next action.
|
|
2299
3072
|
meetingState=null; verifyState=null; pendingMeeting=null; pendingVerify=[]; busy=new Set(); wakeKind=new Map(); currentResident=''; finalizeLock=null
|
|
3073
|
+
paperState=null // an aborted run is over: its paper flow (if any) must not keep driving wakes
|
|
2300
3074
|
await saveAll(); return {ok:true,message:'aborted'} }
|
|
2301
3075
|
function setPause(){ clearHeartbeat(); running=false; return {ok:true,message:'paused'} }
|
|
2302
3076
|
|
|
@@ -2308,6 +3082,9 @@ export function apply(ctx) {
|
|
|
2308
3082
|
rememberAgent, forgetAgent,
|
|
2309
3083
|
setPause, initAbort, postMessage, startMeeting, saveAll, broadcast, configure, loadSettings,
|
|
2310
3084
|
currentResident:()=>currentResident,
|
|
3085
|
+
// Final paper (docs/final-paper.md): the session API used by the `/v4 paper` command and by
|
|
3086
|
+
// the suites. `paperStatus` is the same view `status()`/`report()` expose.
|
|
3087
|
+
runPaperCommand, paperStatus:()=>paperStatusSummary(),
|
|
2311
3088
|
/** Does this session own the given child id? (used by the `subagent/start` capture) */
|
|
2312
3089
|
ownsChild,
|
|
2313
3090
|
/** Stash a live Agent seen at `subagent/start` time; claimed by `spawnResident` on resolve. */
|
|
@@ -2431,7 +3208,7 @@ export function apply(ctx) {
|
|
|
2431
3208
|
// MODE is dynamic — switching it changes the very next prompt), leanCommand/leanArgs select the
|
|
2432
3209
|
// executable, leanTimeoutMs bounds one run. Invalid values fall back to the defaults and an
|
|
2433
3210
|
// unknown mode degrades to 'off' (never to a STRONGER mode).
|
|
2434
|
-
registerTool('vibe_v4_set','Set V4 parameters: residentCount (how many residents a start spawns), compactThreshold (resident context % that triggers a compaction) and compactAfterRounds (rounds between soft compactions), meetingKeepEvery (every N newly accumulated artifacts an automatic sync meeting is convened), maxParallel (how many residents may be woken concurrently), activityTimeoutMs (idle window before a resident is nudged; also the heartbeat/watchdog period), stallAutoMeetingMs (how long the group may make NO progress before an auto sync meeting is convened), verdictMaxRounds (how many verification rounds one object gets: 1 independent round + re-vote debate rounds), 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. A key outside this list is refused (ok:false, ignored:[...]) instead of being silently dropped.',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)=>{ const r=s.setParams(a); const st=s.status(); if(r&&r.ok===false) return Object.assign({},st,{ok:false,warning:r.message,ignored:r.ignored}); return st })
|
|
3211
|
+
registerTool('vibe_v4_set','Set V4 parameters: residentCount (how many residents a start spawns), compactThreshold (resident context % that triggers a compaction) and compactAfterRounds (rounds between soft compactions), meetingKeepEvery (every N newly accumulated artifacts an automatic sync meeting is convened), maxParallel (how many residents may be woken concurrently), activityTimeoutMs (idle window before a resident is nudged; also the heartbeat/watchdog period), stallAutoMeetingMs (how long the group may make NO progress before an auto sync meeting is convened), verdictMaxRounds (how many verification rounds one object gets: 1 independent round + re-vote debate rounds), 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; finalPaper (default true) writes the run\'s final PAPER automatically once the closing meeting votes unanimously to stop, paperFormat: "both" (default) | "md" | "tex" decides which text versions are produced, paperLanguage: "zh" (default) | "en", paperCompilePdf (default true) compiles a PDF when a LaTeX engine is actually detected (xelatex|latexmk|pdflatex|lualatex|tectonic, overridable via paperLatexCommand), paperEditor: "office" (default — the session root/human side) | "resident:<id>" names who finalises the merged draft after the cross-review round. A key outside this list is refused (ok:false, ignored:[...]) instead of being silently dropped.',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'},finalPaper:{type:'boolean'},paperFormat:{type:'string',enum:['both','md','tex']},paperLanguage:{type:'string',enum:['zh','en']},paperCompilePdf:{type:'boolean'},paperEditor:{type:'string'},paperLatexCommand:{type:'string'}}),(s,a)=>{ const r=s.setParams(a); const st=s.status(); if(r&&r.ok===false) return Object.assign({},st,{ok:false,warning:r.message,ignored:r.ignored}); return st })
|
|
2435
3212
|
// resident-facing tools: route to the CALLING resident (exec.agent.id === childId);
|
|
2436
3213
|
// fall back to the last-woken resident when called by the host/assistant.
|
|
2437
3214
|
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) })
|
|
@@ -2478,7 +3255,7 @@ export function apply(ctx) {
|
|
|
2478
3255
|
// registration belongs to this fiber and must be unwound with it.
|
|
2479
3256
|
ctx.effect(() => commands.register({
|
|
2480
3257
|
name:'v4', description:'control the Vibe Math V4 framework',
|
|
2481
|
-
input:{hint:'[configure|start|resume|pause|abort|status|report|message <to|all> <content>|meeting|members|add|remove|set]'},
|
|
3258
|
+
input:{hint:'[configure|start|resume|pause|abort|status|report|message <to|all> <content>|meeting|paper [lang=zh|en] [format=md|tex|both] [editor=office|resident:<id>] [force]|members|add|remove|set]'},
|
|
2482
3259
|
handler: async function(inv){
|
|
2483
3260
|
const s=getSession(inv&&inv.agent); if(!s) return {kind:'success',text:JSON.stringify({ok:false,error:'no session'})}
|
|
2484
3261
|
const line=String(inv&&inv.rawInput?inv.rawInput:'').trim(); const parts=line.split(/\s+/); const cmd=parts[0]||''; const rest=parts.slice(1)
|
|
@@ -2519,7 +3296,15 @@ export function apply(ctx) {
|
|
|
2519
3296
|
r=(res&&res.ok===false)?Object.assign({},st,{ok:false,warning:res.message,ignored:res.ignored}):Object.assign({},st,{applied:res.applied})
|
|
2520
3297
|
}
|
|
2521
3298
|
}
|
|
2522
|
-
else
|
|
3299
|
+
else if(cmd==='paper'){
|
|
3300
|
+
// Manual trigger (docs/final-paper.md §2). `lang=` / `format=` / `editor=` override THIS invocation only;
|
|
3301
|
+
// a bare `force` rewrites an already-finalized paper. An unknown token becomes an unknown
|
|
3302
|
+
// OPTION and is rejected by runPaperCommand → kind:'error' (never a silent success).
|
|
3303
|
+
const o={}; let force=false
|
|
3304
|
+
for(const tok of rest){ const eq=tok.indexOf('='); if(eq>0) o[tok.slice(0,eq)]=tok.slice(eq+1); else if(tok==='force') force=true; else if(tok) o[tok]=true }
|
|
3305
|
+
r=await s.runPaperCommand(o,force)
|
|
3306
|
+
}
|
|
3307
|
+
else r={ok:false,usage:'configure|start [problem]|resume|pause|abort|status|report|message <to|all> <content>|meeting|paper [lang=zh|en] [format=md|tex|both] [editor=office|resident:<id>] [force]|members|add|remove|set <k=v>...'}
|
|
2523
3308
|
// `kind:'error'` on a rejected business action, exactly as v2/v3 do: every branch above used to
|
|
2524
3309
|
// report `success`, so the human's only non-tool control surface read as success on failure.
|
|
2525
3310
|
const failed = !r || r.ok === false || (r.ok === undefined && !!r.error)
|