pi-multi-viewers 0.1.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.
Files changed (35) hide show
  1. package/AGENTS.md +192 -0
  2. package/README.md +153 -0
  3. package/docs/design.md +288 -0
  4. package/docs/examples/first-experiment/README.md +42 -0
  5. package/docs/examples/first-experiment/work-a/AGENTS.md +10 -0
  6. package/docs/examples/first-experiment/work-a/a/0001.md +65 -0
  7. package/docs/examples/first-experiment/work-a/a/0002.md +70 -0
  8. package/docs/examples/first-experiment/work-a/perspective.md +5 -0
  9. package/docs/examples/first-experiment/work-b/AGENTS.md +10 -0
  10. package/docs/examples/first-experiment/work-b/b/0001.md +100 -0
  11. package/docs/examples/first-experiment/work-b/perspective.md +6 -0
  12. package/docs/reviews/2026-09-10-e2e10-fork-source-modes-review.md +366 -0
  13. package/docs/reviews/2026-09-10-e2e11-forkmode-guards-review.md +229 -0
  14. package/docs/reviews/2026-09-10-e2e12-code-review.md +346 -0
  15. package/docs/reviews/2026-09-11-e2e13-code-review.md +284 -0
  16. package/docs/reviews/2026-09-11-e2e14-observability-review.md +216 -0
  17. package/docs/reviews/README.md +36 -0
  18. package/docs/test-methodology.md +258 -0
  19. package/extensions/multi-viewers-say/index.ts +156 -0
  20. package/fake_agent.py +120 -0
  21. package/human_sayer.py +144 -0
  22. package/human_viewer.py +215 -0
  23. package/meeting_core.py +255 -0
  24. package/meeting_engine.py +733 -0
  25. package/meeting_fs.py +1066 -0
  26. package/meeting_loop.py +606 -0
  27. package/package.json +41 -0
  28. package/prompts/multi-viewers.md +94 -0
  29. package/scripts/check-residue.sh +190 -0
  30. package/scripts/mv.sh +325 -0
  31. package/start_discussion.py +1485 -0
  32. package/templates/AGENTS.md.tpl +100 -0
  33. package/templates/agent.md.tpl +9 -0
  34. package/templates/gitignore.tpl +7 -0
  35. package/templates/spec-readme.md.tpl +87 -0
@@ -0,0 +1,255 @@
1
+ """meeting 模式核心逻辑(纯函数,无 I/O)——阶段 1 确定性单测对象。
2
+
3
+ 设计原则(RR 教训):
4
+ - 流程控制归确定性代码,不依赖 LLM
5
+ - 判定函数只吃数据结构(dict/list),I/O 在 meeting_fs.py(本模块不碰文件)
6
+ - 单调性:冻结/af 判定只看"最后一条消息",陈旧视图只会延迟不会误判
7
+
8
+ 消息 frontmatter 关键字段(meeting 模式):
9
+ from: 作者
10
+ type: message | freezing | all-freezing | pass | concluded
11
+ mode: meeting | all-freezing | round-robin | concluded
12
+ to: 目标(meeting 专用:单个 agent 或 all;缺省 = all)
13
+ seen_at: 消息生成时的 git HEAD
14
+ """
15
+
16
+ MEETING_TYPES = {"message", "freezing", "all-freezing", "pass", "concluded"}
17
+ """meeting 模式合法 type 集合(校验器白名单)"""
18
+
19
+ # ---------------------------------------------------------------
20
+ # 1. 冻结判定(阶段 1.1)
21
+ # ---------------------------------------------------------------
22
+
23
+ def last_message_type(messages):
24
+ """给定某 agent 的全部消息(按序号排序),返回最后一条的 type。
25
+
26
+ messages: list[dict](每项含 type 字段,按时间/序号升序)
27
+ 返回: str | None(无消息 → None)
28
+ """
29
+ if not messages:
30
+ return None
31
+ return messages[-1].get("type")
32
+
33
+
34
+ def is_all_last(by_agent, predicate):
35
+ """通用判定:所有参与者最后一条消息都满足 predicate。
36
+
37
+ by_agent: {agent: list[dict]}(每个 agent 的完整消息列表,按序号升序)
38
+ predicate: callable(type_str) -> bool
39
+ 返回: bool
40
+ """
41
+ for agent, msgs in by_agent.items():
42
+ t = last_message_type(msgs)
43
+ if t is None: # 有人从未发言 → 判定不成立
44
+ return False
45
+ if not predicate(t):
46
+ return False
47
+ return True
48
+
49
+
50
+ def is_all_last_in(by_agent, types):
51
+ """所有参与者最后一条消息 ∈ types(宽松确认:freezing 或 af 均可)。"""
52
+ allowed = set(types)
53
+ return is_all_last(by_agent, lambda t: t in allowed)
54
+
55
+
56
+ # ---------------------------------------------------------------
57
+ # 2. 读取点(阶段 1.2)
58
+ # ---------------------------------------------------------------
59
+
60
+ def read_point_seen_at(messages):
61
+ """读取点 = 最后一条消息的 seen_at(用户 10030 定案)。
62
+
63
+ messages: list[dict](按序号升序)
64
+ 返回: str | ""(无消息 → "" 从根读起)
65
+
66
+ 语义:seen_at = loop 交给 pi 处理的那批新消息、且 pi
67
+ run 正确写入新消息后由 loop 填入的值。loop 自己写的新消息(协议信号)
68
+ 沿用上一个 seen_at、不推进——所以无论最后一条是 LLM 消息还是 loop
69
+ 消息,它的 seen_at 都是 LLM 最后处理点,直接取即可(用户 10024:
70
+ 不需要反序找 + 类型判断,直接通过 seen_at)。
71
+ """
72
+ if not messages:
73
+ return ""
74
+ return messages[-1].get("seen_at", "")
75
+
76
+
77
+ # ---------------------------------------------------------------
78
+ # 3. 校验器(阶段 1.3)
79
+ # ---------------------------------------------------------------
80
+
81
+ def validate_and_fix(frontmatter, agent, mode, head,
82
+ allow_protocol_types=False, loop_message=False):
83
+ """校验并确定性修复一条消息的 frontmatter(meeting 版)。
84
+
85
+ frontmatter: dict(agent 写的原始 frontmatter,可变)
86
+ agent: 本 agent 名(from 应等于它)
87
+ mode: 当前模式(meeting 等,确定性修复 mode 字段)
88
+ head: 当前 git HEAD(seen_at 应等于它——仅 LLM 消息)
89
+ allow_protocol_types: 是否放行引擎专用 type(all-freezing/concluded)。
90
+ False(默认,LLM 路径 commit_new_files):只放行 {message, freezing,
91
+ pass}——协议信号全归 loop(设计 11.8),LLM 写引擎专用 type
92
+ (如 concluded)会绕过收尾、result.md 未生成(review4 M1)。
93
+ True(协议信号路径 write_protocol_signal):完整 MEETING_TYPES。
94
+ loop_message: 是否 loop 自己写的消息(协议信号)。True 时 seen_at
95
+ 不强制为 head——沿用上一个(用户 10030 定案:seen_at = LLM 处理
96
+ 过的最新位置,loop 消息不推进)。
97
+
98
+ 确定性修复(不依赖 LLM 判断,直接写):
99
+ - from: 缺失/错误 → agent 名
100
+ - seen_at: 缺失 → head(LLM 消息);loop 消息保留沿用值
101
+ - mode: 缺失 → 当前模式
102
+ 语义校验(非法 → 报告,由调用方决定重写唤醒):
103
+ - type: 必须在白名单内(按来源区分,M1)
104
+ - to: 非法目标 → "all"(容错)
105
+
106
+ 返回: (fixed_frontmatter, errors: list[str])
107
+ """
108
+ errors = []
109
+
110
+ # --- 确定性修复 ---
111
+ if not frontmatter.get("from") or frontmatter.get("from") != agent:
112
+ frontmatter["from"] = agent
113
+ if loop_message:
114
+ # loop 消息:seen_at 沿用(write_protocol_signal 已传读取点)——
115
+ # 只兜底缺失(首条 loop 消息无读取点 → head),不强制为 head
116
+ if not frontmatter.get("seen_at"):
117
+ frontmatter["seen_at"] = head
118
+ elif not frontmatter.get("seen_at") or frontmatter.get("seen_at") != head:
119
+ # LLM 消息:seen_at = 该轮 head(LLM 确实处理到那了)
120
+ frontmatter["seen_at"] = head
121
+ # mode 无条件覆盖为当前阶段(LLM 不该决定状态)——RR 阶段 LLM 照抄
122
+ # 模板的 "mode: meeting" 会导致阶段回退(设计 11.5)
123
+ frontmatter["mode"] = mode
124
+
125
+ # --- 语义校验 ---
126
+ t = frontmatter.get("type")
127
+ allowed = (MEETING_TYPES if allow_protocol_types
128
+ else {"message", "freezing", "pass"})
129
+ if t not in allowed:
130
+ errors.append(f"type '{t}' 非法,应为 {sorted(allowed)}")
131
+ # to 无条件强制 all(审核#3):设计决策"to 定向暂不启用,全 to all"——
132
+ # 消除死参数(此前只容错非法,LLM 写其他值仍是无声死参数)
133
+ frontmatter["to"] = "all"
134
+
135
+ return frontmatter, errors
136
+
137
+
138
+ # ---------------------------------------------------------------
139
+ # 4. 触发条件(阶段 1.4)
140
+ # ---------------------------------------------------------------
141
+
142
+ def has_new_messages_for_me(new_messages, me):
143
+ """meeting 触发条件:有新消息来自别人,且定向匹配我。
144
+
145
+ new_messages: list[dict](每条含 from/to 字段)
146
+ me: 本 agent 名
147
+ 返回: bool
148
+ 定向匹配:to == me 或 to == all 或 to 缺省(缺省 = all)
149
+ """
150
+ for m in new_messages:
151
+ f = m.get("from")
152
+ if f == me:
153
+ continue # 自己的消息不触发
154
+ to = m.get("to", "all")
155
+ if to == me or to == "all":
156
+ return True
157
+ return False
158
+
159
+
160
+ # ---------------------------------------------------------------
161
+ # 5. 冻结级联决策(阶段 3)——确定性判定,归 loop 不归 LLM
162
+ # ---------------------------------------------------------------
163
+
164
+
165
+ def next_in_order(order, agent):
166
+ """轮转顺序的下一位(无 I/O 纯表达式)。
167
+
168
+ RR(round-robin)阶段的 next 字段来源:**协议状态**(LLM 不知道,
169
+ 由 loop 确定性补写)。同一表达式此前在 engine 出现 4 处(starter 首轮、
170
+ 消息修复路径、RR 表态 ×2)——收成一处便于单测与语义统一。
171
+ 边界:agent 不在 order 中 → ValueError(那是调用方的编程错误,
172
+ 不静默返回首位——静默会打乱轮转链)。
173
+ """
174
+ return order[(order.index(agent) + 1) % len(order)]
175
+
176
+
177
+ def meeting_speak_count(messages, agent):
178
+ """该 agent 的 meeting 内容发言轮(**配额消耗口径**,单一实现)。
179
+
180
+ 数 `mode == meeting` 且 `type == message` 的消息——LLM 的内容发言。
181
+ 代写 freezing / all-freezing / pass 等流程信号不计入(设计 11.1)。
182
+ 从共享事实(bare 派生的 messages)推导——loop 崩溃/重启不丢配额。
183
+
184
+ messages: {agent: [frontmatter_dict, ...]}(engine.each_agent_messages 的产物)。
185
+ **这是唯一实现**:此前 engine 私有一份 `_meeting_speak_count`,而
186
+ `--report` 又按 commit subject 另数一遍(两套口径,实测报告出过
187
+ "meeting 6/2" 的超限假象——6 是消息总数、2 是配额上限)。
188
+ """
189
+ return sum(1 for fm in messages.get(agent, [])
190
+ if fm.get("mode") == "meeting" and fm.get("type") == "message")
191
+
192
+
193
+ def frozen_agents(agents, all_last_types):
194
+ """已冻结的 agent 列表(按 participants 顺序)——**冻结集合单一实现**。
195
+
196
+ all_last_types: {agent: type|None}(aggregate_mode 的入参形态)。
197
+ 冻结 = type ∈ {freezing, all-freezing}(af 是冻结级联的推进态,
198
+ 与 engine 循环里"我已 af 则跳过"的宽松语义一致)。
199
+ 消费者:`--report`(现场算冻结进度);engine 循环内的 others_frozen
200
+ 判定仍就地写(热路径、语义略有差别——那里是"除我之外全冻结")。
201
+ """
202
+ return [a for a in agents
203
+ if all_last_types.get(a) in ("freezing", "all-freezing")]
204
+
205
+
206
+ def should_write_af(all_last_types):
207
+ """我是否应写 all-freezing:所有参与者最后一条都是 freezing(或 af)。
208
+
209
+ all_last_types: {agent: type|None}(None = 从未发言)
210
+ 语义(主协议 7.2 + 异步演进修正):
211
+ - 严格版(全员都是 freezing)会卡死——a 先写 af 后,b 看到
212
+ a=af, b/c=freezing → 永远不写 af(异步中间态,阶段 1 属性测试结论)。
213
+ - 宽松版:全员冻结(freezing 或 af)即写 af——异步下自然收敛。
214
+ """
215
+ if not all_last_types:
216
+ return False
217
+ if any(t is None for t in all_last_types.values()):
218
+ return False # 有人从未发言 → 不能全员冻结
219
+ return all(t in ("freezing", "all-freezing") for t in all_last_types.values())
220
+
221
+
222
+ def can_start_rr(all_last_types):
223
+ """starter 是否可启动 RR:所有参与者最后一条都是 all-freezing。
224
+
225
+ all_last_types: {agent: type|None}(None = 从未发言)
226
+ 语义(主协议 7.3):单条件,"所有 agent 最后一条都是 af"。
227
+ """
228
+ if not all_last_types:
229
+ return False
230
+ if any(t is None for t in all_last_types.values()):
231
+ return False
232
+ return all(t == "all-freezing" for t in all_last_types.values())
233
+
234
+
235
+ def aggregate_mode(all_last):
236
+ """全局模式 = 聚合所有 agent 最后一条消息(设计 11.8,审核#6 下沉 core)。
237
+
238
+ all_last: {agent: {type, mode, ...}|None}(每 agent 最后一条消息)
239
+ 返回: "concluded" | "round-robin" | "all-freezing" | "meeting"
240
+ 1. 任一 type == concluded → concluded
241
+ 2. 任一 mode == round-robin → round-robin(pass 存在)
242
+ 3. 所有 type ∈ {freezing, af} → all-freezing
243
+ 4. 否则 → meeting
244
+ """
245
+ if not all_last:
246
+ return "meeting" # 空 → 无任何 agent → 无冻结无 RR(review5 F3)
247
+ types = {a: (v["type"] if v else None) for a, v in all_last.items()}
248
+ modes = {a: (v["mode"] if v else None) for a, v in all_last.items()}
249
+ if any(t == "concluded" for t in types.values()):
250
+ return "concluded"
251
+ if any(m == "round-robin" for m in modes.values()):
252
+ return "round-robin"
253
+ if all(t in ("freezing", "all-freezing") for t in types.values()):
254
+ return "all-freezing"
255
+ return "meeting"