@nathan33/nao-skill 0.18.5 → 0.19.1
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/checklists/comm-templates.md +3 -1
- package/.agents/checklists/pm.md +5 -2
- package/.agents/common/intercom-protocol.md +3 -4
- package/.agents/prompts/infra-engineer.md +4 -2
- package/.agents/prompts/product-manager.md +5 -3
- package/.agents/scripts/nao-fleet.sh +143 -447
- package/.agents/skills/task-completion.md +49 -0
- package/README.md +4 -7
- package/package.json +1 -1
|
@@ -22,10 +22,11 @@ description: 派发消息与回执模板(按需,派发/回执前读取)
|
|
|
22
22
|
## 回执 done(lite):默认(无阻塞、无风险的常规任务,单行)
|
|
23
23
|
|
|
24
24
|
```text
|
|
25
|
-
[编号] done(lite) | <role> | 清单:未过<项 或 无> | 测试:<命令> exit=<n> <文件数>/<例数>/<红数> | 变更:<f1,f2(>3 个写前 3 + "等 N 个")> | commit=<短 hash 或 —> | 详情:docs/reports/<编号>.md
|
|
25
|
+
[编号] done(lite) | <role> | 清单:未过<项 或 无> | 测试:<命令> exit=<n> <文件数>/<例数>/<红数> | 变更:<f1,f2(>3 个写前 3 + "等 N 个")> | 遗留:<无 | n 项 → 台账|tasks-state|Issue|接受(到期)> | commit=<短 hash 或 —> | 详情:docs/reports/<编号>.md
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
> `commit=`(仅 git 流程任务):work 分支最新提交短 hash,便于 PM 核对归因(commit.md 约定);无 push/纯文档任务写 `commit=—`。
|
|
29
|
+
> `遗留:` 取值枚举 `无 | <n> 项 → <台账|tasks-state|Issue|接受(到期)>`;零遗留写 `无`,禁省略字段(三出口定义见 @.agents/skills/task-completion.md §四)。
|
|
29
30
|
|
|
30
31
|
## 回执 done(full):出现阻塞/风险/需决策,或 PM 指定
|
|
31
32
|
|
|
@@ -36,6 +37,7 @@ description: 派发消息与回执模板(按需,派发/回执前读取)
|
|
|
36
37
|
变更:<文件/模块级>
|
|
37
38
|
测试:<命令 + exit code + 文件数/例数/红数(或错误数)>
|
|
38
39
|
commit:<短 hash 或 —>
|
|
40
|
+
遗留:<无 | n 项 → 台账|tasks-state|Issue|接受(到期)>(每条附出处与 owner;接受类必须带到期日)
|
|
39
41
|
风险/阻塞:<无 或 列出>
|
|
40
42
|
详情:<路径>
|
|
41
43
|
```
|
package/.agents/checklists/pm.md
CHANGED
|
@@ -25,7 +25,7 @@ description: PM 交付核对清单(按需,交付/派发前读取)
|
|
|
25
25
|
|
|
26
26
|
> 写作要求(首屏 TL;DR、每格 ≤3 句、不适用项写法、长度上限 150 行):@.agents/checklists/deliverable-docs.md §三(写 PRD 前读)。
|
|
27
27
|
|
|
28
|
-
## 硬性红线(完整
|
|
28
|
+
## 硬性红线(完整 33 项)
|
|
29
29
|
|
|
30
30
|
- [ ] 无目标指标/假设就进开发?
|
|
31
31
|
- [ ] 边界不清晰(未写不做什么)?
|
|
@@ -59,9 +59,10 @@ description: PM 交付核对清单(按需,交付/派发前读取)
|
|
|
59
59
|
- [ ] 未过验收 / 门禁未绿就打 tag 或 release,或 tag 指向非 main 合并提交?(见 `github-flow.md`)
|
|
60
60
|
- [ ] `git push --force` 到 main/共享分支(未经用户明确授权并指明分支)?
|
|
61
61
|
- [ ] release notes 不可读(纯编号/类名/路径),或未落盘 `docs/releases/`?(规范:@.agents/common/release-notes.md —— 面向用户「本次更新」置顶、内部信息下沉、正文不用表格)
|
|
62
|
+
- [ ] 输出里有 `warn`/`error`/`pending approval`/`N vulnerabilities` 却判完成,或遗留项未走三出口(「先放着」不算出口)?(见 @.agents/skills/task-completion.md)
|
|
62
63
|
- [ ] 降级(无 `gh` / 无远端)未在 `tasks-state` 与回执中标注?
|
|
63
64
|
|
|
64
|
-
## 交付检查清单(完整
|
|
65
|
+
## 交付检查清单(完整 40 项)
|
|
65
66
|
|
|
66
67
|
- [ ] 目标/边界/验收三件事齐备
|
|
67
68
|
- [ ] 证据 + 5 Whys 已产出
|
|
@@ -100,6 +101,8 @@ description: PM 交付核对清单(按需,交付/派发前读取)
|
|
|
100
101
|
- [ ] 合并由 RD 执行(`--squash`);main 上本需求恰好 1 条提交且无 `wip()`;需求分支已删除
|
|
101
102
|
- [ ] 发布(若有):版本号符合 SemVer/项目约定;tag 指向 main 合并提交;release notes 用户可读且落盘 `docs/releases/`(核 @.agents/common/release-notes.md)
|
|
102
103
|
- [ ] `gh` 可用则已用 `gh`,降级已注明;发布后 `tasks-state` 版本/Tag 行已更新
|
|
104
|
+
- [ ] 任务 DoD 四类已随**开工确认卡**逐任务实例化(产物路径 / 可执行验证判据 / 遗留项 / 交接),验收按同一份核
|
|
105
|
+
- [ ] 回执含 `遗留:` 字段且遗留项已**三出口**收口(关闭 / 接受含到期日 / 转办;零遗留显式「无」,可执行项已进 `tasks-state` 待派发队列)
|
|
103
106
|
- [ ] 通过上面全部红线
|
|
104
107
|
|
|
105
108
|
## 会话回收 / 收窗核对(验收通过后 / 手工关窗前)
|
|
@@ -29,10 +29,9 @@ description: pi-intercom 多会话协作协议(常驻引用)
|
|
|
29
29
|
- **子代理会话不属舰队会话身份模型**:由 pi-subagents 拉起的子代理会话以 `subagent-<agent>-<id>`(无名时 `subagent-chat-<id>`)进入 intercom 名册,但**不属**舰队会话 —— `status` 只对其汇总计数(不列名、不参与在线/turn 判重),`close subagent-*` 显式拒绝(生命周期归 pi-subagents 的 stop/retention,nao 不接管)。
|
|
30
30
|
|
|
31
31
|
- **判重 / 在线判定**:脚本按 pi 自设的**终端标题**(`π - <会话名> - <仓库名>`)+ `pi-intercom` 名册判在线(**不用 `pgrep --name`**:pi 启动后 argv 被改写为 `pi`,会恒失配);已运行则跳过并 warn。确需重开加 `--force`。
|
|
32
|
-
- **标题被改写时的 pane 定位**(v0.9.4):终端标题可被外部改写(实测变为 `pi:c`),此时 `status`/`close`/`ensure` 按三层后备定位 pane:① 标题契约 → ② pi-intercom 名册 `tmuxPane`(注册时读 `$TMUX_PANE`,与标题改名无关,权威)→ ③ 本仓「未被认领 pi pane
|
|
33
|
-
-
|
|
34
|
-
- **tmux
|
|
35
|
-
- **非 tmux 宿主降级边界(D2/D6)**:④ 路径无 tmux pane 句柄,`status` 会明确标注「turn 判定来源 = 名册」(在跑 turn 判定仅供参照),`close` 按窗口 `app-id`(`org.nao.<别名>`)→ pid(校验 `comm=pi`)回收;无 `hyprctl`/`swaymsg`/`xdotool`/`wmctrl` 等 class 探针时,接受启动并打印一行 warn(**无法校验 ≠ 校验失败**)。
|
|
32
|
+
- **标题被改写时的 pane 定位**(v0.9.4):终端标题可被外部改写(实测变为 `pi:c`),此时 `status`/`close`/`ensure` 按三层后备定位 pane:① 标题契约 → ② pi-intercom 名册 `tmuxPane`(注册时读 `$TMUX_PANE`,与标题改名无关,权威)→ ③ 本仓「未被认领 pi pane」唯一兜底。三层均未命中 ⇒ 视为无句柄:`close` 走「未运行(无需回收)」或(名册报在线但无句柄)exit 非 0。`close` 的在跑 turn 判定叠加名册活动状态(thinking / tool:* 等)与 pane 末 12 行状态行双重信号。
|
|
33
|
+
- **运行前提(唯一路径 · ADR `2026-10-27-strict-tmux-pi`)**:`ensure`/`status`/`close` **只接受 tmux 会话内**(`$TMUX` 非空且 `tmux` 命令可用)**且 `pi ≥ 1.4.0`**;不满足 ⇒ `exit 3`(前提不满足 ⇒ 命令未执行)+ 单行提示(含 `tmux new -s nao`),**不建 session/pane/lock、不写状态**。`cleanup` 豁免(其正常场景就是非 tmux 的隔离实验场)。拉起路径只有一条:**派生宿主窗口**(调用者 pane 所在 window)分屏,显式 `-t @<window_id>`;**失败不假成功**:打印「已拉起」前必先校验 pane 句柄。
|
|
34
|
+
- **tmux 布局**:宿主三选一(`NAO_TMUX_LAYOUT`):默认 `main-row2`(首 pane 全高占左,后续每角色往右开列、每列上下 2 个);`main-col`(其余在右列竖排);`grid`(等大网格)。**窄列守卫**:`main-row2` 最窄非主 pane < `NAO_TMUX_MIN_PANE_WIDTH`(默认 30)时自动回退 `main-col`,仍不足再退 `grid`(打印带数字的 warn,**不中断拉起**)。
|
|
36
35
|
- **模型规则**:用户未指定则**禁止传 `--model`**,也禁止继承 PM 自身模型。
|
|
37
36
|
显式指定时由 `NAO_MODEL_WHITELIST` 在**命令行入口解析阶段**即校验,
|
|
38
37
|
不命中直接 `die`,无部分拉起;PM 只需如实转达用户 `-m` 指定。
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: 工程基座工程师角色 Prompt(短常驻)——CI/构建发布/脚本守卫/依赖安全/仓库治理
|
|
3
3
|
role: rd-infra
|
|
4
|
-
version:
|
|
5
|
-
updated: 2026-10-
|
|
4
|
+
version: 3
|
|
5
|
+
updated: 2026-10-27
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# 工程基座工程师(Infra)
|
|
@@ -36,12 +36,14 @@ updated: 2026-10-13
|
|
|
36
36
|
- [ ] 仓库内引入密钥/机器特定值(token、私钥、绝对家目录路径)?
|
|
37
37
|
- [ ] 无对照就下结论(幂等/提速/体积/宽度/条数须给「改动前 → 改动后」数字)?
|
|
38
38
|
- [ ] 自验自己的守卫(应由 `qa`/`pm` 独立核)?
|
|
39
|
+
- [ ] 维护型动作(依赖/工具/脚本)以 `exit 0` 判完成,`audit`/`install-scripts` 未决策?(见 @.agents/skills/task-completion.md §三)
|
|
39
40
|
|
|
40
41
|
> 完整红线 + 幂等/权限/自证速查 + 交付检查清单:**交付前**读取 @.agents/checklists/rd-infra.md 逐项核对。
|
|
41
42
|
|
|
42
43
|
## 四、关键约定(简)
|
|
43
44
|
|
|
44
45
|
- **幂等与重跑**:脚本/迁移/安装器必须可重入;重复执行不产生重复条目(连跑 2 次自证)。
|
|
46
|
+
- **维护型 DoD(依赖/工具/脚本类)**:命令 `exit 0` **不构成完成**——`npm audit` 与 `install-scripts` 必须各自给出决策(已处置 / 显式接受含到期 / 转台账含 owner),其余 `warn`/`error`/`pending approval`/`N vulnerabilities` 逐条归口,证据落 `docs/reports/<编号>-followups.md`。判据与三出口见 @.agents/skills/task-completion.md §三/§四。
|
|
45
47
|
- **守卫/校验**:新增或改动守卫必须带**失败演练**(破坏 ⇒ 报错 ⇒ 还原复绿),并给精确 rc 与报错项。
|
|
46
48
|
- **CI/权限**:权限最小化;Secret 走仓库变量;日志/回执不输出敏感值,错误体截断。
|
|
47
49
|
- **跨项目**:安装器/脚本在第二个项目或临时目录验证;不假设单仓结构(不写死仓库名/包管理器)。
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: 产品经理角色 Prompt(短常驻)——需求分析/PRD/优先级/验收/多会话调度
|
|
3
3
|
role: pm
|
|
4
|
-
version:
|
|
5
|
-
updated: 2026-10-
|
|
4
|
+
version: 26
|
|
5
|
+
updated: 2026-10-27
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# 产品经理(PM)
|
|
@@ -60,6 +60,7 @@ updated: 2026-10-14
|
|
|
60
60
|
- **架构评审闸门**:PRD 涉及架构/NFR/选型时,开工确认后、派发前 `send` arch-designer 评审。纯 CRUD 跳过。
|
|
61
61
|
- **忙闲闸门**:`list` 见目标在线**且 `idle`** 才派完整任务;忙碌(`thinking`/`tool:*`)→ **不投递任务内容**,记入待排队列(`docs/tasks-state.md`),转 `idle` 再派;**紧急**才抢占(交互会话可 steer 注入并注明 `紧急抢占:…回执须报告挂起任务状态`;非交互会话只能排队)。
|
|
62
62
|
- **终态回执闸门**:每次派发(含架构评审)必须收到 `[编号] done` 回执(两级:默认 `done(lite)`;有阻塞/风险/需决策 `done(full)`,模板见 @.agents/checklists/comm-templates.md);未见回执(含 arch 静默)→ **主动追讨**,必要时上报用户,**不默认成功**。
|
|
63
|
+
- **任务完成闸门(交付 + 维护通用)**:任务单元分**交付型**(Issue 号/批次号)与**维护型**(`M<YYYYMMDD>-<slug>`,外部命令/依赖/环境动作);DoD 四类(产物/验证/遗留项/交接)在**开工确认卡**上逐任务实例化,验收按同一份核;收尾时遗留项必须走**三出口**(关闭 / 接受含到期日 / 转办),零遗留显式写「无」;命中**返工 ≥1 / 漏闸 / 事故 / 同类第二次 / 用户投诉** → L1 完整复盘。协议与判据见 @.agents/skills/task-completion.md。
|
|
63
64
|
- **验收闭环(省 token)**:核对 AC 五覆盖 + 回执「清单」字段(未核对则打回);验收 = 核对回执**全量门禁精确数字** + 读变更文件核对 AC + **异常才复跑/抽查**(**不可改码**、**不重复跑 worker 已跑的门禁**);读码用 `codegraph node --file <f> --offset <n> --limit <m>` 行段,**禁 cat 全文**;**UI 呈现类验收 = 用户验收清单**(QA 产出 → PM 审 → 用户执行回填 → QA 据回填关闭/重开缺陷;清单落 `docs/reports/<编号>-user-acceptance.md`,见 qa 卡)。
|
|
64
65
|
- **合并闸门(PM 控制合并时间点)**:工作期 RD 只在需求分支 `feat/<issue-id>-<slug>`(降级 `nao/<批次-slug>`)提交(`wip(<编号>):` 检查点、路径级暂存、禁 `-A`)。**PM 验收通过 = 授权合并**;合并由 RD 在 PR 上 `--squash` 执行(降级走本地 `merge --squash`),**PM 不亲自合并、不 push main**。验收核对:PR 标题/信息**用户可读** + main 上本需求**恰好 1 条提交且无 `wip()`** + 工作区干净。特例白名单与流水线见 @.agents/skills/github-flow.md。
|
|
65
66
|
- **会话生命周期(PM 专属)**:按批次边界重开(归档完成 / 用户换需求 / `/session` contextTokens 超窗口 40%)+ 落盘接续 + 重开后**回读确认**;细则见 @.agents/skills/pm-operations.md §三。
|
|
@@ -99,9 +100,10 @@ updated: 2026-10-14
|
|
|
99
100
|
- [ ] Issue 与 `docs/` 双源(正文抄进 Issue / 状态只留平台)?(§十三)
|
|
100
101
|
- [ ] `git push --force` 到 main/共享分支(未经用户明确授权并指明分支)?
|
|
101
102
|
- [ ] release notes 不可读(纯编号/类名/路径),或未落盘 `docs/releases/`?
|
|
103
|
+
- [ ] 输出里有 `warn`/`error`/`pending approval`/`N vulnerabilities` 却判完成?(`exit 0` ≠ 完成,见 @.agents/skills/task-completion.md §三)
|
|
102
104
|
- [ ] 降级(无 `gh` / 无远端)未在 `tasks-state` 与回执中标注?
|
|
103
105
|
|
|
104
|
-
> 完整红线(
|
|
106
|
+
> 完整红线(33 项)与交付检查清单(40 项):**交付/派发前**读取 @.agents/checklists/pm.md 逐项核对。
|
|
105
107
|
|
|
106
108
|
## 十一、交付检查清单
|
|
107
109
|
|
|
@@ -2,32 +2,36 @@
|
|
|
2
2
|
# =============================================================================
|
|
3
3
|
# nao-fleet.sh — 按角色一键拉起 pi 会话窗口(nao 团队工具箱)
|
|
4
4
|
#
|
|
5
|
+
# 运行前提(ADR 2026-10-27-strict-tmux-pi · 唯一路径)
|
|
6
|
+
# 必须在 tmux 会话内($TMUX 非空且 tmux 命令可用)· pi ≥ 1.4.0(check 断言)
|
|
7
|
+
# 非 tmux ⇒ ensure/status/close 一律 exit 3 + 一行提示,且不建 session/pane/lock、不写状态(BR3)
|
|
8
|
+
# cleanup 豁免本前提:其正常场景就是「非 tmux 的隔离实验场」(env -u TMUX ... -S <socket>)
|
|
9
|
+
#
|
|
5
10
|
# 用法
|
|
6
|
-
# nao-fleet.sh check [--strict] [-v] 静态体检:roles.yaml/缩进/EOL/角色卡/交叉引用/PR
|
|
11
|
+
# nao-fleet.sh check [--strict] [-v] 静态体检:roles.yaml/缩进/EOL/角色卡/交叉引用/PR 模板/白名单/布局/tmux+pi 前提
|
|
7
12
|
# 默认单行摘要(含 warn 计数);-v 展开完整报告;失败始终展开
|
|
8
|
-
# nao-fleet.sh status
|
|
13
|
+
# nao-fleet.sh status 角色会话在线状态(按 tmux 终端标题/名册判定;权威名单见 intercom list)
|
|
9
14
|
# nao-fleet.sh ensure <别名>[@<repo>] [更多...] 拉起角色窗口(默认工作区=roles.yaml workspace)
|
|
10
15
|
# nao-fleet.sh ensure -m <model> <别名>... 显式指定模型(须命中白名单)
|
|
11
16
|
# nao-fleet.sh ensure --task <编号> <别名>[@<repo>] 任务派生会话:--name <别名>-<编号>(并行隔离,避免同名冲突)
|
|
12
17
|
# nao-fleet.sh ensure --force <别名>... 忽略"已在运行"判重
|
|
13
|
-
# nao-fleet.sh ensure --background <别名>... 无图形会话时显式允许后台 detached(否则 exit 2)
|
|
14
18
|
# nao-fleet.sh close <别名|派生会话名> [--task <编号>] [--force]
|
|
15
19
|
# 回收已完成会话(派生名如 rd-be-T1;跨仓定位;定位失败/名册报在线但无句柄 ⇒ 非 0 + 诊断)
|
|
16
20
|
# (闸门:在跑 turn / tasks-state 未推进 → 拒绝,--force 跳过;
|
|
17
21
|
# 常驻缺席而其派生在场 ⇒ 目标歧义拒绝,T516 AC3)
|
|
18
|
-
# nao-fleet.sh cleanup -S <socket> 终结实验场 tmux server(**强制**显式 -S socket)
|
|
22
|
+
# nao-fleet.sh cleanup -S <socket> 终结实验场 tmux server(**强制**显式 -S socket;不看 $TMUX)
|
|
19
23
|
# 不带 -S / 命中生产默认 socket ⇒ exit 2 + 单行拒绝(真 server 不受影响)
|
|
20
24
|
# ⚠ 生产默认 socket 判定 = $TMUX 句柄 + /tmp/tmux-<uid>/default(+$TMPDIR 变体),
|
|
21
25
|
# **与 TMUX_TMPDIR 无关**(T516 事故:夹具设 TMUX_TMPDIR 曾令真默认 socket 被放行而连坐自灭)
|
|
22
26
|
# ⚠ 实验纪律:一律 `env -u TMUX` + `-S <隔离 socket>`(T515 血训:
|
|
23
27
|
# tmux 客户端在 $TMUX 已设时无视 TMUX_TMPDIR,裸 tmux/kill-server 会连坐真 server);
|
|
24
|
-
# 验证脚本勿在承载自己会话的 tmux
|
|
28
|
+
# 验证脚本勿在承载自己会话的 tmux 内执行(F1 自 pane 闸门已改为
|
|
29
|
+
# 进程归属判定 pane_hosts_caller,不再依赖 $TMUX_PANE——T530)
|
|
25
30
|
#
|
|
26
31
|
# 角色别名 → 角色卡:见 .agents/roles.yaml(单一事实来源)
|
|
27
32
|
# 当前:pm / arch-designer(arch) / rd-fe / rd-be / qa / rd-infra(infra)
|
|
28
33
|
#
|
|
29
34
|
# 环境变量
|
|
30
|
-
# NAO_TERMINAL=tmux|foot|ghostty|ptyxis|screen 强制宿主(非法/命令缺失 ⇒ exit 2)
|
|
31
35
|
# NAO_TMUX_LAYOUT=main-row2|main-col|grid tmux 布局(默认 main-row2)
|
|
32
36
|
# NAO_TMUX_MAIN_WIDTH=<10..90> 主 pane 宽度百分比(默认 35;main-row2/main-col 共用)
|
|
33
37
|
# NAO_TMUX_MIN_PANE_WIDTH=<10..80> 最小非主 pane 列宽守卫(默认 30;低于则回退)
|
|
@@ -38,6 +42,8 @@
|
|
|
38
42
|
# NAO_TASKS_STATE=<path> 任务状态文件(默认 docs/tasks-state.md,供残留检测/回收闸门)
|
|
39
43
|
#
|
|
40
44
|
# 在线判定与 pane 定位(status / close / ensure 判重共用)
|
|
45
|
+
# ⚠️ pane id(%N)是 **server 局部**命名空间(每个 server 首块 pane 都是 %0)⇒ 任何「跨 server 比较
|
|
46
|
+
# pane id」的判据都是错的(T530 误拒根因);「解析句柄」与「执行 kill」必须同 server(同 $TMUX)。
|
|
41
47
|
# pi 启动后用 OSC 0 把终端标题设为 "π - <会话名> - <cwd basename>" 并改写 argv
|
|
42
48
|
# (/proc/<pid>/cmdline 只剩 "pi")⇒ 不使用 `pgrep --name`(历史假阴性根因)。
|
|
43
49
|
# ⚠️ 标题可能被外部改写(PM 实测变为 "pi:c")⇒ pane 定位按三层后备(find_pane_for):
|
|
@@ -45,19 +51,12 @@
|
|
|
45
51
|
# ② pi-intercom 名册 tmuxPane(注册时读 $TMUX_PANE;与标题改名无关,权威)
|
|
46
52
|
# ③ 本仓「未被认领 pi pane」唯一兜底(启发式;歧义即 warn,不猜)
|
|
47
53
|
# 名册由 intercom-probe.mts(一次调用、进程内缓存,含 tmuxPane)取;探针不可用回退
|
|
48
|
-
# pi-intercom 官方 cli.ts(无 tmuxPane),再不可用回退 tmux
|
|
49
|
-
# 仍无法确认「不在线」时 close 不做静默 no-op(exit 非 0)。
|
|
50
|
-
#
|
|
51
|
-
# 宿主阶梯(非 tmux 用户;ADR 2026-10-12-non-tmux-host-spawn)
|
|
52
|
-
# ① NAO_TERMINAL 显式 → ② $TMUX 内分屏 → ③ tmux+图形:tmux 会话 + 可见窗口 attach
|
|
53
|
-
# → ④ 图形无 tmux:xdg-terminal-exec --app-id=org.nao.<角色> --dir=<repo> 直开窗
|
|
54
|
-
# → ⑤ 无图形:仅 --background 时 detached tmux/screen → ⑥ 否则 exit 2 + 一行 DEGRADED 诊断
|
|
54
|
+
# pi-intercom 官方 cli.ts(无 tmuxPane),再不可用回退 tmux 会话名。
|
|
55
55
|
#
|
|
56
|
-
# tmux
|
|
57
|
-
# -
|
|
56
|
+
# tmux 宿主行为(唯一路径)
|
|
57
|
+
# - 以派生宿主窗口(调用者 pane 所在 window)分屏拉起,不新建窗口;
|
|
58
58
|
# 显式 `-t @<window_id>` 目标(定位链:NAO_TMUX_HOST_WINDOW > $TMUX_PANE > 名册唯一命中;
|
|
59
59
|
# 否则 warn + 跳过该 pane,不落入当前窗口)
|
|
60
|
-
# - 不在 tmux 内:创建 detached 会话 nao-<角色>,需 tmux attach -t nao-<角色>。
|
|
61
60
|
# - main-row2:第 1 个 pane 全高占左,后续每角色往右开列、每列上下 2 个(默认):
|
|
62
61
|
# 1 | 2 | 4
|
|
63
62
|
# 1 | 3 | 5
|
|
@@ -97,20 +96,18 @@ VERBOSE=false
|
|
|
97
96
|
CLOSE_BUSY_PATTERN="${NAO_CLOSE_BUSY_PATTERN:-Working \(|Thinking\.\.\.|to interrupt|to cancel|Retrying \(|Compacting|Summarizing branch}"
|
|
98
97
|
# 任务状态文件(相对当前目录;供残留检测与回收闸门)
|
|
99
98
|
TASKS_STATE="${NAO_TASKS_STATE:-docs/tasks-state.md}"
|
|
100
|
-
# T513-S2 F2:窗口/分离宿主派生前清 TMUX/TMUX_PANE —— 防子进程继承调用者的 pane 句柄,
|
|
101
|
-
# 导致 pi-intercom 按调用者 pane 错注册名册、close 反噬调用者(见 ADR 不变量)。
|
|
102
|
-
# ⚠️ 只用于「非 tmux 窗口/screen」宿主:tmux 自身的 split-window/new-session/attach
|
|
103
|
-
# 会为新 pane 注入正确的 TMUX_PANE(实测 -L 隔离 server:外层 TMUX_PANE=%FAKE 被覆盖为 %0),
|
|
104
|
-
# 对这些路径前缀剥离反而会抹掉正确句柄。
|
|
105
|
-
TMUX_ENV_SCRUB="env -u TMUX -u TMUX_PANE"
|
|
106
99
|
|
|
107
100
|
log() { printf '\033[1;32m[fleet]\033[0m %s\n' "$*"; }
|
|
108
101
|
warn() { printf '\033[1;33m[fleet]\033[0m %s\n' "$*" >&2; }
|
|
109
102
|
die() { printf '\033[1;31m[fleet]\033[0m %s\n' "$*" >&2; exit 1; }
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
#
|
|
113
|
-
|
|
103
|
+
# 唯一运行前提(BR1/BR3 · ADR 2026-10-27-strict-tmux-pi):$TMUX 非空且 tmux 可用。
|
|
104
|
+
# 不满足 ⇒ exit 3 + 单行提示;调用点在建 session / load_manifest / 写状态**之前**(失败零副作用)。
|
|
105
|
+
# rc 语义:3 = 前提不满足 ⇒ 命令未执行;不复用 1(已执行但失败)/ 2(用法守卫,现仅 cleanup)。
|
|
106
|
+
require_tmux() {
|
|
107
|
+
if [[ -n "${TMUX:-}" ]] && command -v tmux >/dev/null 2>&1; then return 0; fi
|
|
108
|
+
printf '需要 tmux:nao-fleet 只支持在 tmux 会话内运行(先 `tmux new -s nao`,再在该会话内重试)\n' >&2
|
|
109
|
+
exit 3
|
|
110
|
+
}
|
|
114
111
|
|
|
115
112
|
# ---------------------------------------------------------------------------
|
|
116
113
|
# roles.yaml 解析(严格子集:顶层 roles:,2 空格角色条目 / 4 空格字段,禁 Tab)
|
|
@@ -299,6 +296,25 @@ check_codegraph() {
|
|
|
299
296
|
fi
|
|
300
297
|
}
|
|
301
298
|
|
|
299
|
+
# pi 运行前提(BR4/AC3):版本源 = `pi --version` 首个 semver token;预发布(含 `-`)视为不满足。
|
|
300
|
+
# 输出当前版本(缺失/不可解析也给占位),rc 0 = 满足 ≥1.4.0;失败只置 rc,由 check 聚合后继续跑完其余体检。
|
|
301
|
+
pi_version_check() {
|
|
302
|
+
if ! command -v pi >/dev/null 2>&1; then printf '%s' '<缺失>'; return 1; fi
|
|
303
|
+
local raw cur base c1 c2
|
|
304
|
+
raw="$(pi --version 2>/dev/null || true)"
|
|
305
|
+
cur="$(printf '%s\n' "$raw" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?' | head -1 || true)"
|
|
306
|
+
if [[ -z "$cur" ]]; then printf '%s' '<无法解析>'; return 1; fi
|
|
307
|
+
printf '%s' "$cur"
|
|
308
|
+
[[ "$cur" == *-* ]] && return 1 # 预发布不满足(BR4)
|
|
309
|
+
base="${cur%%+*}" # 忽略 build metadata
|
|
310
|
+
c1="${base%%.*}"; base="${base#*.}"
|
|
311
|
+
c2="${base%%.*}"
|
|
312
|
+
if (( c1 > 1 )); then return 0; fi
|
|
313
|
+
if (( c1 < 1 )); then return 1; fi
|
|
314
|
+
if (( c2 >= 4 )); then return 0; fi
|
|
315
|
+
return 1
|
|
316
|
+
}
|
|
317
|
+
|
|
302
318
|
# ---------------------------------------------------------------------------
|
|
303
319
|
resolve_role() {
|
|
304
320
|
local id="${ALIAS_ROLE[$1]:-}"
|
|
@@ -307,22 +323,8 @@ resolve_role() {
|
|
|
307
323
|
PROMPT="${ROLE_CARDS[$id]}"
|
|
308
324
|
}
|
|
309
325
|
|
|
310
|
-
#
|
|
311
|
-
#
|
|
312
|
-
# ② $TMUX 存在 ⇒ 派生宿主窗口(调用者 pane 所在 window)split-window(显式 -t @<window_id>;单窗口逐值零回归)
|
|
313
|
-
# ③ 非 tmux + tmux 可用 + 图形 ⇒ new-session -d + xdg-terminal-exec 开窗 attach(默认 socket)
|
|
314
|
-
# ④ 无 tmux + 图形 ⇒ xdg-terminal-exec --app-id=org.nao.<角色> --dir=<repo> 直开窗
|
|
315
|
-
# ⑤ 无图形 + 显式 --background ⇒ detached tmux/screen
|
|
316
|
-
# ⑥ 否则 exit 2 + 单行 DEGRADED 诊断 + 可复制恢复命令
|
|
317
|
-
NAO_TERMINAL_ALLOWED=(tmux foot ghostty ptyxis screen)
|
|
318
|
-
NAO_TERMINAL_LIST='tmux|foot|ghostty|ptyxis|screen' # 文案用(与 ALLOWED 同步)
|
|
319
|
-
|
|
320
|
-
# 图形会话探针(ADR 门禁):有显示变量且 `xdg-terminal-exec --print-id` 成功
|
|
321
|
-
xdg_launcher_ok() {
|
|
322
|
-
command -v xdg-terminal-exec >/dev/null 2>&1 || return 1
|
|
323
|
-
[[ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]] || return 1
|
|
324
|
-
xdg-terminal-exec --print-id >/dev/null 2>&1
|
|
325
|
-
}
|
|
326
|
+
# 宿主阶梯(已删 · ADR 2026-10-27-strict-tmux-pi):单一前提 = tmux 会话内;
|
|
327
|
+
# ② $TMUX 存在 ⇒ 派生宿主窗口(调用者 pane 所在 window)split-window 显式 -t @<window_id>。
|
|
326
328
|
|
|
327
329
|
# ---------------------------------------------------------------------------
|
|
328
330
|
# 会话在线判定
|
|
@@ -332,9 +334,9 @@ xdg_launcher_ok() {
|
|
|
332
334
|
# 并同步改写 argv —— /proc/<pid>/cmdline 只剩 "pi",`pgrep -f --name` 恒失配,
|
|
333
335
|
# 于是 status/close 长期判「不在线」(close 退化为静默空转、exit 0)。
|
|
334
336
|
# 现改为按「终端标题契约」取句柄,argv 扫描一律不用:
|
|
335
|
-
# ① pi-intercom
|
|
337
|
+
# ① pi-intercom 名册(权威在线名单)
|
|
336
338
|
# ② tmux pane_title(pi 自设;与 pane_start_command 是否为空无关)
|
|
337
|
-
# ③ tmux detached
|
|
339
|
+
# ③ tmux detached 会话名 nao-<会话名>
|
|
338
340
|
# ⚠️ 标题可被外部改写(实测 "pi:c")⇒ find_pane_for 再叠三层后备:标题契约 →
|
|
339
341
|
# 名册 tmuxPane($TMUX_PANE,权威)→ 本仓未认领 pi pane 唯一兜底。
|
|
340
342
|
# repo(可选):给出时要求标题 basename / 名册 cwd 一致,避免跨项目同名会话互串。
|
|
@@ -442,6 +444,51 @@ tmux_pane_live() {
|
|
|
442
444
|
tmux list-panes -a -F '#{pane_id}' 2>/dev/null | grep -qxF "$1"
|
|
443
445
|
}
|
|
444
446
|
|
|
447
|
+
# 目标 pane 是否就是**调用者自己所在的 pane**(F1 不变量:close 任何路径不得回收调用者的 pane)。
|
|
448
|
+
# T530 修正根因:旧判据是裸 pane id 比较(`"$pane" == "$TMUX_PANE"`)。pane id 是 **server 局部**
|
|
449
|
+
# 命名空间(每个 server 的首块 pane 都是 %0)⇒ 跨 server 撞号时必然误判:隔离/custom socket 的
|
|
450
|
+
# 测试 server 里解析出的 %0 与调用者默认 server 的 $TMUX_PANE=%0 字符串相同 → 误判「是自己」→ **误拒**
|
|
451
|
+
# 正常回收(安全侧失败,不误杀,但功能不可用)。
|
|
452
|
+
# 为什么不用「(socket, pane_id) 二元组相等」:撞号的成因就是 $TMUX_PANE 是**陈旧/外来**的
|
|
453
|
+
# ($TMUX 指向隔离 server,而 $TMUX_PANE 还是默认 server 的 %0)—— 两边读出来的 socket 恰好都等于
|
|
454
|
+
# $TMUX 首字段、id 又都是 %0,二元组照样相等 ⇒ 仍误拒。所以判据必须是「我是否真在这个 pane 里」。
|
|
455
|
+
# 实现:目标 pane 的 pane_pid 是否是本进程($$)的祖先 —— OS 级事实,与 pane id 命名空间无关,
|
|
456
|
+
# 天然免疫跨 server 撞号,且顺带覆盖旧闸门的已知缺口($TMUX_PANE 被清/未设时旧判据完全失效)。
|
|
457
|
+
# 语义不放松:真自身 pane(如派生时继承 $TMUX_PANE 致名册错注册,T513-S2/T523/T525)⇒ 祖先命中 ⇒ 仍拒。
|
|
458
|
+
#
|
|
459
|
+
# T530-D1(QA 复核):判据必须 **fail-closed**(三态),不得「不可判定 ⇒ 不声称是自己」。
|
|
460
|
+
# QA 实证:伪造 `ps` 令祖先链不可读 + 在自身 pane 内跑 close ⇒ 自身 pane 被回收(SELF_KILLED)。
|
|
461
|
+
# F1「绝不杀自己」是硬不变量 ⇒ 判不了就**拒**(成本是少回收一次,代价是误杀一个会话)。
|
|
462
|
+
# 返回:0 = 确定是自己 · 1 = 确定不是 · 2 = 不可判定(pane_pid 取不到 / ps 读不出 / 链断)
|
|
463
|
+
pane_hosts_caller() {
|
|
464
|
+
local pane="$1" p_pid cur
|
|
465
|
+
[[ -n "$pane" ]] || return 2
|
|
466
|
+
p_pid="$(tmux display-message -p -t "$pane" '#{pane_pid}' 2>/dev/null || true)"
|
|
467
|
+
[[ "$p_pid" =~ ^[0-9]+$ ]] || return 2 # pane_pid 取不到 ⇒ 不可判定
|
|
468
|
+
cur="$$"
|
|
469
|
+
while [[ "$cur" =~ ^[0-9]+$ ]] && (( cur > 1 )); do
|
|
470
|
+
[[ "$cur" == "$p_pid" ]] && return 0
|
|
471
|
+
cur="$(ps -o ppid= -p "$cur" 2>/dev/null | tr -d '[:space:]')"
|
|
472
|
+
[[ -n "$cur" ]] || return 2 # 链断(ps 不可读/被伪造)⇒ 不可判定
|
|
473
|
+
done
|
|
474
|
+
return 1 # 走到 init 仍未命中 ⇒ 确定不是
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
# F1 闸门统一判据(两个调用点共用):0 = 必须拒绝回收,1 = 放行。
|
|
478
|
+
# 不可判定(rc=2)时**仅当**目标 pane id 与调用者句柄 `$TMUX_PANE` 撞号才拒(保守侧)。
|
|
479
|
+
# ⛔ 不得把「rc=2 且 id 不等」也拒:跨 server 撞号是陈旧句柄造成的,若一律拒会把 v0.19.0
|
|
480
|
+
# 那种「隔离 server 的 %0 被当成自己」的误拒(发布阻断红态)重新引回来。
|
|
481
|
+
self_pane_refuse() {
|
|
482
|
+
local pane="$1" rc=0
|
|
483
|
+
pane_hosts_caller "$pane" || rc=$?
|
|
484
|
+
case "$rc" in
|
|
485
|
+
0) return 0 ;; # 确定是自己 ⇒ 拒
|
|
486
|
+
2) [[ -n "${TMUX_PANE:-}" && "$pane" == "$TMUX_PANE" ]] && return 0 # 不可判定但撞号 ⇒ 保守拒
|
|
487
|
+
return 1 ;;
|
|
488
|
+
*) return 1 ;; # 确定不是 ⇒ 放行
|
|
489
|
+
esac
|
|
490
|
+
}
|
|
491
|
+
|
|
445
492
|
# 名册中该会话的 tmux pane(注册时读 $TMUX_PANE;标题被改写仍有效)
|
|
446
493
|
intercom_pane_for() {
|
|
447
494
|
local name="$1" repo="${2:-}" abs="" json
|
|
@@ -552,7 +599,8 @@ pi_pids_for() {
|
|
|
552
599
|
pane="$(find_pane_for "$1" "${2:-}")"
|
|
553
600
|
[[ -n "$pane" ]] || return 0
|
|
554
601
|
# F1 纵深(T513-S2):绝不据「调用者自己的 pane」反查 pid,否则会 kill 到自身进程。
|
|
555
|
-
|
|
602
|
+
# T530:判据=进程归属(pane_hosts_caller 三态,fail-closed),不比较裸 pane id(server 局部命名空间)。
|
|
603
|
+
self_pane_refuse "$pane" && return 0
|
|
556
604
|
root="$(tmux display-message -p -t "$pane" '#{pane_pid}' 2>/dev/null)"
|
|
557
605
|
[[ -n "$root" ]] || return 0
|
|
558
606
|
{ printf '%s\n' "$root"; pgrep -P "$root" 2>/dev/null || true; } | while IFS= read -r pid; do
|
|
@@ -561,50 +609,7 @@ pi_pids_for() {
|
|
|
561
609
|
done
|
|
562
610
|
}
|
|
563
611
|
|
|
564
|
-
# ---- ④ 路径句柄(app-id/class
|
|
565
|
-
# 窗口 client pid:Hyprland `hyprctl clients -j` 主路;sway 树后备(仅存在性探测;NFR4/NFR5)
|
|
566
|
-
app_client_pid() { # <name> → echo 窗口进程 pid(无则空)
|
|
567
|
-
local name="$1" cls="org.nao.$name" json=""
|
|
568
|
-
if command -v hyprctl >/dev/null 2>&1; then
|
|
569
|
-
json="$(hyprctl clients -j 2>/dev/null || true)"
|
|
570
|
-
elif command -v swaymsg >/dev/null 2>&1; then
|
|
571
|
-
json="$(swaymsg -t get_tree 2>/dev/null || true)"
|
|
572
|
-
fi
|
|
573
|
-
[[ -n "$json" ]] || return 0
|
|
574
|
-
printf '%s' "$json" | node -e '
|
|
575
|
-
let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{
|
|
576
|
-
try{const a=JSON.parse(s);const arr=Array.isArray(a)?a:[a];
|
|
577
|
-
const walk=x=>{for(const n of (x||[])){const c=n.class||n.initialClass||n.app_id;
|
|
578
|
-
if(c===process.argv[1]&&n.pid!=null){process.stdout.write(String(n.pid));return;}
|
|
579
|
-
walk(n.nodes);walk(n.floating_nodes);}};
|
|
580
|
-
walk(arr);}catch(e){}
|
|
581
|
-
});' "$cls" 2>/dev/null || true
|
|
582
|
-
return 0
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
# 深度优先找 comm=pi 的后代(窗口进程往往是终端,pi 在其下)
|
|
586
|
-
pi_descendant_of() { # <root pid> → echo 首个 pi 后代 pid
|
|
587
|
-
local root="$1" c r
|
|
588
|
-
for c in $(pgrep -P "$root" 2>/dev/null); do
|
|
589
|
-
[[ "$(ps -o comm= -p "$c" 2>/dev/null | tr -d ' ')" == "pi" ]] && { printf '%s\n' "$c"; return 0; }
|
|
590
|
-
r="$(pi_descendant_of "$c")"
|
|
591
|
-
[[ -n "$r" ]] && { printf '%s\n' "$r"; return 0; }
|
|
592
|
-
done
|
|
593
|
-
return 0
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
# app-id/class 句柄 → 校验 comm=pi 后的 pi pid(BR7:取 pid 后校验 comm=pi)
|
|
597
|
-
app_pid_for() { # <name> → echo pi pid(无句柄/不可校验则空)
|
|
598
|
-
local name="$1" pid d
|
|
599
|
-
pid="$(app_client_pid "$name")"
|
|
600
|
-
[[ -n "$pid" ]] || return 0
|
|
601
|
-
if [[ "$(ps -o comm= -p "$pid" 2>/dev/null | tr -d ' ')" == "pi" ]]; then printf '%s\n' "$pid"; return 0; fi
|
|
602
|
-
d="$(pi_descendant_of "$pid")"
|
|
603
|
-
[[ -n "$d" ]] && printf '%s\n' "$d"
|
|
604
|
-
return 0
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
# ④ 路径 class 探针存在性由 app_class_visible() 返回码 2 表达(无独立 helper,避免死代码)
|
|
612
|
+
# ---- ④ 路径句柄(app-id/class)已随宿主阶梯删除(ADR 2026-10-27-strict-tmux-pi)---------
|
|
608
613
|
|
|
609
614
|
# 派生会话在场检测(T516 AC3):visible_session_names 中形如 <name>-<非空后缀> 者。
|
|
610
615
|
# 用于 close <常驻名> 时判目标歧义:常驻精确句柄缺席而其派生在场 ⇒ 拒绝 + 指路(禁静默谎报「未运行」)。
|
|
@@ -626,46 +631,18 @@ subagent_in_field() { # → echo 子代理会话数(0..N)
|
|
|
626
631
|
printf '%s\n' "$c"
|
|
627
632
|
}
|
|
628
633
|
|
|
629
|
-
# screen 会话精确令牌(T516 F1/F2 · arch 评审):从 `screen -ls` 取**精确** nao-<名> 会话的 `pid.name` 令牌。
|
|
630
|
-
# 与 running()/close 检测同一锚定(名字后必须是空白或行尾;`\b` 在派生名的 "-" 处仍成立,故不用)。
|
|
631
|
-
# 检测与销毁共用本 helper ⇒ 检测锚与销毁目标不会漂移;**取不到令牌即空,调用方不得 quit**(宁不回收,也不误伤派生)。
|
|
632
|
-
screen_token_for() { # <name> → echo pid.name(无精确匹配则空;恒 rc 0)
|
|
633
|
-
local name="$1"
|
|
634
|
-
command -v screen >/dev/null 2>&1 || return 0
|
|
635
|
-
# 左锚:`[0-9]+\.nao-` 必须紧接 pid 点(弃 `[^[:space:]]*` —— 旧式允许 `pid.Xnao-<名>` 型非目标会话被当精确令牌,
|
|
636
|
-
# QA 波4 (e) 红:误杀非目标 + 幻影成功 + 误报在线)。右锚 `([[:space:]]|$)` 同时排除派生 `nao-<名>-<任务>`。
|
|
637
|
-
screen -ls 2>/dev/null \
|
|
638
|
-
| grep -oE '[0-9]+\.nao-'"$name"'([[:space:]]|$)' \
|
|
639
|
-
| sed -E 's/[[:space:]]+$//' \
|
|
640
|
-
| head -1 || true
|
|
641
|
-
return 0
|
|
642
|
-
}
|
|
643
|
-
screen_present() { # <name> → 0 = 精确 screen 会话在场
|
|
644
|
-
[[ -n "$(screen_token_for "$1")" ]]
|
|
645
|
-
}
|
|
646
|
-
|
|
647
634
|
running() {
|
|
648
635
|
local name="$1" repo="${2:-}"
|
|
649
636
|
if intercom_online "$name" "$repo"; then return 0; fi
|
|
650
637
|
if [[ -n "$(find_pane_for "$name" "$repo")" ]]; then return 0; fi
|
|
638
|
+
# T516 #1(=锚):tmux 会话名唯一前缀匹配曾误命中派生 nao-<名>-<任务>(T515 V4 实证)⇒ 一律 =nao-<名>。
|
|
651
639
|
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "=nao-$name" 2>/dev/null; then return 0; fi
|
|
652
|
-
# T516 #1(=锚):tmux 会话名唯一前缀匹配曾误命中派生 nao-<名>-<任务>(T515 V4 实证)⇒ 一律 =nao-<名>;
|
|
653
|
-
# screen 同理:锚定收敛到 screen_present()(同一 helper 服务检测与销毁,见 screen_token_for)。
|
|
654
|
-
if command -v screen >/dev/null 2>&1 && screen_present "$name"; then return 0; fi
|
|
655
640
|
return 1
|
|
656
641
|
}
|
|
657
642
|
|
|
658
|
-
#
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
[[ -n "${TMUX:-}" ]] && return 0
|
|
662
|
-
case "${NAO_TERMINAL:-}" in tmux|screen) return 0 ;; esac
|
|
663
|
-
return 1
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
# 本机可见的会话名(tmux 标题 + tmux/screen 会话 + 名册),供 status 枚举;已去重
|
|
667
|
-
# ⚠️ 每条枚举 `|| true`:set -e + pipefail 下 `screen -ls`(无 socket 时 rc=1)或
|
|
668
|
-
# `tmux`(无 server 时 rc=1)会中断其余枚举,使「名册回退」永远走不到 —— 必疾。
|
|
643
|
+
# 本机可见的会话名(tmux 标题 + tmux 会话 + 名册),供 status 枚举;已去重
|
|
644
|
+
# ⚠️ 每条枚举 `|| true`:set -e + pipefail 下 `tmux`(无 server 时 rc=1)会中断其余枚举,
|
|
645
|
+
# 使「名册回退」永远走不到 —— 必疾。
|
|
669
646
|
visible_session_names() {
|
|
670
647
|
{ if command -v tmux >/dev/null 2>&1; then
|
|
671
648
|
{ tmux list-panes -a -F '#{pane_title}' 2>/dev/null || true; } \
|
|
@@ -673,10 +650,6 @@ visible_session_names() {
|
|
|
673
650
|
{ tmux list-sessions -F '#{session_name}' 2>/dev/null || true; } \
|
|
674
651
|
| sed -n 's/^nao-//p'
|
|
675
652
|
fi
|
|
676
|
-
if command -v screen >/dev/null 2>&1; then
|
|
677
|
-
{ screen -ls 2>/dev/null || true; } \
|
|
678
|
-
| sed -n 's/.*[0-9]\.nao-\([A-Za-z0-9_-]*\).*/\1/p'
|
|
679
|
-
fi
|
|
680
653
|
intercom_roster 2>/dev/null || true
|
|
681
654
|
} | awk -F'\t' 'NF {print $1}' | awk '!seen[$0]++'
|
|
682
655
|
}
|
|
@@ -1040,23 +1013,18 @@ spawn_tmux() {
|
|
|
1040
1013
|
*) die "NAO_TMUX_LAYOUT 无效: $TMUX_LAYOUT(可选 main-row2|main-col|grid)" ;;
|
|
1041
1014
|
esac
|
|
1042
1015
|
local wrapped="bash -lc $(printf %q "$inner")"
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
return 0
|
|
1054
|
-
fi
|
|
1055
|
-
apply_tmux_layout
|
|
1056
|
-
else
|
|
1057
|
-
tmux new-session -d -s "nao-$name" "$wrapped"
|
|
1058
|
-
log "tmux 会话 nao-$name 已创建(附加: tmux attach -t nao-$name)"
|
|
1016
|
+
# T517:目标显式化——HOST_WINDOW 解析失败时此处已由解析处 warn(每 run 一次),
|
|
1017
|
+
# 跳过该 pane;禁止无 -t 续建(tmux 会落「当前窗口」= 用户视图,即缺陷根因)。
|
|
1018
|
+
[[ -n "$HOST_WINDOW" ]] || return 0
|
|
1019
|
+
# 窄窗口/宿主上限会让 split-window 失败(no space for new pane,含宿主窗已关竞态);
|
|
1020
|
+
# 捕获后 warn 并继续,不因单个 pane 失败中断整轮 ensure。
|
|
1021
|
+
local serr
|
|
1022
|
+
if ! serr=$(tmux split-window -h -t "$HOST_WINDOW" "$wrapped" 2>&1); then
|
|
1023
|
+
warn "pane 创建失败(宿主窗口 $HOST_WINDOW 过窄/已关或已达上限):${serr:-<no message>}"
|
|
1024
|
+
warn " 已跳过该 pane,其余角色继续拉起;可放大窗口后重跑 ensure"
|
|
1025
|
+
return 0
|
|
1059
1026
|
fi
|
|
1027
|
+
apply_tmux_layout
|
|
1060
1028
|
}
|
|
1061
1029
|
|
|
1062
1030
|
# 组装系统提示文件:角色卡 + 环境锚点(本机绝对路径,供跨仓库会话解析 @ 引用失败时兜底)
|
|
@@ -1070,262 +1038,22 @@ build_system_prompt() {
|
|
|
1070
1038
|
echo "$f"
|
|
1071
1039
|
}
|
|
1072
1040
|
|
|
1073
|
-
# ---- 派生 pid 传递(T513-S5 · M2 设计修复)--------------------------------
|
|
1074
|
-
# 父进程建「匿名管道」(在 mktemp -d 内 mkfifo,打开 fd 后立即 rm -rf 该目录 ⇒ 仅 fd 存活)
|
|
1075
|
-
# → 子进程经**继承 fd** 写自身 pid → 父进程读。子进程**不打开任何路径**,因此
|
|
1076
|
-
# 「父先清理、子后写重建」的竞态与 /tmp 残留从设计上不可能(无文件系统对象可残留);
|
|
1077
|
-
# 失败时父仍能拿到 pid ⇒ M1 回收能力保留。
|
|
1078
|
-
# ⚠ 管道以 O_RDWR 打开(exec {fd}<>):父读端提前关闭时,子进程写不会 SIGPIPE(实测)。
|
|
1079
|
-
# ⚠ 实测 foot 等终端保留继承 fd,故 ④ 路径的 M1 仍可拿到 pid。
|
|
1080
|
-
PID_PIPE_FD=""
|
|
1081
|
-
open_pid_pipe() {
|
|
1082
|
-
local d
|
|
1083
|
-
d="$(mktemp -d "/tmp/nao-fleet-pidpipe-XXXXXX")" || return 1
|
|
1084
|
-
mkfifo "$d/p" 2>/dev/null || { rm -rf -- "$d"; return 1; }
|
|
1085
|
-
# 不得在 exec 的 fd 分配上加 `2>/dev/null`:exec 重定向会**持久**作用于当前 shell(会静默后续所有 warn/xtrace)。
|
|
1086
|
-
if ! exec {PID_PIPE_FD}<>"$d/p"; then rm -rf -- "$d"; return 1; fi
|
|
1087
|
-
rm -rf -- "$d" # 立即 unlink:路径消失,仅 fd 存活
|
|
1088
|
-
return 0
|
|
1089
|
-
}
|
|
1090
|
-
close_pid_pipe() {
|
|
1091
|
-
if [[ -n "${PID_PIPE_FD:-}" ]]; then exec {PID_PIPE_FD}>&- || true; fi
|
|
1092
|
-
PID_PIPE_FD=""
|
|
1093
|
-
return 0
|
|
1094
|
-
}
|
|
1095
|
-
# 读子进程 pid(有界轮询;echo pid / 超时 rc=1)
|
|
1096
|
-
wait_for_pid_pipe() { # [tries]
|
|
1097
|
-
local tries="${1:-30}" pid=""
|
|
1098
|
-
[[ -n "${PID_PIPE_FD:-}" ]] || return 1
|
|
1099
|
-
while (( tries-- > 0 )); do
|
|
1100
|
-
if IFS= read -r -t 0.1 -u "$PID_PIPE_FD" pid 2>/dev/null && [[ -n "$pid" ]]; then
|
|
1101
|
-
printf '%s' "$pid"; return 0
|
|
1102
|
-
fi
|
|
1103
|
-
done
|
|
1104
|
-
return 1
|
|
1105
|
-
}
|
|
1106
|
-
# 子进程包装命令:把自身 $$ 写入继承 fd(写成后 exec 目标 ⇒ pid 不变,即最终 pi/宿主 pid)
|
|
1107
|
-
pid_pipe_wrapper() { printf 'echo $$ >&%s; exec "$@"' "$PID_PIPE_FD"; }
|
|
1108
|
-
|
|
1109
|
-
# 非 tmux 窗口宿主(ghostty/ptyxis/foot)派生:P4 根因 = `setsid -f <缺失命令>` 仍 rc=0,
|
|
1110
|
-
# 故不能以 setsid 的 rc 当成功 —— 必须验证子进程 pid 存活(BR2)。
|
|
1111
|
-
# pid 经继承管道回传(M2:无 pidfile ⇒ 无残留);失败即回收 pid(不产生孤儿窗口,AC6)。
|
|
1112
|
-
spawn_fork_window() { # <name> <cmd...>
|
|
1113
|
-
local name="$1"; shift
|
|
1114
|
-
local pid=""
|
|
1115
|
-
open_pid_pipe || return 1
|
|
1116
|
-
if ! setsid -f bash -c "$(pid_pipe_wrapper)" _ "$@" >/dev/null 2>&1; then
|
|
1117
|
-
close_pid_pipe; return 1
|
|
1118
|
-
fi
|
|
1119
|
-
pid="$(wait_for_pid_pipe)" || pid=""
|
|
1120
|
-
close_pid_pipe
|
|
1121
|
-
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then return 0; fi
|
|
1122
|
-
[[ -n "$pid" ]] && kill "$pid" 2>/dev/null || true
|
|
1123
|
-
return 1
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
# screen 宿主派生:以「会话存在」为「已拉起」前置;未出现即回收会话(AC6)。
|
|
1127
|
-
spawn_screen() { # <name> <inner>
|
|
1128
|
-
local name="$1" inner="$2" tries=30 stok
|
|
1129
|
-
screen -dmS "nao-$name" $TMUX_ENV_SCRUB bash -lc "$inner" >/dev/null 2>&1 || return 1
|
|
1130
|
-
while (( tries-- > 0 )); do
|
|
1131
|
-
if screen_present "$name"; then return 0; fi
|
|
1132
|
-
sleep 0.1
|
|
1133
|
-
done
|
|
1134
|
-
# F1(arch 评审):失败清理**只 quit 锚定令牌**指向的会话;取不到令牌即不 quit(不对 nao-<名> 前缀下手)。
|
|
1135
|
-
stok="$(screen_token_for "$name")"
|
|
1136
|
-
[[ -n "$stok" ]] && screen -S "$stok" -X quit >/dev/null 2>&1 || true
|
|
1137
|
-
return 1
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
# ---- 阶梯③/④ 原子件:可见窗口 + 限时句柄校验 -----------------------------
|
|
1141
|
-
# tmux 会话是否有客户端 attach(ADR:会话在、窗口无 ⇒ 降级④;防隐形会话)
|
|
1142
|
-
wait_for_tmux_client() { # <sess> [tries]
|
|
1143
|
-
local sess="$1" tries="${2:-50}" out
|
|
1144
|
-
while (( tries-- > 0 )); do
|
|
1145
|
-
out="$(tmux list-clients -t "=$sess" 2>/dev/null || true)"
|
|
1146
|
-
[[ -n "$out" ]] && return 0
|
|
1147
|
-
sleep 0.1
|
|
1148
|
-
done
|
|
1149
|
-
return 1
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
# 开可见窗口(只启动、不校验):xdg-terminal-exec;--app-id 含角色名,--dir 为 repo
|
|
1153
|
-
# 默认 socket(无 -L);setsid -f 的 rc 不作成功依据,由调用方做句柄前置校验(BR2)。
|
|
1154
|
-
open_xdg_window() { # <name> <repo> <cmd...>
|
|
1155
|
-
local name="$1" repo="$2"; shift 2
|
|
1156
|
-
command -v xdg-terminal-exec >/dev/null 2>&1 || return 1
|
|
1157
|
-
setsid -f xdg-terminal-exec --app-id="org.nao.$name" --dir="$repo" -- "$@" >/dev/null 2>&1 || return 1
|
|
1158
|
-
return 0
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
# 窗口 class 可见性(④ 句柄):Hyprland 主路;其它合成器/X11 仅存在性探测(NFR4;覆盖限制 NFR5)
|
|
1162
|
-
# 返回 0=可见 / 1=不可见 / 2=无查询器(不可校验)
|
|
1163
|
-
app_class_visible() { # <name>
|
|
1164
|
-
local name="$1" cls="org.nao.$name" out
|
|
1165
|
-
if command -v hyprctl >/dev/null 2>&1; then
|
|
1166
|
-
out="$(hyprctl clients -j 2>/dev/null || true)"
|
|
1167
|
-
[[ "$out" == *"$cls"* ]] && return 0
|
|
1168
|
-
return 1
|
|
1169
|
-
fi
|
|
1170
|
-
if command -v swaymsg >/dev/null 2>&1; then
|
|
1171
|
-
out="$(swaymsg -t get_tree 2>/dev/null || true)"
|
|
1172
|
-
[[ "$out" == *"$cls"* ]] && return 0
|
|
1173
|
-
return 1
|
|
1174
|
-
fi
|
|
1175
|
-
if command -v xdotool >/dev/null 2>&1; then
|
|
1176
|
-
xdotool search --class "$cls" >/dev/null 2>&1 && return 0
|
|
1177
|
-
return 1
|
|
1178
|
-
fi
|
|
1179
|
-
if command -v wmctrl >/dev/null 2>&1; then
|
|
1180
|
-
out="$(wmctrl -lx 2>/dev/null || true)"
|
|
1181
|
-
[[ "$out" == *"$cls"* ]] && return 0
|
|
1182
|
-
return 1
|
|
1183
|
-
fi
|
|
1184
|
-
return 2
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
wait_for_app_class() { # <name> [tries] → 0 可见 / 1 未出现 / 2 无查询器
|
|
1188
|
-
local name="$1" tries="${2:-50}" rc
|
|
1189
|
-
while (( tries-- > 0 )); do
|
|
1190
|
-
rc=0; app_class_visible "$name" || rc=$?
|
|
1191
|
-
(( rc == 0 )) && return 0
|
|
1192
|
-
(( rc == 2 )) && return 2
|
|
1193
|
-
sleep 0.1
|
|
1194
|
-
done
|
|
1195
|
-
return 1
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
# ④ 直开窗:启动 + 限时 class 出现校验;失败尽力回收(M1:不假称已清理)
|
|
1199
|
-
# pid 经继承管道回传(wrapper exec 后即 pi)⇒ 终端单实例/daemon 化形态下仍能可靠定位 pi,
|
|
1200
|
-
# 不依赖窗口生命周期;无 pidfile ⇒ 父先返回也不会被「子进程晚写」残留(M2 设计修复)。
|
|
1201
|
-
spawn_xdg_window() { # <name> <repo> <cmd...>
|
|
1202
|
-
local name="$1" repo="$2"; shift 2
|
|
1203
|
-
local pid="" vrc=0
|
|
1204
|
-
open_pid_pipe || return 1
|
|
1205
|
-
if ! open_xdg_window "$name" "$repo" bash -c "$(pid_pipe_wrapper)" _ "$@"; then
|
|
1206
|
-
close_pid_pipe; return 1
|
|
1207
|
-
fi
|
|
1208
|
-
wait_for_app_class "$name" || vrc=$?
|
|
1209
|
-
# 成功 / D6 接受:无需 pid,直接关管道(子进程 O_RDWR 写不会 SIGPIPE;无路径 ⇒ 无残留)
|
|
1210
|
-
if (( vrc == 0 )); then close_pid_pipe; return 0; fi
|
|
1211
|
-
if (( vrc == 2 )); then
|
|
1212
|
-
warn "无 class 探针(hyprctl/swaymsg/xdotool/wmctrl 均缺):未能校验 $name 窗口可见性(D6)"
|
|
1213
|
-
close_pid_pipe; return 0
|
|
1214
|
-
fi
|
|
1215
|
-
# 有探针但限时未出现 class ⇒ 尽力回收 pi,如实报告回收结果(M1)
|
|
1216
|
-
pid="$(wait_for_pid_pipe 10)" || pid=""
|
|
1217
|
-
close_pid_pipe
|
|
1218
|
-
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
1219
|
-
kill "$pid" 2>/dev/null || true
|
|
1220
|
-
warn "④ 校验失败:已尽力回收 $name 的 pi 进程(pid=$pid);若窗口仍在请手动关闭"
|
|
1221
|
-
else
|
|
1222
|
-
warn "④ 校验失败:未捕获 $name 的 pi 进程 pid,已开窗口可能仍在,请手动关闭"
|
|
1223
|
-
fi
|
|
1224
|
-
return 1
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
# ③ tmux 会话 + 可见窗口 attach:new-session -d 后开窗 attach,限时校验 list-clients;
|
|
1228
|
-
# 无客户端 ⇒ kill-session 并返回非 0(调用方降级④;用户裁量①:降级而非 exit 1)。
|
|
1229
|
-
spawn_tmux_attach() { # <name> <repo> <inner>
|
|
1230
|
-
local name="$1" repo="$2" inner="$3" sess="nao-$name"
|
|
1231
|
-
tmux new-session -d -s "$sess" "bash -lc $(printf %q "$inner")" >/dev/null 2>&1 || return 1
|
|
1232
|
-
if ! open_xdg_window "$name" "$repo" tmux attach -t "$sess"; then
|
|
1233
|
-
tmux kill-session -t "=$sess" >/dev/null 2>&1 || true
|
|
1234
|
-
return 1
|
|
1235
|
-
fi
|
|
1236
|
-
if wait_for_tmux_client "$sess"; then
|
|
1237
|
-
log "tmux 会话 $sess 已创建 + 可见窗口 attach(默认 socket;pane 句柄完整)"
|
|
1238
|
-
return 0
|
|
1239
|
-
fi
|
|
1240
|
-
tmux kill-session -t "=$sess" >/dev/null 2>&1 || true
|
|
1241
|
-
return 1
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
# 是否有 tmux pane 句柄(L6:status 降级标注按「④ 路径」而非「tmux 命令缺失」判断)
|
|
1245
|
-
# in tmux|NAO_TERMINAL=tmux|未显式指定且本机有 tmux(将走 ②/③)⇒ 有 pane 句柄
|
|
1246
|
-
has_tmux_handle() {
|
|
1247
|
-
[[ -n "${TMUX:-}" ]] && return 0
|
|
1248
|
-
[[ "${NAO_TERMINAL:-}" == tmux ]] && return 0
|
|
1249
|
-
[[ -z "${NAO_TERMINAL:-}" ]] && command -v tmux >/dev/null 2>&1 && return 0
|
|
1250
|
-
return 1
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
# 宿主阶梯判定(ADR 实施闸门):echo 动作标签;rc 11=档值非法 / 12=命令缺失 / 1=无可用宿主
|
|
1254
|
-
resolve_action() {
|
|
1255
|
-
if [[ -n "${NAO_TERMINAL:-}" ]]; then
|
|
1256
|
-
local h ok=0
|
|
1257
|
-
for h in "${NAO_TERMINAL_ALLOWED[@]}"; do [[ "$h" == "$NAO_TERMINAL" ]] && { ok=1; break; }; done
|
|
1258
|
-
(( ok )) || return 11
|
|
1259
|
-
command -v "$NAO_TERMINAL" >/dev/null 2>&1 || return 12
|
|
1260
|
-
printf 'nao:%s\n' "$NAO_TERMINAL"; return 0
|
|
1261
|
-
fi
|
|
1262
|
-
if [[ -n "${TMUX:-}" ]]; then
|
|
1263
|
-
if command -v tmux >/dev/null 2>&1; then printf 'tmux-in\n'; return 0; fi
|
|
1264
|
-
# L1(PM 裁定):$TMUX 已设但 tmux 命令缺失 ⇒ warn + 继续(不因升级中途变量残留卡死)
|
|
1265
|
-
warn "TMUX 已设置但 tmux 命令缺失:忽略 tmux 句柄,按环境继续选择宿主"
|
|
1266
|
-
fi
|
|
1267
|
-
if command -v tmux >/dev/null 2>&1 && xdg_launcher_ok; then printf 'tmux-attach\n'; return 0; fi
|
|
1268
|
-
if xdg_launcher_ok; then printf 'xdg-window\n'; return 0; fi
|
|
1269
|
-
if [[ "${BACKGROUND:-false}" == true ]]; then
|
|
1270
|
-
if command -v tmux >/dev/null 2>&1; then printf 'tmux-bg\n'; return 0; fi
|
|
1271
|
-
if command -v screen >/dev/null 2>&1; then printf 'screen-bg\n'; return 0; fi
|
|
1272
|
-
fi
|
|
1273
|
-
return 1
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
1041
|
spawn_one() {
|
|
1277
|
-
local name="$1" repo="$2" model="$3" card prompt_file inner
|
|
1042
|
+
local name="$1" repo="$2" model="$3" card prompt_file inner
|
|
1278
1043
|
card="$PROMPTS_DIR/$PROMPT"
|
|
1279
1044
|
[[ -f "$card" ]] || die "角色卡不存在: $card"
|
|
1280
1045
|
[[ -d "$repo" ]] || die "工作区不存在: $repo"
|
|
1281
1046
|
|
|
1282
|
-
#
|
|
1283
|
-
action="$(resolve_action)" || rrc=$?
|
|
1284
|
-
case "$rrc" in
|
|
1285
|
-
0) ;;
|
|
1286
|
-
11) die_env "不支持的宿主:NAO_TERMINAL=$NAO_TERMINAL(可选 ${NAO_TERMINAL_ALLOWED[*]})" \
|
|
1287
|
-
"改用受支持宿主:\`NAO_TERMINAL=<$NAO_TERMINAL_LIST> nao-fleet.sh ensure $name\`" ;;
|
|
1288
|
-
12) die_env "宿主不可用:NAO_TERMINAL=$NAO_TERMINAL 未找到命令 $NAO_TERMINAL" \
|
|
1289
|
-
"先用 \`command -v $NAO_TERMINAL\` 确认安装,或改用已安装宿主(如 \`NAO_TERMINAL=tmux nao-fleet.sh ensure $name\`)" ;;
|
|
1290
|
-
*) die_env "无可用宿主:无 tmux / 无图形窗口宿主(xdg-terminal-exec)且未显式 --background" \
|
|
1291
|
-
"安装或指定已安装宿主:\`NAO_TERMINAL=<$NAO_TERMINAL_LIST> nao-fleet.sh ensure $name\`;无图形会话时用 \`nao-fleet.sh ensure --background $name\`" ;;
|
|
1292
|
-
esac
|
|
1293
|
-
|
|
1047
|
+
# 前提已由入口 require_tmux 硬校验(exit 3);此处只走唯一路径(BR1/BR3)。
|
|
1294
1048
|
prompt_file="$(build_system_prompt "$card")"
|
|
1295
1049
|
inner="cd $(printf %q "$repo") && exec pi --name $(printf %q "$name")"
|
|
1296
1050
|
[[ -n "$model" ]] && inner+=" --model $(printf %q "$model")"
|
|
1297
1051
|
inner+=" --append-system-prompt $(printf %q "$prompt_file")"
|
|
1298
1052
|
( sleep 60; rm -f -- "$prompt_file" ) & disown 2>/dev/null || true
|
|
1299
1053
|
|
|
1300
|
-
#
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
tmux-attach)
|
|
1304
|
-
if ! spawn_tmux_attach "$name" "$repo" "$inner"; then
|
|
1305
|
-
warn "tmux 会话 attach 限时内无客户端 → 降级为 xdg-terminal-exec 直开窗(阶梯④;用户裁量①)"
|
|
1306
|
-
spawn_xdg_window "$name" "$repo" $TMUX_ENV_SCRUB bash -lc "$inner" || spawn_rc=1
|
|
1307
|
-
fi ;;
|
|
1308
|
-
xdg-window) spawn_xdg_window "$name" "$repo" $TMUX_ENV_SCRUB bash -lc "$inner" || spawn_rc=1 ;;
|
|
1309
|
-
screen-bg|nao:screen) spawn_screen "$name" "$inner" || spawn_rc=1 ;;
|
|
1310
|
-
nao:foot) spawn_fork_window "$name" $TMUX_ENV_SCRUB foot --app-id="org.nao.$name" --working-directory="$repo" -e bash -lc "$inner" || spawn_rc=1 ;;
|
|
1311
|
-
nao:ghostty) spawn_fork_window "$name" $TMUX_ENV_SCRUB ghostty -e bash -lc "$inner" || spawn_rc=1 ;;
|
|
1312
|
-
nao:ptyxis) spawn_fork_window "$name" $TMUX_ENV_SCRUB ptyxis -- bash -lc "$inner" || spawn_rc=1 ;;
|
|
1313
|
-
*) spawn_rc=1 # 理论上不可达(resolve_action 标签全覆盖);防御性不假成功
|
|
1314
|
-
esac
|
|
1315
|
-
if (( spawn_rc != 0 )); then
|
|
1316
|
-
rm -f -- "$prompt_file" 2>/dev/null || true
|
|
1317
|
-
die_spawn "宿主 $action 未能拉起 $name(动作失败,exit 1;回收结果见上方提示)"
|
|
1318
|
-
fi
|
|
1319
|
-
case "$action" in
|
|
1320
|
-
tmux-in|nao:tmux) htag="tmux" ;;
|
|
1321
|
-
tmux-attach) htag="tmux+window" ;;
|
|
1322
|
-
xdg-window) htag="xdg-terminal-exec" ;;
|
|
1323
|
-
tmux-bg) htag="tmux(detached)" ;;
|
|
1324
|
-
screen-bg|nao:screen) htag="screen" ;;
|
|
1325
|
-
nao:*) htag="${action#nao:}" ;;
|
|
1326
|
-
*) htag="$action" ;;
|
|
1327
|
-
esac
|
|
1328
|
-
log "[$htag] 已拉起 $name @ $repo${model:+(model=$model)}$([[ "$action" == tmux-in || "$action" == nao:tmux ]] && echo "(layout=$TMUX_LAYOUT)")"
|
|
1054
|
+
# 拉起(pane 级失败 = warn + 跳过该角色,D-A);成功后打印「已拉起」
|
|
1055
|
+
spawn_tmux "$name" "$inner"
|
|
1056
|
+
log "[tmux] 已拉起 $name @ $repo${model:+(model=$model)}(layout=$TMUX_LAYOUT)"
|
|
1329
1057
|
}
|
|
1330
1058
|
|
|
1331
1059
|
# ---------------------------------------------------------------------------
|
|
@@ -1528,37 +1256,27 @@ cmd_check() {
|
|
|
1528
1256
|
done
|
|
1529
1257
|
fi
|
|
1530
1258
|
|
|
1531
|
-
echo "==
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
local okh=0 h
|
|
1535
|
-
for h in "${NAO_TERMINAL_ALLOWED[@]}"; do [[ "$h" == "$NAO_TERMINAL" ]] && { okh=1; break; }; done
|
|
1536
|
-
if (( ! okh )); then
|
|
1537
|
-
printf ' ✗ NAO_TERMINAL=%-10s 非法(可选 %s)\n' "$NAO_TERMINAL" "${NAO_TERMINAL_ALLOWED[*]}"; rc=1
|
|
1538
|
-
elif ! command -v "$NAO_TERMINAL" >/dev/null 2>&1; then
|
|
1539
|
-
printf ' ✗ NAO_TERMINAL=%-10s 命令缺失(ensure 将 exit 2)\n' "$NAO_TERMINAL"; rc=1
|
|
1540
|
-
else
|
|
1541
|
-
printf ' ✓ NAO_TERMINAL=%-10s 合法且可用\n' "$NAO_TERMINAL"
|
|
1542
|
-
fi
|
|
1259
|
+
echo "== 运行前提(tmux 内 + pi ≥1.4.0;AC1/AC3)=="
|
|
1260
|
+
if command -v tmux >/dev/null 2>&1; then
|
|
1261
|
+
echo ' ✓ tmux 可用(唯一受支持宿主)'
|
|
1543
1262
|
else
|
|
1544
|
-
|
|
1263
|
+
printf ' ✗ tmux 缺失(ensure/status/close 将 exit 3)\n'; rc=1
|
|
1545
1264
|
fi
|
|
1546
|
-
if
|
|
1547
|
-
echo
|
|
1265
|
+
if [[ -n "${TMUX:-}" ]]; then
|
|
1266
|
+
echo " ✓ 当前在 tmux 会话内($(tmux display-message -p '#S' 2>/dev/null || echo '?'))"
|
|
1548
1267
|
else
|
|
1549
|
-
|
|
1268
|
+
printf ' · 当前不在 tmux 会话内:ensure/status/close 将 exit 3(先 `tmux new -s nao`);纯静态体检可照跑\n'
|
|
1550
1269
|
fi
|
|
1551
|
-
|
|
1552
|
-
|
|
1270
|
+
local pi_cur
|
|
1271
|
+
if pi_cur="$(pi_version_check)"; then
|
|
1272
|
+
printf ' ✓ pi 版本 %s ≥ 1.4.0\n' "$pi_cur"
|
|
1553
1273
|
else
|
|
1554
|
-
|
|
1274
|
+
printf ' ✗ pi 版本 %s < 1.4.0\n' "$pi_cur"; rc=1
|
|
1555
1275
|
fi
|
|
1556
1276
|
|
|
1557
1277
|
echo "== tmux 布局 =="
|
|
1558
|
-
if [[ -
|
|
1559
|
-
echo
|
|
1560
|
-
else
|
|
1561
|
-
echo " (当前不在 tmux 内:宿主阶梯将按 ③/④/⑤ 选择可见窗口或 --background)"
|
|
1278
|
+
if [[ -z "${TMUX:-}" ]]; then
|
|
1279
|
+
echo ' · 当前不在 tmux 内(布局只在 tmux 分屏时生效)'
|
|
1562
1280
|
fi
|
|
1563
1281
|
case "$TMUX_LAYOUT" in
|
|
1564
1282
|
main-row2|main-col|grid)
|
|
@@ -1591,11 +1309,7 @@ cmd_check() {
|
|
|
1591
1309
|
cmd_status() {
|
|
1592
1310
|
local a
|
|
1593
1311
|
intercom_list_json >/dev/null 2>&1 || true # 预热名册缓存(后续子 shell 复用)
|
|
1594
|
-
echo "== 角色会话在线状态(权威名单见 intercom({action:'list'})
|
|
1595
|
-
# ④ 路径降级标注(BR6/D2/L6):无 tmux pane 句柄(含 NAO_TERMINAL=foot 等)时 turn 判定仅靠名册;恰好一行
|
|
1596
|
-
if ! has_tmux_handle; then
|
|
1597
|
-
echo ' (turn 判定来源 = 名册;非 tmux 宿主降级:无 tmux pane 句柄,在跑 turn 判定仅供参考)'
|
|
1598
|
-
fi
|
|
1312
|
+
echo "== 角色会话在线状态(权威名单见 intercom({action:'list'});本表按 tmux 终端标题/名册判定)=="
|
|
1599
1313
|
for a in "${ROLE_ORDER[@]}"; do
|
|
1600
1314
|
if running "$a" "${ROLE_WS[$a]:-}"; then
|
|
1601
1315
|
printf ' ✓ %-14s 在线(--name %s;卡片 %s)\n' "$a" "$a" "${ROLE_CARDS[$a]}"
|
|
@@ -1640,7 +1354,7 @@ cmd_status() {
|
|
|
1640
1354
|
}
|
|
1641
1355
|
|
|
1642
1356
|
# ---------------------------------------------------------------------------
|
|
1643
|
-
# 回收已完成会话:闸门(tasks-state 已推进 + 无在跑 turn)+ 落地(pane
|
|
1357
|
+
# 回收已完成会话:闸门(tasks-state 已推进 + 无在跑 turn)+ 落地(pane/tmux 会话/进程)
|
|
1644
1358
|
cmd_close() {
|
|
1645
1359
|
local force="$1" task="$2" target="${3:-}"
|
|
1646
1360
|
[[ -n "$target" ]] || die "close 需要目标:角色别名或派生会话名(如 rd-be / rd-be-T1 / qa-T1)"
|
|
@@ -1650,7 +1364,7 @@ cmd_close() {
|
|
|
1650
1364
|
if [[ "$target" == subagent-* ]]; then
|
|
1651
1365
|
die "拒绝回收 $target:这是 pi-subagents 子代理会话(subagent-*),其生命周期不归 nao 回收(请用 pi-subagents 的 stop / retention 管理)"
|
|
1652
1366
|
fi
|
|
1653
|
-
local name repo cls bus pane pids st tgt a best=""
|
|
1367
|
+
local name repo cls bus pane pids st tgt a best=""
|
|
1654
1368
|
intercom_list_json >/dev/null 2>&1 || true # 预热名册缓存
|
|
1655
1369
|
|
|
1656
1370
|
# ---- 目标解析:别名/canonical id | 派生会话名 <角色>-<编号> ----
|
|
@@ -1698,33 +1412,19 @@ cmd_close() {
|
|
|
1698
1412
|
# F1 不变量(T513-S2):close 任何路径不得回收调用者自己的 pane/窗口。
|
|
1699
1413
|
# 根因:派生窗口若继承 $TMUX_PANE,子进程按该 pane 注册名册 ⇒ 层② 命中调用者自身;
|
|
1700
1414
|
# 本闸门是纵深防御——无论上游解析为何,命中即拒绝(不 kill-pane、不进入 pi_pids_for 兜底)。
|
|
1701
|
-
|
|
1702
|
-
|
|
1415
|
+
# T530:判据=进程归属三态(fail-closed,见 self_pane_refuse),不是裸 pane id 比较 —— pane id 是
|
|
1416
|
+
# server 局部命名空间,隔离 socket 的 %0 与默认 server 的 $TMUX_PANE=%0 撞号会被误判为「自己」而**误拒**。
|
|
1417
|
+
# 不可判定时只对「撞号」保守拒绝:判不了自己 ⇒ 宁可少回收一次,不得误杀一个会话。
|
|
1418
|
+
if [[ -n "$pane" ]] && self_pane_refuse "$pane"; then
|
|
1419
|
+
warn "拒绝回收 $name:解析出的 pane $pane 判为调用者自身(pane_pid 祖先链命中,或祖先链不可判定且与 \$TMUX_PANE 撞号)"
|
|
1703
1420
|
warn " 名册 tmuxPane 可能因派生时继承 TMUX_PANE 而错注册;请用 intercom({action:'list'}) 核对目标会话后再处理"
|
|
1704
1421
|
return 1
|
|
1705
1422
|
fi
|
|
1706
1423
|
if [[ -z "$pane" ]]; then
|
|
1707
|
-
# nao-<会话名> detached tmux
|
|
1424
|
+
# nao-<会话名> detached tmux 会话(不依赖 repo)
|
|
1708
1425
|
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "=nao-$name" 2>/dev/null; then
|
|
1709
1426
|
if tmux kill-session -t "=nao-$name" 2>/dev/null; then log "已回收 $name(tmux 会话 nao-$name)@ $repo"; return 0; fi
|
|
1710
1427
|
fi
|
|
1711
|
-
# F2(arch 评审):销毁目标 = `screen -ls` 锚定行取出的 `pid.name` 精确令牌;取不到即不 quit
|
|
1712
|
-
# (此前 `screen -S nao-<名> -X quit` 是前缀目标,精确+派生并存时无「只杀精确」保障)。
|
|
1713
|
-
stok="$(screen_token_for "$name")"
|
|
1714
|
-
if [[ -n "$stok" ]] && screen -S "$stok" -X quit 2>/dev/null; then
|
|
1715
|
-
log "已回收 $name(screen 会话 $stok)@ $repo"; return 0
|
|
1716
|
-
fi
|
|
1717
|
-
# ④ 路径句柄(BR7/AC8):app-id/class → pid(校验 comm=pi)→ kill 优先;
|
|
1718
|
-
# 有句柄即回收,不再依赖名册(名册可能滞后)。
|
|
1719
|
-
local app_pid
|
|
1720
|
-
app_pid="$(app_pid_for "$name")"
|
|
1721
|
-
if [[ -n "$app_pid" ]]; then
|
|
1722
|
-
if kill "$app_pid" 2>/dev/null; then
|
|
1723
|
-
log "已回收 $name(app-id=org.nao.$name · pid=$app_pid)@ $repo"; return 0
|
|
1724
|
-
fi
|
|
1725
|
-
warn "已定位 $name 的 app-id 句柄 pid=$app_pid 但回收失败(进程可能刚好退出;核对后可忽略)"
|
|
1726
|
-
return 1
|
|
1727
|
-
fi
|
|
1728
1428
|
# 无任何可杀句柄:名册若报在线 ⇒ 定位失败,必须非静默(禁止「未运行」而 pane 仍在)
|
|
1729
1429
|
if intercom_online "$name"; then
|
|
1730
1430
|
warn "无法回收 $name:pi-intercom 名册报在线(cwd=${repo})但未定位到存活 pane/会话"
|
|
@@ -1733,7 +1433,7 @@ cmd_close() {
|
|
|
1733
1433
|
return 1
|
|
1734
1434
|
fi
|
|
1735
1435
|
# T516 AC3(#1 错定缺陷的负向闸):常驻精确句柄全缺席而其派生在场 ⇒ 目标歧义,
|
|
1736
|
-
#
|
|
1436
|
+
# 拒绝并指路(不得落入「未运行(无需回收)」静默 no-op)。
|
|
1737
1437
|
local -a dfield=()
|
|
1738
1438
|
mapfile -t dfield < <(derived_in_field "$name")
|
|
1739
1439
|
if (( ${#dfield[@]} > 0 )); then
|
|
@@ -1741,13 +1441,8 @@ cmd_close() {
|
|
|
1741
1441
|
warn " 目标歧义(tmux 会话名前缀匹配曾误回收派生,T515 #1):如需回收派生用 close <派生名>(如 close ${dfield[0]});如需拉起常驻用 ensure $tgt"
|
|
1742
1442
|
return 1
|
|
1743
1443
|
fi
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
fi
|
|
1747
|
-
warn "无法确认 $name 在线状态:宿主无 tmux/screen 句柄,且 pi-intercom 名册不可用"
|
|
1748
|
-
warn " pi 启动后 argv 被改写为 \"pi\",进程扫描不可信 ⇒ 不做静默 no-op(本命令 exit 非 0)"
|
|
1749
|
-
warn " 请用 intercom list 人工核对;确认已退出后忽略本提示"
|
|
1750
|
-
return 1
|
|
1444
|
+
# BR5(T529):tmux 下必有句柄,不可判定不再发生 ⇒ 直接报「未运行」(无 offline 分支)
|
|
1445
|
+
log "$name 未运行(无需回收)"; return 0
|
|
1751
1446
|
fi
|
|
1752
1447
|
|
|
1753
1448
|
# 闸门②:在跑 turn(pane 标记 + roster 状态双重信号)
|
|
@@ -1877,7 +1572,7 @@ usage() {
|
|
|
1877
1572
|
}
|
|
1878
1573
|
|
|
1879
1574
|
# ---- 入口 ----
|
|
1880
|
-
CMD=""; FORCE=false; STRICT=false; MODEL=""; TASK="";
|
|
1575
|
+
CMD=""; FORCE=false; STRICT=false; MODEL=""; TASK=""; CLEANUP_SOCK=""; TARGETS=()
|
|
1881
1576
|
while [[ $# -gt 0 ]]; do
|
|
1882
1577
|
case "$1" in
|
|
1883
1578
|
check) CMD="check"; shift ;;
|
|
@@ -1892,7 +1587,6 @@ while [[ $# -gt 0 ]]; do
|
|
|
1892
1587
|
check_model "$MODEL" || die "模型 '$MODEL' 不在白名单内。允许: $MODEL_WHITELIST(可通过 NAO_MODEL_WHITELIST 覆盖)"
|
|
1893
1588
|
shift 2 ;;
|
|
1894
1589
|
--force) FORCE=true; shift ;;
|
|
1895
|
-
--background) BACKGROUND=true; shift ;;
|
|
1896
1590
|
--task) TASK="${2:-}"; [[ -n "$TASK" ]] || die "--task 需要任务编号(如 T1)"; [[ "$TASK" =~ ^[A-Za-z0-9_-]+$ ]] || die "--task 非法: $TASK(仅字母/数字/-/_)"; shift 2 ;;
|
|
1897
1591
|
--strict) STRICT=true; shift ;;
|
|
1898
1592
|
-v|--verbose) VERBOSE=true; shift ;;
|
|
@@ -1901,9 +1595,11 @@ while [[ $# -gt 0 ]]; do
|
|
|
1901
1595
|
esac
|
|
1902
1596
|
done
|
|
1903
1597
|
|
|
1598
|
+
# 唯一运行前提闸门(BR1/BR3):先于 load_manifest 与任何副作用;不满足 ⇒ exit 3(命令未执行)。
|
|
1599
|
+
# cleanup 豁免:其正常场景就是非 tmux 的隔离实验场(env -u TMUX ... -S <socket>)。
|
|
1904
1600
|
case "$CMD" in
|
|
1905
1601
|
check) : ;; # cmd_check 自行先做文本契约体检,再 load_manifest(缩进违例时也能先出报告)
|
|
1906
|
-
ensure|status|close) load_manifest ;;
|
|
1602
|
+
ensure|status|close) require_tmux; load_manifest ;;
|
|
1907
1603
|
esac
|
|
1908
1604
|
case "$CMD" in
|
|
1909
1605
|
cleanup) : ;; # cleanup 不需角色清单
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 任务完成协议(按需,任务收尾/收尾回执前读取)——DoD 四类 · 维护型补充判据 · 遗留项三出口 · 分级复盘
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 任务完成协议(DoD + 遗留项三出口 + 分级复盘)
|
|
6
|
+
|
|
7
|
+
> 与「派发 → 回执 → 验收 → 合并」**正交**:那条链路管「怎么交付需求」,本文件管「怎么判定一个任务单元真的完成」。
|
|
8
|
+
> 既有闸门**只指针不改写**:终态回执 / 验收闭环 / 合并闸门 @.agents/skills/pm-operations.md §一 · 上线闭环 @.agents/skills/github-flow.md §发布(阶段 7) · 回执模板 @.agents/checklists/comm-templates.md。
|
|
9
|
+
|
|
10
|
+
## 一、任务单元
|
|
11
|
+
|
|
12
|
+
- **交付型**:`T<issue-id>`(有 Issue)或降级批次号(`nao/<批次-slug>`)。
|
|
13
|
+
- **维护型**:`M<YYYYMMDD>-<slug>`(如 `M20261014-pi-ext-update`)——外部命令/依赖/环境动作,通常无 PRD。
|
|
14
|
+
- 两套粒度共存,台账字段一致;一个任务单元 = 一个 DoD 实例 = 一条收尾回执。
|
|
15
|
+
|
|
16
|
+
## 二、DoD 四类骨架(逐任务实例化,不套模板空话)
|
|
17
|
+
|
|
18
|
+
1. **产物**:落盘路径(第三方可独立读到);无产物写「无(理由)」。
|
|
19
|
+
2. **验证**:可执行判据 —— 命令 + exit code + 数字(文件数/例数/红数),或结构断言;禁「看起来没问题」。
|
|
20
|
+
3. **遗留项**:按 §四 三出口,**零遗留也必须显式写「无」**。
|
|
21
|
+
4. **交接**:状态外部化(`docs/tasks-state.md`)+ 会话回收/重开 + 接手者第一动作。
|
|
22
|
+
|
|
23
|
+
实例化时机:**开工确认卡**上逐任务写死(@.agents/skills/pm-operations.md §二),验收时按同一份 DoD 核对。
|
|
24
|
+
|
|
25
|
+
## 三、维护型补充判据(命令 exit 0 ≠ 完成)
|
|
26
|
+
|
|
27
|
+
命令 `exit 0` **不构成**完成。输出中每条 `warn` / `error` / `pending approval` / `N vulnerabilities` 必须归入:
|
|
28
|
+
**已处置**(附命令与结果)|**显式接受**(理由 + 到期日)|**转台账**(含 owner)。
|
|
29
|
+
|
|
30
|
+
- **依赖类必须两项决策**:`npm audit`(逐告警处置/接受)与 `install-scripts`(批准或拒绝 pending script)——**不许留空跳过**。
|
|
31
|
+
- 证据落 `docs/reports/<编号>-followups.md`(一行一条:编号/来源/严重度/出口/到期);可执行项同步进项目 backlog(本仓 = `docs/tasks-state.md` 待派发队列)。
|
|
32
|
+
- 超阈值(安全类 / 到期未决 >30 天)升 Issue。
|
|
33
|
+
- **首个实例**:`docs/reports/M20261014-pi-ext-update-followups.md`(`pi update --extensions` 4 漏洞 + 1 待批脚本,exit 0 但留 3 项遗留)。
|
|
34
|
+
|
|
35
|
+
## 四、遗留项三出口(无第四出口)
|
|
36
|
+
|
|
37
|
+
- **关闭**:已解决(附验证)。
|
|
38
|
+
- **接受**:降级 + 理由 + **到期日**(缺到期日 → 验收打回)。
|
|
39
|
+
- **转办**:进 `docs/tasks-state.md` 待派发队列或 Issue(含 owner)。
|
|
40
|
+
- 「先放着」「后续再看」**不是出口**。
|
|
41
|
+
- 无 `tasks-state` 的下游项目回落到「Issue 或报告末尾遗留段」,不阻塞。
|
|
42
|
+
- 回执形态:`遗留:无 | <n> 项 → <台账|tasks-state|Issue|接受(到期)>`(@.agents/checklists/comm-templates.md)。
|
|
43
|
+
|
|
44
|
+
## 五、分级复盘
|
|
45
|
+
|
|
46
|
+
- **L0 一行(默认)**:四字段 `结论 / 遗留 / 日期 / 编号`,落台账(示例:`docs/reports/M20261014-pi-ext-update-followups.md` §四)。
|
|
47
|
+
- **L1 完整复盘**:`docs/reports/<编号>-retro.md` 四节 —— 发生了什么 / 根因 / 改了哪条机制 / 可验证防线。
|
|
48
|
+
- **L1 触发(五条,命中任一)**:返工 ≥1 次|漏闸|事故(线上 / 共享分支受损或用户可见中断)|同类问题第二次出现|用户投诉。
|
|
49
|
+
- 反面示例:单次文档笔误改回 → L0 一行即可;同批两次因未读对方卡版本导致返工 → 命中「返工 ≥1」+「同类第二次」→ L1。
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ nao-skills 是一套 **角色卡 + 协作规矩 + 小工具**。装上以后,
|
|
|
47
47
|
|
|
48
48
|
角色名可以写**别名**或**规范名**(两者等价):`arch` = `arch-designer`、`infra` = `rd-infra`;上例里两者混用也照常拉起。完整别名表见 `.agents/roles.yaml` 或 `nao-fleet.sh --help`。
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
**必须在 tmux 里用**:舰队在 tmux 会话内以派生 pane 拉起(`ensure` / `status` / `close` 都只接受 tmux 内,同时要求 `pi ≥ 1.4.0`),不再提供终端窗口 / 后台会话等多宿主路径。非 tmux 环境会直接报错(`exit 3` + 一行提示 `tmux new -s nao`),**不会**建任何窗口或会话。旧版用 `--background` / screen 拉起过的会话,本版起 `status` 看不到、`close` 收不回,先 `screen -ls` 手工清一次。
|
|
51
51
|
|
|
52
52
|
然后**在 PM 会话里说需求**就行,剩下的它自己流转。大概长这样:
|
|
53
53
|
|
|
@@ -95,14 +95,11 @@ PM:在 PR 核 AC 并评论 → 验收通过(授权合并)→ rd-be squash
|
|
|
95
95
|
|
|
96
96
|
**跑完的会话要留着吗?** 后台派生会话验收完可以回收:`nao-fleet.sh close --task T1 rd-be`(等价写法 `nao-fleet.sh close rd-be-T1`,直接用派生会话名即可);常驻角色留着复用。`nao-fleet.sh status` 会提示哪些是残留。目标在别的项目里也能回收——`close` 会按名册/窗口标题跨项目定位;定位不到时会**非 0 退出并打印诊断**,不会假装已回收。实验/夹具用的隔离 tmux server 用 `nao-fleet.sh cleanup -S <socket>` 清理:**强制显式 socket**,不带 `-S` 或指向生产默认 socket 一律 exit 2 拒绝,绝不会连坐你正在用的真实 server。
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
**为什么要装 tmux?** **必须装**(v0.19.0 起唯一的运行前提):
|
|
99
99
|
|
|
100
100
|
- **布局可控**:6 个角色会话在 tmux 里按三种布局自动排布(`main-row2` / `main-col` / `grid`),间距与列宽可用 `NAO_TMUX_*` 变量调,不会把桌面铺满一圈窗口。
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
- **多种宿主探测路径**:机制里 tmux 属首选阶梯;非 tmux 宿主走外部终端窗口,探测与降级链路更长。
|
|
104
|
-
|
|
105
|
-
没装 tmux 也能正常用(见「不在 tmux 里也能用」),只是上述便利项会依次降级。
|
|
101
|
+
- **句柄完整**:`status` 查残留、`close` 跨项目回收都按会话名册与 pane 标题定位;tmux 宿主下句柄稳定可寻,没有「查不到就只能猜」的分支。
|
|
102
|
+
- **SSH 也能用**:不挑图形环境,起一个 tmux 会话就能拉起整套舰队。
|
|
106
103
|
|
|
107
104
|
**终端窗口太挤怎么办?** tmux 宿主支持三种布局:默认 `main-row2`(主会话占左,其余往右分列)、`main-col`(其余在右列竖排)、`grid`(等大网格)。窗口太窄时 `main-row2` 会**自动回退** `main-col`、仍不够再退 `grid`,并打印带数字的提示(不中断拉起);用 `NAO_TMUX_LAYOUT` / `NAO_TMUX_MAIN_WIDTH` / `NAO_TMUX_MIN_PANE_WIDTH` 调整,详见 `bash .agents/scripts/nao-fleet.sh --help`。
|
|
108
105
|
|