dsh-vibe-math 2.8.3 → 2.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.en.md +19 -4
  2. package/README.md +19 -4
  3. package/cordis.patch.yml +8 -8
  4. package/docs/COMPAT-AUDIT-ROUND2.md +27 -0
  5. package/docs/final-paper.md +13 -0
  6. package/docs/formal-verification.md +6 -6
  7. package/docs/release-notes/README.en.md +2 -0
  8. package/docs/release-notes/README.md +2 -0
  9. package/docs/release-notes/RELEASE-NOTES-2.8.4.en.md +48 -0
  10. package/docs/release-notes/RELEASE-NOTES-2.8.4.md +48 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.8.5.en.md +75 -0
  12. package/docs/release-notes/RELEASE-NOTES-2.8.5.md +75 -0
  13. package/package.json +7 -3
  14. package/prompt-corpus-persona/persona-corpus.json +9 -9
  15. package/prompt-corpus-persona/persona-corpus.md +17 -17
  16. package/prompt-corpus-v2/formal-verify-v2.json +109 -109
  17. package/prompt-corpus-v2/formal-verify-v2.md +216 -216
  18. package/prompt-corpus-v3/formal-verify-v3.json +77 -77
  19. package/prompt-corpus-v3/formal-verify-v3.md +147 -147
  20. package/prompt-corpus-v4/formal-verify-v4.json +10 -10
  21. package/prompt-corpus-v4/formal-verify-v4.md +40 -40
  22. package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
  23. package/prompt-corpus-v5/prompt-corpus-v5.md +613 -186
  24. package/tests/audit-artifact-docs.mjs +38 -2
  25. package/tests/audit-artifact-docs.mutants.mjs +66 -1
  26. package/tests/audit-engine-faces.mjs +2 -2
  27. package/tests/audit-persona-surface.test.mjs +4 -1
  28. package/tests/audit-prompt-invariants.mjs +292 -2
  29. package/tests/e2e-v5-round2.test.mjs +248 -2
  30. package/tests/formal-verify-v2.test.mjs +6 -5
  31. package/tests/formal-verify-v3.test.mjs +6 -5
  32. package/tests/formal-verify-v4.test.mjs +4 -4
  33. package/tests/prompt-v5-integrity.test.mjs +6 -6
  34. package/tests/run-tests.mjs +18 -2
  35. package/tests/v4-final-paper.mutants.mjs +14 -0
  36. package/tests/v4-final-paper.test.mjs +18 -0
  37. package/tests/v5-institute-fixes.mutants.mjs +115 -0
  38. package/vibe-math-v2/agent.cordis.yml +2 -2
  39. package/vibe-math-v2/math-computation.js +5 -5
  40. package/vibe-math-v2/vibe-math-v2.js +66 -17
  41. package/vibe-math-v3/agent.cordis.yml +2 -2
  42. package/vibe-math-v3/math-computation.js +5 -5
  43. package/vibe-math-v3/vibe-math-v3.js +64 -15
  44. package/vibe-math-v4/agent.cordis.yml +6 -6
  45. package/vibe-math-v4/math-computation.js +5 -5
  46. package/vibe-math-v4/vibe-math-v4.js +71 -20
  47. package/vibe-math-v5/agent.cordis.yml +6 -6
  48. package/vibe-math-v5/math-computation.js +5 -5
  49. package/vibe-math-v5/vibe-math-v5.js +385 -34
  50. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +55 -2
@@ -56,6 +56,40 @@
56
56
  // guessed; otherwise the host resolver (PATH) is tried first, then the known install locations for this
57
57
  // platform. Provenance is returned so the caller can report \`leanFoundVia: 'explicit'|'path'|'known-install'\`,
58
58
  // and the probed list travels on failure ("I looked here too"), never just "not found on PATH".
59
+ // ── task-28: TeX Live's DOCUMENTED typical install positions + a bounded probe budget ────────────────
60
+ // A real-host SLV v5 run (15.2 min, candidate PASS) archived compileStatus `not-detected` on a machine
61
+ // whose TeX Live lives at `D:\texlive\2025\bin\windows\xelatex.exe`: the known list only covered
62
+ // `<ProgramFiles>/texlive`, so an engine that IS installed but NOT on PATH was never found and the run
63
+ // degraded to tex+md only. These are DOCUMENTED roots, never a search: no whole-drive scan, no install,
64
+ // no write outside the workspace (the three hard boundaries stay in the guidance text).
65
+ const TEX_CANDIDATE_CAP = 40 // hard cap on TeX known-root candidate paths probed per detection
66
+ const TEX_YEAR_LOOKBACK = 4 // the newest N year dirs of a TeX Live root (current year + 3)
67
+ const TEX_DRIVE_YEAR_ROOTS = ['D:/texlive', 'C:/texlive'] // Windows: TeX Live at a DRIVE ROOT
68
+ const TEX_SYSTEM_BIN_ROOTS = ['/Library/TeX/texbin'] // macOS: system MacTeX (already a bin dir)
69
+ const TEX_SYSTEM_YEAR_ROOTS = ['/usr/local/texlive', '/opt/texlive'] // Unix: TeX Live (year-globbed)
70
+ // ── feedback library (Shared/Feedback/) ──────────────────────────────────────────────────────────────
71
+ // The METHODOLOGY / COLLABORATION layer: how the institute WORKS (organisation, workflow, cooperation,
72
+ // obstacles, friction). Deliberately separate from Progress/ (research progress) and Verified/
73
+ // (established results) — an entry here is never evidence for a mathematical claim.
74
+ const FEEDBACK_CATEGORIES = ['cooperation', 'management', 'process', 'obstacle', 'conflict']
75
+ const FEEDBACK_CATEGORY_LABELS = { cooperation: '合作', management: '管理', process: '流程', obstacle: '障碍', conflict: '矛盾' }
76
+ // The three ROUTES decide what "handling" an entry means — and, crucially, WHO has to do anything:
77
+ // self — my own way of working / habits / pitfalls ⇒ I adjust it MYSELF; nobody's approval,
78
+ // at most a share so colleagues do not hit the same wall.
79
+ // team — how the institute is organised/run (task assignment, workflow, organisation model)
80
+ // ⇒ also self-regulation: no approval, change it as soon as it looks wrong.
81
+ // interpersonal — the problem is caused by SOMEONE ELSE and only they can fix it ⇒ the ONLY route that
82
+ // needs a careful assessment first ("should anything change, and would the change
83
+ // really be better?") and a written-back verification afterwards.
84
+ const FEEDBACK_ROUTES = ['self', 'team', 'interpersonal']
85
+ // `open` = recorded, not yet handled; `adjusted` = the initiator adjusted their own way of working
86
+ // (for self/team this IS the closing record — no approval step exists); `closed` = a result was written
87
+ // back (interpersonal REQUIRES that write-back); `dropped` = evaluated and deliberately NOT changed
88
+ // (needs a reason). list's default "not yet closed" filter = open|adjusted.
89
+ const FEEDBACK_STATUSES = ['open', 'adjusted', 'closed', 'dropped']
90
+ const FEEDBACK_OPEN_STATUSES = ['open', 'adjusted']
91
+ const FEEDBACK_DIR = 'Shared/Feedback'
92
+ const FEEDBACK_DISABLED_MESSAGE = '反馈库已关闭(params.feedback = \'off\'):本条未写入、也未改动任何条目'
59
93
  async function resolveKnownTool(sub, opts) {
60
94
  const name = String((opts && opts.name) || '')
61
95
  const explicit = String((opts && opts.explicit) || '').trim()
@@ -75,12 +109,27 @@ async function resolveKnownTool(sub, opts) {
75
109
  return null
76
110
  }
77
111
  const listDirs = async (p) => { try { const fs = await import('node:fs'); return fs.readdirSync(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) } catch (err) { return [] } }
112
+ // task-29: TEST/DIAGNOSTIC sandbox seam (env-gated; UNSET = identity, i.e. exactly today's behaviour).
113
+ // When `V5_TEX_ROOTS_SANDBOX` is set, every known-install TeX candidate is REBASED under that directory
114
+ // (same relative tail) so a suite can pin its "no LaTeX installed here" premise on ANY host: the
115
+ // candidates are still probed ONE BY ONE (triedPaths stays named, TEX_CANDIDATE_CAP still bounds them,
116
+ // still no whole-drive scan) but a real install is never touched. It is not a product switch: it only
117
+ // relocates where the documented roots are looked for, and it is never set by the plugin itself.
118
+ const texSandbox = kind === 'tex' ? String(env.V5_TEX_ROOTS_SANDBOX || '').trim() : ''
119
+ const texProbeBase = (p) => {
120
+ if (!texSandbox) return p
121
+ const tail = String(p).replace(/\\/g, '/').replace(/^[A-Za-z]:/, '').replace(/^\/+/, '')
122
+ return texSandbox.replace(/\\/g, '/').replace(/\/+$/, '') + '/' + tail
123
+ }
78
124
  const knownLean = win
79
125
  ? [elan ? elan + '/bin' : null, home + '/.elan/bin', local ? local + '/Programs/lean' : null, pf + '/lean/bin']
80
126
  : [elan ? elan + '/bin' : null, home + '/.elan/bin', home + '/.local/bin', '/usr/local/bin', '/opt/lean/bin']
127
+ // task-28: the DRIVE-ROOT TeX Live positions come first on Windows (the observed real-host layout);
128
+ // the legacy entries keep their relative order. Unix keeps the legacy order and adds the system
129
+ // MacTeX bin dir last (a documented root, not a guess).
81
130
  const knownTex = win
82
- ? [pf + '/texlive', pf86 + '/texlive', local ? local + '/Programs/MiKTeX/miktex/bin/x64' : null, pf + '/MiKTeX/miktex/bin/x64']
83
- : [home + '/Library/TeX/texbin', '/usr/local/texlive', home + '/.TinyTeX/bin', '/opt/texlive']
131
+ ? TEX_DRIVE_YEAR_ROOTS.concat([pf + '/texlive', pf86 + '/texlive', local ? local + '/Programs/MiKTeX/miktex/bin/x64' : null, pf + '/MiKTeX/miktex/bin/x64'])
132
+ : [home + '/Library/TeX/texbin'].concat(TEX_SYSTEM_YEAR_ROOTS, [home + '/.TinyTeX/bin'], TEX_SYSTEM_BIN_ROOTS)
84
133
  const list = kind === 'tex' ? knownTex : knownLean
85
134
  // 1) EXPLICIT: only it, never a guess.
86
135
  if (explicit) {
@@ -93,19 +142,35 @@ async function resolveKnownTool(sub, opts) {
93
142
  if (typeof sub.resolveExecutable === 'function') {
94
143
  try { const p = await sub.resolveExecutable(name); if (p) return { exe: String(p), via: 'path', tried, name } } catch (e) { /* PATH miss is expected */ }
95
144
  }
96
- // 3) KNOWN INSTALL LOCATIONS (Windows TeX roots are globbed per release: <root>/<year>/bin/windows).
145
+ // 3) KNOWN INSTALL LOCATIONS. A TeX root WITHOUT a `bin` segment is year-globbed
146
+ // (`<root>/<year>/bin/<platform>`): the years actually on disk (newest first) first, then the
147
+ // recent-year window, at most TEX_YEAR_LOOKBACK of them. task-28: the whole stage is BOUNDED by
148
+ // TEX_CANDIDATE_CAP candidate paths — documented roots, never a whole-drive scan.
149
+ const texYearSubdirs = win ? ['bin/windows'] : ['bin/x86_64-linux', 'bin/aarch64-linux', 'bin/universal-darwin', 'bin']
150
+ const texRecentYears = () => { const now = new Date().getFullYear(); const out = []; for (let i = 0; i < TEX_YEAR_LOOKBACK; i++) out.push(String(now - i)); return out }
151
+ let texProbes = 0
97
152
  for (const base of list) {
98
153
  if (!base) continue
99
154
  if (kind === 'tex' && !/bin/i.test(base)) {
100
- for (const year of await listDirs(base)) {
101
- for (const sub2 of ['bin/windows', 'bin/x86_64-linux', 'bin/universal-darwin', 'bin']) {
102
- const p = await resolvePath(base + '/' + year + '/' + sub2 + '/' + name)
155
+ const onDisk = (await listDirs(texProbeBase(base))).filter((d) => /^\d{4}$/.test(d)).sort((a, b) => Number(b) - Number(a))
156
+ const years = onDisk.concat(texRecentYears().filter((y) => onDisk.indexOf(y) < 0)).slice(0, TEX_YEAR_LOOKBACK)
157
+ for (const year of years) {
158
+ for (const sub2 of texYearSubdirs) {
159
+ if (texProbes >= TEX_CANDIDATE_CAP) break
160
+ texProbes++
161
+ const p = await resolvePath(texProbeBase(base) + '/' + year + '/' + sub2 + '/' + name)
103
162
  if (p) return { exe: p, via: 'known-install', tried, name }
104
163
  }
164
+ if (texProbes >= TEX_CANDIDATE_CAP) break
105
165
  }
166
+ if (texProbes >= TEX_CANDIDATE_CAP) break
106
167
  continue
107
168
  }
108
- const p = await resolvePath(base + '/' + name)
169
+ if (kind === 'tex') {
170
+ if (texProbes >= TEX_CANDIDATE_CAP) break
171
+ texProbes++
172
+ }
173
+ const p = await resolvePath(texProbeBase(base) + '/' + name)
109
174
  if (p) return { exe: p, via: 'known-install', tried, name }
110
175
  }
111
176
  return { exe: null, via: null, tried, name, reason: 'not found on PATH or in the known install locations' }
@@ -188,15 +253,21 @@ const PAPER_SECTIONS = [
188
253
  // the model compete for the same CPU, and the parameter surface deliberately gains only
189
254
  // `leanAsync` (the per-compile budget reuses `leanTimeoutMs`).
190
255
  const LEAN_QUEUE_CONCURRENCY = 1
191
- // ONE place for the flag spelling: a toolchain that does not know `--search-path` only needs
192
- // this constant changed (never the interface).
193
- const LEAN_SEARCH_PATH_FLAG = '--search-path'
256
+ // task-27 (real-host A round + local reproduction): Lean 4 accepts `-R`/`--root` (or LEAN_PATH); `--search-path`
257
+ // is the **Lean 3** spelling and makes `lean` fail at ARGUMENT PARSING (rc=1) before it reads the file — so every
258
+ // compile failed by default. Local check on Lean 4.34.0: `lean --search-path <dir> <f>` ⇒ rc=1; `lean -R <dir> <f>`
259
+ // ⇒ accepted (the compiler then reads the file). ONE place for the flag spelling: change this constant only.
260
+ const LEAN_SEARCH_PATH_FLAG = '-R'
194
261
  const LEAN_READ_MAX_BYTES = 64 * 1024
195
262
  // A user-supplied search root wins: never inject a second one (explicit override first).
196
263
  function leanHasSearchFlag(args) {
264
+ // task-27: the flag we INJECT is `-R` (Lean 4). A user may still state any of these in `leanArgs` — the legacy
265
+ // Lean-3 spelling included — and this guard's only job is "never inject a SECOND search root".
266
+ const ALSO_ACCEPTED = ['--search-path', '--root']
197
267
  return (Array.isArray(args) ? args : []).some((a) => {
198
268
  const t = String(a)
199
- return t === LEAN_SEARCH_PATH_FLAG || t.indexOf(LEAN_SEARCH_PATH_FLAG + '=') === 0 || t === '-R' || t === '--root'
269
+ if (t === LEAN_SEARCH_PATH_FLAG || t.indexOf(LEAN_SEARCH_PATH_FLAG + '=') === 0) return true
270
+ return ALSO_ACCEPTED.some((f) => t === f || t.indexOf(f + '=') === 0)
200
271
  })
201
272
  }
202
273
  // Content identity for queue dedupe: CRLF normalised, trailing blank lines dropped.
@@ -413,6 +484,9 @@ export function apply(ctx) {
413
484
  // The final-paper flow (stage machine + its artifacts state). Durable like everything
414
485
  // else, so a restart resumes the same stage instead of writing a second paper.
415
486
  paper: null,
487
+ // Methodology/collaboration feedback (Shared/Feedback/). Durable like `verdicts`, so a restart
488
+ // keeps the loop closed instead of losing what the institute learned about working together.
489
+ feedback: [],
416
490
  }
417
491
  }
418
492
  // LOW (deep review): `state.order` used to be maintained here and preserved in the diagnostics
@@ -509,6 +583,15 @@ export function apply(ctx) {
509
583
  : patch.paper
510
584
  if (next !== undefined) n.paper = next
511
585
  }
586
+ // Durable methodology/collaboration feedback (Shared/Feedback/). A FUNCTION is a mutation
587
+ // applied INSIDE the fold (the same trick `paper` uses): several members may add or update
588
+ // entries between two commits, and a read-modify-write outside the fold would lose one.
589
+ if (patch.feedback !== undefined) {
590
+ const next = typeof patch.feedback === 'function'
591
+ ? patch.feedback(Array.isArray(n.feedback) ? n.feedback : [])
592
+ : patch.feedback
593
+ if (next !== undefined) n.feedback = Array.isArray(next) ? next : []
594
+ }
512
595
  return n
513
596
  })
514
597
  }
@@ -911,8 +994,8 @@ export function apply(ctx) {
911
994
  // today's behaviour (the reminder rides with formalVerify); 'eager' = push valuable small
912
995
  // lemmas/propositions/definitions into the library even in daily rounds.
913
996
  leanInitiative: 'normal',
914
- // leanSearchPaths: extra `--search-path` roots injected BEFORE the automatic VibeMath
915
- // root (deduped; an explicit --search-path/-R/--root in leanArgs wins).
997
+ // leanSearchPaths: extra `-R` roots injected BEFORE the automatic VibeMath
998
+ // root (deduped; an explicit -R/--root in leanArgs wins).
916
999
  leanSearchPaths: [],
917
1000
  // leanJobsMaxParallel: how many background compiles may run at once (default 1 = serial).
918
1001
  leanJobsMaxParallel: 1,
@@ -948,6 +1031,12 @@ export function apply(ctx) {
948
1031
  paperCompilePdf: true,
949
1032
  paperEditor: 'academician',
950
1033
  paperLatexCommand: '',
1034
+ // ── methodology / collaboration feedback (Shared/Feedback/) ──
1035
+ // feedback — 'on' (default: the tool works AND the short per-round hint is injected) |
1036
+ // 'off' (the hint is NOT injected and every write is refused BY NAME, never
1037
+ // silently dropped). It is a switch, not a severity: recording is encouraged,
1038
+ // never mandatory.
1039
+ feedback: 'on',
951
1040
  // ── model / tools ────────────────────────────────────────────────────
952
1041
  provider: '',
953
1042
  model: '',
@@ -2007,7 +2096,7 @@ export function apply(ctx) {
2007
2096
  // someone who never asked.
2008
2097
  if (m.kind === 'assign') return (m.from === 'office' ? '【所办分派】' : '【院士分派】') + m.text
2009
2098
  if (m.kind === 'nudge') return '【督办 from ' + m.from + '】' + m.text
2010
- if (m.kind === 'notice') return '【框架提示】' + m.text
2099
+ if (m.kind === 'notice') return '【研究所提示】' + m.text
2011
2100
  return '【研究所·' + m.kind + ' from ' + m.from + '】' + m.text
2012
2101
  }
2013
2102
  // Batch plain chat so a chatty institute cannot cause a wake storm; anything
@@ -2436,6 +2525,7 @@ export function apply(ctx) {
2436
2525
  if (member.direction && !resume) { L.push('给你的起点方向:' + member.direction); L.push('') }
2437
2526
  mathPushLine(L)
2438
2527
  paperPushLine(L)
2528
+ feedbackPushLine(L)
2439
2529
  L.push('------------')
2440
2530
  L.push(stateBlock(member, roundNo))
2441
2531
  L.push('------------')
@@ -2456,6 +2546,7 @@ export function apply(ctx) {
2456
2546
  if (leanDailyOn()) { L.push(''); L.push(formalWorkLine()) }
2457
2547
  mathPushLine(L)
2458
2548
  paperPushLine(L)
2549
+ feedbackPushLine(L)
2459
2550
  L.push('')
2460
2551
  L.push('------------')
2461
2552
  L.push(stateBlock(member))
@@ -2474,6 +2565,7 @@ export function apply(ctx) {
2474
2565
  if (leanDailyOn()) { L.push(''); L.push(formalWorkLine()) }
2475
2566
  mathPushLine(L)
2476
2567
  paperPushLine(L)
2568
+ feedbackPushLine(L)
2477
2569
  L.push('')
2478
2570
  L.push('------------')
2479
2571
  L.push(stateBlock(member))
@@ -2510,6 +2602,7 @@ export function apply(ctx) {
2510
2602
  L.push('一致认为是真时,本所才会停下来。')
2511
2603
  mathPushLine(L)
2512
2604
  paperPushLine(L)
2605
+ feedbackPushLine(L)
2513
2606
  L.push('')
2514
2607
  L.push('------------')
2515
2608
  L.push(stateBlock(member))
@@ -2523,7 +2616,12 @@ export function apply(ctx) {
2523
2616
  L.push('')
2524
2617
  L.push('本所正在对下列对象发起共识验证:')
2525
2618
  L.push(' 对象:' + vs.target + '(类型:' + kindLabel2(vs.kind) + ')')
2526
- if (vs.statement) L.push(' 陈述:' + String(vs.statement).slice(0, 800))
2619
+ if (vs.statement) {
2620
+ // task-24: this line feeds the member prompt. Cutting it with a bare `.slice(0, 800)` hid the tail of a
2621
+ // long statement with no trace; make the cut visible instead (the full text stays in the source card).
2622
+ const _st = String(vs.statement)
2623
+ L.push(' 陈述:' + (_st.length > 800 ? _st.slice(0, 800) + '…(已截断;完整陈述见源卡片 ' + String(vs.rel || vs.target || '') + ')' : _st))
2624
+ }
2527
2625
  L.push('')
2528
2626
  L.push('请给出你**诚实独立的判断**:')
2529
2627
  L.push(' verdict = 1 表示你认为该对象**绝对为真**;')
@@ -2556,6 +2654,7 @@ export function apply(ctx) {
2556
2654
  L.push('本所宁可留下未定论,也不要一个骗人的结论。')
2557
2655
  mathPushLine(L)
2558
2656
  paperPushLine(L)
2657
+ feedbackPushLine(L)
2559
2658
  L.push('')
2560
2659
  L.push('------------')
2561
2660
  L.push(stateBlock(member))
@@ -2963,7 +3062,7 @@ export function apply(ctx) {
2963
3062
  for (const j of leanJobs.values()) if (j.state === 'queued' && (!best || (j.enqueuedAt || 0) < (best.enqueuedAt || 0))) best = j
2964
3063
  return best
2965
3064
  }
2966
- // The absolute VibeMath root handed to `--search-path` (the compile runs with cwd=instRoot()).
3065
+ // The absolute VibeMath root handed to `-R` (the compile runs with cwd=instRoot()).
2967
3066
  function leanSearchRootView() { return vibeRoot().replace(/\\/g, '/') }
2968
3067
  async function leanSearchRoot() {
2969
3068
  const root = vibeRoot()
@@ -3536,14 +3635,194 @@ export function apply(ctx) {
3536
3635
  // is the "off means zero mention" discipline).
3537
3636
  function mathPushLine(L) { const b = mathPromptBlock('zh'); if (b) L.push(b) }
3538
3637
 
3539
- // task-9 2b: the LaTeX-missing guidance (four bounded steps + three hard boundaries). Kept
3540
- // BYTE-IDENTICAL with the other presets; per-preset voice may only follow this block.
3638
+ // task-9 2b: the LaTeX-missing guidance (four bounded steps + three hard boundaries). Those four
3639
+ // steps and three boundaries are the shared contract; task-28 (v5 PILOT) additionally names the
3640
+ // "installed but NOT on PATH" case and TeX Live's documented positions — v4/v2/v3 adopt the same
3641
+ // text once v5 is verified, so the wording deliberately leads on v5.
3541
3642
  function paperHintBlock(lang) {
3542
3643
  return lang === 'en'
3543
- ? '\nWhen no LaTeX engine is detected: (1) probe only the documented common TeX roots and PATH (e.g. where xelatex, latexmk --version) - never scan whole drives; (2) once the absolute path is found, write it into paperLatexCommand, re-detect, then continue; (3) if it is still missing, ask the user once (installing TeX requires the user\'s explicit approval); (4) with no answer, degrade exactly as today (deliver paper.tex and paper.md only). Hard boundaries: never auto-install; never write outside the workspace; never treat "not detected" as a failure.'
3544
- : '\n检测不到 LaTeX 引擎时:① 只在文档化的常见 TeX 根与 PATH 上做有界核查(如 where xelatex、latexmk --version),不要全盘扫描;② 找到绝对路径后写入 paperLatexCommand 并重新检测,再继续;③ 仍找不到就向用户问一次(安装 TeX 需用户明确同意);④ 无应答则照旧降级(只交付 paper.tex 与 paper.md)。硬边界:绝不自动安装;绝不写工作区之外;绝不把"未检测到"当失败。'
3644
+ ? '\nWhen no LaTeX engine is detected: (1) probe only the documented common TeX roots and PATH (e.g. where xelatex, latexmk --version) - never scan whole drives; the MOST COMMON case is an engine that IS installed but NOT on PATH — a DRIVE-ROOT Windows TeX Live lives under `texlive\\<year>\\bin\\windows` on that drive, Unix under the standard system paths `/usr/local/texlive/<year>/bin/*` or `/opt/texlive/<year>/bin/*`, macOS at `/Library/TeX/texbin`; (2) once the absolute path is found, write it into paperLatexCommand, re-detect, then continue; (3) if it is still missing, REPORT IT TO THE OFFICE (or the group chat) and let the OFFICE confirm with the user (installing TeX requires the user\'s explicit approval); (4) with no answer yet, degrade exactly as today (deliver paper.tex and paper.md only). Hard boundaries: never auto-install; never write outside the workspace; never treat "not detected" as a failure.'
3645
+ : '\n检测不到 LaTeX 引擎时:① 只在文档化的常见 TeX 根与 PATH 上做有界核查(如 where xelatex、latexmk --version),不要全盘扫描——**最常见的情形是引擎装了但不在 PATH**:TeX Live 若装在 Windows 的某个**盘根**下,看该盘根里的 `texlive\\<年份>\\bin\\windows`;类 Unix 看标准系统路径 `/usr/local/texlive/<年份>/bin/*`、`/opt/texlive/<年份>/bin/*`;macOS 看 `/Library/TeX/texbin`;② 找到绝对路径后写入 paperLatexCommand 并重新检测,再继续;③ 仍找不到就**如实上报所办(或群聊)**,由**所办**向用户确认(安装 TeX 需用户明确同意);④ 尚无回应则照旧降级(只交付 paper.tex 与 paper.md)。硬边界:绝不自动安装;绝不写工作区之外;绝不把"未检测到"当失败。'
3646
+ }
3647
+ function paperPushLine(L) {
3648
+ const b = paperHintBlock('zh')
3649
+ // Iron-rules fix (P4): this block only matters while the member is writing/compiling the paper, yet it was
3650
+ // pushed into EVERY wake — founding, verify and meeting prompts included — where it reads as unexplained
3651
+ // noise ("why is LaTeX guidance in my first round?"). Keep the guidance (the paper builders do NOT emit it,
3652
+ // so removing it here would lose it entirely) but SAY WHEN IT APPLIES. The block itself stays byte-identical
3653
+ // across presets; only this per-preset wrapper changes.
3654
+ if (b) L.push('\n(**以下这段仅在你参与论文写作或编译时适用**;其他阶段可忽略。)' + b)
3655
+ }
3656
+
3657
+ // ---- feedback library (Shared/Feedback/): methodology / collaboration, never research -----------
3658
+ // Entries live in the DURABLE `feedback` array and are written through the fold (patchInstitute with
3659
+ // an UPDATER), so two concurrent adds/updates cannot lose one. The human-readable mirror (one file per
3660
+ // category + a JSON index) is WRITE-ONLY like Shared/TaskBoard.md; the state file stays authoritative.
3661
+ function feedbackOn() { return String(params.feedback || 'on') !== 'off' }
3662
+ function feedbackEntries() { const s = inst(); if (!Array.isArray(s.feedback)) s.feedback = []; return s.feedback }
3663
+ function feedbackCounts() {
3664
+ const all = feedbackEntries()
3665
+ const byCategory = {}, byRoute = {}
3666
+ for (const c of FEEDBACK_CATEGORIES) byCategory[c] = 0
3667
+ for (const r of FEEDBACK_ROUTES) byRoute[r] = 0
3668
+ let open = 0
3669
+ for (const e of all) {
3670
+ if (byCategory[e.category] !== undefined) byCategory[e.category] += 1
3671
+ if (byRoute[e.route] !== undefined) byRoute[e.route] += 1
3672
+ if (FEEDBACK_OPEN_STATUSES.indexOf(e.status) !== -1) open += 1
3673
+ }
3674
+ return { total: all.length, open, closed: all.length - open, byCategory, byRoute }
3675
+ }
3676
+ function feedbackView(e) {
3677
+ return { id: e.id, at: e.at, by: e.by, category: e.category, route: e.route, status: e.status,
3678
+ context: e.context || '', phenomenon: e.phenomenon || '', impact: e.impact || '',
3679
+ action: e.action || '', assessment: e.assessment || '', outcome: e.outcome || '',
3680
+ note: e.note || '', updatedAt: e.updatedAt || '', updatedBy: e.updatedBy || '' }
3681
+ }
3682
+ /** The human-readable mirror + structured index. A failure is REPORTED (mirror:false), never thrown
3683
+ * and never logged: it must not disturb the existing write-failure accounting (F6). */
3684
+ async function feedbackMirror() {
3685
+ const all = feedbackEntries()
3686
+ let ok = true
3687
+ for (const c of FEEDBACK_CATEGORIES) {
3688
+ const rows = all.filter((e) => e.category === c)
3689
+ const md = ['# 反馈库 · ' + FEEDBACK_CATEGORY_LABELS[c] + '(' + c + ')', '',
3690
+ '> 方法论/协作层记录:怎么一起工作(组织、流程、合作、障碍、摩擦)。**不是研究结论**——研究进展见 Progress/,已确立结论见 Verified/。', '']
3691
+ for (const e of rows) {
3692
+ md.push('## ' + e.id + '|' + e.route + '|' + e.status + '|' + (e.at || ''), '',
3693
+ '- 发起人:' + (e.by || '') + '|类别:' + c + '|路由:' + e.route + '|状态:' + e.status,
3694
+ '- 情境:' + (e.context || '(未写)'),
3695
+ '- 现象:' + (e.phenomenon || ''),
3696
+ '- 影响:' + (e.impact || ''),
3697
+ '- 已做的调整或建议:' + (e.action || ''))
3698
+ if (e.route === 'interpersonal') md.push('- 评估(要不要改/改了是否真更优):' + (e.assessment || ''))
3699
+ if (e.outcome) md.push('- 事后验证/结果:' + e.outcome)
3700
+ if (e.note) md.push('- 备注:' + e.note)
3701
+ if (e.updatedAt) md.push('- 最近更新:' + e.updatedAt + '(' + (e.updatedBy || '') + ')')
3702
+ md.push('')
3703
+ }
3704
+ if (!rows.length) md.push('(本类暂无条目)', '')
3705
+ try { if (!(await writeTextRel(FEEDBACK_DIR + '/' + c + '.md', md.join('\n')))) ok = false } catch (err) { ok = false }
3706
+ }
3707
+ try {
3708
+ if (!(await writeTextRel(FEEDBACK_DIR + '/index.json', JSON.stringify({ counts: feedbackCounts(), entries: all }, null, 2) + '\n'))) ok = false
3709
+ } catch (err) { ok = false }
3710
+ return ok
3711
+ }
3712
+ async function feedbackTool(caller, a) {
3713
+ const op = String((a && a.op) || '').trim()
3714
+ // `off` is NEVER a silent write: the call is refused BY NAME with the reason and the remedy.
3715
+ if (!feedbackOn()) {
3716
+ return { ok: false, code: 'V5_FEEDBACK_DISABLED', message: FEEDBACK_DISABLED_MESSAGE,
3717
+ next: { kind: 'note', tool: 'vibe_v5_set', hint: '由所办把 feedback 设回 \'on\' 后重试(本条没有被静默写入)' } }
3718
+ }
3719
+ if (op === 'add') {
3720
+ const category = String(a.category || '').trim()
3721
+ const route = String(a.route || '').trim()
3722
+ if (FEEDBACK_CATEGORIES.indexOf(category) < 0) {
3723
+ return { ok: false, code: 'V5_FEEDBACK_BAD_CATEGORY', message: 'category 必须是 ' + FEEDBACK_CATEGORIES.join('|'),
3724
+ next: { kind: 'note', hint: '合作=cooperation|管理=management|流程=process|障碍=obstacle|矛盾=conflict' } }
3725
+ }
3726
+ if (FEEDBACK_ROUTES.indexOf(route) < 0) {
3727
+ return { ok: false, code: 'V5_FEEDBACK_BAD_ROUTE', message: 'route 必须是 ' + FEEDBACK_ROUTES.join('|'),
3728
+ next: { kind: 'note', hint: 'self=自己的做法(自己调整即可)|team=组织/工作流(自我调节,无需审批)|interpersonal=别人造成的、要别人改变(须评估+事后回填)' } }
3729
+ }
3730
+ const phenomenon = String(a.phenomenon || '').trim()
3731
+ const impact = String(a.impact || '').trim()
3732
+ const action = String(a.action || '').trim()
3733
+ if (!phenomenon || !impact || !action) {
3734
+ return { ok: false, code: 'V5_FEEDBACK_INCOMPLETE', message: 'add 需要 phenomenon/impact/action(现象、影响、已做的调整或建议)',
3735
+ next: { kind: 'note', hint: '写清"看到了什么/造成什么/我已经或打算怎么调整"' } }
3736
+ }
3737
+ const assessment = String(a.assessment || '').trim()
3738
+ // ONLY the interpersonal route gates on the careful evaluation: self/team are self-regulation
3739
+ // (the user ruling: no approval step, adjust as soon as it looks wrong).
3740
+ if (route === 'interpersonal' && !assessment) {
3741
+ return { ok: false, code: 'V5_FEEDBACK_NEEDS_ASSESSMENT', message: 'interpersonal 路由必须带 assessment:先想清楚"要不要让别人改、改了是否真的更优"',
3742
+ next: { kind: 'note', hint: '若发现只能靠自己调整,请改记 self;确实需要别人改变时,写完评估再跟进事后验证' } }
3743
+ }
3744
+ const base = { at: now(), by: caller, category, route, status: 'open',
3745
+ context: String(a.context || ''), phenomenon, impact, action, assessment,
3746
+ outcome: '', note: String(a.note || ''), updatedAt: '', updatedBy: '' }
3747
+ let entry = null
3748
+ await patchInstitute({ feedback: (cur) => {
3749
+ const list = Array.isArray(cur) ? cur.slice() : []
3750
+ entry = Object.assign({ id: 'fb-' + (list.length + 1) }, base)
3751
+ list.push(entry)
3752
+ return list
3753
+ } })
3754
+ const mirror = await feedbackMirror()
3755
+ return { ok: true, entry: feedbackView(entry), mirror, counts: feedbackCounts(),
3756
+ routeHint: route === 'interpersonal'
3757
+ ? 'interpersonal:已记录。请按评估结论处理,并用 update 回填 outcome(事后验证)才算闭环。'
3758
+ : 'self/team:无需任何人采纳——自己调整即可,并用 update 把状态/结果记下来(self/team 的记录本身就是闭环凭据)。' }
3759
+ }
3760
+ if (op === 'update') {
3761
+ const id = String(a.id || '').trim()
3762
+ if (!id) return { ok: false, code: 'V5_FEEDBACK_NO_ID', message: 'update 需要 id', next: { kind: 'note', hint: '先用 op=list(默认只看未闭环)确认 id(形如 fb-1)' } }
3763
+ const cur = feedbackEntries().find((x) => x.id === id)
3764
+ if (!cur) return { ok: false, code: 'V5_FEEDBACK_NOT_FOUND', message: '没有条目 ' + id, next: { kind: 'note', hint: '先用 op=list 确认 id' } }
3765
+ // Permission: the INITIATOR updates their own entry; the OFFICE may update any (mark it resolved /
3766
+ // write the result back). Anyone else is refused BY NAME — never silently ignored.
3767
+ if (caller !== 'office' && caller !== cur.by) {
3768
+ return { ok: false, code: 'V5_FEEDBACK_FORBIDDEN', message: '只有发起人(' + cur.by + ')或所办可以更新 ' + id,
3769
+ next: { kind: 'note', hint: '别人的问题请自己另记一条(你只能更新自己发起的条目)' } }
3770
+ }
3771
+ const status = a.status === undefined ? '' : String(a.status).trim()
3772
+ if (status && FEEDBACK_STATUSES.indexOf(status) < 0) {
3773
+ return { ok: false, code: 'V5_FEEDBACK_BAD_STATUS', message: 'status 必须是 ' + FEEDBACK_STATUSES.join('|'),
3774
+ next: { kind: 'note', hint: 'open=未处理|adjusted=已自我调整|closed=已回填结果|dropped=评估后决定不改(需理由)' } }
3775
+ }
3776
+ const outcome = a.outcome === undefined ? '' : String(a.outcome).trim()
3777
+ const note = a.note === undefined ? '' : String(a.note).trim()
3778
+ // The interpersonal route is the ONLY one that must PROVE the change helped: closing it without
3779
+ // the written-back verification is refused (that is exactly why the route exists).
3780
+ if (cur.route === 'interpersonal' && status === 'closed' && !outcome) {
3781
+ return { ok: false, code: 'V5_FEEDBACK_NEEDS_OUTCOME', message: 'interpersonal 条目闭环必须回填 outcome(事后验证:改了什么、结果如何)',
3782
+ next: { kind: 'note', hint: '若评估结论是"不需要别人改",请用 status=dropped + note 写明理由' } }
3783
+ }
3784
+ if (status === 'dropped' && !outcome && !note) {
3785
+ return { ok: false, code: 'V5_FEEDBACK_NEEDS_REASON', message: 'status=dropped 需要 note 或 outcome 说明"为什么不改"',
3786
+ next: { kind: 'note', hint: '评估结论也要留痕,否则下一轮会重新提同一件事' } }
3787
+ }
3788
+ const changes = { updatedAt: now(), updatedBy: caller }
3789
+ if (status) changes.status = status
3790
+ if (outcome) changes.outcome = outcome
3791
+ if (a.action !== undefined) changes.action = String(a.action)
3792
+ if (a.assessment !== undefined) changes.assessment = String(a.assessment)
3793
+ if (a.note !== undefined) changes.note = note
3794
+ await patchInstitute({ feedback: (list) => (Array.isArray(list) ? list : []).map((x) => (x.id === id ? Object.assign({}, x, changes) : x)) })
3795
+ const after = feedbackEntries().find((x) => x.id === id) || cur
3796
+ const mirror = await feedbackMirror()
3797
+ return { ok: true, entry: feedbackView(after), mirror, counts: feedbackCounts() }
3798
+ }
3799
+ if (op === 'list') {
3800
+ const category = a.category === undefined ? '' : String(a.category).trim()
3801
+ const route = a.route === undefined ? '' : String(a.route).trim()
3802
+ const all = a.all === true
3803
+ const rows = feedbackEntries()
3804
+ .filter((e) => (!category || e.category === category) && (!route || e.route === route))
3805
+ .filter((e) => all || FEEDBACK_OPEN_STATUSES.indexOf(e.status) !== -1)
3806
+ .map(feedbackView)
3807
+ return { ok: true, openOnly: !all, count: rows.length, entries: rows, counts: feedbackCounts(),
3808
+ note: all ? '全部条目(含已闭环)' : '默认只列未闭环(open|adjusted);要看全部用 all:true' }
3809
+ }
3810
+ if (op === 'summary') return { ok: true, counts: feedbackCounts() }
3811
+ return { ok: false, code: 'V5_FEEDBACK_BAD_OP', message: 'op 必须是 add|update|list|summary',
3812
+ next: { kind: 'note', hint: 'add=写一条;update=状态流转+回填;list=看未闭环;summary=按类别/路由计数' } }
3813
+ }
3814
+ /** The per-round line. Deliberately SHORT (P4's lesson: every wake already carries the tool/paper
3815
+ * tails) and it answers WHY / WHAT / the three routes / what happens next, so recording is explained
3816
+ * rather than commanded. */
3817
+ function feedbackPushLine(L) {
3818
+ if (!feedbackOn()) return
3819
+ L.push(''
3820
+ + '\n【工作经验/流程反馈(鼓励积极记录)】' + FEEDBACK_DIR + '/'
3821
+ + '\n· 为什么记:怎么一起工作(组织、流程、合作、障碍、摩擦)属于**方法论层**的自我调节;研究结论请进 Progress/ 与 Verified/,这里不记结论。'
3822
+ + '\n· 记什么:类别(合作/管理/流程/障碍/矛盾)+路由+情境/现象/影响+你**已做的调整或建议**;写到多细由你把控。'
3823
+ + '\n· 三条路由:self=你自己的做法⇒**自己调整,无需谁采纳**;team=组织/工作流/团队运行的调整⇒**同样无需审批**,觉得不好就及时调整;interpersonal=**别人造成的、要别人改变才能解决**⇒只有这条要先**缜密评估**"要不要改、改了是否真更优",并**事后回填验证**。'
3824
+ + '\n· 记了之后:未闭环条目会出现在汇报与总览的计数里;用 vibe_v5_feedback 的 update 回填结果即闭环(interpersonal 必须回填 outcome)。')
3545
3825
  }
3546
- function paperPushLine(L) { const b = paperHintBlock('zh'); if (b) L.push(b) }
3547
3826
 
3548
3827
  function formalPromptBlock(target) {
3549
3828
  if (!formalOn()) return ''
@@ -4168,7 +4447,7 @@ export function apply(ctx) {
4168
4447
  const expected = Number(args.expected_revision !== undefined ? args.expected_revision : args.expectedRevision)
4169
4448
  if (!Number.isFinite(expected)) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'expected_revision is required' }
4170
4449
  if (expected !== task.revision) {
4171
- // F6 (deep-review 5): this message is routed into Chinese frames (`【框架提示】…`), where a
4450
+ // F6 (deep-review 5): this message is routed into Chinese frames (`【研究所提示】…`), where a
4172
4451
  // raw English diagnostic reads as noise. The CODE stays (`V5_TASK_STALE_REVISION`) and the
4173
4452
  // revision numbers stay, but the sentence is Chinese now for the member reading it.
4174
4453
  return { ok: false, code: 'V5_TASK_STALE_REVISION', message: '任务 ' + id + ' 当前 revision 是 ' + task.revision + ',不是 ' + expected + ' —— 请先用 vibe_v5_task_get 重新读取(code: V5_TASK_STALE_REVISION)' }
@@ -4445,7 +4724,17 @@ export function apply(ctx) {
4445
4724
  if (!rel) return { rel: null, statement: '' }
4446
4725
  const text = (await readTextRel(rel)) || ''
4447
4726
  const m = /##\s*陈述\s*\n([\s\S]*?)(?:\n##\s|$)/.exec(text)
4448
- return { rel, statement: String(m ? m[1] : text).trim().slice(0, 1200) }
4727
+ const full = String(m ? m[1] : text).trim()
4728
+ // task-24 (real-host R2, D5'): this used to be `.slice(0, 1200)` with NO marker, so a long statement was
4729
+ // cut silently — and the cut text is exactly what voters read and what the verified record carries
4730
+ // (a real host registered statements >1200 chars). Keep a cap as generous as the paper sections
4731
+ // (20000) and, when a cut DOES happen, say so in-band so the reader knows to open the source file.
4732
+ const cap = 20000
4733
+ if (full.length <= cap) return { rel, statement: full }
4734
+ return {
4735
+ rel, truncated: true,
4736
+ statement: full.slice(0, cap) + '\n(注意:陈述超过 ' + cap + ' 字符已截断,完整文本见 ' + rel + ')',
4737
+ }
4449
4738
  }
4450
4739
  // Update one `- 字段:` of a source card. Members hand-write cards in two shapes —
4451
4740
  // one field per line, or one line with '; '-separated fields — so the anchor may
@@ -4881,7 +5170,9 @@ export function apply(ctx) {
4881
5170
  const allVoted = need.length > 0 && need.every((id) => votes[id])
4882
5171
  if (allVoted) await continueVerifyRound(next)
4883
5172
  else await scheduleNext()
4884
- return { ok: true, voted: memberId, verdict: p, allVoted }
5173
+ // task-25 (real-host R2): an incomplete electorate used to be reported only as `allVoted:false`, so the
5174
+ // caller could not tell WHICH voter was still missing (members probed repeatedly to find out). Name them.
5175
+ return { ok: true, voted: memberId, verdict: p, allVoted, pendingVoters: need.filter((id) => !votes[id]) }
4885
5176
  }
4886
5177
 
4887
5178
  // ---- chat log / meeting plumbing --------------------------------------
@@ -5040,7 +5331,13 @@ export function apply(ctx) {
5040
5331
  // without a heartbeat nothing would retry it once the lock clears.
5041
5332
  if (finalizeLock) { armHeartbeat(); return }
5042
5333
  const stale = now() - Number(meeting.lastInputAt || meeting.startedAt || now())
5043
- if (stale >= recoverStallMs()) {
5334
+ // real1004-stall: a member's TURN can outlive the stall budget on a real host, and the old check ignored
5335
+ // that — the meeting was abandoned while an ASKED member was still busy, so its speech could only land as
5336
+ // a "late note" (measured on one real run: 1067 late notes and 863 refused office finalisations). Never
5337
+ // abandon while an asked speaker is in flight — but keep the watchdog's teeth: past 3× the budget even an
5338
+ // in-flight member no longer holds the meeting open (a genuinely wedged turn must not wedge the meeting).
5339
+ const inFlight = meeting.order.some((id) => busy.has(id))
5340
+ if (stale >= recoverStallMs() && (!inFlight || stale >= recoverStallMs() * 3)) {
5044
5341
  const abandoned = meeting
5045
5342
  meeting = null
5046
5343
  await appendMeetingTail(abandoned, '⚠ 本次会议因长时间无新发言而被放弃(看门狗);团队回到自组织推进。')
@@ -6109,7 +6406,7 @@ export function apply(ctx) {
6109
6406
  if (await fileExistsAbs(paperAbs(id, 'paper.pdf'))) files.push('paper.pdf')
6110
6407
  const warnings = (p.warnings || []).slice()
6111
6408
  if (compile.status === 'failed') warnings.push('LaTeX 编译失败(已保留 paper.tex 与 paper.md,不阻塞定稿):' + (compile.attempts || []).map((a) => a.engine + '/' + a.label + ' exit=' + a.exitCode + (a.message ? '(' + a.message + ')' : '')).join(';'))
6112
- if (compile.status === 'not-detected') warnings.push('未检测到 LaTeX 引擎(' + (compile.reason || '') + '):只交付 paper.tex 与 paper.md。已探测 PATH 与文档化的常见 TeX 根;可用 paperLatexCommand 指定绝对路径。')
6409
+ if (compile.status === 'not-detected') warnings.push('未检测到 LaTeX 引擎(' + (compile.reason || '') + '):只交付 paper.tex 与 paper.md。已探测 PATH 与文档化的常见 TeX 根;可用 paperLatexCommand 指定绝对路径。最常见的情形是引擎已安装但不在 PATH(Windows 盘根下的 texlive\\<年>\\bin\\windows、类 Unix /usr/local|/opt/texlive/<年>/bin/*、macOS /Library/TeX/texbin 已纳入有界探测):把引擎的绝对路径写进 paperLatexCommand 再探测一次;仍找不到就如实上报所办(由所办向用户确认安装),随后照旧安全降级。')
6113
6410
  // F6: the failed REQUIRED writes are named ONCE (for this finalisation), before the meta is built,
6114
6411
  // so the warning lands in `meta.warnings`, in the flow log, in the state and in the return value.
6115
6412
  if (writeFailures.length) warnings.push(paperWriteFailureWarning(writeFailures))
@@ -6178,8 +6475,8 @@ export function apply(ctx) {
6178
6475
  const perCap = Math.max(1, Math.floor(Number(params.maxTempPerMember) || 3))
6179
6476
  const totalCap = Math.max(1, Math.floor(Number(params.maxTempTotal) || 12))
6180
6477
  const mine = employedTemps().filter((m) => m.hiredBy === (office ? 'office' : callerId)).length
6181
- if (mine >= perCap) return { ok: false, code: 'V5_MEMBER_LIMIT', message: '你名下同时最多 ' + perCap + ' 名临时工(先在册 ' + mine + ' 名);请先解雇不再需要的' }
6182
- if (employedTemps().length >= totalCap) return { ok: false, code: 'V5_MEMBER_LIMIT', message: '全所同时在册临时工已达上限 ' + totalCap }
6478
+ if (mine >= perCap) return { ok: false, code: 'V5_MEMBER_LIMIT', scope: 'per-member', message: '你名下同时最多 ' + perCap + ' 名临时工(先在册 ' + mine + ' 名);请先解雇不再需要的' }
6479
+ if (employedTemps().length >= totalCap) return { ok: false, code: 'V5_MEMBER_LIMIT', scope: 'institute', message: '全所同时在册临时工已达上限 ' + totalCap }
6183
6480
  const member = await newMember('temp', {
6184
6481
  direction: purpose, hiredBy: office ? 'office' : callerId, term: String(args.term || ''), provider: pickProvider(),
6185
6482
  })
@@ -6948,7 +7245,7 @@ export function apply(ctx) {
6948
7245
  'compactThreshold', 'compactAfterRounds', 'maxParallel', 'activityTimeoutMs', 'stallAutoMeetingMs',
6949
7246
  'chatDigestMs', 'chatDigestMax', 'meetingKeepEvery', 'leanTimeoutMs']
6950
7247
  const bools = ['academician', 'academicianLeads', 'memberMayRejectAssign', 'finalPaper', 'paperCompilePdf', 'leanAsync']
6951
- const strs = ['quorumMode', 'provider', 'model', 'staffPersona', 'formalVerify', 'leanCommand',
7248
+ const strs = ['feedback', 'quorumMode', 'provider', 'model', 'staffPersona', 'formalVerify', 'leanCommand',
6952
7249
  'paperFormat', 'paperLanguage', 'paperEditor', 'paperLatexCommand', 'leanInitiative',
6953
7250
  'mathComputation', 'mathMode', 'mathInstallScope']
6954
7251
  const arrs = ['toolAllow', 'toolDeny', 'tempToolAllow', 'tempToolDeny', 'leanArgs', 'leanSearchPaths',
@@ -6984,6 +7281,11 @@ export function apply(ctx) {
6984
7281
  if (out.leanInitiative !== undefined) {
6985
7282
  out.leanInitiative = ['off', 'normal', 'eager'].indexOf(String(out.leanInitiative)) !== -1 ? String(out.leanInitiative) : 'normal'
6986
7283
  }
7284
+ // `feedback` is a CLOSED enum too: a typo must keep the documented default ('on') rather than
7285
+ // becoming an unreachable third mode that silently disables the recording hint.
7286
+ if (out.feedback !== undefined) {
7287
+ out.feedback = ['on', 'off'].indexOf(String(out.feedback).trim()) !== -1 ? String(out.feedback).trim() : 'on'
7288
+ }
6987
7289
  // Concurrency floor, same discipline as quorumCap/verdictMaxRounds.
6988
7290
  if (out.leanJobsMaxParallel !== undefined && out.leanJobsMaxParallel < 1) out.leanJobsMaxParallel = 1
6989
7291
  // The six math_computation keys are normalised by the SHARED module (one implementation
@@ -7064,6 +7366,17 @@ export function apply(ctx) {
7064
7366
  async function setParams(input) {
7065
7367
  const droppedSet = []
7066
7368
  const patch = normalizeParams(input || {}, droppedSet)
7369
+ // task-26 (real-host R2): an out-of-range value is coerced by normalizeParams (documented) and an unknown
7370
+ // key is dropped into `droppedSet` (recorded in diagnostics) — but the RESPONSE carried neither, so the
7371
+ // caller could not tell "changed" from "ignored" without a second `status` read. Report both, additively.
7372
+ const adjusted = {}
7373
+ for (const k of Object.keys(patch)) {
7374
+ const raw = (input || {})[k]
7375
+ if (raw === undefined) continue
7376
+ const before = Array.isArray(raw) ? raw.join(',') : String(raw)
7377
+ const after = Array.isArray(patch[k]) ? patch[k].join(',') : String(patch[k])
7378
+ if (before !== after) adjusted[k] = { from: raw, to: patch[k] }
7379
+ }
7067
7380
  reportDroppedStateKeys(inst(), droppedSet, 'set')
7068
7381
  const merged = Object.assign({}, params, patch)
7069
7382
  await patchInstitute({ params: merged })
@@ -7075,7 +7388,7 @@ export function apply(ctx) {
7075
7388
  // activityTimeoutMs (or a raised maxParallel) would not take effect until some
7076
7389
  // unrelated event drove a pass. Tuning must apply immediately.
7077
7390
  if (running && !autoDone) await scheduleNext()
7078
- return { ok: true, params: visibleParams() }
7391
+ return { ok: true, params: visibleParams(), adjusted, dropped: droppedSet.slice() }
7079
7392
  }
7080
7393
  function visibleParams() {
7081
7394
  return {
@@ -7099,6 +7412,8 @@ export function apply(ctx) {
7099
7412
  mathComputation: params.mathComputation, mathMode: params.mathMode,
7100
7413
  mathEngines: (params.mathEngines || []).slice(), mathTimeoutMs: params.mathTimeoutMs,
7101
7414
  mathPackages: (params.mathPackages || []).slice(), mathInstallScope: params.mathInstallScope,
7415
+ // 'on' | 'off' — methodology/collaboration feedback (Shared/Feedback/).
7416
+ feedback: params.feedback,
7102
7417
  // ── final paper ──────────────────────────────────────────────────────
7103
7418
  finalPaper: params.finalPaper, paperFormat: params.paperFormat,
7104
7419
  paperLanguage: params.paperLanguage, paperCompilePdf: params.paperCompilePdf,
@@ -7444,8 +7759,8 @@ export function apply(ctx) {
7444
7759
  // payload, so a reader (or an operator comparing two sessions) cannot mistake them for
7445
7760
  // durable state. Durable = derived from the state file; session = rebuilt on load.
7446
7761
  fieldScopes: {
7447
- session: ['running', 'autoDone(session mirror of phase)', 'leanNotices', 'debug', 'members[].rounds', 'members[].busy', 'members[].contextPct', 'members[].childId', 'meeting', 'parkedMeeting', 'persistence.writeFailures', 'persistence.prematureReads', 'persistence.loadProblem', 'pendingSpawns[].attempts'],
7448
- durable: ['phase', 'runId', 'quorum', 'members[] (except the three session fields)', 'members[].failReason', 'tasks', 'failedMembers', 'pendingSpawns[] (derived from durable failed members)', 'chat', 'officeRequests', 'verify', 'verifyQueue', 'verified', 'undecided', 'solveVotes', 'formal', 'paper', 'lastProgressAt', 'params'],
7762
+ session: ['running', 'autoDone(session mirror of phase)', 'leanNotices', 'debug', 'members[].rounds', 'members[].busy', 'members[].contextPct', 'members[].childId', 'meeting', 'parkedMeeting', 'persistence.writeFailures', 'persistence.prematureReads', 'persistence.loadProblem', 'pendingSpawns[].attempts', 'diagnostics', 'backend'],
7763
+ durable: ['phase', 'runId', 'quorum', 'members[] (except the three session fields)', 'members[].failReason', 'tasks', 'failedMembers', 'pendingSpawns[] (derived from durable failed members)', 'chat', 'officeRequests', 'verify', 'verifyQueue', 'verified', 'verifiedTrue (derived from verdicts)', 'concludedFalse (derived from verdicts)', 'undecided (derived from verdicts)', 'verdicts', 'solve', 'solveVotes', 'formal', 'paper', 'lastProgressAt', 'params'],
7449
7764
  },
7450
7765
  backend: backend ? backend.kind : 'uninitialized',
7451
7766
  // Skipped/malformed events AND state-file load problems. Without this the two
@@ -7688,6 +8003,25 @@ export function apply(ctx) {
7688
8003
  // not a refusal: v5 cannot change the host's fence, and refusing would stop persisting work.
7689
8004
  if (sandboxPolicyMismatch) L.push('- ⚠ 沙箱策略根与会话工作目录不一致:策略=' + sandboxPolicyMismatch.policyRoot + '|会话=' + sandboxPolicyMismatch.expected
7690
8005
  + '(v5 用会话工作目录拼所有路径;若宿主按策略根解析写操作,产物可能落在预期之外的那棵树——请核对 sandbox 策略的 workspaceRoot)')
8006
+ // Methodology/collaboration feedback: the numbers an operator (and the academician's periodic
8007
+ // summary) needs to see whether the loop is actually being USED and closed — not just collected.
8008
+ L.push('')
8009
+ L.push('## 反馈库(方法论/协作层;不是研究结论)')
8010
+ {
8011
+ const fc = feedbackCounts()
8012
+ L.push('- 条目 ' + fc.total + '|未闭环 ' + fc.open + '|已闭环 ' + fc.closed
8013
+ + '|按类别:' + FEEDBACK_CATEGORIES.map((c) => c + '=' + fc.byCategory[c]).join('、')
8014
+ + '(' + FEEDBACK_CATEGORIES.map((c) => FEEDBACK_CATEGORY_LABELS[c]).join('/') + ')'
8015
+ + '|按路由:' + FEEDBACK_ROUTES.map((r) => r + '=' + fc.byRoute[r]).join('、'))
8016
+ if (!feedbackOn()) {
8017
+ L.push('- 反馈开关:off(提示词不注入该段;写入请求会被具名拒绝)')
8018
+ } else {
8019
+ const openRows = feedbackEntries().filter((e) => FEEDBACK_OPEN_STATUSES.indexOf(e.status) !== -1)
8020
+ const inter = openRows.filter((e) => e.route === 'interpersonal')
8021
+ L.push('- 未闭环 ' + openRows.length + ' 条' + (inter.length ? '|其中 interpersonal(需评估+事后回填):' + inter.map((e) => e.id + '(by ' + e.by + ')').join('、') : ''))
8022
+ L.push('- 文件镜像:' + FEEDBACK_DIR + '/(按类别分文件)|明细:vibe_v5_feedback {op:\'list\'}')
8023
+ }
8024
+ }
7691
8025
  return { ok: true, report: L.join('\n'), quorum: qv }
7692
8026
  }
7693
8027
  // Adding/removing a PERMANENT researcher is a change to the institute's public
@@ -7764,6 +8098,8 @@ export function apply(ctx) {
7764
8098
  maybeQueueVerify, castVerdict, currentVerify, hasVerifyInFlight, startMeeting, quorumM, voterCount,
7765
8099
  // final paper (docs/final-paper.md; the phase runs BEFORE finishRun)
7766
8100
  startPaper, paperStatus: paperSummary, finalizePaperByOffice,
8101
+ // methodology/collaboration feedback (Shared/Feedback/): the tool handler + the observers
8102
+ feedbackOn, feedbackCounts, feedbackTool,
7767
8103
  // authorization helpers (used by tool handlers)
7768
8104
  memberIdOfAgent, isOffice, isAcademician, isProvablyOffice, officeCaller, memberById, activeMembers,
7769
8105
  // diagnosis helpers (defects 1/2 of the architecture self-test)
@@ -7887,7 +8223,7 @@ export function apply(ctx) {
7887
8223
  registerTool('vibe_v5_stop', 'Stop the institute: interrupt every member, clear coordination state, and release their child sessions.', objParams({}), (s, a, x) => withOffice(s, x, 'stop the institute', () => s.initStop()))
7888
8224
  registerTool('vibe_v5_status', 'Machine-readable institute status (members, tasks, quorum, meetings, verification, mail).', objParams({}), (s) => s.status())
7889
8225
  registerTool('vibe_v5_report', 'Human-readable institute report (staffing, tasks, consensus, meetings, file locations).', objParams({}), (s) => s.report())
7890
- registerTool('vibe_v5_set', 'Tune institute parameters (persisted in State/<institute>.v5state.json). provider/model override staff LLM routes (empty = inherit the office route). toolAllow/toolDeny restrict PERMANENT staff tools; tempToolAllow/tempToolDeny restrict temp workers. quorumCap sets m = min(quorumCap, voters); an m-vote passes only when at least m Boolean votes (exactly 1 or exactly 0) exist AND no voter returns an opposing Boolean, so with the default roster it degenerates to unanimity among the current voters; only current voters count (a dismissed member\'s earlier ballot is dropped). quorumMode "m-unanimous" (v5) or "all-unanimous" (v4 legacy). formalVerify: "off" (default, no extra requirement) | "encourage" (agents decide by implementation difficulty whether to formalize in Lean; a passing Lean run turns the vote into a FIDELITY review of the Lean statements) | "require" (same, plus a gate: a true/false verdict is withheld as undecided until the object is Lean-passed or has an explicit reasoned blocker record). LEAN TOOLCHAIN: leanCommand names the Lean executable (e.g. "lake" with leanArgs ["env","lean"]); leanArgs are inserted before the file name (the framework appends --search-path <VibeMath root> unless leanArgs already sets one); leanTimeoutMs is the per-run budget in ms (>=1000, and the per-job budget of the async queue). FINAL PAPER: finalPaper (default true) writes the final paper when the run concludes — the paper phase runs BEFORE the run is marked complete, the permanent staff write their own part, cross-review each other, and the editor named by paperEditor finalises; paperFormat "both"|"md"|"tex"; paperLanguage "zh"|"en"; paperCompilePdf compiles a PDF when a LaTeX engine is detected; paperEditor "academician" (default, the only editor an unattended run can reach) | "office" (manual /v5 paper only — the office must first consult the whole institute: >=1 office message AND >=1 meeting, recorded in the finalisation note); paperLatexCommand forces one engine command instead of auto-detection (empty = auto: xelatex -> latexmk -> pdflatex -> lualatex -> tectonic, English prefers pdflatex). LEAN ASYNC: leanAsync (default true) compiles on a per-session background queue (vibe_v5_lean_run / vibe_v5_lean_archive run=true enqueue and return immediately; inspect them with vibe_v5_lean_job or vibe_v5_lean_lib.jobs and wait with vibe_v5_lean_job {jobId,waitMs}); leanAsync=false restores the previous synchronous behaviour. Only a settled job (exit 0, unchanged content hash AND the same build context) may mark an object passed; a job id is the content+build-context digest. leanInitiative "off"|"normal" (default)|"eager" separates DAILY eagerness about formalizing from formalVerify (which stays the verdict-time requirement). leanSearchPaths (string[]) adds extra --search-path roots before the automatic VibeMath root (deduped; an explicit --search-path/-R/--root in leanArgs wins). leanJobsMaxParallel (default 1) caps simultaneous background compiles. MATH COMPUTATION: mathComputation "off"|"auto" (default)|"on" gates the math_computation tool; mathMode "typed+shell" (default: the host shell may be used as a fallback, but a shell run carries no receipt and its conclusion must be marked 未经工具归档/not tool-archived) | "typed" (never mention the shell; engine=cli is refused); mathEngines lists the allowed engines (cli is on by default, SageMath is a later phase); mathTimeoutMs is the per-run budget (>=1000); mathPackages are packages a computation may require; mathInstallScope "user" (default) | "system" (per call only, never remembered). Installs are two-step (plan then confirm-token) and commercial engines are never installed. Unknown spellings of these enums fall back to the documented default.', objParams({
8226
+ registerTool('vibe_v5_set', 'Tune institute parameters (persisted in State/<institute>.v5state.json). provider/model override staff LLM routes (empty = inherit the office route). toolAllow/toolDeny restrict PERMANENT staff tools; tempToolAllow/tempToolDeny restrict temp workers. quorumCap sets m = min(quorumCap, voters); an m-vote passes only when at least m Boolean votes (exactly 1 or exactly 0) exist AND no voter returns an opposing Boolean, so with the default roster it degenerates to unanimity among the current voters; only current voters count (a dismissed member\'s earlier ballot is dropped). quorumMode "m-unanimous" (v5) or "all-unanimous" (v4 legacy). formalVerify: "off" (default, no extra requirement) | "encourage" (agents decide by implementation difficulty whether to formalize in Lean; a passing Lean run turns the vote into a FIDELITY review of the Lean statements) | "require" (same, plus a gate: a true/false verdict is withheld as undecided until the object is Lean-passed or has an explicit reasoned blocker record). LEAN TOOLCHAIN: leanCommand names the Lean executable (e.g. "lake" with leanArgs ["env","lean"]); leanArgs are inserted before the file name (the framework appends -R <VibeMath root> unless leanArgs already sets one); leanTimeoutMs is the per-run budget in ms (>=1000, and the per-job budget of the async queue). FINAL PAPER: finalPaper (default true) writes the final paper when the run concludes — the paper phase runs BEFORE the run is marked complete, the permanent staff write their own part, cross-review each other, and the editor named by paperEditor finalises; paperFormat "both"|"md"|"tex"; paperLanguage "zh"|"en"; paperCompilePdf compiles a PDF when a LaTeX engine is detected; paperEditor "academician" (default, the only editor an unattended run can reach) | "office" (manual /v5 paper only — the office must first consult the whole institute: >=1 office message AND >=1 meeting, recorded in the finalisation note); paperLatexCommand forces one engine command instead of auto-detection (empty = auto: xelatex -> latexmk -> pdflatex -> lualatex -> tectonic, English prefers pdflatex). LEAN ASYNC: leanAsync (default true) compiles on a per-session background queue (vibe_v5_lean_run / vibe_v5_lean_archive run=true enqueue and return immediately; inspect them with vibe_v5_lean_job or vibe_v5_lean_lib.jobs and wait with vibe_v5_lean_job {jobId,waitMs}); leanAsync=false restores the previous synchronous behaviour. Only a settled job (exit 0, unchanged content hash AND the same build context) may mark an object passed; a job id is the content+build-context digest. leanInitiative "off"|"normal" (default)|"eager" separates DAILY eagerness about formalizing from formalVerify (which stays the verdict-time requirement). leanSearchPaths (string[]) adds extra compiler search roots before the automatic VibeMath root (deduped; an explicit -R/--root in leanArgs wins). leanJobsMaxParallel (default 1) caps simultaneous background compiles. MATH COMPUTATION: mathComputation "off"|"auto" (default)|"on" gates the math_computation tool; mathMode "typed+shell" (default: the host shell may be used as a fallback, but a shell run carries no receipt and its conclusion must be marked 未经工具归档/not tool-archived) | "typed" (never mention the shell; engine=cli is refused); mathEngines lists the allowed engines (cli is on by default, SageMath is a later phase); mathTimeoutMs is the per-run budget (>=1000); mathPackages are packages a computation may require; mathInstallScope "user" (default) | "system" (per call only, never remembered). Installs are two-step (plan then confirm-token) and commercial engines are never installed. Unknown spellings of these enums fall back to the documented default. feedback (default "on") = the methodology/collaboration feedback library (Shared/Feedback/): "on" records entries and injects a short per-round hint; "off" injects nothing and refuses every write BY NAME (a switch, not a severity).', objParams({
7891
8227
  academician: B, academicianLeads: B, memberMayRejectAssign: B, researcherCount: I,
7892
8228
  quorumCap: I, quorumMode: S, verdictMaxRounds: I,
7893
8229
  maxTempPerMember: I, maxTempTotal: I,
@@ -7901,6 +8237,7 @@ export function apply(ctx) {
7901
8237
  mathMode: { type: 'string', enum: ['typed', 'typed+shell'] },
7902
8238
  mathEngines: SA, mathTimeoutMs: I, mathPackages: SA,
7903
8239
  mathInstallScope: { type: 'string', enum: ['user', 'system'] },
8240
+ feedback: { type: 'string', enum: ['on', 'off'] },
7904
8241
  finalPaper: B, paperFormat: { type: 'string', enum: ['both', 'md', 'tex'] },
7905
8242
  paperLanguage: { type: 'string', enum: ['zh', 'en'] },
7906
8243
  paperCompilePdf: B, paperEditor: { type: 'string', enum: ['office', 'academician'] }, paperLatexCommand: S,
@@ -7978,6 +8315,7 @@ export function apply(ctx) {
7978
8315
  })
7979
8316
  registerTool('vibe_v5_record_progress', '(member) Append to YOUR progress.md — your research log. Include what you tried, the routes and their obstacles, your current state, your plans, and failed/dead ends (they save the institute from repeating them).', objParams({ content: S }, ['content']), (s, a, x) => s.publishProgress(s.memberIdOfAgent(x), a.content))
7980
8317
  registerTool('vibe_v5_record_proposition', '(member) Record a proposition/lemma in your library. REQUIRES value (价值程度), motive (动机用途计划) and p (your probability that it is true).', objParams({ id: S, title: S, statement: S, value: N, motive: S, p: N }, ['statement', 'value', 'motive', 'p']), (s, a, x) => s.recordCard(s.memberIdOfAgent(x), 'proposition', a))
8318
+ registerTool('vibe_v5_feedback', '(member) 工作经验/流程反馈库(Shared/Feedback/,方法论/协作层——不是研究结论)。op=add(写一条,需 category/route/phenomenon/impact/action;route=interpersonal 还必须带 assessment)| update(状态流转+回填结果:id/status/outcome/note)| list(默认只看未闭环,可用 category/route 过滤,all:true 看全部)| summary(按类别/路由计数)。类别:cooperation 合作|management 管理|process 流程|obstacle 障碍|conflict 矛盾。路由:self 自己调整即可|team 组织/工作流同样无需审批|interpersonal 只有这条必须先评估、再事后回填验证。权限:成员/临时工可 add 且只能更新自己发起的条目;所办可更新任何条目。', objParams({ op: S, id: S, category: S, route: S, context: S, phenomenon: S, impact: S, action: S, assessment: S, outcome: S, status: S, note: S, all: B }, ['op']), (s, a, x) => withCaller(s, x, '使用反馈库', (caller) => s.feedbackTool(caller, a)))
7981
8319
  registerTool('vibe_v5_record_method', '(member) Record a theory/method/tool in your library. REQUIRES value, motive and p.', objParams({ id: S, title: S, type: S, content: S, notation: S, value: N, motive: S, p: N }, ['content', 'value', 'motive', 'p']), (s, a, x) => s.recordCard(s.memberIdOfAgent(x), 'method', a))
7982
8320
  registerTool('vibe_v5_record_subproblem', '(member) Record a sub-problem in your library. REQUIRES value, motive and p.', objParams({ id: S, title: S, statement: S, value: N, motive: S, p: N }, ['statement', 'value', 'motive', 'p']), (s, a, x) => s.recordCard(s.memberIdOfAgent(x), 'subproblem', a))
7983
8321
  registerTool('vibe_v5_read_library', '(member) Read anyone\'s library (read-only): their progress and recorded cards. Omit member to read everyone.', objParams({ member: S, kind: S, id: S }), (s, a) => s.readLibrary(a))
@@ -8023,6 +8361,19 @@ export function apply(ctx) {
8023
8361
  if (todo.length) parts.push('- 形式化待办(定论被搁置):' + todo.map((t) => t.id).join('、') + '(见 Formal/TODO.md)')
8024
8362
  parts.push('- 可复用库:vibe_v5_lean_lib 可列出 Formal/Lib 与 Formal/Proved')
8025
8363
  }
8364
+ // Methodology/collaboration feedback: the academician's periodic summary needs the SAME numbers the
8365
+ // report shows, so "what did we learn about working together" is one glance away (and open
8366
+ // interpersonal entries — the only route that needs a written-back verification — are named).
8367
+ parts.push('## 反馈库(方法论/协作层)')
8368
+ {
8369
+ const fc = s.feedbackCounts()
8370
+ parts.push('- 条目 ' + fc.total + '|未闭环 ' + fc.open + '|已闭环 ' + fc.closed
8371
+ + '|按类别:' + Object.keys(fc.byCategory).map((k) => k + '=' + fc.byCategory[k]).join('、')
8372
+ + '|按路由:' + Object.keys(fc.byRoute).map((k) => k + '=' + fc.byRoute[k]).join('、'))
8373
+ parts.push(s.feedbackOn()
8374
+ ? '- 未闭环明细用 vibe_v5_feedback {op:\'list\'};interpersonal 条目必须回填 outcome 才算闭环'
8375
+ : '- 反馈开关:off(提示词不注入该段;写入请求会被具名拒绝)')
8376
+ }
8026
8377
  parts.push('## 停滞提示')
8027
8378
  const idleFor = Date.now() - st.lastProgressAt
8028
8379
  parts.push('- 距上次实质进展:' + Math.round(idleFor / 1000) + ' 秒')
@@ -8037,7 +8388,7 @@ export function apply(ctx) {
8037
8388
  // dynamic registration would depend on a runtime knob and break the effect discipline),
8038
8389
  // while the MODE only decides whether the framework TELLS members about them. In 'off'
8039
8390
  // mode they still work if a human or agent calls them deliberately.
8040
- registerTool('vibe_v5_lean_run', '(member) Execute the Lean toolchain on one .lean file inside the workspace and report the result. Parameters: file (required; `Formal/x.lean` or the full CWD-relative path — both name the same file), target (optional object id to record the run against), timeout_ms (optional per-run budget). There is NO run/run=false switch here — run-vs-archive are separate tools; use vibe_v5_lean_archive {run:false} to archive WITHOUT compiling. With leanAsync (default true) the compile is ENQUEUED and this returns immediately with async:{jobId,state} — nothing is compiled yet; the result is durable at Formal/Jobs/<jobId>.json (vibe_v5_lean_job {jobId} reads it), is listed by vibe_v5_lean_lib.jobs, and the initiator also gets ONE 【形式化结果】 announcement in its next round. With leanAsync=false it blocks and returns the compiler output (exitCode/stdout/stderr). Never throws: a host with no subprocess service returns NO_SUBPROCESS and a missing toolchain returns LEAN_NOT_FOUND (in both cases the code can still be written down with vibe_v5_lean_archive), a timeout terminates the process and returns LEAN_TIMEOUT. The framework appends `--search-path <VibeMath root>` before the file name (unless leanArgs already sets a search root).', objParams({ file: S, target: S, timeout_ms: I }, ['file']), (s, a, x) => withCaller(s, x, 'a Lean run', (caller) => s.leanRunTool(caller, a)))
8391
+ registerTool('vibe_v5_lean_run', '(member) Execute the Lean toolchain on one .lean file inside the workspace and report the result. Parameters: file (required; `Formal/x.lean` or the full CWD-relative path — both name the same file), target (optional object id to record the run against), timeout_ms (optional per-run budget). There is NO run/run=false switch here — run-vs-archive are separate tools; use vibe_v5_lean_archive {run:false} to archive WITHOUT compiling. With leanAsync (default true) the compile is ENQUEUED and this returns immediately with async:{jobId,state} — nothing is compiled yet; the result is durable at Formal/Jobs/<jobId>.json (vibe_v5_lean_job {jobId} reads it), is listed by vibe_v5_lean_lib.jobs, and the initiator also gets ONE 【形式化结果】 announcement in its next round. With leanAsync=false it blocks and returns the compiler output (exitCode/stdout/stderr). Never throws: a host with no subprocess service returns NO_SUBPROCESS and a missing toolchain returns LEAN_NOT_FOUND (in both cases the code can still be written down with vibe_v5_lean_archive), a timeout terminates the process and returns LEAN_TIMEOUT. The framework appends `-R <VibeMath root>` before the file name (unless leanArgs already sets a search root).', objParams({ file: S, target: S, timeout_ms: I }, ['file']), (s, a, x) => withCaller(s, x, 'a Lean run', (caller) => s.leanRunTool(caller, a)))
8041
8392
  registerTool('vibe_v5_lean_archive', '(member) Archive Lean code. kind="def": a REUSABLE definition/object/assumption → the global cross-project library (Formal/Lib). kind="lemma": a machine-checked lemma → Formal/Proved. kind="proof": the formal proof of a project object → Formal/<target>.lean, and (only once the queued compile settles ok) also Verified/Lean/<target>.lean, marking the object Lean-passed. Re-archiving IDENTICAL content is de-duplicated (deduped:true, no rewrite/recompile). kind="blocked": record an explicit, reasoned "cannot/not worth formalizing" decision (note required). Optional `run` (default true): whether to COMPILE after writing — `run:false` archives the text only (useful when this host has no toolchain; the object then stays `attempted`/unset until something compiles it).', objParams({ kind: { type: 'string', enum: ['def', 'lemma', 'proof', 'blocked'] }, name: S, target: S, content: S, from: S, note: S, run: B }, ['kind']), (s, a, x) => withCaller(s, x, 'a Lean archive', (caller) => s.leanArchive(caller, a)))
8042
8393
  registerTool('vibe_v5_lean_lib', '(member) List (and by default rebuild) the Lean reuse library: your institute\'s Formal/Index.md (with an import-dependency column), plus the global cross-project Formal/Lib and Formal/Proved indexes, the background-compile jobs (jobs) and the injected search path (paths.searchPath). Look here BEFORE writing a new definition so you reuse instead of redefining.', objParams({ refresh: B }), async (s, a) => {
8043
8394
  const r = a && a.refresh === false ? { lib: null, proved: null, objects: Object.keys(s.formalRecords()).length } : await s.rebuildLeanLibIndexes()