@nathan33/nao-skill 0.16.1 → 0.16.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.
@@ -29,7 +29,7 @@ description: pi-intercom 多会话协作协议(常驻引用)
29
29
 
30
30
  - **判重 / 在线判定**:脚本按 pi 自设的**终端标题**(`π - <会话名> - <仓库名>`)+ `pi-intercom` 名册判在线(**不用 `pgrep --name`**:pi 启动后 argv 被改写为 `pi`,会恒失配);已运行则跳过并 warn。确需重开加 `--force`。
31
31
  - **标题被改写时的 pane 定位**(v0.9.4):终端标题可被外部改写(实测变为 `pi:c`),此时 `status`/`close`/`ensure` 按三层后备定位 pane:① 标题契约 → ② pi-intercom 名册 `tmuxPane`(注册时读 `$TMUX_PANE`,与标题改名无关,权威)→ ③ 本仓「未被认领 pi pane」唯一兜底。三层均未命中才认「无法确认」并 exit 非 0(不静默 no-op)。`close` 的在跑 turn 判定叠加名册活动状态(thinking / tool:* 等)与 pane 末 12 行状态行双重信号。
32
- - **宿主阶梯(tmux 外也看得见)**:`nao-fleet.sh ensure` 按环境选宿主:① `NAO_TERMINAL` 显式(`tmux`/`foot`/`ghostty`/`ptyxis`/`screen`;命令缺失或非法 ⇒ `exit 2`)→ ② `$TMUX` 内:当前窗口分屏 → ③ tmux 可用且有图形会话:建 `nao-<别名>` 会话并用 `xdg-terminal-exec` 开**可见窗口** attach(默认 socket,句柄模型完整)→ ④ 无 tmux 有图形:`xdg-terminal-exec --app-id=org.nao.<别名> --dir=<repo>` 直接开窗 → ⑤ 无图形:仅显式 `--background` 才建 detached 会话 → ⑥ 否则 `exit 2` + 一行诊断与恢复命令。**失败不假成功**:打印「已拉起」前必先校验宿主句柄。
32
+ - **宿主阶梯(tmux 外也看得见)**:`nao-fleet.sh ensure` 按环境选宿主:① `NAO_TERMINAL` 显式(`tmux`/`foot`/`ghostty`/`ptyxis`/`screen`;命令缺失或非法 ⇒ `exit 2`)→ ② `$TMUX` 内:**派生宿主窗口**(调用者 pane 所在 window)分屏 → ③ tmux 可用且有图形会话:建 `nao-<别名>` 会话并用 `xdg-terminal-exec` 开**可见窗口** attach(默认 socket,句柄模型完整)→ ④ 无 tmux 有图形:`xdg-terminal-exec --app-id=org.nao.<别名> --dir=<repo>` 直接开窗 → ⑤ 无图形:仅显式 `--background` 才建 detached 会话 → ⑥ 否则 `exit 2` + 一行诊断与恢复命令。**失败不假成功**:打印「已拉起」前必先校验宿主句柄。
33
33
  - **tmux 布局**:②/③ 的 tmux 宿主三选一(`NAO_TMUX_LAYOUT`):默认 `main-row2`(首 pane 全高占左,后续每角色往右开列、每列上下 2 个);`main-col`(其余在右列竖排);`grid`(等大网格)。**窄列守卫**:`main-row2` 最窄非主 pane < `NAO_TMUX_MIN_PANE_WIDTH`(默认 30)时自动回退 `main-col`,仍不足再退 `grid`(打印带数字的 warn,**不中断拉起**)。
34
34
  - **非 tmux 宿主降级边界(D2/D6)**:④ 路径无 tmux pane 句柄,`status` 会明确标注「turn 判定来源 = 名册」(在跑 turn 判定仅供参照),`close` 按窗口 `app-id`(`org.nao.<别名>`)→ pid(校验 `comm=pi`)回收;无 `hyprctl`/`swaymsg`/`xdotool`/`wmctrl` 等 class 探针时,接受启动并打印一行 warn(**无法校验 ≠ 校验失败**)。
35
35
  - **模型规则**:用户未指定则**禁止传 `--model`**,也禁止继承 PM 自身模型。
@@ -31,6 +31,7 @@
31
31
  # NAO_TMUX_LAYOUT=main-row2|main-col|grid tmux 布局(默认 main-row2)
32
32
  # NAO_TMUX_MAIN_WIDTH=<10..90> 主 pane 宽度百分比(默认 35;main-row2/main-col 共用)
33
33
  # NAO_TMUX_MIN_PANE_WIDTH=<10..80> 最小非主 pane 列宽守卫(默认 30;低于则回退)
34
+ # NAO_TMUX_HOST_WINDOW=<window> 显式指定派生宿主窗口(默认自动:$TMUX_PANE→名册;允许跨 session,建议 @id 形式)
34
35
  # NAO_SKILLS=<dir> nao 机制包根目录(默认 <脚本>/../..;项目内 shim 会显式指向包根)
35
36
  # NAO_MODEL_WHITELIST=<glob,...> -m 白名单(默认空=不校验,支持 glob)
36
37
  # NAO_CLOSE_BUSY_PATTERN=<ERE> close 的在跑 turn 判定正则(默认内置 pi 状态行标记)
@@ -53,7 +54,9 @@
53
54
  # → ⑤ 无图形:仅 --background 时 detached tmux/screen → ⑥ 否则 exit 2 + 一行 DEGRADED 诊断
54
55
  #
55
56
  # tmux 宿主行为
56
- # - 已在 tmux 内($TMUX 存在):当前窗口分屏拉起,不新建窗口。
57
+ # - 已在 tmux 内($TMUX 存在):以派生宿主窗口(调用者 pane 所在 window)分屏拉起,不新建窗口;
58
+ # 显式 `-t @<window_id>` 目标(定位链:NAO_TMUX_HOST_WINDOW > $TMUX_PANE > 名册唯一命中;
59
+ # 否则 warn + 跳过该 pane,不落入当前窗口)
57
60
  # - 不在 tmux 内:创建 detached 会话 nao-<角色>,需 tmux attach -t nao-<角色>。
58
61
  # - main-row2:第 1 个 pane 全高占左,后续每角色往右开列、每列上下 2 个(默认):
59
62
  # 1 | 2 | 4
@@ -86,6 +89,8 @@ TMUX_MAIN_WIDTH="${NAO_TMUX_MAIN_WIDTH:-$TMUX_MAIN_WIDTH_DEFAULT}"
86
89
  # 最小非主 pane 列宽守卫:默认值与非法值回退共用同一常量(防漂移)
87
90
  TMUX_MIN_PANE_WIDTH_DEFAULT=30
88
91
  TMUX_MIN_PANE_WIDTH="${NAO_TMUX_MIN_PANE_WIDTH:-$TMUX_MIN_PANE_WIDTH_DEFAULT}"
92
+ # T517:派生宿主窗口 ID(归一化 @<window_id>;空 = 定位失败 ⇒ 该角色 pane warn+skip,禁无 -t 续建)
93
+ HOST_WINDOW=""
89
94
  # check 输出契约:默认单行摘要(省 PM 上下文),-v 展开完整报告;失败始终展开
90
95
  VERBOSE=false
91
96
  # close 的在跑 turn 判定(pi 默认状态行:Working (esc to interrupt) / Thinking... / Retrying / Compacting)
@@ -295,7 +300,7 @@ resolve_role() {
295
300
 
296
301
  # 宿主阶梯(ADR 实施闸门 · T513b/c)——由「枚举模拟器」改为「环境查询 + 显式覆盖」:
297
302
  # ① NAO_TERMINAL 显式(档值 ∈ {tmux,foot,ghostty,ptyxis,screen};命令缺失/非法 ⇒ exit 2)
298
- # ② $TMUX 存在 ⇒ 当前窗口 split-window(逐值零回归)
303
+ # ② $TMUX 存在 ⇒ 派生宿主窗口(调用者 pane 所在 window)split-window(显式 -t @<window_id>;单窗口逐值零回归)
299
304
  # ③ 非 tmux + tmux 可用 + 图形 ⇒ new-session -d + xdg-terminal-exec 开窗 attach(默认 socket)
300
305
  # ④ 无 tmux + 图形 ⇒ xdg-terminal-exec --app-id=org.nao.<角色> --dir=<repo> 直开窗
301
306
  # ⑤ 无图形 + 显式 --background ⇒ detached tmux/screen
@@ -851,37 +856,38 @@ apply_main_col() {
851
856
  else
852
857
  warn "${from} → main-col 右列 ${right_w} < 最小 ${min_w}(${n} pane @ ${win_w} 列)→ 再回退 grid"
853
858
  fi
854
- tmux select-layout tiled >/dev/null 2>&1 || true
859
+ tmux select-layout -t "$HOST_WINDOW" tiled >/dev/null 2>&1 || true
855
860
  return 0
856
861
  fi
857
862
  fi
858
- tmux set-window-option main-pane-width "${pct}%" >/dev/null 2>&1 || true
863
+ tmux set-window-option -t "$HOST_WINDOW" main-pane-width "${pct}%" >/dev/null 2>&1 || true
859
864
  local err
860
- if ! err=$(tmux select-layout main-vertical 2>&1); then
861
- warn "main-col(main-vertical)应用失败,回退 grid"
865
+ if ! err=$(tmux select-layout -t "$HOST_WINDOW" main-vertical 2>&1); then
866
+ warn "main-col(main-vertical)应用失败,回退 grid(宿主窗口 $HOST_WINDOW)"
862
867
  warn " tmux: ${err:-<no message>}"
863
- tmux select-layout tiled >/dev/null 2>&1 || true
868
+ tmux select-layout -t "$HOST_WINDOW" tiled >/dev/null 2>&1 || true
864
869
  fi
865
870
  }
866
871
 
867
872
  apply_tmux_layout() {
868
873
  [[ -n "${TMUX:-}" ]] || return 0
874
+ [[ -n "$HOST_WINDOW" ]] || return 0
869
875
  if [[ "$TMUX_LAYOUT" == "grid" ]]; then
870
- tmux select-layout tiled >/dev/null 2>&1 || true
876
+ tmux select-layout -t "$HOST_WINDOW" tiled >/dev/null 2>&1 || true
871
877
  return
872
878
  fi
873
879
  # main-row2
874
880
  local win_w win_h
875
881
  # D7(T513-S3):display-message 无输出时 `read` 返回 1,set -e 会硬中断整轮 ensure
876
882
  # ⇒ 守卫为 true(读不到尺寸即跳过布局,不硬崩)。
877
- read -r win_w win_h < <(tmux display-message -p '#{window_width} #{window_height}' 2>/dev/null) || true
883
+ read -r win_w win_h < <(tmux display-message -p -t "$HOST_WINDOW" '#{window_width} #{window_height}' 2>/dev/null) || true
878
884
  [[ -n "$win_w" && -n "$win_h" ]] || return 0
879
885
 
880
886
  local -a pane_ids=()
881
887
  local line
882
888
  while IFS= read -r line; do
883
889
  [[ -n "$line" ]] && pane_ids+=("$line")
884
- done < <(tmux list-panes -F '#{pane_id}' 2>/dev/null | sed 's/^%//')
890
+ done < <(tmux list-panes -t "$HOST_WINDOW" -F '#{pane_id}' 2>/dev/null | sed 's/^%//')
885
891
  local n=${#pane_ids[@]}
886
892
  (( n >= 1 )) || return 0
887
893
 
@@ -913,14 +919,85 @@ apply_tmux_layout() {
913
919
 
914
920
  local ck; ck="$(_layout_checksum "$full")"
915
921
  local err
916
- if ! err=$(tmux select-layout "${ck},${full}" 2>&1); then
917
- warn "main-row2 布局应用失败,回退 tiled"
922
+ if ! err=$(tmux select-layout -t "$HOST_WINDOW" "${ck},${full}" 2>&1); then
923
+ warn "main-row2 布局应用失败,回退 tiled(宿主窗口 $HOST_WINDOW)"
918
924
  warn " tmux: ${err:-<no message>}"
919
925
  warn " layout: $full"
920
- tmux select-layout tiled >/dev/null 2>&1 || true
926
+ tmux select-layout -t "$HOST_WINDOW" tiled >/dev/null 2>&1 || true
921
927
  fi
922
928
  }
923
929
 
930
+ # ---------------------------------------------------------------------------
931
+ # T517 派生宿主窗口解析(ADR 2026-10-13-derived-pane-window-addressing · PRD T517 §五)
932
+ # 入口一次性解析 → 全局 HOST_WINDOW=@<window_id>;空 = 定位失败 ⇒ 解析处 warn 一次(每 run 一次),
933
+ # spawn_tmux 对空值跳过该 pane(D-A:warn + skip,禁无 -t 续建——那是拆用户视图)。定位链:
934
+ # L1 NAO_TMUX_HOST_WINDOW 显式覆盖:display-message 探测归一化;允许跨 session(@id 全局唯一),
935
+ # 非 @id 形式 warn 建议用 @id;解析失败(关窗/垃圾值)⇒ warn + 沿链降级(不 die)
936
+ # L2 $TMUX_PANE → window_id 调用者 pane 所在 window(= 派生宿主;不依赖 client 视图;核心路径)
937
+ # L3 名册唯一命中 仅在本 $TMUX 所属 session 内取候选窗(pane→window 全表一次调用
938
+ # + 名册 cwd=本仓 + 活 pane + 窗口去重唯一);歧义/无命中 ⇒ warn 标来源,不猜
939
+ # L4 skip warn + 不创建(其余角色继续;rc 不劣化)
940
+ # 调用预算:L1/L2 各 ≤1 次 tmux 调用;L3 ≤2 次(session 探测 + list-panes 全表)。
941
+ resolve_host_window() {
942
+ local tgt="" sess_id="" pid wid sid abs rp rw
943
+ # L1 显式覆盖
944
+ if [[ -n "${NAO_TMUX_HOST_WINDOW:-}" ]]; then
945
+ tgt="$(tmux display-message -p -t "$NAO_TMUX_HOST_WINDOW" '#{window_id}' 2>/dev/null || true)"
946
+ if [[ -n "$tgt" ]]; then
947
+ [[ "$NAO_TMUX_HOST_WINDOW" == "@"* ]] || \
948
+ warn "NAO_TMUX_HOST_WINDOW=$NAO_TMUX_HOST_WINDOW 非 @id 形式(裸窗口名跨 session 可能歧义)→ 已归一化 $tgt,建议改用 @id"
949
+ log "宿主窗口 = NAO_TMUX_HOST_WINDOW(显式覆盖)→ $tgt"
950
+ HOST_WINDOW="$tgt"; return 0
951
+ fi
952
+ warn "NAO_TMUX_HOST_WINDOW=$NAO_TMUX_HOST_WINDOW 解析失败(窗口已关或非法)→ 降级 TMUX_PANE/名册"
953
+ fi
954
+ # L2 调用者 pane → window_id(同一次调用顺带取 session_id,供 L3 限定本 session)
955
+ if [[ -n "${TMUX_PANE:-}" ]]; then
956
+ read -r tgt sess_id < <(tmux display-message -p -t "$TMUX_PANE" '#{window_id} #{session_id}' 2>/dev/null || true) || true
957
+ if [[ -n "$tgt" ]]; then
958
+ HOST_WINDOW="$tgt"; return 0
959
+ fi
960
+ warn "TMUX_PANE=$TMUX_PANE 解析不到 window_id(pane 已关?)→ 降级名册推断"
961
+ else
962
+ warn "TMUX 已设但 TMUX_PANE 空(AC5b 场景)→ 跳 L2,降级名册推断"
963
+ fi
964
+ # L3 名册唯一命中(D-B:仅本 $TMUX 所属 session 内取候选窗,不跨 session 推断)
965
+ [[ -n "$sess_id" ]] || sess_id="$(tmux display-message -p '#{session_id}' 2>/dev/null || true)"
966
+ if [[ -z "$sess_id" ]]; then
967
+ warn "无法确定本 \$TMUX 所属 session(无 client/无 pane 句柄)→ 名册推断无从限定,L4 skip"
968
+ return 0
969
+ fi
970
+ local -A win_of=()
971
+ # pane→window 全表(1 次调用;pane 在表中即活,无需单独探测)
972
+ while IFS=$'\t' read -r pid wid sid; do
973
+ [[ "$sid" == "$sess_id" ]] && win_of["$pid"]="$wid"
974
+ done < <(tmux list-panes -a -F "#{pane_id}"$'\t'"#{window_id}"$'\t'"#{session_id}" 2>/dev/null || true)
975
+ abs="$(repo_abspath "$PWD")"; [[ -n "$abs" ]] || return 0
976
+ local -A seen=()
977
+ local -a wins=()
978
+ # 名册 tmuxPane(cwd=本仓)→ 本 session 窗口去重(与 fallback_pane_for 同构的 cwd 过滤)
979
+ while IFS= read -r rp; do
980
+ [[ -n "$rp" ]] || continue
981
+ rw="${win_of[$rp]:-}"
982
+ [[ -n "$rw" ]] && { [[ -z "${seen[$rw]:-}" ]] && { seen["$rw"]=1; wins+=("$rw"); }; }
983
+ done < <(intercom_list_json 2>/dev/null | sed 's/},/}\n/g' | awk -v a="$abs" '
984
+ /"cwd":[[:space:]]*"/ { s=$0; sub(/.*"cwd":[[:space:]]*"/,"",s); sub(/".*/,"",s); cc=s }
985
+ /"tmuxPane":[[:space:]]*"/ { s=$0; sub(/.*"tmuxPane":[[:space:]]*"/,"",s); sub(/".*/,"",s); cp=s
986
+ if (cp != "" && cc == a) print cp }
987
+ ' || true) # sed 按对象边界拆行:紧凑/pretty 两种 JSON 布局同语义(逐条目取 cwd+tmuxPane)
988
+ if (( ${#wins[@]} == 1 )); then
989
+ HOST_WINDOW="${wins[0]}"
990
+ warn "名册唯一命中宿主窗口 $HOST_WINDOW(来源:名册 tmuxPane + cwd=本仓 + 仅本 session $sess_id)"
991
+ return 0
992
+ fi
993
+ if (( ${#wins[@]} > 1 )); then
994
+ warn "名册推断歧义:本 session 内候选窗 ${wins[*]}(>1)→ 不猜,L4 skip(可设 NAO_TMUX_HOST_WINDOW 显式指定)"
995
+ else
996
+ warn "名册推断无命中:本 session 内无名册 pane 或 cwd≠本仓 → L4 skip(可设 NAO_TMUX_HOST_WINDOW 显式指定)"
997
+ fi
998
+ return 0 # HOST_WINDOW 保持空 ⇒ spawn_tmux 跳过(D-A:warn + skip,禁无 -t 续建)
999
+ }
1000
+
924
1001
  spawn_tmux() {
925
1002
  local name="$1" inner="$2"
926
1003
  case "$TMUX_LAYOUT" in
@@ -929,11 +1006,14 @@ spawn_tmux() {
929
1006
  esac
930
1007
  local wrapped="bash -lc $(printf %q "$inner")"
931
1008
  if [[ -n "${TMUX:-}" ]]; then
932
- # 窄窗口/宿主上限会让 split-window 失败(no space for new pane);
1009
+ # T517:目标显式化——HOST_WINDOW 解析失败时此处已由解析处 warn(每 run 一次),
1010
+ # 跳过该 pane;禁止无 -t 续建(tmux 会落「当前窗口」= 用户视图,即缺陷根因)。
1011
+ [[ -n "$HOST_WINDOW" ]] || return 0
1012
+ # 窄窗口/宿主上限会让 split-window 失败(no space for new pane,含宿主窗已关竞态);
933
1013
  # 捕获后 warn 并继续,不因单个 pane 失败中断整轮 ensure。
934
1014
  local serr
935
- if ! serr=$(tmux split-window -h "$wrapped" 2>&1); then
936
- warn "pane 创建失败(窗口过窄或宿主已达上限):${serr:-<no message>}"
1015
+ if ! serr=$(tmux split-window -h -t "$HOST_WINDOW" "$wrapped" 2>&1); then
1016
+ warn "pane 创建失败(宿主窗口 $HOST_WINDOW 过窄/已关或已达上限):${serr:-<no message>}"
937
1017
  warn " 已跳过该 pane,其余角色继续拉起;可放大窗口后重跑 ensure"
938
1018
  return 0
939
1019
  fi
@@ -1688,6 +1768,9 @@ cmd_ensure() {
1688
1768
  local spec role repo key seen k
1689
1769
  local -a cg_done=()
1690
1770
  [[ $# -eq 0 ]] && die "ensure 需要至少一个角色,如: nao-fleet.sh ensure arch rd-fe rd-infra"
1771
+ # T517:入口一次性解析派生宿主窗口(仅 $TMUX 分支);定位失败 ⇒ 解析处 warn 一次,各角色 pane 跳过
1772
+ HOST_WINDOW=""
1773
+ if [[ -n "${TMUX:-}" ]] && command -v tmux >/dev/null 2>&1; then resolve_host_window; fi
1691
1774
  for spec in "$@"; do
1692
1775
  if [[ "$spec" == *"@"* ]]; then
1693
1776
  role="${spec%%@*}"; repo="${spec#*@}"
package/README.md CHANGED
@@ -24,7 +24,7 @@ nao-skills 是一套 **角色卡 + 协作规矩 + 小工具**。装上以后,
24
24
 
25
25
  ```bash
26
26
  cd 你的项目
27
- pi install --local npm:@nathan33/nao-skill@0.16.1 # 声明 pin + 把机制包物化到项目内 .pi/npm
27
+ pi install --local npm:@nathan33/nao-skill@0.16.2 # 声明 pin + 把机制包物化到项目内 .pi/npm
28
28
  npx @nathan33/nao-skill init # 写入转发 shim + 生成 AGENTS.md
29
29
  ```
30
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathan33/nao-skill",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "nao 多角色 AI 开发舰队:角色卡 / 技能 / 协作协议 / 工具链,一键接入项目(pi / Claude / 任意 Agent)",
5
5
  "type": "module",
6
6
  "bin": {