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.
@@ -179,6 +179,12 @@ export function apply(ctx) {
179
179
  leanCommand: 'lean', // 要执行的 Lean 可执行文件(例:'lake')
180
180
  leanArgs: [], // 插在文件名之前的附加参数(例:['env','lean'] 配 leanCommand='lake')
181
181
  leanTimeoutMs: 120000, // 单次 Lean 运行超时上限(毫秒,正整数)
182
+ // ---- 最终论文(规格:docs/final-paper.md;v2/v3 为单作者变体)----
183
+ finalPaper: true, // 收口时是否自动撰写最终论文(false 只关自动触发;/vibe paper 仍可用)
184
+ paperFormat: 'both', // both = md + tex | md = 只写 markdown | tex = 只写 latex
185
+ paperLanguage: 'zh', // zh = 中文(ctexart/xelatex 优先)| en = 英文(article/pdflatex 优先)
186
+ paperCompilePdf: true, // 检测到 LaTeX 时是否编译 paper.pdf
187
+ paperLatexCommand: '', // 指定 LaTeX 引擎(空 = 按语言探测 xelatex/latexmk/pdflatex/lualatex/tectonic)
182
188
  }
183
189
  let params = Object.assign({}, DEFAULT_PARAMS)
184
190
  let scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null } // activeCount 由 activeCount() 从 agentRegistry 推导(防漂移,同 v2)
@@ -299,6 +305,11 @@ export function apply(ctx) {
299
305
  { name: 'leanCommand', type: 'string', description: 'Lean 可执行文件(例:lean / lake;配合 leanArgs=[env,lean] 用 lake)', suggestion: 'lean' },
300
306
  { name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加参数', suggestion: [] },
301
307
  { name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,非正数回退默认)', suggestion: 120000 },
308
+ { name: 'finalPaper', type: 'boolean', description: '收口(checkTermination 的完整收口分支:无未解决问题、无待验证对象、无任务/计划/门)时自动撰写最终论文:派遣一名专职「论文撰写」子代理,把已定论命题/解法/方法整理成 Paper/<项目>/{paper.md,paper.tex,paper.meta.json,paper.log.md}。false = 只关自动触发,/vibe paper 手动命令仍可用', suggestion: true },
309
+ { name: 'paperFormat', type: 'enum', options: ['both', 'md', 'tex'], description: '论文产出格式:both = markdown + latex;md = 只写 paper.md(跳过编译);tex = 只写 paper.tex(tex 才会尝试编译 pdf)', suggestion: 'both' },
310
+ { name: 'paperLanguage', type: 'enum', options: ['zh', 'en'], description: '论文语言:zh = 中文(LaTeX 用 ctexart,引擎优先 xelatex);en = 英文(article,引擎优先 pdflatex/latexmk)', suggestion: 'zh' },
311
+ { name: 'paperCompilePdf', type: 'boolean', description: '检测到 LaTeX 时是否编译 paper.pdf(-interaction=nonstopmode 跑两遍;失败先尝试修复:换引擎/去不支持宏包/最小模板)。false 或无 LaTeX 时只保留 tex+md 并记日志(不阻塞定稿)', suggestion: true },
312
+ { name: 'paperLatexCommand', type: 'string', description: '指定 LaTeX 引擎可执行文件(空 = 按语言探测:中文 xelatex > latexmk > pdflatex > lualatex > tectonic;英文 pdflatex 优先)。解析不到时按"未检测到"降级', suggestion: '' },
302
313
  ]
303
314
 
304
315
  // ================= fs (adapted to DSH 0.1.1: resolve returns {targetKey, displayPath}) =================
@@ -389,7 +400,7 @@ export function apply(ctx) {
389
400
  const intFields = ['maxParallelThreshold', 'solverMaxRounds', 'directionsPerSolver', 'verifierCount', 'debateMaxRounds', 'solverMaxToolCalls', 'verifierMaxToolCalls', 'reportIntervalMs', 'tickIntervalMs', 'activityLogCap', 'maxExplorerRetries', 'planningHorizon', 'planMinIntervalMs', 'plannerMaxFails', 'methodKeepIntervalMs', 'methodKeepEvery', 'projectLockTimeoutMs']
390
401
  const numFields = ['promoteValueThreshold']
391
402
  const arrayFields = ['solverToolAllow', 'solverToolDeny', 'verifierToolAllow', 'verifierToolDeny', 'leanArgs']
392
- const boolFields = ['plannerEnabled', 'methodAutoPromote', 'indexAutoRebuild']
403
+ const boolFields = ['plannerEnabled', 'methodAutoPromote', 'indexAutoRebuild', 'finalPaper', 'paperCompilePdf']
393
404
  for (const k of Object.keys(DEFAULT_PARAMS)) {
394
405
  if (!(k in obj)) continue
395
406
  const v = obj[k]
@@ -408,6 +419,10 @@ export function apply(ctx) {
408
419
  else if (k === 'leanCommand') { const s = String(v == null ? '' : v).trim(); out[k] = s || 'lean' }
409
420
  else if (k === 'leanTimeoutMs') { const n = Number(v); out[k] = (Number.isFinite(n) && n > 0) ? Math.floor(n) : DEFAULT_PARAMS[k] }
410
421
  else if (k === 'solverAllowNetwork' || k === 'verifierAllowNetwork' || k === 'solverAllowScripts' || k === 'verifierAllowScripts') { out[k] = (v === true || v === false || v === '') ? v : DEFAULT_PARAMS[k] }
422
+ // 最终论文(spec v2 §B):布尔/枚举必须**显式归一化**,不允许未知键直通('false' 会保持真值)。
423
+ else if (k === 'paperFormat') { out[k] = (v === 'both' || v === 'md' || v === 'tex') ? v : DEFAULT_PARAMS[k] }
424
+ else if (k === 'paperLanguage') { out[k] = (v === 'zh' || v === 'en') ? v : DEFAULT_PARAMS[k] }
425
+ else if (k === 'paperLatexCommand') { const s = String(v == null ? '' : v).trim(); out[k] = s }
411
426
  else { out[k] = v }
412
427
  }
413
428
  return out
@@ -3289,13 +3304,493 @@ export function apply(ctx) {
3289
3304
  return best
3290
3305
  }
3291
3306
 
3307
+ // ==========================================================================================
3308
+ // 最终论文(规格:docs/final-paper.md)—— 会话侧(v3 数据来源:md 知识库)。
3309
+ //
3310
+ // 时序(修订 §A2/§A3):v3 的收口信号是 checkTermination() 的完整收口分支(无未解决问题、
3311
+ // `leftoverVerify` 为空——**这才是真实完整性判据**(§A4)、无真实在途代理、无任务/计划/挂起门)。
3312
+ // 撰写子代理必须在 `scheduler.running=false` 与 `releaseProjectLock()` **之前**派遣
3313
+ // (停止后 scheduleTick() 是空操作;提前释放项目锁后另一会话可能写同一棵树)。派不出去
3314
+ // (宿主激活上限 ACTIVATION_LIMIT_REACHED)→ 排队 + apply 级心跳重试 + 可见告警。
3315
+ // 落盘(§C/§D):md/tex 是文本;**paper.pdf 只能由编译器子进程生成**、插件只 stat 且**永不删除/覆盖**;
3316
+ // paper.meta.json 最后写(原子提交点);幂等 = run id + finalizedAt + 逐产物存在性(不用稳定哈希)。
3317
+ // ==========================================================================================
3318
+ let paperInFlight = '' // 正在撰写的 childId
3319
+ let paperInFlightAt = 0 // 派遣时刻(判定卡死窗口;force 与心跳都据它降级)
3320
+ let paperReaps = 0 // 本 run 内已回收卡死撰写者的次数(自动重派上限)
3321
+ const paperAbandoned = {} // childId -> true:已放弃的撰写者;它事后返回时输出被丢弃并留痕
3322
+ let paperPending = null // 激活上限排队重试:{ tries, retryAt, reason, limit, trigger, opts }
3323
+ const PAPER_RETRY_MS = 5000
3324
+ const PAPER_MAX_RETRIES = 12
3325
+ const PAPER_STALE_MS = 10 * 60 * 1000 // 撰写者卡死窗口:超过它,force 与心跳都会回收并重派(10 分钟)
3326
+ const PAPER_MAX_AUTO_REAPS = 2 // 心跳自动回收+重派的次数上限,之后交给 /vibe paper force
3327
+ const PAPER_LOCK_STALE_MS = 120000
3328
+ function activationLimitFrom(message) { const m = /active child limit:\s*(\d+)/.exec(String(message == null ? '' : message)); return m ? Number(m[1]) : undefined }
3329
+ function paperId(opts) { return paperDirId((opts && opts.id) || currentProject) }
3330
+ function paperDir(id) { return 'Paper/' + paperDirId(id) }
3331
+ function paperAbsDir(id) { return frameworkRoot() + '/' + paperDir(id) }
3332
+ function paperAuthorLine() { return 'Vibe Math V3(单作者:论文撰写子代理)· 项目 ' + currentProject }
3333
+ function paperOpts(opts) {
3334
+ const o = opts || {}
3335
+ const fmt = (o.format === 'md' || o.format === 'tex' || o.format === 'both') ? o.format : ((params.paperFormat === 'md' || params.paperFormat === 'tex') ? params.paperFormat : 'both')
3336
+ const lang = (o.lang === 'en' || o.lang === 'zh') ? o.lang : (params.paperLanguage === 'en' ? 'en' : 'zh')
3337
+ const compilePdf = (o.compilePdf === true || o.compilePdf === false) ? o.compilePdf : (params.paperCompilePdf !== false)
3338
+ return { format: fmt, lang: lang, compilePdf: compilePdf, force: o.force === true, id: o.id }
3339
+ }
3340
+ async function paperReadMeta(id) { const m = await readJson(paperDir(id) + '/paper.meta.json'); return (m && typeof m === 'object' && !Array.isArray(m)) ? m : undefined }
3341
+ async function paperPathExists(rel) { try { const t = await fsTarget(rel); return (await fs.stat(t)) !== undefined } catch (e) { return false } }
3342
+ async function paperAppendLog(id, tag, text) {
3343
+ const rel = paperDir(id) + '/paper.log.md'
3344
+ const prev = await readText(rel)
3345
+ const head = (prev === undefined || !String(prev).trim()) ? (paperLogHead(id).join('\n') + '\n') : String(prev)
3346
+ await writeText(rel, head + paperLogLine(now(), tag, text) + '\n')
3347
+ }
3348
+ /** paper 作用域锁(修订 §A3):v3 在收口时已释放项目锁,用它防止两个会话同时写同一棵 Paper/ 树。 */
3349
+ async function paperLockAcquire(id) {
3350
+ const rel = paperDir(id) + '/paper.lock.json'
3351
+ const cur = await readJson(rel)
3352
+ if (cur && cur.sessionId && cur.sessionId !== sessionId && (now() - Number(cur.at || 0)) < PAPER_LOCK_STALE_MS) return { ok: false, holder: String(cur.sessionId), at: Number(cur.at || 0) }
3353
+ await writeJson(rel, { sessionId: sessionId, at: now(), project: currentProject })
3354
+ return { ok: true }
3355
+ }
3356
+ async function paperLockRelease(id) {
3357
+ const rel = paperDir(id) + '/paper.lock.json'
3358
+ const cur = await readJson(rel)
3359
+ if (cur && cur.sessionId === sessionId) await writeJson(rel, { sessionId: '', at: now(), released: true })
3360
+ }
3361
+ /**
3362
+ * 回收卡死的撰写者(修订 §1:死掉的撰写者绝不能把论文永久卡住)。清状态 + 尽力中断 +
3363
+ * 从 agentRegistry 摘除(否则一直被算作活跃子代理)+ 记进 paperAbandoned(事后返回的输出被明确
3364
+ * 丢弃并留痕)+ 可见告警。
3365
+ */
3366
+ async function paperReapWriter(id, why) {
3367
+ const cid = paperInFlight
3368
+ paperInFlight = ''; paperInFlightAt = 0
3369
+ if (cid) {
3370
+ try { await interruptChild(cid) } catch (e) { /* best effort */ }
3371
+ delete agentRegistry[cid]
3372
+ paperAbandoned[cid] = true
3373
+ }
3374
+ await paperAppendLog(id, 'reap', '回收卡死的撰写者 ' + (cid || '(未知)') + ':' + why + '(论文不会被永久卡住;force/心跳会继续)')
3375
+ logActivity('paper', '回收卡死的论文撰写子代理 ' + (cid || '(未知)') + ':' + why)
3376
+ console.error('vibe-math-v3: reaped a stalled paper writer (' + (cid || 'unknown') + '): ' + why)
3377
+ reportDirty = true
3378
+ return cid
3379
+ }
3380
+ function paperWriterVerdictNow(force) {
3381
+ return paperWriterVerdict({ inFlight: !!paperInFlight, force: !!force, ageMs: now() - Number(paperInFlightAt || 0), staleMs: PAPER_STALE_MS, childInRegistry: paperInFlight ? (agentRegistry[paperInFlight] !== undefined) : true })
3382
+ }
3383
+ function paperOneLine(s, cap) { return String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, cap || 400) }
3384
+ /** 证据索引(spec §3.9):只列**确实存在**的文件;排除 Paper/ 自己(避免自我污染)。
3385
+ * formalVerify=off 必须真的是无操作(formal-verify-v3 的语料守卫断言"off 的提示词里零 Lean 文本"),
3386
+ * 所以 off 档不把形式化路径写进材料。 */
3387
+ async function paperEvidenceIndex() {
3388
+ const formalNow = formalOn()
3389
+ const out = []
3390
+ if (formalNow && await paperPathExists('State/formal.json')) out.push('State/formal.json')
3391
+ if (await paperPathExists('State/index.json')) out.push('State/index.json')
3392
+ for (const p of allProblems()) out.push(problemRel(p))
3393
+ for (const p of allPropos()) out.push(propositionRel(p))
3394
+ for (const m of methods.values()) out.push(methodRel(m, false))
3395
+ const dirs = ['Verified/命题', 'Verified/问题', 'Logs/Verification', 'Logs/Plans', 'Progress'].concat(formalNow ? ['Verified/Lean', 'Formal'] : [])
3396
+ for (let i = 0; i < dirs.length; i++) { const files = await listFiles(dirs[i]); for (let j = 0; j < files.length; j++) out.push(dirs[i] + '/' + files[j]) }
3397
+ const seen = {}, uniq = []
3398
+ for (let i = 0; i < out.length; i++) { const x = String(out[i]); if (seen[x] || x.indexOf('Paper/') === 0) continue; seen[x] = true; uniq.push(x) }
3399
+ return uniq.sort()
3400
+ }
3401
+ /** 汇总材料(spec §4 + 修订 §A4):只含既有证据;未决项(含 leftoverVerify 的待验证对象)显式标注。 */
3402
+ async function buildPaperDigest() {
3403
+ const L = []
3404
+ L.push('PRESET: vibe-math-v3 (single-author)')
3405
+ L.push('PROJECT: ' + currentProject)
3406
+ const qs = allProblems()
3407
+ L.push('')
3408
+ L.push('[ORIGINAL PROBLEMS] (Problems/*.md)')
3409
+ if (qs.length === 0) L.push('- (none)')
3410
+ for (let i = 0; i < qs.length; i++) {
3411
+ const q = qs[i]
3412
+ L.push('- id=' + q.id + ' | 状态=' + q.状态 + ' | 优先级=' + q.优先级 + ' | 依赖=' + JSON.stringify(q.依赖 || []) + ' | 来源=' + (q.来源 || '原始') + ' | 陈述=' + paperOneLine(q.陈述, 800))
3413
+ const dirs = getDirState(q.id)
3414
+ if (dirs.length) L.push(' · 方向:' + dirs.map(function (d) { return d.id + ':' + d.status }).join(', '))
3415
+ const sols = q.solutions || []
3416
+ if (sols.length === 0) L.push(' · 解法:无')
3417
+ for (let j = 0; j < sols.length; j++) L.push(' · 解法#' + j + ' 概率=' + sols[j].prob + ' 状态=' + (sols[j].status || '') + ' 证据=Verified/问题/ 与 Logs/Verification/ | ' + paperOneLine(sols[j].text, 1200))
3418
+ }
3419
+ const ps = allPropos()
3420
+ L.push('')
3421
+ L.push('[PROPOSITIONS] (Propos/<分类>/<id>.md)')
3422
+ if (ps.length === 0) L.push('- (none)')
3423
+ for (let i = 0; i < ps.length; i++) {
3424
+ const p = ps[i]
3425
+ const rec = formalOn() ? formalOf(p.id) : null // off 档不把形式化字段写进材料(off 是真无操作)
3426
+ L.push('- id=' + p.id + ' | 概率=' + p.概率 + ' | 状态=' + p.状态 + ' | 优先级=' + p.优先级 + ' | 价值/关键性=' + p.价值关键性 + ' | 来源问题=' + (p.来源问题 || '') + (rec ? (' | 形式化=' + ((rec && rec.status) || 'none') + (rec && rec.proof ? ('(' + rec.proof + ')') : '')) : '') + ' | 陈述=' + paperOneLine(p.陈述, 800))
3427
+ const sides = [['证明', p.proofs || []], ['证伪', p.refutes || []]]
3428
+ for (let k = 0; k < sides.length; k++) { const arr = sides[k][1]; for (let j = 0; j < arr.length; j++) if (arr[j] && arr[j].prob === 1) L.push(' · 已检验通过:' + sides[k][0] + '#' + j + ' | ' + paperOneLine(arr[j].text, 900)) }
3429
+ }
3430
+ L.push('')
3431
+ L.push('[METHODS / ARTEFACTS] (Methods/ + 全局 VibeMath/Methods/)')
3432
+ const ms = Array.from(methods.values()).concat(Array.from(globalMethods.values()))
3433
+ if (ms.length === 0) L.push('- (none)')
3434
+ for (let i = 0; i < ms.length; i++) {
3435
+ const m = ms[i]
3436
+ L.push('- id=' + m.id + ' | 标题=' + m.标题 + ' | 类型=' + m.类型 + ' | 状态=' + m.状态 + ' | 可信断言=' + JSON.stringify(m.可信断言 || []) + ' | 应用次数=' + ((m.applications || []).length) + ' | 核心内容=' + paperOneLine(m.核心内容, 900))
3437
+ }
3438
+ // §A4:v3 的真实完整性判据是 leftoverVerify(buildVerifyCandidates)——把仍未验证的对象列出来。
3439
+ let cands = []
3440
+ try { cands = await buildVerifyCandidates() } catch (e) { /* 尽力而为 */ }
3441
+ L.push('')
3442
+ L.push('[STILL UNVERIFIED — v3 的完整性判据 leftoverVerify(buildVerifyCandidates);必须标注为未决]')
3443
+ if (cands.length === 0) L.push('- (none — 没有待验证对象)')
3444
+ for (let i = 0; i < cands.length; i++) L.push('- 未决:' + cands[i].rId + '(' + cands[i].kind + ',' + paperOneLine(cands[i].概述, 200) + (cands[i].prob === undefined ? '' : (',prob=' + cands[i].prob)) + ')')
3445
+ L.push('')
3446
+ L.push('[UNRESOLVED / REFUTED — 论文里必须显式标注,不得当成已成立的结论]')
3447
+ let any = false
3448
+ for (let i = 0; i < ps.length; i++) {
3449
+ if (!(ps[i].概率 === 1 || ps[i].概率 === 0)) { any = true; L.push('- 未定论:命题 ' + ps[i].id + '(概率=' + ps[i].概率 + ')') }
3450
+ else if (ps[i].概率 === 0) L.push('- 已被否证:命题 ' + ps[i].id)
3451
+ }
3452
+ for (let i = 0; i < qs.length; i++) {
3453
+ const sols = qs[i].solutions || []
3454
+ for (let j = 0; j < sols.length; j++) if (!(sols[j].prob === 1 || sols[j].prob === 0)) { any = true; L.push('- 未定论:问题 ' + qs[i].id + ' 的解法#' + j + '(概率=' + sols[j].prob + ')') }
3455
+ }
3456
+ if (!any && cands.length === 0) L.push('- (none — 所有对象均已定论)')
3457
+ L.push('')
3458
+ L.push('[EVIDENCE INDEX] (only files that exist)')
3459
+ const ev = await paperEvidenceIndex()
3460
+ for (let i = 0; i < ev.length; i++) L.push('- ' + ev[i])
3461
+ return L.join('\n')
3462
+ }
3463
+ /** 「论文撰写」子代理提示词(单作者;与 v2 同一形状/同一骨架)。 */
3464
+ function paperWriterPrompt(digest, o) {
3465
+ const langName = o.lang === 'en' ? 'English' : '中文(Chinese)'
3466
+ return 'You are the DEDICATED PAPER WRITER (single-author mode) of a math research run that has just CONVERGED.\n' +
3467
+ 'Write its final paper in ' + langName + ', using ONLY the evidence in the MATERIAL section below.\n\n' +
3468
+ 'HARD RULES:\n' +
3469
+ '- NEVER invent content: no new proposition, no new computation, no citation that is not in the MATERIAL.\n' +
3470
+ '- Unresolved or refuted items MUST be explicitly labelled (「未定论」/「已被否证」, or "unresolved"/"refuted" in English); never present them as established.\n' +
3471
+ '- Fixed 9-section skeleton — provide bodies for these EXACT `## ` headings (the framework writes the headings, author/date and the evidence index itself):\n' +
3472
+ PAPER_SKELETON.map(function (s, i) { return ' ' + (i + 1) + '. ' + s.key + ' — ' + s.spec }).join('\n') + '\n' +
3473
+ '- A section with no evidence must be exactly 「' + PAPER_NO_EVIDENCE + '」 (do not pad it).\n' +
3474
+ '- Markdown subset only: `#`/`##`/`###`, `- ` lists, `**bold**`, `*em*`, `` `code` ``, and inline math as `$...$`. No tables, images, footnotes or raw HTML.\n\n' +
3475
+ 'OUTPUT CONTRACT — respond with ONLY one ```json code fence, no prose:\n' +
3476
+ '{"title":"<paper title>","abstract":"<original problem + main results>","sections":[{"name":"<one of the 9 headings>","body":"<markdown>"}, ...]}\n\n' +
3477
+ 'MATERIAL (evidence only — do not add anything beyond it):\n' + digest
3478
+ }
3479
+ /** LaTeX 检测(spec §5 + 修订 §D):paperLatexCommand 非空时只用它;否则按语言顺序探测。 */
3480
+ async function paperDetectLatex(lang) {
3481
+ const sub = subprocessOf()
3482
+ if (sub === undefined || typeof sub.spawn !== 'function') return { available: [], reason: 'no-subprocess' }
3483
+ if (typeof sub.resolveExecutable !== 'function') return { available: [], reason: 'no-resolveExecutable' }
3484
+ const forced = String(params.paperLatexCommand || '').trim()
3485
+ if (forced) {
3486
+ try { const p = await sub.resolveExecutable(forced); return p ? { available: [{ name: forced, path: String(p) }], reason: 'ok', forced: true } : { available: [], reason: 'paperLatexCommand not found: ' + forced, forced: true } }
3487
+ catch (e) { return { available: [], reason: 'paperLatexCommand not found: ' + forced, forced: true } }
3488
+ }
3489
+ const order = lang === 'en' ? PAPER_ENGINE_ORDER_EN : PAPER_ENGINE_ORDER_ZH
3490
+ const available = []
3491
+ for (let i = 0; i < order.length; i++) {
3492
+ try { const p = await sub.resolveExecutable(order[i]); if (p) available.push({ name: order[i], path: String(p) }) } catch (e) { /* 未安装 */ }
3493
+ }
3494
+ return { available: available, reason: available.length ? 'ok' : 'none' }
3495
+ }
3496
+ function paperArgvFor(engine) {
3497
+ if (engine.name === 'latexmk') return [engine.path, '-pdf', '-interaction=nonstopmode', '-halt-on-error', 'paper.tex']
3498
+ if (engine.name === 'tectonic') return [engine.path, '--keep-logs', 'paper.tex']
3499
+ return [engine.path, '-interaction=nonstopmode', '-halt-on-error', 'paper.tex']
3500
+ }
3501
+ /** 一次编译(nonstopmode),带主动超时 terminate。 */
3502
+ async function paperRunOnce(engine, dirAbs, capMs) {
3503
+ const sub = subprocessOf()
3504
+ if (sub === undefined || typeof sub.spawn !== 'function') return { ok: false, exitCode: null, code: 'NO_SUBPROCESS' }
3505
+ let handle
3506
+ try { handle = sub.spawn({ argv: paperArgvFor(engine), cwd: dirAbs, stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } }, graceMs: capMs }) }
3507
+ catch (e) { return { ok: false, exitCode: null, code: 'SPAWN_FAILED', message: String((e && e.message) || e) } }
3508
+ let timedOut = false, timer = null, outcome
3509
+ try {
3510
+ outcome = await Promise.race([
3511
+ handle.done,
3512
+ new Promise(function (resolve) { timer = setTimeout(function () { timedOut = true; try { if (handle && typeof handle.terminate === 'function') handle.terminate() } catch (e) { /* best effort */ } resolve({ exitCode: null }) }, capMs) }),
3513
+ ])
3514
+ } catch (e) { if (timer !== null) clearTimeout(timer); return { ok: false, exitCode: null, code: 'RUN_FAILED', message: String((e && e.message) || e) } }
3515
+ if (timer !== null) clearTimeout(timer)
3516
+ let err = ''
3517
+ try { if (handle.collected && handle.collected.stderr) err = String(handle.collected.stderr.readFrom(0).text || '') } catch (e) { /* best effort */ }
3518
+ return { ok: !timedOut && !!outcome && outcome.exitCode === 0, exitCode: outcome ? outcome.exitCode : null, timedOut: timedOut, stderr: err.slice(-1200) }
3519
+ }
3520
+ /**
3521
+ * 编译(spec §5 + 修订 §D/§E):主引擎(nonstopmode,两遍)→ 换引擎 → 去不支持宏包 → 最小模板再试一次;
3522
+ * 然后报告并降级。**永不删除/覆盖已有 pdf**:成功判据 = 本次运行 exit 0 **且** pdf 存在。
3523
+ */
3524
+ async function paperCompile(id, o, texPrimary, bodyTex) {
3525
+ const relDir = paperDir(id), absDir = paperAbsDir(id)
3526
+ const attempts = []
3527
+ if (!o.compilePdf) return { compile: 'skipped', reason: 'paperCompilePdf=false', engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
3528
+ if (o.format === 'md') return { compile: 'skipped', reason: 'paperFormat=md(不产出 tex,跳过编译)', engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
3529
+ const det = await paperDetectLatex(o.lang)
3530
+ if (det.available.length === 0) {
3531
+ await paperAppendLog(id, 'latex', '未检测到任何 LaTeX 引擎(' + det.reason + ')→ 只保留 paper.tex + paper.md,不编译 pdf(spec §5:不阻塞定稿)')
3532
+ return { compile: 'not-detected', reason: det.reason, engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
3533
+ }
3534
+ await paperAppendLog(id, 'latex', '检测到引擎 ' + det.available.map(function (x) { return x.name }).join(', ') + (det.forced ? '(paperLatexCommand 指定)' : ('(顺序 ' + (o.lang === 'en' ? PAPER_ENGINE_ORDER_EN : PAPER_ENGINE_ORDER_ZH).join(' > ') + ')')))
3535
+ const plan = [{ engine: det.available[0], tex: texPrimary, mode: 'primary' }]
3536
+ if (det.available[1]) plan.push({ engine: det.available[1], tex: texPrimary, mode: 'engine-fallback' })
3537
+ plan.push({ engine: det.available[0], tex: paperSanitizeTex(texPrimary), mode: 'drop-unsupported-packages' })
3538
+ plan.push({ engine: det.available[0], tex: paperMinimalTexDoc({ lang: o.lang, bodyTex: bodyTex }), mode: 'minimal-template' })
3539
+ const cap = 120000
3540
+ for (let i = 0; i < plan.length; i++) {
3541
+ const step = plan[i]
3542
+ await writeText(relDir + '/paper.tex', step.tex)
3543
+ const runs = []
3544
+ for (let pass = 0; pass < 2; pass++) runs.push(await paperRunOnce(step.engine, absDir, cap))
3545
+ const last = runs[runs.length - 1]
3546
+ const pdf = !!last.ok && await paperPathExists(relDir + '/paper.pdf')
3547
+ attempts.push({ engine: step.engine.name, mode: step.mode, ok: pdf, exitCode: last.exitCode, timedOut: !!last.timedOut, stderr: last.stderr || '' })
3548
+ await paperAppendLog(id, 'compile', step.mode + ' engine=' + step.engine.name + ' → ' + (pdf ? 'ok(paper.pdf 已生成)' : ('failed(exit=' + String(last.exitCode) + (last.timedOut ? ', timeout' : '') + ')')))
3549
+ if (pdf) return { compile: (i === 0) ? 'ok' : 'repaired', engine: step.engine.name, mode: step.mode, attempts: attempts, pdfPreserved: false }
3550
+ }
3551
+ await writeText(relDir + '/paper.tex', texPrimary)
3552
+ const preserved = await paperPathExists(relDir + '/paper.pdf')
3553
+ await paperAppendLog(id, 'compile', '所有尝试均失败:保留 paper.tex + paper.md' + (preserved ? '(paper.pdf 是**上一次成功编译**留下的,未被覆盖)' : ',未生成 paper.pdf') + '(不阻塞定稿)')
3554
+ logActivity('paper', '论文 pdf 编译失败(已尝试换引擎/去不支持宏包/最小模板),已保留 tex+md:' + relDir)
3555
+ reportDirty = true
3556
+ return { compile: 'failed', engine: det.available[0].name, attempts: attempts, pdfPreserved: preserved }
3557
+ }
3558
+ /** 落盘 md/tex/pdf/meta(meta **最后**写 = 原子提交点;spec §2/§5)。 */
3559
+ async function paperEmitArtifacts(id, o, info) {
3560
+ const relDir = paperDir(id)
3561
+ const dateStr = fmtTime(now()).slice(0, 10)
3562
+ const author = paperAuthorLine()
3563
+ const evidence = info.evidence || (await paperEvidenceIndex())
3564
+ const title = String(info.title || '').trim() || ('研究报告:' + currentProject)
3565
+ const md = paperBuildMarkdown({ title: title, author: author, date: dateStr, abstract: info.abstract, sections: info.sections, evidence: evidence })
3566
+ const artifacts = { md: false, tex: false, pdf: false }
3567
+ let compile = 'skipped', engine = null, attempts = [], pdfPreserved = false
3568
+ if (o.format === 'md' || o.format === 'both') { await writeText(relDir + '/paper.md', md); artifacts.md = true }
3569
+ if (o.format === 'tex' || o.format === 'both') {
3570
+ const bodyTex = paperMdToTexBody(md)
3571
+ const tex = paperTexDoc({ title: title, author: author, date: dateStr, lang: o.lang, bodyTex: bodyTex })
3572
+ await writeText(relDir + '/paper.tex', tex)
3573
+ artifacts.tex = true
3574
+ const c = await paperCompile(id, o, tex, bodyTex)
3575
+ compile = c.compile; engine = c.engine || null; attempts = c.attempts || []
3576
+ pdfPreserved = !!c.pdfPreserved
3577
+ if (compile === 'ok' || compile === 'repaired') artifacts.pdf = await paperPathExists(relDir + '/paper.pdf')
3578
+ }
3579
+ const filled = PAPER_SKELETON.map(function (s) { return s.key }).filter(function (k) { return !!(info.sections && String(info.sections[k] || '').trim()) })
3580
+ const meta = {
3581
+ id: id, dir: relDir, preset: 'vibe-math-v3', singleAuthor: true, project: currentProject,
3582
+ title: title, author: author, date: dateStr, abstract: String(info.abstract || ''),
3583
+ sectionBodies: info.sections || {}, sections: PAPER_SKELETON.map(function (s) { return s.key }), filledSections: filled,
3584
+ raw: String(info.raw == null ? '' : info.raw), notes: Array.isArray(info.notes) ? info.notes : [],
3585
+ trigger: info.trigger || 'manual', runStartedAt: Number(scheduler.startedAt) || 0,
3586
+ params: { finalPaper: params.finalPaper !== false, paperFormat: o.format, paperLanguage: o.lang, paperCompilePdf: !!o.compilePdf, paperLatexCommand: String(params.paperLatexCommand || '') },
3587
+ finalizedAt: now(), finalizedAtText: fmtTime(now()),
3588
+ artifacts: artifacts, compile: compile, compileEngine: engine, compileAttempts: attempts, pdfPreserved: pdfPreserved,
3589
+ material: info.material || null, evidence: evidence,
3590
+ }
3591
+ await writeJson(relDir + '/paper.meta.json', meta)
3592
+ return meta
3593
+ }
3594
+ async function paperMissingArtifacts(id, o) {
3595
+ const relDir = paperDir(id)
3596
+ const need = []
3597
+ if (o.format === 'md' || o.format === 'both') { if (!(await paperPathExists(relDir + '/paper.md'))) need.push('paper.md') }
3598
+ if (o.format === 'tex' || o.format === 'both') { if (!(await paperPathExists(relDir + '/paper.tex'))) need.push('paper.tex') }
3599
+ return need
3600
+ }
3601
+ /** 派遣(内部):激活上限拒绝 → 排队重试(修订 §A2)。 */
3602
+ async function paperDispatch(trigger, o, id, prevTries) {
3603
+ const relDir = paperDir(id)
3604
+ const digest = await buildPaperDigest()
3605
+ let childId
3606
+ try {
3607
+ childId = await spawnChild('paper-writer:' + id, paperWriterPrompt(digest, o), { role: 'paper', paperId: id, trigger: trigger, lang: o.lang, format: o.format, compilePdf: o.compilePdf })
3608
+ } catch (e) {
3609
+ const message = String((e && e.message) || e)
3610
+ const limit = activationLimitFrom(message)
3611
+ if (limit !== undefined || /ACTIVATION_LIMIT_REACHED|active child limit/i.test(message)) {
3612
+ const tries = Number(prevTries || 0) + 1
3613
+ if (tries > PAPER_MAX_RETRIES) {
3614
+ paperPending = null
3615
+ await paperAppendLog(id, 'dispatch-failed', '宿主激活上限连续拒绝 ' + PAPER_MAX_RETRIES + ' 次,已放弃自动派遣:' + message)
3616
+ logActivity('paper', '论文撰写子代理被宿主激活上限连续拒绝(' + PAPER_MAX_RETRIES + ' 次),已放弃自动派遣(稍后可 /vibe paper force 重试):' + message)
3617
+ console.error('vibe-math-v3: paper writer refused by the host activation limit ' + PAPER_MAX_RETRIES + ' times; giving up (retry later with /vibe paper force) — ' + message)
3618
+ await paperLockRelease(id)
3619
+ return { ok: false, message: 'paper writer refused by the activation limit ' + PAPER_MAX_RETRIES + ' times: ' + message, id: id, dir: relDir, limit: limit }
3620
+ }
3621
+ paperPending = { tries: tries, retryAt: now() + PAPER_RETRY_MS, reason: message, limit: limit, trigger: trigger, opts: { lang: o.lang, format: o.format, compilePdf: o.compilePdf } }
3622
+ if (tries === 1 || tries % 3 === 0) {
3623
+ await paperAppendLog(id, 'dispatch-queued', '宿主激活上限(' + (limit === undefined ? '?' : limit) + ')拒绝派遣(第 ' + tries + ' 次):' + (PAPER_RETRY_MS / 1000) + 's 后重试')
3624
+ logActivity('paper', '论文撰写子代理被宿主激活上限拒绝(第 ' + tries + ' 次),已排队重试:' + message)
3625
+ console.error('vibe-math-v3: paper writer queued after an activation-limit refusal (try ' + tries + '): ' + message)
3626
+ }
3627
+ reportDirty = true
3628
+ return { ok: true, queued: true, reason: 'activation-limit-reached', tries: tries, retryAt: paperPending.retryAt, limit: limit, id: id, dir: relDir }
3629
+ }
3630
+ await paperAppendLog(id, 'dispatch-failed', message)
3631
+ logActivity('paper', '「论文撰写」子代理派遣失败:' + message)
3632
+ await paperLockRelease(id)
3633
+ return { ok: false, message: 'paper writer dispatch failed: ' + message, id: id, dir: relDir }
3634
+ }
3635
+ paperInFlight = childId
3636
+ paperInFlightAt = now()
3637
+ // 只有"非自动重试"的派遣才重置回收计数:auto-retry 必须保留计数,否则卡死重派会无限循环。
3638
+ if (trigger !== 'auto-retry') paperReaps = 0
3639
+ paperPending = null
3640
+ const material = paperMaterialSummary(digest)
3641
+ await paperAppendLog(id, 'dispatch', 'trigger=' + trigger + ' child=' + childId + ' format=' + o.format + ' lang=' + o.lang + ' compilePdf=' + o.compilePdf + (o.force ? ' force=true' : '') + ' run=' + (Number(scheduler.startedAt) || 0) + ' material=' + material.chars + ' chars/' + material.lines + ' lines')
3642
+ logActivity('paper', '已派遣「论文撰写」子代理(' + trigger + ',' + o.format + '/' + o.lang + ')→ ' + relDir)
3643
+ reportDirty = true
3644
+ return { ok: true, dispatched: true, childId: childId, id: id, dir: relDir, trigger: trigger, format: o.format, lang: o.lang, compilePdf: o.compilePdf }
3645
+ }
3646
+ /** 触发入口(spec §2/§3 + 修订 §A2/§A3):自动(收口)与手动命令共用。 */
3647
+ async function maybeWritePaper(trigger, opts) {
3648
+ const o = paperOpts(opts)
3649
+ const id = paperId(opts)
3650
+ const relDir = paperDir(id)
3651
+ const meta = await paperReadMeta(id)
3652
+ const sameRun = !!meta && Number(meta.runStartedAt || 0) === (Number(scheduler.startedAt) || 0)
3653
+ if (!o.force && meta && meta.finalizedAt && sameRun) {
3654
+ const missing = await paperMissingArtifacts(id, o)
3655
+ if (missing.length === 0) {
3656
+ await paperAppendLog(id, 'skip', '幂等:同一 run(' + (Number(scheduler.startedAt) || 0) + ')已有定稿且产物齐全 → 不重复撰写(/vibe paper force 可重写)')
3657
+ return { ok: true, skipped: true, reason: 'already-finalized-this-run', id: id, dir: relDir, finalizedAt: meta.finalizedAt, artifacts: meta.artifacts || {}, compile: meta.compile, note: '同一 run 只写一次;重复触发只补写缺失产物(spec §2)' }
3658
+ }
3659
+ await paperAppendLog(id, 'fill', '幂等:已有定稿但缺少 ' + missing.join(', ') + ' → 用已存的定稿内容补写(不重新派遣作者)')
3660
+ const filled = await paperEmitArtifacts(id, o, { title: meta.title, abstract: meta.abstract, sections: meta.sectionBodies || {}, evidence: meta.evidence, raw: meta.raw, notes: (meta.notes || []).concat(['补写缺失产物:' + missing.join(', ')]), trigger: meta.trigger || trigger, material: meta.material })
3661
+ return { ok: true, skipped: true, reason: 'filled-missing-artifacts', filled: missing, id: id, dir: relDir, finalizedAt: filled.finalizedAt, artifacts: filled.artifacts, compile: filled.compile }
3662
+ }
3663
+ if (paperInFlight) {
3664
+ const verdict = paperWriterVerdictNow(o.force)
3665
+ if (verdict === 'reap') {
3666
+ const gone = agentRegistry[paperInFlight] === undefined
3667
+ await paperReapWriter(id, gone ? '子代理已不在 agentRegistry(end 事件丢失 / 宿主丢弃)' : ('超过卡死窗口 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟未返回'))
3668
+ } else if (verdict === 'refuse') {
3669
+ const ageMs = now() - Number(paperInFlightAt || 0)
3670
+ await paperAppendLog(id, 'force-refused', '仍有在途撰写者 ' + paperInFlight + '(' + Math.round(ageMs / 1000) + 's):超过 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟才会被 force 放弃')
3671
+ return { ok: false, reason: 'writer-in-flight', childId: paperInFlight, ageMs: ageMs, staleMs: PAPER_STALE_MS, id: id, dir: relDir, message: '已有在途的论文撰写子代理 ' + paperInFlight + '(已 ' + Math.round(ageMs / 1000) + 's 未返回)。等它返回,或等超过 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟后 force 会自动回收并重派(心跳也会自动回收)。' }
3672
+ } else {
3673
+ return { ok: true, skipped: true, reason: 'writer-in-flight', childId: paperInFlight, ageMs: now() - Number(paperInFlightAt || 0), staleMs: PAPER_STALE_MS, id: id, dir: relDir }
3674
+ }
3675
+ }
3676
+ if (paperPending) return { ok: true, queued: true, reason: 'retry-queued', retryAt: paperPending.retryAt, tries: paperPending.tries, id: id, dir: relDir }
3677
+ const lock = await paperLockAcquire(id)
3678
+ if (!lock.ok) {
3679
+ await paperAppendLog(id, 'skip', 'paper 锁由会话 ' + lock.holder + ' 持有(未过期)→ 本次不派遣(修订 §A3 的 paper 作用域锁)')
3680
+ return { ok: true, skipped: true, reason: 'locked-by-another-session', holder: lock.holder, id: id, dir: relDir }
3681
+ }
3682
+ return await paperDispatch(trigger, o, id, 0)
3683
+ }
3684
+ function paperRetryDue() { return !!(paperPending && now() >= Number(paperPending.retryAt || 0)) }
3685
+ /**
3686
+ * 心跳(修订 §1):在途 → 续租 paper 锁;**卡死则自动回收**(child 不在注册表,或超过卡死窗口),
3687
+ * 并自动重派(上限 PAPER_MAX_AUTO_REAPS,之后给出可执行提示交给 /vibe paper force);有排队则重试派遣。
3688
+ */
3689
+ async function runPaperRetry() {
3690
+ const id = paperId()
3691
+ if (paperInFlight) {
3692
+ if (paperWriterVerdictNow(false) === 'reap') {
3693
+ const gone = agentRegistry[paperInFlight] === undefined
3694
+ await paperReapWriter(id, gone ? '子代理已不在 agentRegistry(end 事件丢失 / 宿主丢弃)' : ('超过卡死窗口 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟未返回'))
3695
+ const meta = await paperReadMeta(id)
3696
+ if (params.finalPaper !== false && !(meta && meta.finalizedAt) && Number(paperReaps || 0) < PAPER_MAX_AUTO_REAPS) {
3697
+ paperReaps = Number(paperReaps || 0) + 1
3698
+ await paperAppendLog(id, 'retry-after-reap', '第 ' + paperReaps + ' 次自动重派(上限 ' + PAPER_MAX_AUTO_REAPS + ')')
3699
+ await paperDispatch('auto-retry', paperOpts({}), id, 0)
3700
+ } else {
3701
+ await paperAppendLog(id, 'reap-giveup', '自动重派已达上限 ' + PAPER_MAX_AUTO_REAPS + '(或已有定稿):不再自动派遣,请用 /vibe paper force 手动重写')
3702
+ logActivity('paper', '论文撰写子代理连续卡死/已放弃:不再自动重派,可用 /vibe paper force 手动重写')
3703
+ }
3704
+ return
3705
+ }
3706
+ try { await paperLockAcquire(id) } catch (e) { /* 续租失败不致命 */ }
3707
+ return
3708
+ }
3709
+ if (!paperRetryDue()) return
3710
+ const p = paperPending
3711
+ paperPending = null
3712
+ const o = paperOpts(p.opts)
3713
+ await paperDispatch(p.trigger || 'auto-retry', o, id, Number(p.tries || 0))
3714
+ }
3715
+ /** `/vibe paper [lang=zh|en] [format=both|md|tex] [force]`(spec §2/§6)。 */
3716
+ async function paperCommand(args) {
3717
+ const rest = Array.isArray(args) ? args : []
3718
+ const opts = {}
3719
+ for (let i = 0; i < rest.length; i++) {
3720
+ const a = String(rest[i] || '')
3721
+ if (a === 'force') { opts.force = true; continue }
3722
+ const eq = a.indexOf('=')
3723
+ const k = eq > 0 ? a.slice(0, eq).toLowerCase() : ''
3724
+ const v = eq > 0 ? a.slice(eq + 1).toLowerCase() : ''
3725
+ if (k === 'lang') { if (v !== 'zh' && v !== 'en') return { ok: false, message: '/vibe paper lang= 只接受 zh|en(收到 ' + JSON.stringify(a) + ')' }; opts.lang = v; continue }
3726
+ if (k === 'format') { if (v !== 'both' && v !== 'md' && v !== 'tex') return { ok: false, message: '/vibe paper format= 只接受 both|md|tex(收到 ' + JSON.stringify(a) + ')' }; opts.format = v; continue }
3727
+ return { ok: false, message: 'unknown /vibe paper option: ' + a + '(可用 lang=zh|en、format=both|md|tex、force)' }
3728
+ }
3729
+ const r = await maybeWritePaper('manual', opts)
3730
+ if (r.ok === false) return r
3731
+ const autoNote = params.finalPaper === false ? '(自动触发已关闭 finalPaper=false;手动命令仍可用)' : ''
3732
+ const msg = r.dispatched ? ('已派遣「论文撰写」子代理,产物将写入 ' + r.dir + '/')
3733
+ : r.queued ? ('宿主激活上限已满:论文撰写已排队,' + (PAPER_RETRY_MS / 1000) + 's 后自动重试(第 ' + r.tries + ' 次)')
3734
+ : (r.reason === 'already-finalized-this-run' ? '同一 run 已有定稿且产物齐全:不重复撰写(需要重写请用 /vibe paper force)'
3735
+ : (r.reason === 'filled-missing-artifacts' ? ('已用既有定稿补写缺失产物:' + (r.filled || []).join(', ')) : ('未重新派遣:' + (r.reason || ''))))
3736
+ return Object.assign({}, r, { message: msg + autoNote, autoDisabled: params.finalPaper === false })
3737
+ }
3738
+ /** 论文撰写子代理结束(spec §4):解析回复 → 组装 9 节 → 落盘 → 编译 → 记日志 → 释放 paper 锁。 */
3739
+ async function handlePaperWriter(childId, meta, output) {
3740
+ const id = meta.paperId || paperId()
3741
+ paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0
3742
+ delete paperAbandoned[childId]
3743
+ const o = paperOpts({ lang: meta.lang, format: meta.format, compilePdf: meta.compilePdf })
3744
+ const parsed = parseJson(output)
3745
+ const info = paperSkeletonFromReply(parsed, output)
3746
+ if (!info.title) info.title = paperTitleFromMd(String(output || '')) || ('研究报告:' + currentProject)
3747
+ for (let i = 0; i < info.notes.length; i++) await paperAppendLog(id, 'note', info.notes[i])
3748
+ if (!String(output || '').trim()) await paperAppendLog(id, 'reply', '作者回复为空:只写骨架 + 占位说明(绝不编造内容)')
3749
+ let written
3750
+ try {
3751
+ written = await paperEmitArtifacts(id, o, { title: info.title, abstract: info.abstract, sections: info.sections, raw: String(output || ''), notes: info.notes, trigger: meta.trigger || 'auto', material: paperMaterialSummary(await buildPaperDigest()) })
3752
+ } catch (e) {
3753
+ await paperAppendLog(id, 'finalize-failed', String((e && e.message) || e))
3754
+ logActivity('paper', '论文落盘失败:' + String((e && e.message) || e))
3755
+ await paperLockRelease(id)
3756
+ return
3757
+ }
3758
+ await paperAppendLog(id, 'finalize', '产物 ' + JSON.stringify(written.artifacts) + ' compile=' + written.compile + (written.compileEngine ? (' engine=' + written.compileEngine) : '') + ' run=' + (Number(scheduler.startedAt) || 0))
3759
+ logActivity('paper', '论文(最终稿)已落盘:' + paperDir(id) + '/(' + Object.keys(written.artifacts).filter(function (k) { return written.artifacts[k] }).join('/') + ',编译=' + written.compile + ')')
3760
+ await paperLockRelease(id)
3761
+ reportDirty = true
3762
+ await saveAll()
3763
+ }
3764
+ /** status 里的论文视图(spec §2/§6)。 */
3765
+ async function paperStatusView() {
3766
+ const id = paperId()
3767
+ const meta = await paperReadMeta(id)
3768
+ return {
3769
+ id: id, dir: paperDir(id), inFlight: paperInFlight || null,
3770
+ inFlightSince: paperInFlight ? Number(paperInFlightAt || 0) : null,
3771
+ inFlightAgeMs: paperInFlight ? (now() - Number(paperInFlightAt || 0)) : null,
3772
+ staleMs: PAPER_STALE_MS, reapedThisRun: Number(paperReaps || 0),
3773
+ queued: paperPending ? { tries: paperPending.tries, retryAt: paperPending.retryAt, reason: paperPending.reason, limit: paperPending.limit === undefined ? null : paperPending.limit } : null,
3774
+ autoFinalPaper: params.finalPaper !== false, format: params.paperFormat, language: params.paperLanguage, compilePdf: params.paperCompilePdf !== false,
3775
+ finalizedAt: meta ? meta.finalizedAt : null, artifacts: meta ? (meta.artifacts || {}) : {}, compile: meta ? meta.compile : null,
3776
+ }
3777
+ }
3778
+
3292
3779
  // ================= child result dispatch =================
3293
3780
  async function onChildEnd(info) {
3294
3781
  // 子代理结束/中断:自动释放它持有的所有写锁(防锁残留导致文件被永久锁住)
3295
3782
  const endedId = String(info.id)
3296
3783
  for (const k of Object.keys(fileOwner)) { if (String(fileOwner[k].childId) === endedId) delete fileOwner[k] }
3297
3784
  const meta = agentRegistry[info.id]
3298
- if (meta === undefined) return
3785
+ if (meta === undefined) {
3786
+ // 已回收的撰写者事后才返回:输出被丢弃,但必须**留痕**,不能静默消失(修订 §1)。
3787
+ if (paperAbandoned[info.id]) {
3788
+ delete paperAbandoned[info.id]
3789
+ logActivity('paper', '已回收的论文撰写子代理 ' + info.id + ' 事后返回:其输出被丢弃(已重派新的撰写者)')
3790
+ reportDirty = true
3791
+ }
3792
+ return
3793
+ }
3299
3794
  const output = blocksToText(info.lastAssistantMessage)
3300
3795
  try {
3301
3796
  if (meta.role === 'explorer') await handleExplorer(info.id, meta, output)
@@ -3303,6 +3798,9 @@ export function apply(ctx) {
3303
3798
  else if (meta.role === 'verifier') await handleVerifier(info.id, meta, output, info.stopReason)
3304
3799
  else if (meta.role === 'planner') await handlePlanner(info.id, meta, output)
3305
3800
  else if (meta.role === 'method-keeper') await handleMethodKeeper(info.id, meta, output)
3801
+ // 修订 §A2:论文撰写子代理必须有**自己的分支**——未知 role 的产出会被静默丢弃。
3802
+ else if (meta.role === 'paper') await handlePaperWriter(info.id, meta, output)
3803
+ else console.error('vibe-math-v3: subagent ' + info.id + ' ended with an unknown role "' + String(meta.role) + '" — its output is dropped')
3306
3804
  } catch (e) { console.error('vibe-math-v3 onChildEnd error: ' + String((e && e.stack) || e)) }
3307
3805
  await saveAll()
3308
3806
  await maybePromoteMethods()
@@ -3321,6 +3819,9 @@ export function apply(ctx) {
3321
3819
  if (Object.keys(agentRegistry).length > 0 || Object.keys(tasks).length > 0) {
3322
3820
  logActivity(fresh ? 'start' : 'resume', 'cleared ' + Object.keys(agentRegistry).length + ' agent(s) and ' + Object.keys(tasks).length + ' task(s) (' + (fresh ? 'restart' : 'stale from previous process') + ')')
3323
3821
  agentRegistry = {}; tasks = {}
3822
+ // 论文派遣态是本进程内存态:子代理已被中断/丢弃,排队与在途标记必须一起清掉(修订 §A2)。
3823
+ if (paperInFlight || paperPending) logActivity('paper', '重启/跨进程恢复:清空论文撰写态(inFlight=' + (paperInFlight || '-') + (paperPending ? ', queued' : '') + ')')
3824
+ paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null
3324
3825
  }
3325
3826
  }
3326
3827
  await writeJson('State/process_epoch.json', processEpoch)
@@ -3376,7 +3877,7 @@ export function apply(ctx) {
3376
3877
  async function startScheduler(override) { const r = await init(true); if (!r.ok) return r; const lock = await acquireProjectLock(override === true); if (!lock.ok) return lock; scheduler.running = true; scheduler.startedAt = now(); scheduler.gate = null; logActivity('start', 'scheduler started for project ' + currentProject + '(v3:md 知识库 + 规划代理调度 + 方法库)'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
3377
3878
  async function resumeScheduler(override) { const r = await init(false); if (!r.ok) return r; const lock = await acquireProjectLock(override === true); if (!lock.ok) return lock; scheduler.running = true; scheduler.gate = null; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
3378
3879
  async function pauseScheduler() { scheduler.running = false; await releaseProjectLock(); logActivity('pause', 'scheduler paused'); await saveAll(); return { ok: true, message: 'scheduler paused' } }
3379
- async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; planQueue = []; await releaseProjectLock(); logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
3880
+ async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; planQueue = []; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null; await releaseProjectLock(); logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
3380
3881
  async function autoResolvePending() {
3381
3882
  const pending = decisionQueue.filter(function (d) { return d.status === 'pending' })
3382
3883
  for (let i = 0; i < pending.length; i++) {
@@ -3413,6 +3914,8 @@ export function apply(ctx) {
3413
3914
  plannerEnabled: params.plannerEnabled, plannerFails: plannerFails,
3414
3915
  formal: formalSummary(),
3415
3916
  recentActivity: activityLog.slice(-Math.min(10, Number(params.activityLogCap) || 100)), params: params,
3917
+ // 最终论文的可观测面(spec §2/§6)。
3918
+ paper: await paperStatusView(),
3416
3919
  }
3417
3920
  }
3418
3921
  async function checkTermination() {
@@ -3431,8 +3934,14 @@ export function apply(ctx) {
3431
3934
  // H1:在途的 **planner 不算"活跃工作"**。planner 不产出知识、不持有验证任务,只是"问下一步做什么";
3432
3935
  // 旧判定把刚派出的 planner 也算成活跃代理,于是本 tick 的停机判定永远被自己派出的 planner 挡掉,
3433
3936
  // 空闲时形成"每 tick 一个 planner"的活锁。排除它之后,空闲的那一 tick 就能正常收口停机。
3434
- const realAgents = Object.keys(agentRegistry).filter(function (cid) { const m = agentRegistry[cid]; return m && m.role !== 'planner' }).length
3937
+ const realAgents = Object.keys(agentRegistry).filter(function (cid) { const m = agentRegistry[cid]; return m && m.role !== 'planner' && m.role !== 'paper' }).length
3435
3938
  if (unsolved.length === 0 && !leftoverVerify && realAgents === 0 && Object.keys(tasks).length === 0 && planQueue.length === 0) {
3939
+ // ★ 最终论文(修订 §A2/§A3):必须在 running=false 与 releaseProjectLock() **之前**派遣——
3940
+ // 停止之后 scheduleTick() 是空操作;提前放锁后另一会话可能开始写同一棵树(paper 锁兜底)。
3941
+ // 触发点就是这个分支本身:`leftoverVerify` 为空才是 v3 的真实完整性判据(§A4)。
3942
+ if (params.finalPaper !== false) {
3943
+ try { await maybeWritePaper('auto') } catch (e) { logActivity('paper', '自动撰写最终论文失败:' + String((e && e.message) || e)) }
3944
+ }
3436
3945
  scheduler.running = false
3437
3946
  await releaseProjectLock()
3438
3947
  logActivity('stop', 'all active problems solved (never-priority excluded) and no active agents/tasks/plans — scheduler stopped (strict termination)')
@@ -3464,7 +3973,7 @@ export function apply(ctx) {
3464
3973
  if (!create && !exists) return { ok: false, message: 'project not found: ' + slug }
3465
3974
  if (scheduler.running) await abortScheduler()
3466
3975
  currentProject = slug; await writeCurrentProject(); await ensureDirs()
3467
- params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; planQueue = []; plannerFails = 0; methodLog = { pendingInventions: [], keepCount: 0, lastKeepAt: 0 }; projectLock = { sessionId: '', at: 0 }; lastReportWrite = 0; lastPushReport = 0; reportDirty = false; lastPlanSummary = null; archivedJ = {}; lastIndexWrite = 0; formalState = { records: {}, todo: [], libRuns: {} }
3976
+ params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; planQueue = []; plannerFails = 0; methodLog = { pendingInventions: [], keepCount: 0, lastKeepAt: 0 }; projectLock = { sessionId: '', at: 0 }; lastReportWrite = 0; lastPushReport = 0; reportDirty = false; lastPlanSummary = null; archivedJ = {}; lastIndexWrite = 0; formalState = { records: {}, todo: [], libRuns: {} }; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null
3468
3977
  await loadSettings(); await migrateLegacyParams(); await loadState(); await loadKnowledgeBase(); await saveAll()
3469
3978
  if (params.indexAutoRebuild) await rebuildIndex()
3470
3979
  return { ok: true, project: slug, frameworkRoot: frameworkRoot() }
@@ -3483,7 +3992,7 @@ export function apply(ctx) {
3483
3992
  registerTool('vibe_math_status', TOOL_DESC.vibe_math_status, objParams({}), async function () { await refreshParams(); return await getStatus() })
3484
3993
  registerTool('vibe_math_report', TOOL_DESC.vibe_math_report, objParams({}), async function () { await refreshParams(); await maybeWriteReport(true); return await buildReport() })
3485
3994
  registerTool('vibe_math_set_mode', TOOL_DESC.vibe_math_set_mode, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), async function (args) { params.mode = args.mode; await saveAll(); await saveSettings(); if (params.mode === 'auto') await autoResolvePending(); return { ok: true, mode: params.mode } })
3486
- registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
3995
+ registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' }, finalPaper: { type: 'boolean' }, paperFormat: { type: 'string', enum: ['both', 'md', 'tex'] }, paperLanguage: { type: 'string', enum: ['zh', 'en'] }, paperCompilePdf: { type: 'boolean' }, paperLatexCommand: { type: 'string' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
3487
3996
  registerTool('vibe_math_setup', TOOL_DESC.vibe_math_setup, objParams({}), async function () { await refreshParams(); const list = PARAM_SCHEMA.map(function (p) { const out = Object.assign({}, p); out.current = params[p.name]; out.default = DEFAULT_PARAMS[p.name]; return out }); return { ok: true, parameters: list, saveTo: frameworkRoot() + '/vibe_math_setting.json' } })
3488
3997
  registerTool('vibe_math_save_settings', TOOL_DESC.vibe_math_save_settings, objParams({}), async function () { return await saveSettings() })
3489
3998
  registerTool('vibe_math_template', TOOL_DESC.vibe_math_template, objParams({ where: { type: 'string', enum: ['global', 'project'] } }), async function (args) { return await createTemplate((args && args.where) || 'global') })
@@ -3745,7 +4254,8 @@ export function apply(ctx) {
3745
4254
  }
3746
4255
  if (cmd === 'decisions') return { ok: true, decisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }) }
3747
4256
  if (cmd === 'agents') { const out = []; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) { const m = agentRegistry[ids[i]]; out.push({ childId: ids[i], role: m.role, qid: m.qid, direction: m.direction, round: m.round }) } return { ok: true, agents: out } }
3748
- return { ok: false, usage: 'start [override] | resume [override] | pause | abort | status | report | mode <auto|manual> | setup | save | template [global|project] | add <id> <desc> | add-proposition <id> <概述> | list-propositions | methods | index | plan | lock | project [list|new <name>|<name>] | decisions | agents', message: 'unknown /vibe subcommand: ' + (cmd || '(empty)') }
4257
+ if (cmd === 'paper') return await paperCommand(args)
4258
+ return { ok: false, usage: 'start [override] | resume [override] | pause | abort | status | report | mode <auto|manual> | setup | save | template [global|project] | add <id> <desc> | add-proposition <id> <概述> | list-propositions | methods | index | plan | lock | project [list|new <name>|<name>] | decisions | agents | paper [lang=zh|en] [format=both|md|tex] [force]', message: 'unknown /vibe subcommand: ' + (cmd || '(empty)') }
3749
4259
  }
3750
4260
 
3751
4261
  // ================= session surface =================
@@ -3766,6 +4276,10 @@ export function apply(ctx) {
3766
4276
  handlers: handlers,
3767
4277
  refreshProject: async function () { if (rootAgent) currentProject = await readCurrentProject() },
3768
4278
  getRunning: function () { return scheduler.running },
4279
+ // apply 级心跳用(修订 §A2):running=false 之后 scheduleTick() 是空操作,论文的排队重试
4280
+ // (激活上限)与 paper 锁续租必须由**独立于调度器**的心跳驱动。
4281
+ paperRetryDue: paperRetryDue,
4282
+ runPaperRetry: runPaperRetry,
3769
4283
  // childOwner 裁剪用(审计 L1):这个会话当前仍"可能再发 subagent/end"的 child
3770
4284
  // = 在册子代理 + 任何任务正在等的那几个。
3771
4285
  referencedChildIds: function () {
@@ -3804,7 +4318,7 @@ export function apply(ctx) {
3804
4318
  registerTool('vibe_math_status', TOOL_DESC.vibe_math_status, objParams({}), 'vibe_math_status')
3805
4319
  registerTool('vibe_math_report', TOOL_DESC.vibe_math_report, objParams({}), 'vibe_math_report')
3806
4320
  registerTool('vibe_math_set_mode', TOOL_DESC.vibe_math_set_mode, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), 'vibe_math_set_mode')
3807
- registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' } }), 'vibe_math_set_params')
4321
+ registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' }, finalPaper: { type: 'boolean' }, paperFormat: { type: 'string', enum: ['both', 'md', 'tex'] }, paperLanguage: { type: 'string', enum: ['zh', 'en'] }, paperCompilePdf: { type: 'boolean' }, paperLatexCommand: { type: 'string' } }), 'vibe_math_set_params')
3808
4322
  registerTool('vibe_math_setup', TOOL_DESC.vibe_math_setup, objParams({}), 'vibe_math_setup')
3809
4323
  registerTool('vibe_math_save_settings', TOOL_DESC.vibe_math_save_settings, objParams({}), 'vibe_math_save_settings')
3810
4324
  registerTool('vibe_math_template', TOOL_DESC.vibe_math_template, objParams({ where: { type: 'string', enum: ['global', 'project'] } }), 'vibe_math_template')
@@ -3837,7 +4351,7 @@ export function apply(ctx) {
3837
4351
  ctx.effect(() => commands.register({
3838
4352
  name: 'vibe',
3839
4353
  description: 'control the Vibe Math V3 solver (start/pause/projects/setup/save/decisions/agents/methods/index/plan/lock)',
3840
- input: { hint: '[start [override]|resume [override]|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|methods|index|plan|lock|project [list|new <name>|<name>]|decisions|agents]' },
4354
+ input: { hint: '[start [override]|resume [override]|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|methods|index|plan|lock|project [list|new <name>|<name>]|decisions|agents|paper [lang=zh|en] [format=both|md|tex] [force]]' },
3841
4355
  handler: async function (invocation) {
3842
4356
  const s = getSession(invocation && invocation.agent)
3843
4357
  if (!s) return { kind: 'error', text: JSON.stringify({ ok: false, error: 'no vibe-math session for this agent' }) }
@@ -3899,7 +4413,12 @@ export function apply(ctx) {
3899
4413
  ctx.effect(() => {
3900
4414
  let beat = 0
3901
4415
  return everyMs(1000, function () {
3902
- for (const s of sessions.values()) { if (s.getRunning() && !s.tickInFlight && s.tickDue() && s.scheduler.gate === null) s.scheduleTick() }
4416
+ for (const s of sessions.values()) {
4417
+ if (s.getRunning() && !s.tickInFlight && s.tickDue() && s.scheduler.gate === null) s.scheduleTick()
4418
+ // 论文心跳(修订 §A2):**独立于 scheduler.running** —— 收口后 running=false,但被激活上限
4419
+ // 拒绝的撰写派遣仍要重试,在途时还要续租 paper 锁。runPaperRetry() 无事可做时立即返回。
4420
+ if (typeof s.runPaperRetry === 'function') s.runPaperRetry().catch(function (e) { console.error('vibe-math-v3: paper retry failed: ' + String((e && e.message) || e)) })
4421
+ }
3903
4422
  // childOwner 裁剪(审计 L1):每 30 拍(约 30s)一次,成本是"会话数 × 映射数"的一次扫描。
3904
4423
  if ((++beat % 30) === 0) {
3905
4424
  try { pruneChildOwner() } catch (e) { console.error('vibe-math-v3: pruneChildOwner failed: ' + String((e && e.message) || e)) }
@@ -3921,6 +4440,255 @@ export function apply(ctx) {
3921
4440
  // to the clock) and `parseProgress` normalises the object it is handed in place (pre-existing).
3922
4441
  // Nothing here is used by the plugin at runtime except through `apply()`, and behaviour is
3923
4442
  // byte-identical to the previous in-`apply` declarations.
4443
+ // ============================================================================================
4444
+ // 最终论文(规格:docs/final-paper.md)—— **纯函数部分**(module scope;
4445
+ // 与 v2 逐字同构,只有数据来源不同)。会话相关的落盘/派遣/编译在 makeSession 里。
4446
+ // ============================================================================================
4447
+ /** 固定 9 节骨架(spec §3)。key 同时是 md 的 `## ` 标题与 tex 的 `\section{}`。 */
4448
+ const PAPER_SKELETON = [
4449
+ { key: '摘要', spec: '1 标题、作者、日期、摘要(原问题 + 主要结论)' },
4450
+ { key: '引言与问题背景', spec: '2 原问题的完整陈述' },
4451
+ { key: '原问题的完整解法', spec: '3 最终答案 + 完整推理链' },
4452
+ { key: '已检验通过的命题', spec: '4 逐条列出,含判定为真的估计值与证据来源' },
4453
+ { key: '已解决的子问题与中间成果', spec: '5' },
4454
+ { key: '创造或发现的有价值之物', spec: '6 方法、理论、思想、有价值经验、数学理解' },
4455
+ { key: '规律总结', spec: '7 从上述条目归纳出的可复用规律' },
4456
+ { key: '讨论、局限与展望', spec: '8' },
4457
+ { key: '附录:证据与文件索引', spec: '9 Verified/、Logs/、关键卡片路径' },
4458
+ ]
4459
+ const PAPER_NO_EVIDENCE = '(本节暂无证据支持的内容——不编造。)'
4460
+ const PAPER_EVIDENCE_HEADING = '证据与文件索引'
4461
+ /** LaTeX 引擎检测顺序(spec §5):中文优先 xelatex,英文优先 pdflatex/latexmk。 */
4462
+ const PAPER_ENGINE_ORDER_ZH = ['xelatex', 'latexmk', 'pdflatex', 'lualatex', 'tectonic']
4463
+ const PAPER_ENGINE_ORDER_EN = ['pdflatex', 'latexmk', 'xelatex', 'lualatex', 'tectonic']
4464
+ /** 只用常见宏包(spec §5);修复阶段会丢弃不在这个名单里的 \usepackage。 */
4465
+ const PAPER_ALLOWED_PACKAGES = ['amsmath', 'amssymb', 'amsthm', 'geometry', 'hyperref', 'longtable', 'booktabs']
4466
+ /** tex 必须转义的 10 个字符(spec §5):\ _ % & # $ { } ~ ^ ——一次替换,避免二次转义。 */
4467
+ const PAPER_TEX_MAP = { '\\': '\\textbackslash{}', '_': '\\_', '%': '\\%', '&': '\\&', '#': '\\#', '$': '\\$', '{': '\\{', '}': '\\}', '~': '\\textasciitilde{}', '^': '\\textasciicircum{}' }
4468
+ function texEscape(s) { return String(s == null ? '' : s).replace(/[\\_%&#${}~^]/g, function (c) { return PAPER_TEX_MAP[c] }) }
4469
+ /** 材料摘要(只作记录;幂等判定用 run id + finalizedAt + 逐产物存在性,修订 §C)。 */
4470
+ function paperMaterialSummary(digest) {
4471
+ const t = String(digest == null ? '' : digest)
4472
+ return { chars: t.length, lines: t.split('\n').length }
4473
+ }
4474
+ /**
4475
+ * 撰写者卡死判定(纯函数,便于守卫覆盖两条分支):
4476
+ * 'none' 正常在途(还没到卡死窗口,且 force 未要求放弃)
4477
+ * 'reap' 必须回收:child 已不在 agentRegistry(end 事件丢失 / 宿主丢弃,它永远不会再回来),
4478
+ * 或已超过卡死窗口——force 与心跳都据此自动降级,论文不能被一个死掉的撰写者永久卡住
4479
+ * 'refuse' force 但还没到窗口:调用方必须给出**可执行**的报错(告诉调用者还要等多久)
4480
+ */
4481
+ function paperWriterVerdict(o) {
4482
+ if (!(o && o.inFlight)) return 'none'
4483
+ if (o.childInRegistry === false) return 'reap'
4484
+ const age = Number((o && o.ageMs) || 0)
4485
+ const stale = Math.max(1, Number((o && o.staleMs) || 0))
4486
+ if (age >= stale) return 'reap'
4487
+ return (o && o.force) ? 'refuse' : 'none'
4488
+ }
4489
+ /** 行内 md → tex:``code`` / **bold** / *em* 转成命令,其余按 spec §5 转义;`$...$` 原样保留。 */
4490
+ function paperInlineToTex(s) {
4491
+ const t = String(s == null ? '' : s)
4492
+ let out = ''
4493
+ let i = 0
4494
+ while (i < t.length) {
4495
+ const c = t[i]
4496
+ if (c === '$') { const j = t.indexOf('$', i + 1); if (j > i) { out += t.slice(i, j + 1); i = j + 1; continue } }
4497
+ if (c === '`') { const j = t.indexOf('`', i + 1); if (j > i) { out += '\\texttt{' + texEscape(t.slice(i + 1, j)) + '}'; i = j + 1; continue } }
4498
+ if (c === '*' && t[i + 1] === '*') { const j = t.indexOf('**', i + 2); if (j > i) { out += '\\textbf{' + paperInlineToTex(t.slice(i + 2, j)) + '}'; i = j + 2; continue } }
4499
+ if (c === '*') { const j = t.indexOf('*', i + 1); if (j > i) { out += '\\emph{' + paperInlineToTex(t.slice(i + 1, j)) + '}'; i = j + 1; continue } }
4500
+ out += PAPER_TEX_MAP[c] !== undefined ? PAPER_TEX_MAP[c] : c
4501
+ i++
4502
+ }
4503
+ return out
4504
+ }
4505
+ /** md 正文 → tex 正文(标题/列表/段落;只认 compose 侧约定的最小语法)。 */
4506
+ function paperMdToTexBody(md) {
4507
+ const out = []
4508
+ let inList = false
4509
+ let buf = []
4510
+ const flush = function () { if (buf.length) { out.push(paperInlineToTex(buf.join(' '))); buf = [] } }
4511
+ const closeList = function () { if (inList) { out.push('\\end{itemize}'); inList = false } }
4512
+ const lines = String(md == null ? '' : md).split(/\r?\n/)
4513
+ for (let i = 0; i < lines.length; i++) {
4514
+ const line = lines[i].replace(/\s+$/, '')
4515
+ const h = /^(#{1,4})\s+(.*)$/.exec(line)
4516
+ if (h) {
4517
+ flush(); closeList()
4518
+ const txt = paperInlineToTex(h[2])
4519
+ const lv = h[1].length
4520
+ out.push(lv <= 1 ? ('\\section*{' + txt + '}') : lv === 2 ? ('\\section{' + txt + '}') : lv === 3 ? ('\\subsection{' + txt + '}') : ('\\subsubsection{' + txt + '}'))
4521
+ continue
4522
+ }
4523
+ const li = /^\s*[-*]\s+(.*)$/.exec(line)
4524
+ if (li) { flush(); if (!inList) { out.push('\\begin{itemize}'); inList = true } out.push(' \\item ' + paperInlineToTex(li[1])); continue }
4525
+ if (!line.trim()) { flush(); closeList(); out.push(''); continue }
4526
+ buf.push(line.trim())
4527
+ }
4528
+ flush(); closeList()
4529
+ return out.join('\n').replace(/\n{3,}/g, '\n\n').trim()
4530
+ }
4531
+ /** 主模板(spec §5):中文 ctexart / 英文 article,只加常见宏包。 */
4532
+ function paperTexDoc(o) {
4533
+ const lang = (o && o.lang === 'en') ? 'en' : 'zh'
4534
+ return [
4535
+ '\\documentclass[11pt]{' + (lang === 'en' ? 'article' : 'ctexart') + '}',
4536
+ '\\usepackage[margin=2.5cm]{geometry}',
4537
+ '\\usepackage{amsmath,amssymb,amsthm}',
4538
+ '\\usepackage{hyperref}',
4539
+ '\\usepackage{longtable,booktabs}',
4540
+ '\\title{' + texEscape(o && o.title) + '}',
4541
+ '\\author{' + texEscape(o && o.author) + '}',
4542
+ '\\date{' + texEscape(o && o.date) + '}',
4543
+ '\\begin{document}',
4544
+ '\\maketitle',
4545
+ String((o && o.bodyTex) || ''),
4546
+ '\\end{document}',
4547
+ ].join('\n') + '\n'
4548
+ }
4549
+ /** 修复阶段的**最小模板**(spec §5):只留 documentclass + 正文。 */
4550
+ function paperMinimalTexDoc(o) {
4551
+ const lang = (o && o.lang === 'en') ? 'en' : 'zh'
4552
+ return [
4553
+ '\\documentclass[11pt]{' + (lang === 'en' ? 'article' : 'ctexart') + '}',
4554
+ '\\begin{document}',
4555
+ String((o && o.bodyTex) || ''),
4556
+ '\\end{document}',
4557
+ ].join('\n') + '\n'
4558
+ }
4559
+ /** 修复阶段:丢弃不在常见宏包名单里的 \usepackage(其余原样保留)。 */
4560
+ function paperSanitizeTex(tex) {
4561
+ const keep = []
4562
+ const lines = String(tex == null ? '' : tex).split('\n')
4563
+ for (let i = 0; i < lines.length; i++) {
4564
+ const m = /^\s*\\usepackage(\[[^\]]*\])?\{([^}]*)\}\s*$/.exec(lines[i])
4565
+ if (m) {
4566
+ const pkgs = String(m[2]).split(',').map(function (x) { return x.trim() }).filter(Boolean)
4567
+ const ok = pkgs.filter(function (p) { return PAPER_ALLOWED_PACKAGES.indexOf(p) >= 0 })
4568
+ if (ok.length === 0) continue
4569
+ keep.push('\\usepackage' + (m[1] || '') + '{' + ok.join(',') + '}')
4570
+ continue
4571
+ }
4572
+ keep.push(lines[i])
4573
+ }
4574
+ return keep.join('\n')
4575
+ }
4576
+ /** 骨架名匹配:允许带序号/前缀,或只写节名的一部分(例如「附录」)。 */
4577
+ function paperSkeletonKey(name) {
4578
+ const n = String(name == null ? '' : name).replace(/^#+\s*/, '').trim()
4579
+ if (!n) return undefined
4580
+ for (const s of PAPER_SKELETON) if (n === s.key) return s.key
4581
+ for (const s of PAPER_SKELETON) if (n.indexOf(s.key) !== -1 || s.key.indexOf(n) !== -1) return s.key
4582
+ return undefined
4583
+ }
4584
+ /** 按 `## ` 把任意 markdown 切成 [{name,text}](无标题时 name='')。 */
4585
+ function paperSplitByHeadings(md) {
4586
+ const out = []
4587
+ let cur = { name: '', text: [] }
4588
+ const lines = String(md == null ? '' : md).split(/\r?\n/)
4589
+ for (let i = 0; i < lines.length; i++) {
4590
+ const h = /^##\s+(.*?)\s*$/.exec(lines[i])
4591
+ if (h) { if (cur.name || cur.text.length) out.push({ name: cur.name, text: cur.text.join('\n').trim() }); cur = { name: h[1], text: [] }; continue }
4592
+ cur.text.push(lines[i])
4593
+ }
4594
+ if (cur.name || cur.text.length) out.push({ name: cur.name, text: cur.text.join('\n').trim() })
4595
+ return out
4596
+ }
4597
+ /** 把「论文撰写」子代理的回复映射到 9 节骨架上(**绝不编造**:缺的节由调用方填占位说明)。 */
4598
+ function paperSkeletonFromReply(reply, fallbackText) {
4599
+ const sections = {}
4600
+ const notes = []
4601
+ let title = ''
4602
+ let abstract = ''
4603
+ const put = function (key, body) {
4604
+ const b = String(body == null ? '' : body).trim()
4605
+ if (!b) return
4606
+ sections[key] = sections[key] ? (sections[key] + '\n\n' + b) : b
4607
+ }
4608
+ const absorbUnknown = function (name, text) {
4609
+ if (!name && !String(text || '').trim()) return
4610
+ notes.push('作者写了一节「' + (name || '(无名)') + '」,不属于固定 9 节骨架,已并入「讨论、局限与展望」')
4611
+ put('讨论、局限与展望', (name ? ('**' + name + '**\n\n') : '') + text)
4612
+ }
4613
+ const obj = (reply && typeof reply === 'object' && !Array.isArray(reply)) ? reply : null
4614
+ if (obj) {
4615
+ title = String(obj.title || obj.标题 || '').trim()
4616
+ abstract = String(obj.abstract || obj.摘要 || '').trim()
4617
+ const list = Array.isArray(obj.sections) ? obj.sections : (Array.isArray(obj.章节) ? obj.章节 : [])
4618
+ for (let i = 0; i < list.length; i++) {
4619
+ const s = list[i]
4620
+ if (!s) continue
4621
+ const name = String(s.name || s.key || s.heading || s.标题 || '')
4622
+ const body = String(s.body != null ? s.body : (s.content != null ? s.content : (s.内容 != null ? s.内容 : '')))
4623
+ const key = paperSkeletonKey(name)
4624
+ if (key === undefined) absorbUnknown(name.trim(), body)
4625
+ else put(key, body)
4626
+ }
4627
+ const bodyField = String(obj.body || obj.content || obj.md || obj.正文 || '')
4628
+ if (bodyField.trim()) {
4629
+ const parts = paperSplitByHeadings(bodyField)
4630
+ for (let i = 0; i < parts.length; i++) {
4631
+ const key = paperSkeletonKey(parts[i].name)
4632
+ if (key === undefined) { if (!parts[i].name && !abstract && !sections['摘要']) abstract = parts[i].text; else if (parts[i].name) absorbUnknown(parts[i].name, parts[i].text) }
4633
+ else put(key, parts[i].text)
4634
+ }
4635
+ }
4636
+ } else {
4637
+ const parts = paperSplitByHeadings(String(fallbackText == null ? '' : fallbackText))
4638
+ let matched = 0
4639
+ for (let i = 0; i < parts.length; i++) {
4640
+ const key = paperSkeletonKey(parts[i].name)
4641
+ if (key === undefined) { if (parts[i].name) absorbUnknown(parts[i].name, parts[i].text) }
4642
+ else { put(key, parts[i].text); matched++ }
4643
+ }
4644
+ if (matched === 0) {
4645
+ const raw = String(fallbackText == null ? '' : fallbackText).trim()
4646
+ if (raw) { notes.push('作者回复不是契约 JSON,也没有可识别的 `## ` 节标题;其原样正文已记入「讨论、局限与展望」'); put('讨论、局限与展望', raw) }
4647
+ else notes.push('作者回复为空:没有任何内容可写(不得编造)')
4648
+ }
4649
+ }
4650
+ return { title: title, abstract: abstract, sections: sections, notes: notes }
4651
+ }
4652
+ /** 组装 md(spec §3):9 节固定骨架 + 确定性证据索引;缺证据的节写占位说明。 */
4653
+ function paperBuildMarkdown(o) {
4654
+ const sec = (o && o.sections) || {}
4655
+ const ev = ((o && o.evidence) || []).slice()
4656
+ const L = []
4657
+ const ttl = String((o && o.title) || '').trim() || '研究报告'
4658
+ L.push('# ' + ttl)
4659
+ L.push('')
4660
+ L.push('- 作者:' + String((o && o.author) || ''))
4661
+ L.push('- 日期:' + String((o && o.date) || ''))
4662
+ L.push('')
4663
+ for (let i = 0; i < PAPER_SKELETON.length; i++) {
4664
+ const key = PAPER_SKELETON[i].key
4665
+ L.push('## ' + key)
4666
+ L.push('')
4667
+ let body = String(sec[key] || '').trim()
4668
+ if (key === '摘要') { const ab = String((o && o.abstract) || '').trim(); if (ab) body = body ? (ab + '\n\n' + body) : ab }
4669
+ if (key === '附录:证据与文件索引' && ev.length) {
4670
+ const idx = '### ' + PAPER_EVIDENCE_HEADING + '\n\n' + ev.map(function (x) { return '- `' + String(x) + '`' }).join('\n')
4671
+ body = body ? (body + '\n\n' + idx) : idx
4672
+ }
4673
+ L.push(body || PAPER_NO_EVIDENCE)
4674
+ L.push('')
4675
+ }
4676
+ return L.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'
4677
+ }
4678
+ /** paper.log.md 的**唯一格式**(v2/v3 逐字一致,spec §4)。 */
4679
+ function paperLogHead(id) {
4680
+ return ['# 论文写作日志|' + String(id), '',
4681
+ '> 单作者流程(v2/v3 同一格式):收口触发 → 派遣「论文撰写」子代理 → 落盘 md/tex → 编译 pdf。', '']
4682
+ }
4683
+ function paperLogLine(at, tag, text) { return '- ' + fmtTime(at) + ' [' + String(tag) + '] ' + String(text == null ? '' : text) }
4684
+ /** 论文目录 id:只允许安全字符(同一把归一化 ⇒ 不可能写出 Paper/ 之外,spec §6 越权用例)。 */
4685
+ function paperDirId(raw) { return (idSafe(String(raw == null ? '' : raw).trim()) || 'run') }
4686
+ /** 从回复里抽出可能存在的标题行(md 非契约路径的兜底)。 */
4687
+ function paperTitleFromMd(md) {
4688
+ const m = /^#\s+(.*?)\s*$/m.exec(String(md == null ? '' : md))
4689
+ return m ? m[1].trim() : ''
4690
+ }
4691
+
3924
4692
  export const __testHelpers = {
3925
4693
  uuid,
3926
4694
  shortId,
@@ -3948,6 +4716,27 @@ export const __testHelpers = {
3948
4716
  parseMethodMd,
3949
4717
  sanitizeToolFilter,
3950
4718
  registeredToolsFromError,
4719
+ // final paper(spec §6 的纯函数守卫面;与 v2 同构)
4720
+ PAPER_SKELETON,
4721
+ PAPER_NO_EVIDENCE,
4722
+ PAPER_ALLOWED_PACKAGES,
4723
+ PAPER_ENGINE_ORDER_ZH,
4724
+ PAPER_ENGINE_ORDER_EN,
4725
+ texEscape,
4726
+ paperMaterialSummary,
4727
+ paperWriterVerdict,
4728
+ paperInlineToTex,
4729
+ paperMdToTexBody,
4730
+ paperTexDoc,
4731
+ paperMinimalTexDoc,
4732
+ paperSanitizeTex,
4733
+ paperSkeletonKey,
4734
+ paperSkeletonFromReply,
4735
+ paperBuildMarkdown,
4736
+ paperLogHead,
4737
+ paperLogLine,
4738
+ paperDirId,
4739
+ paperTitleFromMd,
3951
4740
  }
3952
4741
 
3953
4742
  function now() { return Date.now() }
@@ -3968,7 +4757,7 @@ const TOOL_DESC = {
3968
4757
  vibe_math_set_mode: 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.',
3969
4758
  // set_params 的 schema 现在也收 mode(M10):同一能力既有专用工具 vibe_math_set_mode,也可用这个键;
3970
4759
  // 两条注册路径共用这一份描述,`--self-probe` 会证明漂移能被抓到。
3971
- vibe_math_set_params: 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象未达到 Lean 已通过或已记录显式阻塞原因之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式。',
4760
+ vibe_math_set_params: 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象未达到 Lean 已通过或已记录显式阻塞原因之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测 xelatex→latexmk→pdflatex→lualatex→tectonic),产物在 Paper/<项目>/。',
3972
4761
  vibe_math_setup: 'Return the interactive parameter schema for guided configuration.',
3973
4762
  vibe_math_save_settings: 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.',
3974
4763
  vibe_math_template: 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.',