@nathan33/nao-skill 0.18.6 → 0.19.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/checklists/pm.md
CHANGED
|
@@ -62,7 +62,7 @@ description: PM 交付核对清单(按需,交付/派发前读取)
|
|
|
62
62
|
- [ ] 输出里有 `warn`/`error`/`pending approval`/`N vulnerabilities` 却判完成,或遗留项未走三出口(「先放着」不算出口)?(见 @.agents/skills/task-completion.md)
|
|
63
63
|
- [ ] 降级(无 `gh` / 无远端)未在 `tasks-state` 与回执中标注?
|
|
64
64
|
|
|
65
|
-
## 交付检查清单(完整
|
|
65
|
+
## 交付检查清单(完整 41 项)
|
|
66
66
|
|
|
67
67
|
- [ ] 目标/边界/验收三件事齐备
|
|
68
68
|
- [ ] 证据 + 5 Whys 已产出
|
|
@@ -90,6 +90,7 @@ description: PM 交付核对清单(按需,交付/派发前读取)
|
|
|
90
90
|
- [ ] 项目 `AGENTS.md` 已建立/同步(项目级属性与约束,见 @.agents/skills/pm-operations.md §四)
|
|
91
91
|
- [ ] worker 回执含**全量**门禁精确数字(命令 + exit code + 文件数/例数/红数);PM 仅核对数字 + 读变更文件 + 异常才抽查(未重复跑)
|
|
92
92
|
- [ ] 派发消息只给引用路径(`docs/...md#section`),无 PRD/方案全文或附件传全文
|
|
93
|
+
- [ ] 派发含「根因 + 修复方向」的指令已附**一条已执行的实测命令与输出**(无实测只能派「诊断/复现任务」,不得先给方向)
|
|
93
94
|
- [ ] 派发时已声明回执级别(未声明默认 `done(lite)`;有阻塞/风险/需决策已要求 `done(full)`);模板已按需读取 `@.agents/checklists/comm-templates.md`
|
|
94
95
|
- [ ] 用户口头约束已**当场落盘**(`AGENTS.md` 项目级 / PRD「变更治理」),未等归档
|
|
95
96
|
- [ ] 归档前已更新接续快照 + 记录会话体检(归档后按 @.agents/skills/pm-operations.md §三 重开会话)
|
|
@@ -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(短常驻)——需求分析/PRD/优先级/验收/多会话调度
|
|
3
3
|
role: pm
|
|
4
|
-
version:
|
|
5
|
-
updated: 2026-10-
|
|
4
|
+
version: 27
|
|
5
|
+
updated: 2026-10-28
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# 产品经理(PM)
|
|
@@ -66,7 +66,7 @@ updated: 2026-10-27
|
|
|
66
66
|
- **会话生命周期(PM 专属)**:按批次边界重开(归档完成 / 用户换需求 / `/session` contextTokens 超窗口 40%)+ 落盘接续 + 重开后**回读确认**;细则见 @.agents/skills/pm-operations.md §三。
|
|
67
67
|
- **状态外部化**:任务状态落 `docs/tasks-state.md`(含接续快照),每次派发/回执/验收后更新;重开先读它。
|
|
68
68
|
- **回收与残留**:派生会话验收通过即 `close --task <编号> <别名>`;常驻会话不回收,需重开时 `ensure --force`;`status` 残留即核对回收。
|
|
69
|
-
- **派发协议(省 token)**:一条消息给 `编号 + 范围 + 引用路径(docs/...md#section)+ AC 编号 + 回执级别 + 需求分支`;**不贴 PRD/方案全文、不用 `attachments`**;worker `ask` → PM `reply
|
|
69
|
+
- **派发协议(省 token)**:一条消息给 `编号 + 范围 + 引用路径(docs/...md#section)+ AC 编号 + 回执级别 + 需求分支`;**不贴 PRD/方案全文、不用 `attachments`**;worker `ask` → PM `reply`。**根因类派发须附实测**:消息含「根因 + 修复方向」时,必附**一条已执行的实测命令与其输出**(不是推测、不是转述他人结论);无实测只能派「诊断 / 复现任务」,不得先给修复方向(T529 教训:无实测的根因被 QA A/B 推翻)。
|
|
70
70
|
|
|
71
71
|
## 七、AGENTS.md 项目上下文治理(PM 独有职责)
|
|
72
72
|
|
|
@@ -92,6 +92,7 @@ updated: 2026-10-27
|
|
|
92
92
|
- [ ] PM 以「亲自探查技术实现」替代转角色、并据此形成 PRD 前提?(§九 技术调研边界)
|
|
93
93
|
- [ ] 派发消息贴了 PRD/方案全文、或用 `attachments` 传全文?(应给引用路径,worker 自己读文件)
|
|
94
94
|
- [ ] 跨批次未重开会话、未更新接续快照?(长上下文丢红线/闸门;见 §六 生命周期)
|
|
95
|
+
- [ ] 派发含「根因 + 修复方向」的指令未附**一条已执行的实测命令与输出**?(无实测只能派「诊断/复现任务」,不得先给方向;§六 派发协议)
|
|
95
96
|
- [ ] 用外部调研(文章/竞品/开源实现)替代 arch 技术取舍,或把外部做法写成既定技术前提?(§九 + @.agents/skills/research.md)
|
|
96
97
|
- [ ] PM 改源码/测试/构建配置,或手改冲突内容?(应转 worker;PM 只做仓库治理与发布)
|
|
97
98
|
- [ ] PM 亲自合并 PR / `push` main?(合并由 RD 执行,PM 只验收授权;§十三)
|
|
@@ -103,7 +104,7 @@ updated: 2026-10-27
|
|
|
103
104
|
- [ ] 输出里有 `warn`/`error`/`pending approval`/`N vulnerabilities` 却判完成?(`exit 0` ≠ 完成,见 @.agents/skills/task-completion.md §三)
|
|
104
105
|
- [ ] 降级(无 `gh` / 无远端)未在 `tasks-state` 与回执中标注?
|
|
105
106
|
|
|
106
|
-
> 完整红线(33 项)与交付检查清单(
|
|
107
|
+
> 完整红线(33 项)与交付检查清单(41 项):**交付/派发前**读取 @.agents/checklists/pm.md 逐项核对。
|
|
107
108
|
|
|
108
109
|
## 十一、交付检查清单
|
|
109
110
|
|
|
@@ -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 不需角色清单
|
|
@@ -53,7 +53,7 @@ PM 在**立项 / 终签 / 发布**时读取;RD 在**起分支 / 开 PR / 合
|
|
|
53
53
|
|
|
54
54
|
- 版本号:SemVer,PM 按批次范围定(feat→MINOR / fix→PATCH / 破坏性→MAJOR);项目 `AGENTS.md` 有约定则从之。
|
|
55
55
|
- 门禁:AC 全过 + 验收闭环 + main 门禁全绿 + 工作区干净 + PR 已合并。
|
|
56
|
-
-
|
|
56
|
+
- **发布前置(严格闸门 · 双环境)**:若仓库提供 `test:t4:full:dual` 脚本,发布前须先 `npm run test:t4:full:dual` **rc=0**(**两组**= `$TMUX_PANE` 已设 / `env -u TMUX_PANE`;判据 = **两组 SKIP 计数与 DEGRADE 计数均为 0,且非白名单 NOTE 计数为 0**,`SKIP`/`DEGRADE`/非白名单 `NOTE` 即 FAIL)。仅有 `test:t4:full`(单环境)的仓库退用 `npm run test:t4:full` rc=0;双环境 gate 误红时可临时回落单环境并记台账。
|
|
57
57
|
- **优先 `gh`**:`gh release create <tag> --title "vX.Y.Z" --notes-file docs/releases/<version>.md`(**标题口径(强约束):@.agents/common/release-notes.md** —— 标题仅裸版本号,描述进正文首段)。
|
|
58
58
|
- 探测:`gh auth status` 按 **exit code** 判定(`keyring` 警告但 exit 0 **仍可用**,可 `gh api user` 复核);**不自动 `gh auth login`**(涉用户凭证)。
|
|
59
59
|
- release notes 落盘 `docs/releases/<version>.md`(**写作规范(强约束):@.agents/common/release-notes.md** —— 面向用户「本次更新」置顶、内部编号 / 文件名 / 属性名一律下沉「技术细节」、正文不用表格;骨架见 @.agents/templates/release-notes.md.example)。
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathan33/nao-skill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "nao 多角色 AI 开发舰队:角色卡 / 技能 / 协作协议 / 工具链,一键接入项目(pi / Claude / 任意 Agent)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"test:agents": "bash .agents/scripts/nao-fleet.sh check && npm run check:agents",
|
|
47
47
|
"test:t4": "bash tests/t4/run.sh",
|
|
48
48
|
"test:t4:full": "T4_ALLOW_MISSING=0 T4_REQUIRE_FULL=1 bash tests/t4/run.sh",
|
|
49
|
+
"test:t4:full:dual": "bash tests/t4/run-dual.sh",
|
|
49
50
|
"check:agents": "tsc -p tsconfig.agents.json",
|
|
50
51
|
"prepack": "npm run test:agents"
|
|
51
52
|
},
|