pi-multi-viewers 0.8.1 → 0.8.2

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
@@ -28,13 +28,15 @@ human_sayer.py 【human 通道】插话命令(单次/stdin/交互 -i)
28
28
  scripts/mv.sh 稳定入口 shim(exec mv_cli.py;路径被 prompt/README 引用)
29
29
  scripts/pi-probe.sh LLM 探针(跑 pi + 登记新 session → 残留检查器可追溯)
30
30
  scripts/check-residue.sh 残留检查(session/进程/目录三类;增删 scripts/ 时同步本节)
31
- mv_cli.py 命令行实现(prepare/start/status/report/wait/cleanup/view/say/viewers)
31
+ scripts/archive-result.sh 归档 result.md(机械部分:逐字复制+校验/存档头骨架/索引行/删源副本)
32
+ mv_cli.py 命令行实现(prepare/start/status/report/wait/cleanup/view/say/viewers/set-viewer)
32
33
  extensions/multi-viewers/ 【单一扩展单元】index.ts = 三命令 + shared.ts = 助手
33
34
  /multi-viewers 分析入口(prepare→暂停点弹窗→start→预填+打印观看命令)
34
35
  /multi-viewers-finish 收尾(status→确认→cleanup)
35
36
  /multi-viewers-say 插话(零 LLM,直接 spawn human_sayer.py)
36
37
  ⚠ extensions/ 平级禁放 .ts 助手(加载器会把平级文件当独立扩展)
37
- prompts/multi-viewers-setup.md /multi-viewers-setup 建视角入口(建议→你定→落盘→给审)
38
+ prompts/multi-viewers-setup.md /multi-viewers-setup 建视角入口(建议→你定→**mv.sh --set-viewer** 落盘→给审)
39
+ # 那四条本来写进 prompt 的纪律(命名/不覆盖/非空/校验)改由命令保证
38
40
  docs/design.md 设计文档(fork 源模式与规模口径 + 决策记录)
39
41
  package.json npm 包 pi-multi-viewers(pi.prompts 注册;**版本号唯一事实源**)
40
42
  templates/ AGENTS.md.tpl / agent.md.tpl / gitignore.tpl / spec-readme.md.tpl
package/README.md CHANGED
@@ -112,15 +112,16 @@ scripts/mv.sh --view # 一次性增量查看(主 pi
112
112
  scripts/mv.sh --say "<文本>" # 插话(命令行形态;pi 内用 /multi-viewers-say)
113
113
  scripts/mv.sh --status # 状态 + 路径(取值与含义以该命令输出为准)
114
114
  scripts/mv.sh --report # 只读报告(流程/配额/进程/LLM/档位对照;冷路径,不持久化)
115
- scripts/mv.sh --cleanup # 收尾(result.md 自动留存到 <dir>-result.md)
115
+ scripts/mv.sh --cleanup # 收尾(result.md + 报告都留存到 <dir>-*.md/.txt)
116
116
  scripts/mv.sh --viewers # 列出+校验当前项目 viewers/(只读;建视角时用)
117
+ scripts/mv.sh --set-viewer <名字> # 新建视角文件(正文从 stdin 读;只新建不覆盖)
117
118
  ```
118
119
 
119
120
  ## 视角文件写什么(`viewers/<视角名>.md`)
120
121
 
121
122
  建视角**推荐**走 `/multi-viewers-setup`(交互式:先给候选建议 → 你定建哪几个 →
122
- 落盘 → 展示给你审);也可以手写。写完用 `scripts/mv.sh --viewers` 自查(列出并
123
- 用代码判据校验名字与空正文)。
123
+ `--set-viewer` 落盘 → 展示给你审);也可以手写,写完用 `scripts/mv.sh --viewers`
124
+ 自查(列出并用代码判据校验名字与空正文)。
124
125
 
125
126
  一个视角文件 = **一份视角说明**,纯内容、无格式要求(无 frontmatter、
126
127
  无需标题,**文件名就是全部元数据**)。三个要点(措辞经实验验证):
package/docs/design.md CHANGED
@@ -103,7 +103,7 @@ compaction 的 `firstKeptEntryId` 起 + 其后的条目"——窗口内含 compa
103
103
  | `status-<agent>.json` | loop | `{"sessionID": ...}` | 流程(崩溃恢复) | 是(恢复用) | O(1) |
104
104
  | `pi-sessions/fork-src-*.jsonl` | pi | 文档化 session schema(`usage`/`stopReason`/`timestamp`/`thinkingLevel`) | fork 构建 + `--report` | 否(报告用) | O(MB) 全量 → **禁轮询** |
105
105
  | `result.md`(固定位) | resultWriter loop | 结论文档 | 人 | 是(收尾判据) | — |
106
- | `--report`(视图) | observability | 文本行 | 人(**三个出口**,见下) | **否**(不得升级为验收 gate) | 冷路径一次性 —— **O(session 大小)**:每 agent 读整个 fork-src jsonl(实测 3 × 789KB ≈ 2.4MB/次、50–150ms/次,×3 出口 <0.3s/次分析),**不得进入任何轮询路径**(e2e16 评审量化) |
106
+ | `--report`(视图) | observability | 文本行(`--cleanup` 另落盘 `<base>-report.txt`) | 人(**三个出口**,见下) | **否**(不得升级为验收 gate) | 冷路径一次性 —— **O(session 大小)**:每 agent 读整个 fork-src jsonl(实测 3 × 789KB ≈ 2.4MB/次、50–150ms/次,×3 出口 <0.3s/次分析),**不得进入任何轮询路径**(e2e16 评审量化) |
107
107
 
108
108
  **报告的字段集**(e2e17 评审后定稿,后续增补不计数——字段行以本表为准)——
109
109
  **谓词分组 + 对照 + 事实行**,
@@ -168,7 +168,7 @@ BOUNDARY_TYPE`)——**显式登记"历史(fork 携带)/ 本轮"的分界*
168
168
 
169
169
  **报告的打印位置**:`--report`(手动,任意时刻)+ `--cleanup` 前(自动,
170
170
  删目录前最后一次可读——目录删后 `--report` 不可用)。cleanup 层对报告
171
- fail-open(报告失败不阻断清理,且**打印**失败原因不静默)。
171
+ fail-open(报告失败不阻断清理,且**打印**失败原因不静默)。报告随 `--cleanup` **落盘**一份到 `<base>-report.txt`(与 `-result.md` 同级)——原先只在终端出现一次,目录删掉后无法复查(复盘时长口径时踩到,用户 2026-09-25 定)。
172
172
 
173
173
  消费规则:`meeting_fs.iter_after_boundary` 只产出边界之后的条目;**未找到
174
174
  边界(老产物/手工 session)→ 返回空、按 n/a 处理,不得退回全文扫描**
@@ -575,6 +575,10 @@ commit 是溯源记录、本节是长期引用点——不并存两份权威值
575
575
  resume 新命令面、env 回退配置、`runCli` 超时、启动路径继续优化(已在 1–2s 地板)、
576
576
  给 stalled 加第三种动作。**契约例外**:扩展作为包内第一方消费者**直连**
577
577
  `mv_cli.py`(实测 shim 61ms vs 直连 58–66ms,性能上零差异;按契约一致性记例外一行)。
578
+ **建视角流程(`/multi-viewers-setup`,仍是 prompt)**:机械部分下移到
579
+ `mv.sh --set-viewer <名字>`(正文从 stdin 读)——**命名规则 / 不覆盖已有 / 空正文拒绝 /
580
+ 写完校验并回显**四条由命令保证(此前是 prompt 里给 LLM 的纪律,会漏);prompt 只负责
581
+ **看项目给候选 + 内容撰写 + 与用户来回**(那才是 LLM 该做的)。
578
582
  **UI 通道按 mode 分级(2026-09-25 实测)**:dialog(`select`/`confirm`/`input`/`editor`)
579
583
  全模式可用(RPC/web 走请求-响应子协议、阻塞等用户;不带 `timeout` 即不倒计时,
580
584
  暂停点成立);`notify` 全模式可用(TUI = showStatus 行;pi-web = **追加进聊天流的
@@ -11,6 +11,9 @@
11
11
  真实缺陷(见下)。
12
12
  2. **决策溯源**:修复批次(commit)常引用报告中的编号(如"P0/P1/P2"),
13
13
  保留原文才能核对当时的事实与推理。
14
+
15
+ 归档动作的**机械部分**由 `scripts/archive-result.sh` 做(命名、正文逐字复制+字节校验、
16
+ 存档头骨架、索引行、删仓库根松散副本)——判断部分(主题/问题/commit)仍由人写。
14
17
  3. **方法论的样本**:报告本身演示了"观察句 vs 机制句"的证据强度要求
15
18
  (`docs/test-methodology.md` 方法 16)——其中也有几处机制句在讨论中被
16
19
  交叉复核推翻,是活教材。
package/meeting_fs.py CHANGED
@@ -47,6 +47,17 @@ def result_path(base):
47
47
  """
48
48
  return f"{base}-{RESULT_MD}"
49
49
 
50
+
51
+ def report_path(base):
52
+ """分析报告的固定落盘位(`<分析目录>-report.txt`,与 result.md 同级)。
53
+
54
+ 报告原本只在 `--cleanup` 时往终端打一次,目录一删就再也拿不到——复盘
55
+ (时长口径、配额、provider 失败这类)时无据可查(用户 2026-09-25 复盘
56
+ 回算不出实际时长,正是此缺口)。观测数字应当可复查,故随清理落盘一份。
57
+ `--report` 命令本身仍不持久化(冷路径视图,三个出口共享同一实现)。
58
+ """
59
+ return f"{base}-report.txt"
60
+
50
61
  # 协议参数默认值(gen_protocol 固化进 protocol.json)——**唯一声明点**:
51
62
  # CLI default、engine 签名默认、observability 的兜底读取都引用这里
52
63
  # (此前 10/7 在三处各写一遍,改一处不改另一处就会漂移)。
package/mv_cli.py CHANGED
@@ -54,6 +54,7 @@ USAGE = f"""用法:
54
54
  {PROG} --view [dir] [--since <ref>]
55
55
  {PROG} --say [dir] "<文本>"
56
56
  {PROG} --viewers # 列出并校验当前项目的 viewers/(只读;建视角时用)
57
+ {PROG} --set-viewer <名字> # 新建一个视角文件(正文从 stdin 读;只新建不覆盖)
57
58
 
58
59
  消费命令的 <dir> 可省略(自动发现本 session 当前分析——按 cwd 下
59
60
  mv-<PI_SESSION_ID>-* 最新;无匹配则报错要求显式传目录)
@@ -153,9 +154,6 @@ def cmd_viewers(args):
153
154
  目录**(消费命令的目录自动发现对此不适用——它找的是 mv-<sid>-*)。判据全部
154
155
  复用 `spec_gen` 的单一实现(列举 `list_agent_md` / 名字 `check_agent_name` /
155
156
  集合 `viewer_set_error`),这一层不另写一套规则。
156
-
157
- 数量不足(<2)在这里是**提示**不是错误:建 1 个是合法的中间状态,只有启动
158
- 一次分析时才要求 ≥2(那条判据仍由 `viewer_set_error` 独占)。
159
157
  """
160
158
  if args:
161
159
  fail(f"未知参数: {' '.join(args)}(--viewers 不接受参数——只检查项目 cwd 的 viewers/)")
@@ -163,6 +161,17 @@ def cmd_viewers(args):
163
161
  if not os.path.isdir(vdir):
164
162
  fail(f"未找到 {vdir}——视角文件放在项目 cwd 的 viewers/<视角名>.md"
165
163
  f"(文件名即视角名;可跑 /multi-viewers-setup 交互式建立)")
164
+ return _validate_and_print_viewers(vdir)
165
+
166
+
167
+ def _validate_and_print_viewers(vdir):
168
+ """列出 + 校验 + 打印(`--viewers` 与 `--set-viewer` 的**同一实现**)。
169
+
170
+ 判据全部复用 `spec_gen` 的单一实现(列举 `list_agent_md` / 名字
171
+ `check_agent_name` / 集合 `viewer_set_error`)——这一层不另写规则。
172
+ 数量不足(<2)是**提示**不是错误:建 1 个是合法中间状态(只有启动一次
173
+ 分析才要求 ≥2,那条判据由 `viewer_set_error` 独占)。
174
+ """
166
175
  names, _briefs, empty = spec_gen._discover_viewers(vdir)
167
176
  if not names:
168
177
  fail(f"{vdir} 下没有 *.md——文件名即视角名(如 viewers/效率.md)")
@@ -190,6 +199,38 @@ def cmd_viewers(args):
190
199
  return 0
191
200
 
192
201
 
202
+ def cmd_set_viewer(args):
203
+ """新建一个视角文件:`--set-viewer <名字>`,正文**从 stdin 读**。
204
+
205
+ 为什么是命令而不是"让 LLM 直接写文件":文件名即视角名,于是**命名规则 /
206
+ 不覆盖已有 / 空正文拒绝 / 写完校验并回显**这四条本来只能写在 prompt 里当
207
+ 纪律(LLM 会漏),现在由机制保证(判据复用 `spec_gen` 单一实现)。LLM/人
208
+ 只负责**内容**——那是它该做的部分。不提供 `--force`:本命令语义 = 只新建,
209
+ 改已有视角请直接编辑文件。
210
+ """
211
+ if len(args) != 1:
212
+ fail("用法: --set-viewer <名字>(正文从 stdin 读;如 "
213
+ "`mv.sh --set-viewer 效率 <<'EOF' … EOF`)")
214
+ name = args[0]
215
+ err = spec_gen.check_agent_name(name)
216
+ if err:
217
+ fail(f"非法视角名({err}):{name}")
218
+ vdir = os.path.join(os.getcwd(), "viewers")
219
+ target = os.path.join(vdir, f"{name}.md")
220
+ if os.path.exists(target):
221
+ fail(f"视角已存在,不覆盖: {target}(改名,或直接编辑该文件)")
222
+ body = sys.stdin.read().strip()
223
+ if not body:
224
+ fail(f"视角内容为空({name})——正文从 stdin 传入;空视角没有 lenses,"
225
+ f"分析会退化成同名随机视角")
226
+ os.makedirs(vdir, exist_ok=True)
227
+ with open(target, "w", encoding="utf-8") as f:
228
+ f.write(body + "\n")
229
+ print(f"[set-viewer] 已写入 {target}\n")
230
+ print(body + "\n")
231
+ return _validate_and_print_viewers(vdir)
232
+
233
+
193
234
  def cmd_status(args):
194
235
  d = _dir_only("--status", args)
195
236
  return _call([PYTHON, START_DISCUSSION, "--dir", d, "--status"])
@@ -418,6 +459,8 @@ def main(argv=None):
418
459
  return cmd_start(rest[0], rest[1:])
419
460
  if cmd == "--viewers":
420
461
  return cmd_viewers(rest)
462
+ if cmd == "--set-viewer":
463
+ return cmd_set_viewer(rest)
421
464
  if cmd == "--status":
422
465
  return cmd_status(rest)
423
466
  if cmd == "--report":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multi-viewers",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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,
@@ -15,8 +15,8 @@ description: 建立多视角分析的 viewers/ 视角文件(交互式:先建
15
15
  1. <名字> —— <一行镜头>(与其它候选怎样互补/对立)
16
16
 
17
17
  - 这一步**只提议**:不建文件、不改任何东西
18
- - 名字:中文、短(≤32 字符)、不含空白与路径分隔符、**不要用 `human`**(保留名)
19
18
  - 建几个**由用户决定**(只建 1 个也行)
19
+ - 名字规则由第 3 步的命令强制(顺带一提:中文、短、不含空白与路径分隔符)
20
20
 
21
21
  **然后结束本回合,等用户输入。**
22
22
 
@@ -25,10 +25,9 @@ description: 建立多视角分析的 viewers/ 视角文件(交互式:先建
25
25
  用户会告诉你建哪几个(可能只有一个、也可能点名不在候选里的)。**以用户输入为准**;
26
26
  若某个视角该用什么镜头没说清 → **问一句**,不要替他定。
27
27
 
28
- ## 第 3 步:按用户输入建文件(只新建)
28
+ ## 第 3 步:用命令建文件(内容经 stdin 传给命令,不要直接写文件)
29
29
 
30
- 写之前**先读 `README.md` 的「视角文件写什么」节**(唯一事实源:三个要点 + 正误对照 +
31
- 命名规则)。要点:
30
+ 先读 `README.md` 的「视角文件写什么」节(唯一事实源:三个要点 + 正误对照)。要点:
32
31
 
33
32
  - 纯内容、无 frontmatter、无标题——**文件名就是全部元数据**
34
33
  - 必含三条:① **单一 lenses**(所有观点必须从该视角出发)② **不越界**(其它视角由
@@ -37,19 +36,23 @@ description: 建立多视角分析的 viewers/ 视角文件(交互式:先建
37
36
  - **只写视角本身**:身份("你是 X")、参与者名单、消息格式、独立参与者纪律都由脚本
38
37
  生成,**不要写进文件**
39
38
 
40
- 写完跑一次只读检查(会列出视角并用代码判据校验名字与空正文):
39
+ 每个视角调用一次(正文经 stdin;**命名合法性、不覆盖已有、空正文拒绝、写完校验并
40
+ 回显**都由命令保证,不需要你另外检查):
41
41
 
42
42
  ```bash
43
- ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --viewers
43
+ ~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --set-viewer <视角名> <<'EOF'
44
+ <视角正文>
45
+ EOF
44
46
  ```
45
47
 
46
- **已有同名文件 → 不覆盖**:告诉用户"该视角已存在",停下等他决定(改名,或他明确
47
- 要求改动)。
48
+ 命令会回显**写入路径 + 正文全文 + 校验结果**——那就是第 4 步要展示的东西。
49
+ 若命令报"视角已存在":告诉用户,停下等他决定(改名,或他明确要求改动)。
48
50
 
49
51
  ## 第 4 步:展示,等审阅
50
52
 
51
- 把每个新建文件的**路径 + 全文**展示给用户,请他提修改意见或确认:
53
+ 把命令的回显(路径 + 全文 + 校验)给用户看,请他提修改意见或确认:
52
54
 
53
- - 有意见 → 改 → **再展示**(反复直到他确认)
55
+ - 有意见 → **直接编辑该文件**(`viewers/<名字>.md`)→ 跑一次
56
+ `~/.pi/agent/npm/node_modules/pi-multi-viewers/scripts/mv.sh --viewers` 复核 → 再展示(反复直到他确认)
54
57
  - 确认后一行交接:视角已建好(启动一次分析至少需要 2 个视角),接下来
55
58
  `/multi-viewers "<主题>"` 即可
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env bash
2
+ # archive-result.sh —— 把一场分析的 result.md 归档进 docs/reviews/
3
+ #
4
+ # 为什么是脚本:每场分析后都要做同一串机械动作,靠人/LLM 记(实测会漏——
5
+ # 最容易漏的是索引行)。这里把**机械部分**固化成命令:
6
+ # · 命名(<日期>-<slug>.md)
7
+ # · 正文**逐字复制**并做字节校验(存档不改原文)
8
+ # · 存档头骨架(TODO 标记留在文件里,判断内容仍由人写)
9
+ # · 索引表加一行(插在最后一行存档之后)
10
+ # · 删掉仓库根的松散副本
11
+ # **判断部分不代劳**:写什么主题/抓到什么问题/落地哪个 commit,是人的判断。
12
+ #
13
+ # 用法:
14
+ # scripts/archive-result.sh <result.md> --slug <slug> [--date YYYY-MM-DD] [--dry-run]
15
+ # 例:
16
+ # scripts/archive-result.sh mv-mv-main-20260925-105614-result.md \
17
+ # --slug multi-viewers-postfix-review
18
+ #
19
+ # 退出码:0 成功(或 dry-run);1 参数/环境错;2 目标已存在(不覆盖)
20
+
21
+ set -u
22
+ HERE="$(cd "$(dirname "$0")/.." && pwd)"
23
+ REVIEWS="$HERE/docs/reviews"
24
+ INDEX="$REVIEWS/README.md"
25
+
26
+ SRC=""; SLUG=""; DATE="$(date +%F)"; DRY=0
27
+ while [ $# -gt 0 ]; do
28
+ case "$1" in
29
+ --slug) SLUG="${2:-}"; shift 2 ;;
30
+ --date) DATE="${2:-}"; shift 2 ;;
31
+ --dry-run) DRY=1; shift ;;
32
+ -h|--help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
33
+ -*) echo "错误: 未知参数 $1" >&2; exit 1 ;;
34
+ *) SRC="$1"; shift ;;
35
+ esac
36
+ done
37
+
38
+ [ -n "$SRC" ] || { echo "错误: 缺少 result.md 路径(--help 看用法)" >&2; exit 1; }
39
+ [ -n "$SLUG" ] || { echo "错误: 缺少 --slug <slug>" >&2; exit 1; }
40
+ [ -f "$SRC" ] || { echo "错误: 文件不存在: $SRC" >&2; exit 1; }
41
+ case "$SLUG" in *[!a-zA-Z0-9._-]*) echo "错误: slug 只允许字母/数字/._-(当前: $SLUG)" >&2; exit 1 ;; esac
42
+ [ -f "$INDEX" ] || { echo "错误: 找不到索引 $INDEX" >&2; exit 1; }
43
+
44
+ DEST="$REVIEWS/$DATE-$SLUG.md"
45
+ if [ -e "$DEST" ]; then
46
+ echo "错误: 目标已存在(不覆盖): $DEST" >&2; exit 2
47
+ fi
48
+
49
+ if [ "$DRY" -eq 1 ]; then
50
+ echo "[dry-run] 将归档: $SRC → $DEST"
51
+ echo "[dry-run] 正文 $(wc -c < "$SRC") 字节,逐字复制"
52
+ echo "[dry-run] 将写存档头骨架(含 TODO)+ 在 $INDEX 加一行 + 删 $SRC"
53
+ exit 0
54
+ fi
55
+
56
+ # 1) 存档头骨架 + 正文(逐字)
57
+ {
58
+ cat <<EOF
59
+ <!-- 存档:docs/reviews/$DATE-$SLUG.md
60
+ 来源:一次真实多视角分析的 result.md 原文(未删改,仅加本头与下方说明)。
61
+ 分析场次目录已随 cleanup 删除;文中消息编号不可再核验,仅作溯源线索
62
+ (与代码注释引用约定一致:行为以自描述为准)。 -->
63
+
64
+ # 存档说明
65
+
66
+ - **主题**:TODO(一句话)
67
+ - **场次**:\`TODO(分析目录名)\`(视角 / 档位 / 收敛方式)
68
+ - **判定/发现**:TODO
69
+ - **落地**:TODO(commit 或"未落地")
70
+ - **备注**:TODO(可不填则删掉本行)
71
+
72
+ EOF
73
+ cat "$SRC"
74
+ } > "$DEST"
75
+
76
+ # 2) 逐字校验(正文必须与源**逐字相同**——存档不改原文)
77
+ if ! cmp -s <(tail -c "$(wc -c < "$SRC")" "$DEST") "$SRC"; then
78
+ echo "错误: 正文校验失败(存档与源不一致)——已保留 $DEST 供排查" >&2
79
+ exit 1
80
+ fi
81
+ echo "[archive] 正文逐字校验通过($(wc -c < "$SRC") 字节)"
82
+
83
+ # 3) 索引加一行(紧跟最后一行存档;TODO 留给写索引的人)
84
+ python3 - "$INDEX" "$DATE-$SLUG.md" <<'PY'
85
+ import sys
86
+ idx_path, fname = sys.argv[1], sys.argv[2]
87
+ lines = open(idx_path, encoding="utf-8").read().splitlines(True)
88
+ hits = [i for i, l in enumerate(lines) if l.startswith("| `2026-")]
89
+ if not hits:
90
+ print("错误: 索引里找不到存档行(| `2026-…)", file=sys.stderr)
91
+ sys.exit(1)
92
+ row = f"| `{fname}` | TODO 主题 | TODO 抓到的问题 | TODO 落地 commit |\n"
93
+ lines.insert(hits[-1] + 1, row)
94
+ open(idx_path, "w", encoding="utf-8").write("".join(lines))
95
+ print(f"[archive] 索引已加一行({idx_path};TODO 待补)")
96
+ PY
97
+ [ $? -eq 0 ] || exit 1
98
+
99
+ # 4) 删仓库根松散副本(**只删源文件**,且必须在写成功后)
100
+ rm -f "$SRC"
101
+ echo "[archive] 已写 $DEST;源副本已删;下一步:补 $DEST 与索引行里的 TODO"
@@ -450,11 +450,24 @@ def cleanup_discussion(base):
450
450
  # 报告是附加信息、清理是主职责:报告生成失败**不阻断**清理
451
451
  # (fail-open 只在这一层兜底——build_report 内部各段已各自 fail-open)。
452
452
  print("[cleanup] —— 本次分析报告(删除目录前最后一次可读)——")
453
+ lines = None
453
454
  try:
454
- for line in build_report(base):
455
+ lines = list(build_report(base))
456
+ for line in lines:
455
457
  print(line)
456
458
  except Exception as e: # noqa: BLE001(兜底不吞:打印)
457
459
  print(f"[cleanup] 报告生成失败(不影响清理): {e!r}")
460
+ if lines is not None:
461
+ # 落盘一份(与 <base>-result.md 同级):报告本来只在终端出现一次,
462
+ # 目录删掉后 --report 也不可用 → 观测数字不可复查(复盘时长口径时
463
+ # 踩过)。与 result.md 同样的 fail-open:写不动不阻断清理。
464
+ rp = meeting_fs.report_path(base)
465
+ try:
466
+ with open(rp, "w", encoding="utf-8") as f:
467
+ f.write("\n".join(lines) + "\n")
468
+ print(f"[cleanup] 报告已保存 → {rp}")
469
+ except OSError as e:
470
+ print(f"[cleanup] 报告保存失败(不影响清理): {e!r}")
458
471
  shutil.rmtree(base)
459
472
  print(f"[cleanup] 已删除目录 {base}(含 pi-sessions)")
460
473