pi-multi-viewers 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -30,10 +30,11 @@ scripts/pi-probe.sh LLM 探针(跑 pi + 登记新 session → 残留检查器
30
30
  scripts/check-residue.sh 残留检查(session/进程/目录三类;增删 scripts/ 时同步本节)
31
31
  scripts/archive-result.sh 归档 result.md(机械部分:逐字复制+校验/存档头骨架/索引行/删源副本)
32
32
  mv_cli.py 命令行实现(prepare/start/status/report/wait/cleanup/view/say/viewers/set-viewer)
33
- extensions/multi-viewers/ 【单一扩展单元】index.ts = 三命令 + shared.ts = 助手
33
+ extensions/multi-viewers/ 【单一扩展单元】index.ts = 四命令 + shared.ts = 助手
34
34
  /multi-viewers 分析入口(prepare→暂停点弹窗→start→预填+打印观看命令)
35
35
  /multi-viewers-finish 收尾(status→确认→cleanup)
36
36
  /multi-viewers-say 插话(零 LLM,直接 spawn human_sayer.py)
37
+ /multi-viewers-config 启动参数默认值(零 LLM,调 --set-default)
37
38
  ⚠ extensions/ 平级禁放 .ts 助手(加载器会把平级文件当独立扩展)
38
39
  prompts/multi-viewers-setup.md /multi-viewers-setup 建视角入口(建议→你定→**mv.sh --set-viewer** 落盘→给审)
39
40
  # 那四条本来写进 prompt 的纪律(命名/不覆盖/非空/校验)改由命令保证
@@ -234,11 +235,11 @@ loop、状态从 git 共享事实推导、单一事实源 = protocol.json、无
234
235
 
235
236
  ## 安装/发版状态(2026-09-11)
236
237
 
237
- - **当前形态**:prompt × 1(multi-viewers-setup 建视角)+ extension × 1(一单元注册三命令)
238
- ——一个扩展单元内注册三命令(`multi-viewers` 分析 / `multi-viewers-finish` 收尾 /
239
- `multi-viewers-say` 插话;前两个靠 CLI 机器标记行取值 `[prepare] spec=` /
240
- `[start] dir=` / `[start] watch=` / `[status]`,后者零 LLM 直接 spawn human_sayer.py;
241
- 目录发现 = `<cwd>/mv-<sessionId>-*` 最新——**无兜底**:未匹配即报错 rc 1)+ wrapper。
238
+ - **当前形态**:prompt × 1(multi-viewers-setup 建视角)+ extension × 1(一单元注册四命令)
239
+ ——一个扩展单元内注册四命令(`multi-viewers` 分析 / `multi-viewers-finish` 收尾 /
240
+ `multi-viewers-say` 插话 / `multi-viewers-config` 启动参数默认值);前两个靠 CLI
241
+ 机器标记行取值(`[prepare] spec=` / `[start] dir=` / `[start] watch=` / `[status]`),
242
+ say 与 config 零 LLM 直接转发给 `human_sayer.py` / `mv_cli`;
242
243
  扩展层测试:`tests/extension_harness.ts`(真扩展代码 + 假 python3 装置,零 LLM;
243
244
  `run_tests.sh` 自动带上——缺 bun 可见跳过、`MV_REQUIRE_BUN=1` 严格)。
244
245
  **npm 已发布**(版本以 `package.json` / registry 为准)。
@@ -260,7 +261,9 @@ loop、状态从 git 共享事实推导、单一事实源 = protocol.json、无
260
261
  - **核验法**(照上游约定,不用命令行长度判断):
261
262
  `readlink -f ~/.pi/agent/npm/node_modules/pi-multi-viewers` 指向仓库根,
262
263
  且该路径下 `scripts/mv.sh` 存在。
263
- - **registry 核验别只看 `/latest`**(实测踩两次):npm 的 abbreviated 元数据
264
- (`registry.npmjs.org/<pkg>/latest`)有缓存,发布后可能持续返回旧版本;
265
- **权威判据 = 完整文档的 `dist-tags`**:
266
- `curl -s https://registry.npmjs.org/pi-multi-viewers | python3 -c "import json,sys;print(json.load(sys.stdin)['dist-tags'])"`
264
+ - **registry 传播有滞后,别把「读不到」当「没发成功」**(实测三次不同的滞后形态):
265
+ `/latest`(abbreviated)可能持续返回旧版本;完整文档的 `dist-tags` 也可能滞后;
266
+ 本次 0.9.1 **两个读端点都还是旧值**(版本专属端点一度 404),而发布其实已被接受。
267
+ **权威判据 = 本地发布日志**:`~/.npm/_logs/<最新>-debug-0.log` 里 `PUT https://registry.npmjs.org/<pkg> 202`
268
+ + `exit 0` + `info ok`(被接受);随后再用**版本专属端点**确认已可读:
269
+ `curl -s -o /dev/null -w '%{http_code}\n' https://registry.npmjs.org/<pkg>/<version>`(200 = 已上架)
package/README.md CHANGED
@@ -93,6 +93,7 @@ ls ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh
93
93
  | `/multi-viewers "<主题>"` | extension | 分析:prepare → **暂停点弹窗** → start → 交付观看命令 |
94
94
  | `/multi-viewers-finish` | extension | 收尾:status → 确认 → cleanup(报告随清理打印并落盘) |
95
95
  | `/multi-viewers-say "<文本>"` | extension | 插话(human 消息,各视角可见可回应) |
96
+ | `/multi-viewers-config [<键> <值>]` | extension | 查看/修改**启动参数默认值**(零 LLM) |
96
97
  | `scripts/mv.sh <子命令>` | CLI | 终端侧等价入口(`--prepare` / `--start` / `--status` / `--view` / `--say` / `--report` / `--wait` / `--cleanup` / `--viewers` / `--set-viewer`)——pi 内命令内部也走它 |
97
98
 
98
99
  按使用顺序:先 `/multi-viewers-setup` 建视角(一次就够),之后 `/multi-viewers "<主题>"` 跑分析,
@@ -138,8 +139,26 @@ scripts/mv.sh --report # 只读报告(流程/配额/进
138
139
  scripts/mv.sh --cleanup # 收尾(result.md + 报告都留存到 <dir>-*.md/.txt)
139
140
  scripts/mv.sh --viewers # 列出+校验当前项目 viewers/(只读;建视角时用)
140
141
  scripts/mv.sh --set-viewer <名字> # 新建视角文件(正文从 stdin 读;只新建不覆盖)
142
+ scripts/mv.sh --set-default [<键> <值>] # 启动参数默认值(无参数=查看)
141
143
  ```
142
144
 
145
+ ## 启动参数与默认值
146
+
147
+ 配额这类参数可以在**跑之前**设成默认值,之后每次生成 spec 都会沿用:
148
+
149
+ ```
150
+ /multi-viewers-config max-meeting 20 # 设默认值(等价 mv.sh --set-default max-meeting 20)
151
+ /multi-viewers-config # 查看当前默认值(哪些来自配置文件、哪些是内置)
152
+ ```
153
+
154
+ **取值优先级**(后者覆盖前者):内置默认 → 你设的默认值 → `spec/startup.md`
155
+ (每次分析生成,**你能看也能改** = 只影响本轮)→ `--start` 的显式 flag(临时覆盖一次)。
156
+ 生效值与来源在启动时打印;运行期唯一权威始终是分析环境里的 `protocol.json`
157
+ (loop 每轮只读它,中途不可改)。
158
+
159
+ 可设的键:`max-meeting`(meeting 阶段每 agent 发言配额,默认 15)、
160
+ `max-rr`(RR 轮次配额,默认 7)、`stall-timeout`(无进展超时秒数,默认 600)。
161
+
143
162
  ## 视角文件写什么(`viewers/<视角名>.md`)
144
163
 
145
164
  建视角**推荐**走 `/multi-viewers-setup`(交互式:先给候选建议 → 你定建哪几个 →
package/docs/design.md CHANGED
@@ -639,6 +639,22 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
639
639
 
640
640
  ---
641
641
 
642
+
643
+ 23. **启动参数默认值走「配置文件 → spec → protocol.json」**(2026-09-27 用户定):
644
+ `/multi-viewers-config <键> <值>`(= `mv.sh --set-default`)改的是**默认值**,
645
+ 存在 pi agent 目录的 `multi-viewers.json`(用户级;键名与 CLI flag 同名)。
646
+ **取值优先级(唯一实现在 `spec_gen.resolve_startup`)**:
647
+ `--start` 显式 flag > `spec/startup.md` > 默认值配置 > 内置默认。
648
+ 为什么经 spec 而不是直接进 `protocol.json`:spec 是**用户审阅的产物**——
649
+ 把值写进 `spec/startup.md` 让"这次到底用多少"在暂停点可见可改(= 本轮的
650
+ "特别指定"),运行期权威仍是 `protocol.json`(loop 每轮只读它,中途不可改)。
651
+ **踩过的坑(用户点名要确认的那件事)**:`--start` 三个 flag 原先
652
+ `default=DEFAULT_*`,而 `/multi-viewers` 这条路径不带 flag → argparse 默认值
653
+ **无条件覆盖**任何偏好(永远 15/7/600)。修法 = 默认值改 `None`,让"没指定"
654
+ 与"指定成默认值"可区分;四层优先级由 `tests/test_startup_defaults.py` 锁
655
+ (含"谁都没设 → 内置默认"这条反例断言)。生效值与来源在启动时逐键打印
656
+ (`max-meeting=20(默认值配置)`),不存在静默覆盖。
657
+
642
658
  ## 四、记录项(不修;每项必带**现在就能用的观测点**)
643
659
 
644
660
  | 项 | 实测/性质(口径) | 触发条件(观测点) |
@@ -233,4 +233,22 @@ export default function register(pi: any) {
233
233
  }
234
234
  },
235
235
  });
236
+
237
+ // ------------------------------------------------------ 启动参数默认值
238
+ // 零 LLM:直接调 `mv.sh --set-default`(写法与校验都在 python 侧单点实现)。
239
+ // 语义:这里改的是**默认值**;spec/startup.md 或 --start 的显式 flag 优先。
240
+ pi.registerCommand("multi-viewers-config", {
241
+ description: "查看/修改启动参数默认值(max-meeting / max-rr / stall-timeout)",
242
+ argumentHint: "[<键> <值>]",
243
+ handler: async (args: string, ctx: any) => {
244
+ const rest = args.trim().split(/\s+/).filter(Boolean);
245
+ const { rc, output } = await runCli(
246
+ ["--set-default", ...rest], ctx.cwd, ctx.sessionManager.getSessionId(),
247
+ );
248
+ ctx.ui.notify(
249
+ output.trim() || (rc === 0 ? "已处理" : "修改失败"),
250
+ rc === 0 ? "success" : "error",
251
+ );
252
+ },
253
+ });
236
254
  }
package/meeting_fs.py CHANGED
@@ -74,6 +74,99 @@ DEFAULT_STALL_TIMEOUT = 600
74
74
  # 显式档位),不是把默认值挪到便宜侧。
75
75
  DEFAULT_THINKING = "max"
76
76
 
77
+ # ---------------------------------------------------------------
78
+ # 启动参数的「默认值」配置(用户级)——`/multi-viewers-config` 与
79
+ # `mv.sh --set-default` 写的就是它
80
+ # ---------------------------------------------------------------
81
+ # 为什么需要:配额此前只能在 `--start` 那一刻用命令行 flag 指定,而
82
+ # `/multi-viewers` 这条主路径不带 flag → 永远拿 argparse 的 default。
83
+ # 用户要的是"设一次默认值,以后每次生成 spec 就沿用"(2026-09-27 定)。
84
+ #
85
+ # **取值优先级(唯一实现见 spec_gen.resolve_startup)**:
86
+ # 命令行显式 flag > spec/startup.md > 本配置文件 > 内置默认
87
+ # 运行时权威仍是 protocol.json(--start 固化;loop 只读它、不接 flag)。
88
+ #
89
+ # 文件位置 = pi agent 目录下(`$PI_CODING_AGENT_DIR` 或 `~/.pi/agent`)——
90
+ # 复用 `pi_agent_dir()` 单一实现,测试靠改该环境变量重定向(不新增测试专用开关)。
91
+ # 形状:`{"max-meeting": 20, "max-rr": 10, "stall-timeout": 600}`
92
+ # (键名与 CLI flag 同名,少一层映射)。
93
+ STARTUP_DEFAULTS = { # 键名 → 内置默认(引用上方常量,不重复字面量)
94
+ "max-meeting": DEFAULT_MAX_MEETING,
95
+ "max-rr": DEFAULT_MAX_RR,
96
+ "stall-timeout": DEFAULT_STALL_TIMEOUT,
97
+ }
98
+
99
+
100
+ def startup_config_path(agent_dir=None):
101
+ """用户级启动参数配置文件的路径(单一实现)。"""
102
+ return os.path.join(agent_dir or pi_agent_dir(), "multi-viewers.json")
103
+
104
+
105
+ def read_startup_config(agent_dir=None):
106
+ """读用户级配置 → (overrides, error)。
107
+
108
+ 只保留**已知键**且值合法(正整数)——未知键忽略但**可见**(warning),
109
+ 坏文件返回 ({} , 原因) 由调用方决定如何提示(fail-open,不阻断分析)。
110
+ """
111
+ path = startup_config_path(agent_dir)
112
+ if not os.path.exists(path):
113
+ return {}, ""
114
+ try:
115
+ with open(path, encoding="utf-8") as f:
116
+ raw = json.load(f)
117
+ except (OSError, ValueError) as e:
118
+ return {}, f"读不到/解析失败 {path}: {e}"
119
+ if not isinstance(raw, dict):
120
+ return {}, f"{path} 顶层不是对象"
121
+ out, unknown = {}, []
122
+ for k, v in raw.items():
123
+ if k not in STARTUP_DEFAULTS:
124
+ unknown.append(k)
125
+ continue
126
+ try:
127
+ n = int(v)
128
+ except (TypeError, ValueError):
129
+ unknown.append(k)
130
+ continue
131
+ if n < 1:
132
+ unknown.append(k)
133
+ continue
134
+ out[k] = n
135
+ warn = f"{path} 里这些键被忽略(未知或非法):{', '.join(unknown)}" if unknown else ""
136
+ return out, warn
137
+
138
+
139
+ def write_startup_config(key, value, agent_dir=None):
140
+ """把 `key: value` 写进用户级配置(保留其它键)→ (path, error)。
141
+
142
+ 校验在调用方(`parse_startup_kv`)——本函数只管读改写。
143
+ """
144
+ path = startup_config_path(agent_dir)
145
+ cur, _err = read_startup_config(agent_dir)
146
+ cur[key] = value
147
+ try:
148
+ os.makedirs(os.path.dirname(path), exist_ok=True)
149
+ with open(path, "w", encoding="utf-8") as f:
150
+ json.dump(cur, f, indent=2, ensure_ascii=False)
151
+ f.write("\n")
152
+ except OSError as e:
153
+ return path, f"写不了 {path}: {e}"
154
+ return path, ""
155
+
156
+
157
+ def parse_startup_kv(key, raw):
158
+ """校验 `--set-default <key> <value>` 的入参 → (value, error)。"""
159
+ if key not in STARTUP_DEFAULTS:
160
+ return None, (f"未知的键 {key!r}——合法键:"
161
+ + "、".join(STARTUP_DEFAULTS))
162
+ try:
163
+ n = int(raw)
164
+ except (TypeError, ValueError):
165
+ return None, f"{key} 需要整数,收到 {raw!r}"
166
+ if n < 1:
167
+ return None, f"{key} 需要 ≥1,收到 {n}"
168
+ return n, ""
169
+
77
170
 
78
171
  def _entry_source(entry):
79
172
  """pi 的 packages 条目 → 源字符串(两种形态共用;非字符串形态 → "")。
package/mv_cli.py CHANGED
@@ -29,6 +29,7 @@ import sys
29
29
  from datetime import datetime
30
30
 
31
31
  import spec_gen # --viewers 复用其单一判据(列举/名字/集合校验)
32
+ import meeting_fs # --set-default 的配置读写(单一实现)
32
33
 
33
34
  HERE = os.path.dirname(os.path.abspath(__file__))
34
35
  PYTHON = os.environ.get("PYTHON") or "python3"
@@ -55,6 +56,7 @@ USAGE = f"""用法:
55
56
  {PROG} --say [dir] "<文本>"
56
57
  {PROG} --viewers # 列出并校验当前项目的 viewers/(只读;建视角时用)
57
58
  {PROG} --set-viewer <名字> # 新建一个视角文件(正文从 stdin 读;只新建不覆盖)
59
+ {PROG} --set-default [<键> <值>] # 启动参数默认值(无参数=查看;键:max-meeting/max-rr/stall-timeout)
58
60
 
59
61
  消费命令的 <dir> 可省略(自动发现本 session 当前分析——按 cwd 下
60
62
  mv-<PI_SESSION_ID>-* 最新;无匹配则报错要求显式传目录)
@@ -197,6 +199,43 @@ def _validate_and_print_viewers(vdir):
197
199
  return 0
198
200
 
199
201
 
202
+ def cmd_set_default(args):
203
+ """查看 / 修改**启动参数的默认值**:`--set-default [<键> <值>]`。
204
+
205
+ 语义(用户 2026-09-27 定):这里改的是**默认值**;`spec/startup.md` 或
206
+ `--start` 的显式 flag 属于"特别指定",优先于它(取值优先级唯一实现在
207
+ `spec_gen.resolve_startup`)。无参数 = 打印当前默认值(含内置 fallback
208
+ 与来源),便于自查"我设的值到底生效没有"。
209
+ """
210
+ if not args:
211
+ cfg, warn = meeting_fs.read_startup_config()
212
+ print(f"默认值配置文件: {meeting_fs.startup_config_path()}"
213
+ + ("" if os.path.exists(meeting_fs.startup_config_path()) else "(尚未创建)"))
214
+ for k, builtin in meeting_fs.STARTUP_DEFAULTS.items():
215
+ if k in cfg:
216
+ print(f" {k} = {cfg[k]}(配置文件)")
217
+ else:
218
+ print(f" {k} = {builtin}(内置默认)")
219
+ if warn:
220
+ print(f" ⚠ {warn}", file=sys.stderr)
221
+ print("改法: --set-default <键> <值> | 本轮单独指定: 改 spec/startup.md")
222
+ return 0
223
+ if len(args) != 2:
224
+ fail(f"用法: --set-default <键> <值>(无参数 = 查看当前默认值);"
225
+ f"合法键: {'、'.join(meeting_fs.STARTUP_DEFAULTS)}")
226
+ key, raw = args
227
+ value, err = meeting_fs.parse_startup_kv(key, raw)
228
+ if err:
229
+ fail(err)
230
+ path, err2 = meeting_fs.write_startup_config(key, value)
231
+ if err2:
232
+ fail(err2)
233
+ print(f"已设默认值: {key} = {value}({path})")
234
+ print("以后 `/multi-viewers \"<主题>\"` 生成的 spec 会沿用;"
235
+ "想只给本轮不同 → 改 spec/startup.md 或 --start 时显式给 flag。")
236
+ return 0
237
+
238
+
200
239
  def cmd_set_viewer(args):
201
240
  """新建一个视角文件:`--set-viewer <名字>`,正文**从 stdin 读**。
202
241
 
@@ -470,6 +509,8 @@ def main(argv=None):
470
509
  return cmd_viewers(rest)
471
510
  if cmd == "--set-viewer":
472
511
  return cmd_set_viewer(rest)
512
+ if cmd == "--set-default":
513
+ return cmd_set_default(rest)
473
514
  if cmd == "--status":
474
515
  return cmd_status(rest)
475
516
  if cmd == "--report":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multi-viewers",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Multi-perspective analysis for Pi: fork the main session into N perspective agents over the meeting protocol.",
5
5
  "type": "module",
6
6
  "private": false,
package/spec_gen.py CHANGED
@@ -257,6 +257,9 @@ def gen_spec_skeleton(spec_dir, participants, topic=None, background=None,
257
257
  # README.md:从模板复制(内容不变——模板化,用户 7909)
258
258
  shutil.copyfile(os.path.join(TPL_DIR, "spec-readme.md.tpl"),
259
259
  os.path.join(spec_dir, "README.md"))
260
+ # startup.md:本轮启动参数(= `--prepare` 时解析出的默认值;用户 2026-09-27)
261
+ # —— 写进 spec 的目的是**可见可改**:审阅暂停点里直接改这里 = 只影响本轮。
262
+ write_spec_startup(spec_dir, resolve_startup({})[0])
260
263
  # question.md:第一行说明 + 基本结构模板(用户 7713:提供基本结构)
261
264
  q = [
262
265
  "# question.md——分析起点(话题/立场/待答问题,自由 markdown)。本行是说明行,不会注入。",
@@ -387,6 +390,92 @@ def gen_question(topic, stances, background, questions):
387
390
  return "\n".join(lines)
388
391
 
389
392
 
393
+
394
+ def spec_startup_path(spec_dir):
395
+ """spec 里的启动参数文件(`startup.md`)——`--prepare` 写、`--start` 读。"""
396
+ return os.path.join(spec_dir, "startup.md")
397
+
398
+
399
+ _SPEC_STARTUP_HEADER = (
400
+ "# startup.md——本轮的启动参数(键: 值)。本行是说明行,不会注入。\n"
401
+ "#\n"
402
+ "# 这些值由 `--prepare` 按「你的默认值配置」填入;**在这里改 = 只影响本轮**\n"
403
+ "# (相当于\"特别指定\")。删除某行 = 该键回落到默认值配置。\n"
404
+ "# 合法键:max-meeting(meeting 每 agent 发言配额)、max-rr(RR 轮次配额)、\n"
405
+ "# stall-timeout(无进展超时秒数)。值必须是 ≥1 的整数。\n"
406
+ )
407
+
408
+
409
+ def write_spec_startup(spec_dir, values):
410
+ """写 `spec/startup.md`(prepare 时;values = 解析后的默认值)。"""
411
+ lines = [_SPEC_STARTUP_HEADER]
412
+ for k in meeting_fs.STARTUP_DEFAULTS:
413
+ if k in values:
414
+ lines.append(f"{k}: {values[k]}\n")
415
+ with open(spec_startup_path(spec_dir), "w", encoding="utf-8") as f:
416
+ f.write("".join(lines))
417
+
418
+
419
+ def read_spec_startup(spec_dir):
420
+ """读 `spec/startup.md` → (values, error)。
421
+
422
+ 容错:文件不存在 → ({}, "")(旧 spec 兼容,回落默认值);坏行忽略但可见。
423
+ """
424
+ path = spec_startup_path(spec_dir)
425
+ if not os.path.exists(path):
426
+ return {}, ""
427
+ out, bad = {}, []
428
+ try:
429
+ with open(path, encoding="utf-8") as f:
430
+ for raw in f:
431
+ line = raw.strip()
432
+ if not line or line.startswith("#"):
433
+ continue
434
+ if ":" not in line:
435
+ bad.append(line)
436
+ continue
437
+ k, v = (x.strip() for x in line.split(":", 1))
438
+ val, err = meeting_fs.parse_startup_kv(k, v)
439
+ if err:
440
+ bad.append(line)
441
+ continue
442
+ out[k] = val
443
+ except OSError as e:
444
+ return {}, f"读不到 {path}: {e}"
445
+ return out, ("忽略这些行:" + ";".join(bad) if bad else "")
446
+
447
+
448
+ def resolve_startup(cli_values, spec_dir=None):
449
+ """**取值优先级唯一实现** → (values, sources, notes)。
450
+
451
+ 优先级(后者覆盖前者):
452
+ 内置默认 → 用户级配置(`meeting_fs.read_startup_config`)
453
+ → `spec/startup.md` → 命令行显式 flag
454
+
455
+ cli_values:只放**命令行真的给了**的键(未指定 = 不在字典里 ✗)——
456
+ 这正是 argparse 默认值必须改成 None 的原因:默认值会让"没指定"和
457
+ "指定成 15"无法区分,从而静默覆盖用户设的默认值。
458
+ """
459
+ values = dict(meeting_fs.STARTUP_DEFAULTS)
460
+ sources = {k: "内置默认" for k in values}
461
+ notes = []
462
+ cfg, warn = meeting_fs.read_startup_config()
463
+ if warn:
464
+ notes.append(warn)
465
+ for k, v in cfg.items():
466
+ values[k], sources[k] = v, "默认值配置"
467
+ if spec_dir:
468
+ spec_vals, warn2 = read_spec_startup(spec_dir)
469
+ if warn2:
470
+ notes.append(warn2)
471
+ for k, v in spec_vals.items():
472
+ values[k], sources[k] = v, "spec"
473
+ for k, v in (cli_values or {}).items():
474
+ if v is not None:
475
+ values[k], sources[k] = v, "命令行"
476
+ return values, sources, notes
477
+
478
+
390
479
  def gen_protocol(topic, participants, max_meeting, max_rr,
391
480
  extension_policy=meeting_fs.DEFAULT_EXTENSION_POLICY,
392
481
  result_writer=None,
@@ -348,11 +348,22 @@ def setup_environment(args, participants, base, spec_dir=None,
348
348
  shutil.rmtree(wa)
349
349
  _clone_work(base, participants[0]) # clone + git 身份 + 建目录
350
350
 
351
+ # 启动参数解析(**唯一实现**在 spec_gen.resolve_startup):命令行显式 > spec >
352
+ # 默认值配置 > 内置默认。三个 flag 的 argparse 默认是 None,所以"没给"不会被
353
+ # 误当成"显式给了默认值"(2026-09-27 用户要求:不能有强制设置配额的操作)。
354
+ startup, startup_src, startup_notes = spec_gen.resolve_startup(
355
+ {"max-meeting": args.max_meeting, "max-rr": args.max_rr,
356
+ "stall-timeout": args.stall_timeout},
357
+ spec_dir=spec_dir)
358
+ for _n in startup_notes:
359
+ print(f"[startup] {_n}")
360
+
351
361
  # 共享配置(work-a 提交,setup commit 进 bare)
352
362
  with open(os.path.join(wa, "protocol.json"), "w") as f:
353
- json.dump(gen_protocol(spec_topic or args.topic, participants, args.max_meeting,
354
- args.max_rr, args.extension_policy, args.result_writer,
355
- args.stall_timeout,
363
+ json.dump(gen_protocol(spec_topic or args.topic, participants,
364
+ startup["max-meeting"],
365
+ startup["max-rr"], args.extension_policy, args.result_writer,
366
+ startup["stall-timeout"],
356
367
  fork_source=getattr(args, "fork_source", None),
357
368
  fork_cwd=os.getcwd(),
358
369
  fork_mode=getattr(args, "fork_mode", meeting_fs.DEFAULT_FORK_MODE)),
@@ -420,7 +431,9 @@ def setup_environment(args, participants, base, spec_dir=None,
420
431
  shutil.copy(os.path.join(HERE, mod), os.path.join(base, mod))
421
432
  rw = args.result_writer or participants[-1]
422
433
  print(f"[setup] 环境就绪: {base}({len(participants)} agents: {', '.join(participants)})")
423
- print(f"[setup] resultWriter={rw}, maxMeeting={args.max_meeting}, maxRR={args.max_rr}, "
434
+ # 生效值 + **来源**一并打印(用户 2026-09-27 的疑虑:默认值有没有被静默覆盖)
435
+ q = " · ".join(f"{k}={startup[k]}({startup_src[k]})" for k in meeting_fs.STARTUP_DEFAULTS)
436
+ print(f"[setup] resultWriter={rw}, 配额 {q}, "
424
437
  f"立场={'有' if (args.stances or spec_dir) else '无'}, "
425
438
  f"extensionPolicy={args.extension_policy}")
426
439
 
@@ -583,12 +596,17 @@ def main():
583
596
  parser.add_argument("--questions", default=None, help="待回答问题(|分隔,对齐 RR)")
584
597
  parser.add_argument("--models", default=None, help='JSON: {"a": "provider/model"}')
585
598
  parser.add_argument("--result-writer", default=None, help="resultWriter(默认最后一位参与者)")
586
- parser.add_argument("--max-meeting", type=int, default=meeting_fs.DEFAULT_MAX_MEETING,
587
- help="meeting 阶段发言配额(每 agent)")
588
- parser.add_argument("--max-rr", type=int, default=7, help="RR 阶段轮次配额(starter)")
589
- parser.add_argument("--stall-timeout", type=int,
590
- default=meeting_fs.DEFAULT_STALL_TIMEOUT,
591
- help="无进展超时兜底(秒,默认 600;防 provider API 慢)")
599
+ # 配额三个 flag:**默认值一律 None**(2026-09-27 用户要求)——这样才能区分
600
+ # "命令行没给"(→ 交给 spec/默认值配置/内置默认,见 spec_gen.resolve_startup)
601
+ # 与"命令行显式给了"。此前的 default=DEFAULT_* 会在 `/multi-viewers` 这条
602
+ # 不带 flag 的路径上**无条件覆盖**用户设的默认值(用户的疑虑,已成事实:
603
+ # --start <spec> 无 flag → argparse 15 → protocol.json 15)。
604
+ parser.add_argument("--max-meeting", type=int, default=None,
605
+ help="meeting 阶段发言配额(每 agent;不给则用默认值配置/spec)")
606
+ parser.add_argument("--max-rr", type=int, default=None,
607
+ help="RR 阶段轮次配额(starter;不给则用默认值配置/spec)")
608
+ parser.add_argument("--stall-timeout", type=int, default=None,
609
+ help="无进展超时兜底(秒;不给则用默认值配置/spec)")
592
610
  parser.add_argument("--spec-gen", metavar="DIR", default=None,
593
611
  help="生成 spec 骨架到 DIR(如 --spec-gen myspec/;不需 --dir)")
594
612
  parser.add_argument("--spec", default=None,
@@ -46,6 +46,14 @@ fork 机制已让每个 agent 携带发起分析时的对话上下文(默认 b
46
46
 
47
47
  没有就留空。
48
48
 
49
+ ## startup.md —— 启动参数(自动生成,一般不用改)
50
+
51
+ **作用**:本轮的配额等启动参数。`--prepare` 按你设的默认值写好;**直接改这里
52
+ = 只影响本轮**(相当于"特别指定"),删掉某行则该键回落到默认值。
53
+
54
+ 可设:`max-meeting`(meeting 每 agent 发言配额)、`max-rr`(RR 轮次配额)、
55
+ `stall-timeout`(无进展超时秒数)。改默认值:`/multi-viewers-config <键> <值>`。
56
+
49
57
  ## models.md —— 模型配置(可选)
50
58
 
51
59
  每行:`agent名: model, variant`(**两个槽都显式写出**,一眼可见本场跑在