pi-legion 2.0.3 → 3.0.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/CHANGELOG.md +93 -1
- package/CONTRIBUTING.md +27 -24
- package/README.md +103 -19
- package/docs/contracts/osc-busy-plugin.md +72 -0
- package/docs/contracts/spawn-default-inprocess-v3.md +322 -0
- package/docs/contracts/wait-not-stop.md +30 -0
- package/docs/design.md +2 -2
- package/docs/headless-child-contract.md +83 -0
- package/docs/inprocess-supervisor.md +140 -0
- package/docs/qa-hunting-round.md +150 -0
- package/docs/subagents-teams.md +55 -18
- package/docs/tui-qa-playbook.md +284 -0
- package/index.ts +396 -62
- package/package.json +15 -8
- package/skills/pi-legion/SKILL.md +5 -0
- package/src/agents/agents.ts +34 -6
- package/src/agents/context.ts +83 -1
- package/src/agents/overrides.ts +1 -0
- package/src/agents/settings.ts +122 -1
- package/src/api.ts +23 -0
- package/src/extension/child-guard.ts +42 -3
- package/src/extension/legion-commands.ts +15 -3
- package/src/extension/legion-doctor.ts +55 -0
- package/src/extension/legion-stop.ts +6 -2
- package/src/extension/playbook.ts +3 -3
- package/src/extension/runtime.ts +57 -8
- package/src/extension/scout-guard.ts +39 -1
- package/src/extension/session-errors.ts +191 -0
- package/src/extension/timing.ts +391 -0
- package/src/runs/args.ts +8 -0
- package/src/runs/orchestrator.ts +125 -16
- package/src/shared/output-cap.ts +2 -2
- package/src/shared/session.ts +2 -1
- package/src/shared/timing-breakdown.ts +23 -0
- package/src/shared/types.ts +74 -0
- package/src/supervisor/child-context.ts +90 -0
- package/src/supervisor/child-memory.ts +27 -3
- package/src/supervisor/inprocess-depth.ts +71 -0
- package/src/supervisor/inprocess-supervisor.ts +1011 -0
- package/src/supervisor/routing-supervisor.ts +179 -0
- package/src/supervisor/rpc-supervisor.ts +14 -0
- package/src/supervisor/spawn-mode.ts +35 -0
- package/src/supervisor/types.ts +13 -0
- package/src/supervisor/ui-proxy.ts +78 -0
- package/src/tui/live-overlay.ts +297 -24
- package/src/tui/status.ts +71 -35
- package/src/tui/viz/layout.ts +50 -6
- package/src/tui/viz/model.ts +6 -0
- package/src/tui/viz/panels.ts +164 -12
- package/src/tui/viz/railway.ts +154 -21
- package/src/tui/viz/rate.ts +36 -14
- package/src/tui/viz/view.ts +69 -10
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# QA 狩猎 round 编排手册(multi-agent orchestration)
|
|
2
|
+
|
|
3
|
+
> **给谁看**:任何要在这个仓库组织「一轮」多子 agent 缺陷狩猎的主 agent(或人)。读完即可开跑。
|
|
4
|
+
>
|
|
5
|
+
> **和 `tui-qa-playbook.md` 的分工**:那份是**单人狩猎手册**——三层证据链、容器/tmux 基建、
|
|
6
|
+
> 单个角度怎么取证、issue 怎么写。本手册是**round 级编排**——多角色怎么拆、派发顺序、
|
|
7
|
+
> 判据纪律怎么跨角色执行、卡死了怎么止损。两份都要读,先读那份。
|
|
8
|
+
>
|
|
9
|
+
> **实战背书**:2026-10-16 round2(v2.0.4 @ b00d4c7):4 角度并行 → 29 candidate →
|
|
10
|
+
> 判 true 9(major 6 / minor 3)→ issue #172–#181,转化率 31%。全程含一次卡死事故处置(§5)。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 0. 核心原则:四角色拆权,发现者不验证,验证者不提交
|
|
15
|
+
|
|
16
|
+
单路 hunter 自搜自证,误报率高且慢。流水线把权力拆开:
|
|
17
|
+
|
|
18
|
+
| 角色 | 只做 | 永不 |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| 主 agent(你) | 冻结狩猎面、派发、steer 补给、裁决、收官 | 自己跑侦察读代码 |
|
|
21
|
+
| `scout` | 冻结面事实清单(带 file:line) | 出方案、下结论 |
|
|
22
|
+
| `qa-hunter` × N | 按一个角度找 candidate + 原始证据 + **detector 原文** + 自陈伪影风险 | 验证、自证、提 issue |
|
|
23
|
+
| `qa-repro` × N | 独立复现,判 true / false / downgraded | 沿用 hunter 的判据、提 issue |
|
|
24
|
+
| `qa-reporter` × 1 | 查重后按模板提交已判 true 的 finding | 提 candidate、并发多路提交 |
|
|
25
|
+
|
|
26
|
+
角色卡在 `agents/`(`agents/qa-hunter.md` 等),团队编排在 `.pi/settings.json` 的
|
|
27
|
+
`subagents.teams["qa-hunting"]`。切换团队:会话里 `/subagents` 或写 settings 后重启会话。
|
|
28
|
+
|
|
29
|
+
## 1. Round 流程(按序,别跳步)
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
0. 冻结基线:commit hash / 版本 / 容器 / 已 open issue 清单 / 上一轮已推翻的怀疑点
|
|
33
|
+
1. 冻结面 scout(1 路,只读,先发)
|
|
34
|
+
└─ 产出:带 file:line 的事实清单——env 读取点 / 契约条款落点 / 跨面口径 /
|
|
35
|
+
状态机转移表 / 可执行复现入口
|
|
36
|
+
2. hunter 并行派发(4 路足够,角度互斥见 §2)——不等 scout 完也可以先发,
|
|
37
|
+
但 scout 产出一到就 steer 补给各 hunter
|
|
38
|
+
3. steer 补给:把 scout 事实按角度切给各 hunter,并声明「哪些怀疑点已被推翻,别再扫」
|
|
39
|
+
4. 每路 hunter 交卷 → 立即派对应 qa-repro(流水线化,别攒齐)
|
|
40
|
+
5. repro 判 true → 派 reporter(全轮只用 1 路串行提交,防查重竞态;
|
|
41
|
+
后续判 true 的追加 steer 给同一个 reporter)
|
|
42
|
+
6. 收官:round notes 落库 `.qa-hunt/<date>-roundN/notes.md`(战果表 +
|
|
43
|
+
被推翻清单 + 事故 + 下轮改进 + 未覆盖)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**实测收益**:冻结面 scout 前置为每路 hunter 省 ~20 分钟定位时间,且常直接推翻
|
|
47
|
+
主 agent 一半的怀疑点(round2:「env 全带 `Number.isFinite`,NaN 穿透 0 命中」)。
|
|
48
|
+
|
|
49
|
+
## 2. 角度切法(互斥,4 路足够)
|
|
50
|
+
|
|
51
|
+
| 角度 | 内容 | 边界(不做什么) |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| A 契约 vs 实现 | 冻结契约(`docs/tui-viewer-contract.md` 等)逐条在真实 TUI 跑 | 不做非契约项的打磨建议 |
|
|
54
|
+
| B 配置边界 | 每个 env / settings 字段喂 `""`/`0`/`-1`/`NaN`/`Infinity`/浮点/`abc`/`1e18` | 不做 TUI 视觉 |
|
|
55
|
+
| C 跨面口径 + 单位混淆 | 同一数据在多处渲染是否同口径;截断按码点还是显示宽度 | 不做交互/键位 |
|
|
56
|
+
| D 状态机 / 生命周期 / 清理 | 转移表交叉、槽位释放、清理阈值、子进程回收、doctor | 不做 A/B/C 已覆盖面 |
|
|
57
|
+
|
|
58
|
+
角度数 >4 时 hunter 上下文互相重叠,总产出反而下降。
|
|
59
|
+
|
|
60
|
+
## 3. 派发任务卡(hunter 模板,必填项别省)
|
|
61
|
+
|
|
62
|
+
每路 hunter 的任务卡必须包含:
|
|
63
|
+
|
|
64
|
+
```markdown
|
|
65
|
+
## 环境
|
|
66
|
+
- 仓库 / 版本 / commit
|
|
67
|
+
- 容器:docker exec <name> bash -lc '<cmd>'(/plugin 挂载,pi/tmux 可用)
|
|
68
|
+
- 先读 docs/tui-qa-playbook.md §3 铁律,再读相关契约章节
|
|
69
|
+
|
|
70
|
+
## 你的角度(+ 明确边界:不做什么,撞到别的是别人的)
|
|
71
|
+
## 时间预算(如 60min)+ candidate 上限(≤8,防跑飞)
|
|
72
|
+
## 硬性规则
|
|
73
|
+
- 每个 bash 调用必须 `timeout 60 <cmd>` 包裹(命令级 timeout 是必填项,
|
|
74
|
+
时间预算替代不了它——见 §5 事故)
|
|
75
|
+
- 禁止调用 poll-loop 等待型 API(如 acquireVerifySlot);长脚本内加
|
|
76
|
+
`setTimeout(() => process.exit(9), 30_000)` 自杀兜底
|
|
77
|
+
- 产物落 .qa-hunt/<round>/<角度>/,issue 内联最小复现脚本
|
|
78
|
+
|
|
79
|
+
## 输出格式
|
|
80
|
+
- 每个 candidate:症状 + file:line + detector 原文(「我用什么表达式/符号判断状态成立」)
|
|
81
|
+
+ 原始证据路径 + 自陈「我未排除的伪影可能」
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**明确列出已提交 issue 的根因**,hunter 撞到同根因直接跳过。
|
|
85
|
+
|
|
86
|
+
## 4. 判据纪律(决定转化率的关键)
|
|
87
|
+
|
|
88
|
+
### hunter 侧
|
|
89
|
+
- **detector 必须写进报告**,供 repro 独立审查——这是 repro 换判据的依据。
|
|
90
|
+
- **禁止自证**:别用被测代码自己的查询去数被测代码。要验槽位上限,别用编排器内部那条
|
|
91
|
+
SQL 去数,把同一份 DB 状态喂给**已发布的** `checkLaunchBudget()` 交叉比对。
|
|
92
|
+
- **报下游可观测后果**:别报「解析器返回了这个值」,报「用户实际看到的字节数 /
|
|
93
|
+
目录在不在 / 子进程起没起来」。
|
|
94
|
+
|
|
95
|
+
### repro 侧
|
|
96
|
+
- **判据必须与 hunter 不同**,否则等于自证。先 grep hunter 的 detector 是否有歧义,再自建。
|
|
97
|
+
- 优先攻 hunter 自陈的伪影风险。不可达路径按 playbook §4 降级为理论风险。
|
|
98
|
+
- **先读既有测试再判**:作者的测试可能已把某行为断言为设计意图(round2:
|
|
99
|
+
`state-machine.ts:108` `parent_decision` 直放被单测断言为设计意图,不报)。
|
|
100
|
+
- 修过的历史行为要查 issue 历史,别把有意的两套语义判成不一致。
|
|
101
|
+
|
|
102
|
+
### 高频证伪模式(round2 推翻 20 条中的代表,新 round 先对照这份清单)
|
|
103
|
+
- 「A/B 两面不一致」→ 查契约是否**明文定义**了这个拆分(fanout vs maxActiveNodes,
|
|
104
|
+
契约 §11.1 明文,非缺陷)
|
|
105
|
+
- 「状态字面冲突」→ 查**可达性**(`awaiting`/`unknown` 经 `runtime.ts:1210-1216`
|
|
106
|
+
映射掉,永不进 `node.status`)
|
|
107
|
+
- 「边界系统性差 1」→ 实测是不是只有极窄边界带(round vs floor 仅 500ms 带)
|
|
108
|
+
- 「截断/宽度有 bug」→ pi-tui 自己处理 ANSI/OSC8/宽字符/ZWJ,**先证伪再报**
|
|
109
|
+
- 「某模式下功能静默」→ 用**非本仓库的对照扩展**证明是宿主行为还是本仓库行为
|
|
110
|
+
(`print` 下 `/legion` 静默 → 根因在 pi 宿主 `runner.js:132` noOpUIContext,本仓库无责)
|
|
111
|
+
- 「敏感信息泄露」→ **必须真实触发**,别用构造的假密钥(round2 真实触发后结论反转)
|
|
112
|
+
|
|
113
|
+
## 5. ⚠️ 卡死止损(round2 事故:130 分钟)
|
|
114
|
+
|
|
115
|
+
**现象**:`subagent status` 一直 `working`,但零进展。
|
|
116
|
+
|
|
117
|
+
**判据(三条同时成立即卡死,不是慢)**:
|
|
118
|
+
1. 子会话 JSONL 静默 >60min(`stat` mtime)——改进阈值:**>15min 即查**
|
|
119
|
+
2. 最后一条记录是 `toolCall` 而**无对应 toolResult**
|
|
120
|
+
3. 产物目录 mtime 冻结,之后 0 个新文件
|
|
121
|
+
|
|
122
|
+
**止损流程**:
|
|
123
|
+
```text
|
|
124
|
+
1. 取证不急着 kill:stat JSONL mtime / tail 最后一条 / 查产物 mtime / ps 找子进程
|
|
125
|
+
2. subagent({ action: "stop", name: <child> }) → 确认 RPC 子进程回收(ps 复查 gone)
|
|
126
|
+
3. 清理孤儿进程(mock-llm 等,ps -eo pid,cmd | grep "[m]ock-llm" 定点 kill,
|
|
127
|
+
禁 pkill -f 自杀)
|
|
128
|
+
4. 补派接手者:任务卡带上已完成部分 + 防挂死硬性规则(§3),别从零重跑
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
round2 根因:hunter 调了 `acquireVerifySlot()`(轮询等槽位的阻塞 API),bash 调用无
|
|
132
|
+
`timeout` 包裹 → 子进程挂死 → 永久 working。**根因是派发漏洞**:任务卡写了时间预算
|
|
133
|
+
却没写命令级 timeout。这就是 §3 把它列为必填项的原因。
|
|
134
|
+
|
|
135
|
+
## 6. 收官 checklist
|
|
136
|
+
|
|
137
|
+
- [ ] 所有判 true 的 finding 已提交 issue(reporter 报告里有 # 号清单)
|
|
138
|
+
- [ ] round notes 写入 `.qa-hunt/<date>-roundN/notes.md`:战果表 / 被推翻清单及原因 /
|
|
139
|
+
事故记录 / 下轮改进 / 未覆盖留给下轮
|
|
140
|
+
- [ ] `.qa-hunt/` 目前 untracked:issue 里必须内联最小复现脚本,别只引用本地路径
|
|
141
|
+
- [ ] 检查无遗留 mock-llm / pi 孤儿进程
|
|
142
|
+
- [ ] 仓库 `git status` 干净(hunter/repro 只许写 `.qa-hunt/`,不许碰 `src/`)
|
|
143
|
+
|
|
144
|
+
## 7. 角色卡与团队配置
|
|
145
|
+
|
|
146
|
+
- 角色定义:`agents/qa-hunter.md`、`agents/qa-repro.md`(若有)、`agents/scout.md`、
|
|
147
|
+
`agents/advisor.md`
|
|
148
|
+
- 团队编排示例:`docs/subagents-teams.md`;本仓库实配:`.pi/settings.json`
|
|
149
|
+
→ `subagents.teams["qa-hunting"]`
|
|
150
|
+
- 子 agent 卡死重试 2 次仍失败 → 升级咨询 `advisor`(见各角色卡的失败止损节)
|
package/docs/subagents-teams.md
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
# Team configuration examples
|
|
2
2
|
|
|
3
|
-
`subagents.teams` maps team names to an ordered roster
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
`subagents.teams` maps team names to an ordered roster **and optional
|
|
4
|
+
team-scoped parent/child context**. A member can be a role name string or an
|
|
5
|
+
object with an `agent` name and per-member overrides. Each team may also set
|
|
6
|
+
`parentContext` / `childContext` (same forms as the global keys: inline
|
|
7
|
+
markdown, `@path`, or a string array; `false` disables that side). Team
|
|
8
|
+
context **replaces** the global value for that side — it never merges.
|
|
9
|
+
|
|
10
|
+
The examples below show two squads with **different operating workflows**, not
|
|
11
|
+
just different models. Replace every model placeholder with an ID available
|
|
12
|
+
from your provider.
|
|
8
13
|
|
|
9
14
|
```json
|
|
10
15
|
{
|
|
11
16
|
"subagents": {
|
|
17
|
+
"parentContext": "Global fallback: prefer named teams for real workflows.",
|
|
12
18
|
"teams": {
|
|
13
19
|
"fe-premium": {
|
|
14
20
|
"description": "Frontend · complex projects",
|
|
21
|
+
"parentContext": "FE premium parent workflow:\n1. Scout UX surface and design tokens first.\n2. Prototype before planner locks the slice.\n3. Designer gate before worker lands UI.\n4. Reviewer checks a11y + visual regress; escalate to advisor on product ambiguity.\n5. Stop when designer+reviewer both approve; do not expand scope mid-flight.",
|
|
22
|
+
"childContext": "FE premium child guardrails: stay in the assigned component tree; no API shape changes without planner; hand off diffs with screenshot notes.",
|
|
15
23
|
"members": [
|
|
16
24
|
{ "agent": "scout", "model": "your-provider/strong-model", "thinking": "high" },
|
|
17
25
|
{ "agent": "prototype", "model": "your-provider/strong-model", "thinking": "high" },
|
|
@@ -23,8 +31,21 @@ model is required by these templates.
|
|
|
23
31
|
"centurion"
|
|
24
32
|
]
|
|
25
33
|
},
|
|
34
|
+
"be-value": {
|
|
35
|
+
"description": "Backend · standard projects",
|
|
36
|
+
"parentContext": "BE value parent workflow:\n1. Scout existing handlers and data model only — no greenfield redesign.\n2. Planner writes a thin acceptance list (tests + curl).\n3. Worker implements behind feature flags; one PR-sized slice.\n4. Reviewer focuses on contracts/errors; advisor only on cross-service risk.\n5. Stop when acceptance tests pass; escalate early if schema migration is required.",
|
|
37
|
+
"childContext": "BE value child guardrails: do not touch frontend; prefer existing patterns; document migration needs instead of applying them.",
|
|
38
|
+
"members": [
|
|
39
|
+
{ "agent": "scout", "model": "your-provider/value-model", "thinking": "high" },
|
|
40
|
+
{ "agent": "planner", "model": "your-provider/strong-model", "thinking": "xhigh" },
|
|
41
|
+
{ "agent": "worker", "model": "your-provider/value-model", "thinking": "xhigh" },
|
|
42
|
+
{ "agent": "reviewer", "model": "your-provider/value-model", "thinking": "max" },
|
|
43
|
+
{ "agent": "advisor", "model": "your-provider/strong-model", "thinking": "xhigh" },
|
|
44
|
+
"centurion"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
26
47
|
"fe-value": {
|
|
27
|
-
"description": "Frontend · standard projects",
|
|
48
|
+
"description": "Frontend · standard projects (roster only — context falls back to global)",
|
|
28
49
|
"members": [
|
|
29
50
|
{ "agent": "scout", "model": "your-provider/value-model", "thinking": "high" },
|
|
30
51
|
{ "agent": "prototype", "model": "your-provider/value-model", "thinking": "high" },
|
|
@@ -38,6 +59,8 @@ model is required by these templates.
|
|
|
38
59
|
},
|
|
39
60
|
"be-premium": {
|
|
40
61
|
"description": "Backend · complex projects",
|
|
62
|
+
"parentContext": false,
|
|
63
|
+
"childContext": "@be-premium-child.md",
|
|
41
64
|
"members": [
|
|
42
65
|
{ "agent": "scout", "model": "your-provider/strong-model", "thinking": "high" },
|
|
43
66
|
{ "agent": "planner", "model": "your-provider/strong-model", "thinking": "max" },
|
|
@@ -46,23 +69,23 @@ model is required by these templates.
|
|
|
46
69
|
{ "agent": "advisor", "model": "your-provider/strong-model", "thinking": "xhigh" },
|
|
47
70
|
"centurion"
|
|
48
71
|
]
|
|
49
|
-
},
|
|
50
|
-
"be-value": {
|
|
51
|
-
"description": "Backend · standard projects",
|
|
52
|
-
"members": [
|
|
53
|
-
{ "agent": "scout", "model": "your-provider/value-model", "thinking": "high" },
|
|
54
|
-
{ "agent": "planner", "model": "your-provider/strong-model", "thinking": "xhigh" },
|
|
55
|
-
{ "agent": "worker", "model": "your-provider/value-model", "thinking": "xhigh" },
|
|
56
|
-
{ "agent": "reviewer", "model": "your-provider/value-model", "thinking": "max" },
|
|
57
|
-
{ "agent": "advisor", "model": "your-provider/strong-model", "thinking": "xhigh" },
|
|
58
|
-
"centurion"
|
|
59
|
-
]
|
|
60
72
|
}
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
75
|
}
|
|
64
76
|
```
|
|
65
77
|
|
|
78
|
+
Notes on the examples:
|
|
79
|
+
|
|
80
|
+
- **`fe-premium` vs `be-value`**: different parent/child workflow text so
|
|
81
|
+
switching `subagents.team` / `PI_SUBAGENTS_TEAM` changes operating
|
|
82
|
+
discipline, not only the roster/models.
|
|
83
|
+
- **`fe-value`**: omits team context → falls back to global
|
|
84
|
+
`parentContext` / `childContext` (or convention files).
|
|
85
|
+
- **`be-premium`**: `parentContext: false` disables parent injection for that
|
|
86
|
+
team (no fallthrough to global). `childContext: "@be-premium-child.md"` is
|
|
87
|
+
resolved relative to the settings file that declares the team.
|
|
88
|
+
|
|
66
89
|
Each team's `description` and `members` shape follows
|
|
67
90
|
`subagents.teams.<name>`. Member objects apply their own `model` and
|
|
68
91
|
`thinking` values to that role in this team; those values override the same
|
|
@@ -73,4 +96,18 @@ plain roster entry with no member-level override.
|
|
|
73
96
|
To make one team the settings default, add a sibling `"team": "be-value"`
|
|
74
97
|
under `"subagents"`. `PI_SUBAGENTS_TEAM` takes precedence over that configured
|
|
75
98
|
`subagents.team` value when selecting the team. See the README's [Team
|
|
76
|
-
配置](../README.md#team-配置) section for the selection
|
|
99
|
+
配置](../README.md#team-配置) section for the selection, context resolve order,
|
|
100
|
+
and override semantics.
|
|
101
|
+
|
|
102
|
+
## spawnMode (rpc | inprocess)
|
|
103
|
+
|
|
104
|
+
Omitted → `inprocess`, with an automatic `rpc` fallback when the resolved spawn
|
|
105
|
+
needs an OS worktree (`worktree: true`, the writer/centurion default). A
|
|
106
|
+
non-`pi` role is rejected before this point, so it never reaches the fallback.
|
|
107
|
+
An explicit `spawnMode: "inprocess"` + `worktree: true` is
|
|
108
|
+
still refused with `INVALID_PARAMS`. Precedence: frontmatter →
|
|
109
|
+
`subagents.agentOverrides.<role>` → team member override.
|
|
110
|
+
|
|
111
|
+
See [inprocess-supervisor.md](./inprocess-supervisor.md) (#197) for the backend
|
|
112
|
+
details, the separate `subagents.inprocess.*` limits, and the
|
|
113
|
+
AsyncLocalStorage identity channel.
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# TUI 缺陷狩猎手册(QA playbook)
|
|
2
|
+
|
|
3
|
+
> **给谁看**:任何要在本仓库做「视觉/交互验证」的 agent 或人。读完即可执行。
|
|
4
|
+
>
|
|
5
|
+
> **为什么有这份文档**:`CONTRIBUTING.md` 只覆盖 `typecheck` / `test` / `test:docker`,而本仓库有冻结的 TUI 契约(`docs/tui-viewer-contract.md`),其验收项(T1 渲染对齐主上下文、T5 follow-end、T12 流式去重)本质是**渲染与交互**断言,无法用普通单测覆盖。本手册补上这一段:如何在容器里跑真实 TUI、如何截图取证、如何造确定性复现、如何把发现变成 issue。多 agent 整轮狩猎的编排见 `docs/qa-hunting-round.md`。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 0. 核心原则:三层证据链
|
|
10
|
+
|
|
11
|
+
**每个缺陷必须有这三层,缺一层就别报。**
|
|
12
|
+
|
|
13
|
+
| 层 | 目的 | 手段 |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| ① 纯函数最小复现 | 精确定位,无需环境 | `node --experimental-strip-types` 直调被测函数 |
|
|
16
|
+
| ② 无头端到端 | 证明真实调用路径可达 | 直驱组件 / registry / orchestrator(不起 TUI) |
|
|
17
|
+
| ③ TUI 截图/帧 | 证明用户可见 | tmux 捕获 ANSI → 渲染 PNG 给我看 |
|
|
18
|
+
|
|
19
|
+
**只做 ③ 会误报**(把测试环境噪声当产品缺陷)。**只做 ① 会漏报**(函数本身正确但调用方错)。
|
|
20
|
+
|
|
21
|
+
**无视觉能力时**:如果你的模型看不了图,以明文帧/码点断言为准(见 §3 铁律 1),PNG 只作为附给有视觉能力的会话/人的辅助证据。不要因为"看不了图"就跳过第 ③ 层——明文帧同样能证明用户可见性。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. 基建:容器 + 可脚本化 LLM
|
|
26
|
+
|
|
27
|
+
### 1.1 为什么必须容器化
|
|
28
|
+
|
|
29
|
+
- TUI 渲染依赖终端宽度/高度,本地终端不可控;容器里可精确设 `tmux -x <cols> -y <rows>`。
|
|
30
|
+
- 本仓库改 TUI 行为会动 `ctx.ui.custom()`;容器快照可重建。
|
|
31
|
+
|
|
32
|
+
### 1.2 用仓库自带的 mock LLM(已存在,别自造)
|
|
33
|
+
|
|
34
|
+
`test/mock-llm/` 已是**可脚本化的 OpenAI 兼容端点**(`npm run mock-llm`,默认 `:8765`,scenario 可选,见 `test/mock-llm/README.md` 的 `MOCK_LLM_SCENARIO=spawn-scout`)。
|
|
35
|
+
|
|
36
|
+
- 模型名固定为 `--provider mock --model flash --thinking off`(`writeMockPiHome` 已把 provider 配好,dummy key 恒为 `mock`)。
|
|
37
|
+
- 驱动指令用 `MOCK_SAY: <text>` 等 inline directives(README「Inline directives」一节)。
|
|
38
|
+
|
|
39
|
+
### 1.3 端口纪律(血泪)
|
|
40
|
+
|
|
41
|
+
**永远用随机端口**,不要硬编码 `8765`:旧进程残留会静默串台,产物看起来干净但跑的是错的 scenario。
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
PORT=$(python3 -c 'import socket,random
|
|
45
|
+
for _ in range(300):
|
|
46
|
+
p=random.randint(20000,29999); s=socket.socket()
|
|
47
|
+
try: s.bind(("127.0.0.1",p)); s.close(); print(p); break
|
|
48
|
+
except OSError: s.close()')
|
|
49
|
+
# 起 mock 后必须等就绪再启动 pi
|
|
50
|
+
for _ in $(seq 1 40); do curl -sf -m2 "http://127.0.0.1:$PORT/v1/models" >/dev/null && break; sleep 0.5; done
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**禁止 `pkill -f mock-llm`**(会匹配到自己的 shell 把自己杀掉)。用:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
ps -eo pid,cmd | grep "[m]ock-llm" | awk '{print $1}' | xargs -r kill -9
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 1.4 tmux 捕获(本仓库尚缺的一环)
|
|
60
|
+
|
|
61
|
+
`test/docker/` 已有容器基建;需要补一个 tmux 捕获步骤拿到真像素。建议新增 `test/docker/verify-tui-visual.sh`(**当前尚未落库,见 §11 状态表**):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 在容器内
|
|
65
|
+
tmux new-session -d -s qa -x 120 -y 40
|
|
66
|
+
tmux send-keys -t qa "HOME=$PIHOME TERM=xterm-256color pi --provider mock --model flash --thinking off" C-m
|
|
67
|
+
sleep 12
|
|
68
|
+
tmux capture-pane -p -t qa -e > /tmp/frame.ansi
|
|
69
|
+
python3 /opt/tui/ansi2png.py /tmp/frame.ansi /tmp/frame.png --cols 120
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
渲染注意:然后交给宿主渲染成 PNG(镜像内置 `test/tui/ansi2png.py`,PIL 逐单元格):
|
|
73
|
+
|
|
74
|
+
- **不要用 ImageMagick 的 Pango**——它吞掉嵌套 `foreground` 颜色,会让颜色类取证失真。
|
|
75
|
+
- **渲染器必须支持 CJK 字体回退**,否则中文渲染成方框,**会掩盖 CJK 宽度/溢出缺陷本身**。
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 2. 执行循环
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# 1) 起隔离 home + 随机端口 mock(见 §10 快速开始)
|
|
83
|
+
# 2) tmux 起 pi,按场景送键
|
|
84
|
+
# 3) 每步 capture-pane,需要时渲染 PNG 核对渲染与交互
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**等待要基于"渲染稳定"而不是拍脑袋 sleep**。推荐 capture-until-stable:连续两次捕获(间隔 ≥1s)diff 为空才算渲染完成,再送下一键。
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# capture-until-stable:稳定才返回,最多等 30s
|
|
91
|
+
stable() {
|
|
92
|
+
local prev="" cur=""
|
|
93
|
+
for _ in $(seq 1 30); do
|
|
94
|
+
sleep 1
|
|
95
|
+
cur=$(tmux capture-pane -p -t qa -e)
|
|
96
|
+
[ "$cur" = "$prev" ] && { printf '%s' "$cur"; return 0; }
|
|
97
|
+
prev="$cur"
|
|
98
|
+
done
|
|
99
|
+
printf '%s' "$cur"; return 1 # 超时:视为异常,记录后处理
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
连续送键不给渲染周期 = 误判为缺陷(曾把连按两次 Esc 判成"面板没关",其实是时序太快;放慢后正确:Esc 从详情→列表→关闭)。
|
|
104
|
+
|
|
105
|
+
tmux 特有的坑:
|
|
106
|
+
|
|
107
|
+
- `send-keys Escape` 与后续按键之间同样要等稳定,tmux 不做合并。
|
|
108
|
+
- `tmux -x 120 -y 40` 是**初始**尺寸;若程序随后收到 resize 事件(如进入备用屏幕),以程序日志/探测输出里的实际宽高为准,别假设仍是 120×40。
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 3. 铁律(踩过的坑,别再犯)
|
|
113
|
+
|
|
114
|
+
1. **先读明文再读图**。低分辨率下 `▶`(U+25B6) 会被误读成 `◀`,差点误报。下结论前先 `sed 's/\x1b\[[0-9;]*m//g'` 看明文或查码点。
|
|
115
|
+
2. **区分环境噪声 vs 产品缺陷**。截图里的 `â`/`Ã` 是 xterm 没开 UTF-8(用 `-u8` + `LANG=C.utf8`);root 绕过权限位(`chmod 500` 仍可写)。都不是产品问题。
|
|
116
|
+
3. **工具执行要查请求日志/产物,不能只看回复**。模型完全可能回复"I started the task"却没调用工具。判据:`mock` 请求数 + 产物文件存在。
|
|
117
|
+
4. **报之前先查重**。先 `gh issue list --search <关键词>` 确认没报过,再在当前 main 上复跑最小复现确认缺陷仍在(§9 列的类别尤甚,可能已修)。
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 4. 判定标准(报不报)
|
|
122
|
+
|
|
123
|
+
| 报 issue | 只记录 |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| 可复现的功能/数据/安全缺陷 | 理论风险(无法复现) |
|
|
126
|
+
| 契约明文与实现不符 | 契约未规定的设计取舍 |
|
|
127
|
+
| API 语义误导 | 低价值打磨 |
|
|
128
|
+
| 跨面口径不一致(同一数据多解释) | 环境限制导致的观察 |
|
|
129
|
+
|
|
130
|
+
**必须能复现才报。** 不可复现的一律降级为"记录为风险"。
|
|
131
|
+
|
|
132
|
+
怀疑回归类缺陷时,加一步**负向对照**:在已知 good commit(`git log` 找缺陷引入前的版本)上跑同样步骤。以前没有 → 新回归(更紧急);以前也有 → 一直如此的设计,回到 §4 表格判定。
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 5. 高 ROI 角度(按序)
|
|
137
|
+
|
|
138
|
+
1. **跨面口径一致**:同一数据在多处(tree / viewer / status)是否同口径。
|
|
139
|
+
2. **契约 vs 实现逐条对照**:把冻结契约(如 `tui-viewer-contract.md` 的 T1–T13)grep 出来逐条验证。
|
|
140
|
+
3. **单位混淆**:字节/码点/显示宽度。
|
|
141
|
+
4. **配置边界**:每个 env 变量试 `""`/`0`/`-1`/`NaN`/`Infinity`/浮点。
|
|
142
|
+
5. **UI 状态模型**:滚动/过滤/选中是否有统一模型。
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 6. 写 issue 的结构(接 CONTRIBUTING 的 "Issues first")
|
|
147
|
+
|
|
148
|
+
CONTRIBUTING 要求 issue 含 repro / 期望 / 实际 / 环境(Node / OS / pi-legion 版本)。渲染类 issue 再加一段:
|
|
149
|
+
|
|
150
|
+
```markdown
|
|
151
|
+
## Summary
|
|
152
|
+
一句话说清症状 + 影响。
|
|
153
|
+
## Severity
|
|
154
|
+
critical / major / minor + 一句理由(维护者据此分诊,49 个 issue 没有分级无法排优先级)。
|
|
155
|
+
## Where
|
|
156
|
+
精确到 `file:line` + 关键代码。
|
|
157
|
+
## Reproduction
|
|
158
|
+
**实测输出**(不是"应该看到什么")。贴 `tmux capture-pane / strings / ps` 的真实结果。
|
|
159
|
+
最小复现脚本路径(见 §7 产物落库,随 issue 提交或附 PR)。
|
|
160
|
+
## Through the TUI
|
|
161
|
+
渲染/交互问题附帧内容(明文或 PNG)。
|
|
162
|
+
## Impact
|
|
163
|
+
为什么值得修(用户/模型/数据视角)。
|
|
164
|
+
## Environment
|
|
165
|
+
pi-legion 版本、Node、终端尺寸、容器/宿主、mock scenario。
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 7. 产物落库(不要只写 /tmp)
|
|
171
|
+
|
|
172
|
+
容器一销毁,`/tmp` 里的帧/PNG/脚本全丢——issue 里的证据链就断了。每个发现固定一个产物目录:
|
|
173
|
+
|
|
174
|
+
```text
|
|
175
|
+
test/tui/artifacts/<YYYYMMDD>-<issue-slug>/
|
|
176
|
+
├─ frame.ansi # 关键帧(capture-pane -e 原始输出)
|
|
177
|
+
├─ repro.mjs # ① 层最小复现脚本,node repro.mjs 直接跑
|
|
178
|
+
├─ frame.png # 可选,渲染后的取证图
|
|
179
|
+
└─ notes.md # 复现步骤、码点分析、结论
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- issue 的 Reproduction 一节引用上述相对路径;修复 PR 把 `repro.mjs` 转成正式回归测试(先失败再修,见 §8)。
|
|
183
|
+
- 如果产物含敏感路径/密钥,写之前先检查(CONTRIBUTING:never commit real keys)。
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 8. 与 CONTRIBUTING 的衔接
|
|
188
|
+
|
|
189
|
+
- 本手册产出的 issue 走同一流程:issue → 一个 PR 一个 concern → draft → Ready for review(`ci` 绿)。
|
|
190
|
+
- 修复时按 CONTRIBUTING 的回归测试要求:**先失败测试,再生产代码**。本手册的三层证据天然提供了测试用例(`repro.mjs` → 测试)。
|
|
191
|
+
- 涉及 `src/tui/` 的改动,务必跑 `npm run test:integration`(`FakeSupervisor`),因为本手册发现的多数缺陷是「调用方错」而非「组件错」——单测覆盖不到。
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 9. 已验证的生效案例(pi-async,49 issues)
|
|
196
|
+
|
|
197
|
+
本方法论在 `zzjcool/pi-async` 上跑通,产出 49 个 issue。代表性几类:
|
|
198
|
+
|
|
199
|
+
- **渲染层**:`rail` 未剥离裸 C0(`\x07`/`\x08`/`\x0c`)——控制字符破坏布局(可复现最小用例 + TUI 帧)。
|
|
200
|
+
- **宽度单位**:截断按**码点**而非显示宽度 → CJK/emoji 溢出挤掉 `elapsed` 字段。
|
|
201
|
+
- **状态语义**:`sync + abort` 返回 `mode:"sync"` + `status:"running"` 自相矛盾(作者的测试已断言 `status:"running"` 是设计意图,`mode` 才是缺陷——**先读测试再报**)。
|
|
202
|
+
- **安全**:正则缺 `m` flag → 契约建议的锚定 `^READY$` **永不匹配**;且 **JS 无内联 `?m`**(我最初的 workaround 错了,已更正)
|
|
203
|
+
- **架构根因**:4 组概念 9 份副本(`stripAnsi`×2、duration×2、env×3、width×2),由并行开发计划解释,直接产生 4 组缺陷。合并为共享助手可关闭整类。
|
|
204
|
+
|
|
205
|
+
**关键教训**:建议(修复方案/workaround)**必须先跑一遍**再写进 issue。我曾把 `(?m)` 当 workaround 写进去,而 JS 正则不支持内联 flag——差点误导维护者。
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 10. 快速开始(复制即用)
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# ── 0. 随机端口起 mock ──────────────────────────────
|
|
213
|
+
PORT=$(python3 -c 'import socket,random
|
|
214
|
+
for _ in range(300):
|
|
215
|
+
p=random.randint(20000,29999); s=socket.socket()
|
|
216
|
+
try: s.bind(("127.0.0.1",p)); s.close(); print(p); break
|
|
217
|
+
except OSError: s.close()')
|
|
218
|
+
MOCK_LLM_SCENARIO=spawn-scout npm run mock-llm -- --port "$PORT" &
|
|
219
|
+
for _ in $(seq 1 40); do curl -sf -m2 "http://127.0.0.1:$PORT/v1/models" >/dev/null && break; sleep 0.5; done
|
|
220
|
+
|
|
221
|
+
# ── 1. 生成隔离 pi home(打印路径,后续要用)──────────
|
|
222
|
+
PIHOME=$(node --experimental-strip-types -e '
|
|
223
|
+
import { writeMockPiHome } from "./test/mock-llm/index.ts";
|
|
224
|
+
import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os";
|
|
225
|
+
const dir = mkdtempSync(`${tmpdir()}/qa-`);
|
|
226
|
+
writeMockPiHome({ dir, baseUrl: `http://127.0.0.1:${process.env.PORT}/v1` });
|
|
227
|
+
console.log(dir);
|
|
228
|
+
')
|
|
229
|
+
echo "PIHOME=$PIHOME"
|
|
230
|
+
|
|
231
|
+
# ── 2. tmux 起 pi ────────────────────────────────────
|
|
232
|
+
tmux new-session -d -s qa -x 120 -y 40
|
|
233
|
+
tmux send-keys -t qa "HOME=$PIHOME TERM=xterm-256color PI_CODING_AGENT_DIR=$PIHOME pi --provider mock --model flash --thinking off" C-m
|
|
234
|
+
# 等启动稳定(连续两帧相同,见 §2 stable())
|
|
235
|
+
sleep 12; tmux capture-pane -p -t qa -e > /tmp/boot.ansi
|
|
236
|
+
|
|
237
|
+
# ── 3. 交互 + 捕获 ───────────────────────────────────
|
|
238
|
+
tmux send-keys -t qa "MOCK_SAY: hello" C-m
|
|
239
|
+
sleep 2
|
|
240
|
+
tmux capture-pane -p -t qa -e > /tmp/frame.ansi
|
|
241
|
+
# 渲染 PNG(ansi2png.py 尚未落库,见 §11 状态表)
|
|
242
|
+
# python3 test/tui/ansi2png.py /tmp/frame.ansi /tmp/frame.png --cols 120
|
|
243
|
+
|
|
244
|
+
# ── 4. 清理 ─────────────────────────────────────────
|
|
245
|
+
tmux kill-session -t qa
|
|
246
|
+
ps -eo pid,cmd | grep "[m]ock-llm" | awk '{print $1}' | xargs -r kill -9
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## 11. 维护
|
|
252
|
+
|
|
253
|
+
### 基建状态表(改一处,同步此处)
|
|
254
|
+
|
|
255
|
+
| 资产 | 位置 | 状态 |
|
|
256
|
+
| --- | --- | --- |
|
|
257
|
+
| 脚本化 mock LLM | `test/mock-llm/` | ✅ 已有 |
|
|
258
|
+
| 隔离 pi home | `writeMockPiHome({ dir, baseUrl })` | ✅ 已有 |
|
|
259
|
+
| 沙箱镜像 | `ghcr.io/zzjcool/pi-legion/tui-sandbox`(Dockerfile: `test/docker/Dockerfile.tui-sandbox`,CI 自动发布) | ✅ 已有 |
|
|
260
|
+
| tmux 捕获 harness | `test/docker/verify-tui-visual.sh`(`npm run test:tui-visual` 一键跑) | ✅ 已有 |
|
|
261
|
+
| ANSI→PNG 渲染器 | `test/tui/ansi2png.py`(镜像内置 `/opt/tui/`) | ✅ 已有 |
|
|
262
|
+
| 容器基建 | `test/docker/` | ✅ 已有 |
|
|
263
|
+
| 冻结契约 | `docs/tui-viewer-contract.md` | ✅ 已有 |
|
|
264
|
+
|
|
265
|
+
镜像发布:push 到 master 且上述文件变更时 CI 自动构建推送(`.github/workflows/docker-tui-sandbox.yml`);本地修改 Dockerfile 后想立即验证可直接 `npm run test:tui-visual`(本地 build,无需 push)。要手动 push 需要 `write:packages` scope 的 token(`gh auth token` 默认没有):`echo <TOKEN> | docker login ghcr.io -u zzjcool --password-stdin && docker push ghcr.io/zzjcool/pi-legion/tui-sandbox:latest`。
|
|
266
|
+
|
|
267
|
+
证据产物(`test/tui/artifacts/`)已 gitignore,不进仓库;issue 里引用时贴关键帧明文/PNG 内容而非路径。
|
|
268
|
+
|
|
269
|
+
### 其他维护项
|
|
270
|
+
|
|
271
|
+
- 新增 tmux 捕获脚本后,把它纳入 `npm run test:docker`(或单独 `test:tui-visual`,**不进必需 `ci`**,因为依赖容器与像素)——已落 `test:tui-visual`,同样不进 `ci`。
|
|
272
|
+
- 每次发现新的"陷阱"(环境噪声/判定边界)补进 §3/§4。
|
|
273
|
+
- 渲染器与 harness 保持单一实现,**不要 sed 派生副本**(派生副本会与主版本漂移)。
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## 12. 停止条件(防止无限狩猎)
|
|
278
|
+
|
|
279
|
+
开放式狩猎没有自然终点,agent 必须主动收尾:
|
|
280
|
+
|
|
281
|
+
- **角度穷尽即停**:§5 五个角度各跑完一轮后收尾,不要在同一角度反复深挖。
|
|
282
|
+
- **零发现止损**:连续 2 个角度零新发现(查重后),提前结束本轮。
|
|
283
|
+
- **时间预算**:单轮狩猎建议 ≤60 分钟容器时间;超时把已完成的部分按 §7 落库,未跑完的角度写进 notes.md 留给下一轮。
|
|
284
|
+
- **发现量上限**:单轮报 ≤10 个 issue(pi-async 一轮 49 个已属极限,多数维护者一次消化不了);超出部分落库为 notes.md 里的 candidate 列表,下轮先查重再升级为 issue。
|