@nathan33/nao-skill 0.9.3 → 0.9.5
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.
|
@@ -28,6 +28,7 @@ description: pi-intercom 多会话协作协议(常驻引用)
|
|
|
28
28
|
- **任务派生**:`ensure --task <编号> <别名>[@<repo>]` 创建 `<角色>-<编号>` 独立会话(如 `rd-be-T1`),并行隔离、互不排队/打断,避免多任务同名冲突;任务闭环即**回收**:`nao-fleet.sh close --task <编号> <别名>`(内置在跑 turn / tasks-state 闸门)。
|
|
29
29
|
|
|
30
30
|
- **判重 / 在线判定**:脚本按 pi 自设的**终端标题**(`π - <会话名> - <仓库名>`)+ `pi-intercom` 名册判在线(**不用 `pgrep --name`**:pi 启动后 argv 被改写为 `pi`,会恒失配);已运行则跳过并 warn。确需重开加 `--force`。
|
|
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 行状态行双重信号。
|
|
31
32
|
- **tmux 宿主**:`$TMUX` 存在时在当前窗口分屏拉起。布局三选一(`NAO_TMUX_LAYOUT`):默认 `main-row2`
|
|
32
33
|
(首 pane 全高占左,后续每角色往右开列、每列上下 2 个);`main-col`(其余在右列竖排);`grid`
|
|
33
34
|
(等大网格)。**窄列守卫**:`main-row2` 最窄非主 pane < `NAO_TMUX_MIN_PANE_WIDTH`(默认 30)时
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// intercom-probe.mts — nao-fleet.sh 的 pi-intercom 名册探针(含 tmuxPane)
|
|
2
|
+
//
|
|
3
|
+
// 用法:node <tsx cli.mjs> intercom-probe.mts <pi-agent-dir>
|
|
4
|
+
// 输出(stdout):{"ok":true,"sessions":[{"name","id","model","status","cwd","tmuxPane","pid"},…]}
|
|
5
|
+
//
|
|
6
|
+
// 为什么需要它(依据):
|
|
7
|
+
// pi 启动后用 OSC 0 把终端标题设为 "π - <会话名> - <cwd basename>" 并改写 argv,
|
|
8
|
+
// nao-fleet 旧实现只按终端标题定位 pane ⇒ 标题一旦被改写(实测变为 "pi:c"),
|
|
9
|
+
// close/status/ensure 全部失配。而 pi-intercom 在会话注册时读取 $TMUX_PANE
|
|
10
|
+
// (types.ts: tmuxPane —— 进程生命周期内不可变,与窗口/标题改名无关),这是
|
|
11
|
+
// 标题被改写后仍能精确定位 pane 的权威依据。
|
|
12
|
+
// 官方 CLI(cli.ts list --json)的 sessionRow 未透出 tmuxPane,故此处直接用同一
|
|
13
|
+
// 包内的 IntercomClient 读取完整 SessionInfo。
|
|
14
|
+
//
|
|
15
|
+
// 兼容性:pi-intercom 内部 API 若变化 → 本探针非 0 退出,nao-fleet 回退官方 CLI
|
|
16
|
+
// (无 tmuxPane)再叠加「仓库内未认领 pi pane 唯一兜底」,不静默 no-op。
|
|
17
|
+
//
|
|
18
|
+
// 类型自检:`npm run check:agents`(tsconfig.agents.json)。动态 import 使 list 结果为
|
|
19
|
+
// any,下游 TS 项目(nao-todo/nue-ui 的 `vp check` 扫 `.agents/**`)会报 TS7006,
|
|
20
|
+
// 故此处用本地 ProbeSession 给 filter/map 显式定型(strict/noImplicitAny 下必需)。
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
|
|
23
|
+
type ProbeSession = {
|
|
24
|
+
id: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
status?: string;
|
|
28
|
+
cwd: string;
|
|
29
|
+
tmuxPane?: string;
|
|
30
|
+
pid?: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const agentDir = process.argv[2];
|
|
34
|
+
if (!agentDir) {
|
|
35
|
+
process.stderr.write("usage: intercom-probe.mts <pi-agent-dir>\n");
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
const mod = join(agentDir, "npm/node_modules/pi-intercom");
|
|
39
|
+
const PROBE_NAME = "nao-fleet-probe";
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const { IntercomClient } = await import(join(mod, "broker/client.ts"));
|
|
43
|
+
const { spawnBrokerIfNeeded } = await import(join(mod, "broker/spawn.ts"));
|
|
44
|
+
const client = new IntercomClient();
|
|
45
|
+
await spawnBrokerIfNeeded();
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
await client.connect({
|
|
48
|
+
cwd: process.cwd(),
|
|
49
|
+
model: "nao-fleet-probe",
|
|
50
|
+
pid: process.pid,
|
|
51
|
+
startedAt: now,
|
|
52
|
+
lastActivity: now,
|
|
53
|
+
name: PROBE_NAME,
|
|
54
|
+
status: "idle",
|
|
55
|
+
});
|
|
56
|
+
const sessions = (await client.listSessions()) as ProbeSession[];
|
|
57
|
+
const rows = sessions
|
|
58
|
+
.filter((s) => s.id !== client.sessionId)
|
|
59
|
+
.map((s) => ({
|
|
60
|
+
name: s.name ?? "",
|
|
61
|
+
id: s.id,
|
|
62
|
+
model: s.model,
|
|
63
|
+
status: s.status ?? "?",
|
|
64
|
+
cwd: s.cwd,
|
|
65
|
+
tmuxPane: s.tmuxPane ?? null,
|
|
66
|
+
pid: s.pid ?? null,
|
|
67
|
+
}));
|
|
68
|
+
process.stdout.write(JSON.stringify({ ok: true, sessions: rows }, null, 2));
|
|
69
|
+
await client.disconnect().catch(() => {});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
process.stderr.write(`intercom-probe failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
@@ -26,12 +26,16 @@
|
|
|
26
26
|
# NAO_CLOSE_BUSY_PATTERN=<ERE> close 的在跑 turn 判定正则(默认内置 pi 状态行标记)
|
|
27
27
|
# NAO_TASKS_STATE=<path> 任务状态文件(默认 docs/tasks-state.md,供残留检测/回收闸门)
|
|
28
28
|
#
|
|
29
|
-
#
|
|
29
|
+
# 在线判定与 pane 定位(status / close / ensure 判重共用)
|
|
30
30
|
# pi 启动后用 OSC 0 把终端标题设为 "π - <会话名> - <cwd basename>" 并改写 argv
|
|
31
|
-
# (/proc/<pid>/cmdline 只剩 "pi")⇒
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
31
|
+
# (/proc/<pid>/cmdline 只剩 "pi")⇒ 不使用 `pgrep --name`(历史假阴性根因)。
|
|
32
|
+
# ⚠️ 标题可能被外部改写(PM 实测变为 "pi:c")⇒ pane 定位按三层后备(find_pane_for):
|
|
33
|
+
# ① 终端标题契约(最快)
|
|
34
|
+
# ② pi-intercom 名册 tmuxPane(注册时读 $TMUX_PANE;与标题改名无关,权威)
|
|
35
|
+
# ③ 本仓「未被认领 pi pane」唯一兜底(启发式;歧义即 warn,不猜)
|
|
36
|
+
# 名册由 intercom-probe.mts(一次调用、进程内缓存,含 tmuxPane)取;探针不可用回退
|
|
37
|
+
# pi-intercom 官方 cli.ts(无 tmuxPane),再不可用回退 tmux/screen 会话名;
|
|
38
|
+
# 仍无法确认「不在线」时 close 不做静默 no-op(exit 非 0)。
|
|
35
39
|
#
|
|
36
40
|
# tmux 宿主行为
|
|
37
41
|
# - 已在 tmux 内($TMUX 存在):当前窗口分屏拉起,不新建窗口。
|
|
@@ -295,12 +299,26 @@ detect_host() {
|
|
|
295
299
|
# ① pi-intercom 名册(权威在线名单;跨 tmux/ghostty/ptyxis/screen 宿主)
|
|
296
300
|
# ② tmux pane_title(pi 自设;与 pane_start_command 是否为空无关)
|
|
297
301
|
# ③ tmux detached 会话 / screen 会话名 nao-<会话名>
|
|
302
|
+
# ⚠️ 标题可被外部改写(实测 "pi:c")⇒ find_pane_for 再叠三层后备:标题契约 →
|
|
303
|
+
# 名册 tmuxPane($TMUX_PANE,权威)→ 本仓未认领 pi pane 唯一兜底。
|
|
298
304
|
# repo(可选):给出时要求标题 basename / 名册 cwd 一致,避免跨项目同名会话互串。
|
|
299
305
|
|
|
300
306
|
PI_AGENT_DIR="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
|
301
307
|
PI_TITLE="π"
|
|
308
|
+
# 含 tmuxPane 的名册探针(标题被改写时定位 pane 的权威依据)
|
|
309
|
+
INTERCOM_PROBE="$SKILLS_DIR/.agents/scripts/intercom-probe.mts"
|
|
302
310
|
_INTERCOM_JSON=""; _INTERCOM_PROBED=0
|
|
303
311
|
|
|
312
|
+
# 以 15s 超时跑 tsx(有 timeout 才用;无则裸跑);stdout/stderr 原样透传
|
|
313
|
+
_node_tsx() {
|
|
314
|
+
local tsx="$1"; shift
|
|
315
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
316
|
+
timeout 15 node "$tsx" "$@"
|
|
317
|
+
else
|
|
318
|
+
node "$tsx" "$@"
|
|
319
|
+
fi
|
|
320
|
+
}
|
|
321
|
+
|
|
304
322
|
repo_abspath() { ( cd "$1" >/dev/null 2>&1 && pwd ) 2>/dev/null || true; }
|
|
305
323
|
repo_basename() {
|
|
306
324
|
local abs; abs="$(repo_abspath "$1")"
|
|
@@ -309,6 +327,8 @@ repo_basename() {
|
|
|
309
327
|
}
|
|
310
328
|
|
|
311
329
|
# intercom 名册 JSON(一次调用、进程内缓存);不可用返回 1(不 die)
|
|
330
|
+
# ① intercom-probe.mts(含 tmuxPane,标题被改写仍可定位 pane)
|
|
331
|
+
# ② pi-intercom 官方 cli.ts list --json(无 tmuxPane,仅 name/cwd/status)
|
|
312
332
|
intercom_list_json() {
|
|
313
333
|
if (( _INTERCOM_PROBED )); then
|
|
314
334
|
[[ -n "$_INTERCOM_JSON" ]] && printf '%s' "$_INTERCOM_JSON"
|
|
@@ -317,13 +337,14 @@ intercom_list_json() {
|
|
|
317
337
|
_INTERCOM_PROBED=1
|
|
318
338
|
local dir="$PI_AGENT_DIR/npm/node_modules/pi-intercom"
|
|
319
339
|
local tsx="$PI_AGENT_DIR/npm/node_modules/tsx/dist/cli.mjs"
|
|
320
|
-
[[ -f "$
|
|
340
|
+
[[ -f "$tsx" ]] || return 1
|
|
321
341
|
command -v node >/dev/null 2>&1 || return 1
|
|
322
|
-
local out
|
|
323
|
-
if
|
|
324
|
-
out="$(
|
|
325
|
-
|
|
326
|
-
|
|
342
|
+
local out=""
|
|
343
|
+
if [[ -f "$INTERCOM_PROBE" ]]; then
|
|
344
|
+
out="$(_node_tsx "$tsx" "$INTERCOM_PROBE" "$PI_AGENT_DIR" 2>/dev/null)" || out=""
|
|
345
|
+
fi
|
|
346
|
+
if [[ -z "$out" && -f "$dir/cli.ts" ]]; then
|
|
347
|
+
out="$(_node_tsx "$tsx" "$dir/cli.ts" list --json 2>/dev/null)" || out=""
|
|
327
348
|
fi
|
|
328
349
|
[[ -n "$out" ]] || return 1
|
|
329
350
|
_INTERCOM_JSON="$out"
|
|
@@ -351,8 +372,13 @@ intercom_online() {
|
|
|
351
372
|
return 1
|
|
352
373
|
}
|
|
353
374
|
|
|
354
|
-
# 会话所在的 tmux pane
|
|
355
|
-
|
|
375
|
+
# 会话所在的 tmux pane。三层后备(标题可被外部改写,如 "pi:c"):
|
|
376
|
+
# ① 终端标题契约 "π - <会话名> - <repo basename>"(pi 自设;最快)
|
|
377
|
+
# ② pi-intercom 名册 tmuxPane(注册时读 $TMUX_PANE;与标题改名无关,权威)
|
|
378
|
+
# ③ 本仓「未被认领 pi pane」唯一兜底(启发式:pane_current_command=pi 且
|
|
379
|
+
# pane_current_path=repo;歧义时 warn,不猜)
|
|
380
|
+
# repo(可选):给出时要求标题 basename / 名册 cwd / pane 当前路径一致,避免跨项目同名互串。
|
|
381
|
+
find_pane_by_title() {
|
|
356
382
|
local name="$1" repo="${2:-}" base=""
|
|
357
383
|
command -v tmux >/dev/null 2>&1 || return 0
|
|
358
384
|
if [[ -n "$repo" ]]; then base="$(repo_basename "$repo")"; fi
|
|
@@ -361,7 +387,101 @@ find_pane_for() {
|
|
|
361
387
|
{ t=$2
|
|
362
388
|
if (b != "") { if (t == p " - " n " - " b) { print $1; exit } }
|
|
363
389
|
else if (index(t, p " - " n " - ") == 1) { print $1; exit }
|
|
364
|
-
}'
|
|
390
|
+
}' || true
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
# tmux pane 是否存活(名册 tmuxPane 可能滞后于 pane 关闭)
|
|
394
|
+
tmux_pane_live() {
|
|
395
|
+
[[ -n "$1" ]] || return 1
|
|
396
|
+
command -v tmux >/dev/null 2>&1 || return 1
|
|
397
|
+
tmux list-panes -a -F '#{pane_id}' 2>/dev/null | grep -qxF "$1"
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# 名册中该会话的 tmux pane(注册时读 $TMUX_PANE;标题被改写仍有效)
|
|
401
|
+
intercom_pane_for() {
|
|
402
|
+
local name="$1" repo="${2:-}" abs="" json
|
|
403
|
+
json="$(intercom_list_json 2>/dev/null)" || return 0
|
|
404
|
+
[[ -n "$json" ]] || return 0
|
|
405
|
+
if [[ -n "$repo" ]]; then abs="$(repo_abspath "$repo")"; fi
|
|
406
|
+
awk -v n="$name" -v a="$abs" '
|
|
407
|
+
/"name":[[:space:]]*"/ { s=$0; sub(/.*"name":[[:space:]]*"/,"",s); sub(/".*/,"",s); cn=s }
|
|
408
|
+
/"cwd":[[:space:]]*"/ { s=$0; sub(/.*"cwd":[[:space:]]*"/,"",s); sub(/".*/,"",s); cc=s }
|
|
409
|
+
/"tmuxPane":[[:space:]]*"/ {
|
|
410
|
+
s=$0; sub(/.*"tmuxPane":[[:space:]]*"/,"",s); sub(/".*/,"",s); cp=s
|
|
411
|
+
if (cn == n && (a == "" || cc == a) && cp != "") { print cp; exit }
|
|
412
|
+
}
|
|
413
|
+
' <<< "$json"
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
# 名册中该会话的活动状态(无记录则空);pi-intercom 状态值如 idle / thinking / tool:bash
|
|
417
|
+
# 标题被改写或 pane 不可判时,这是「在跑 turn」的权威信号。
|
|
418
|
+
intercom_status_for() {
|
|
419
|
+
local name="$1" repo="${2:-}" abs="" json
|
|
420
|
+
json="$(intercom_list_json 2>/dev/null)" || return 0
|
|
421
|
+
[[ -n "$json" ]] || return 0
|
|
422
|
+
if [[ -n "$repo" ]]; then abs="$(repo_abspath "$repo")"; fi
|
|
423
|
+
awk -v n="$name" -v a="$abs" '
|
|
424
|
+
/"name":[[:space:]]*"/ { s=$0; sub(/.*"name":[[:space:]]*"/,"",s); sub(/".*/,"",s); cn=s; cs="" }
|
|
425
|
+
/"status":[[:space:]]*"/ { s=$0; sub(/.*"status":[[:space:]]*"/,"",s); sub(/".*/,"",s); cs=s }
|
|
426
|
+
/"cwd":[[:space:]]*"/ {
|
|
427
|
+
s=$0; sub(/.*"cwd":[[:space:]]*"/,"",s); sub(/".*/,"",s); cc=s
|
|
428
|
+
if (cn == n && (a == "" || cc == a)) { print cs; exit }
|
|
429
|
+
}
|
|
430
|
+
' <<< "$json"
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
# 本仓所有「当前前台命令为 pi」的 pane:pane_id<TAB>pane_current_path
|
|
434
|
+
# pane_current_command 与标题/argv 无关,是标题被改写时的可靠进程特征。
|
|
435
|
+
pi_panes() {
|
|
436
|
+
command -v tmux >/dev/null 2>&1 || return 0
|
|
437
|
+
tmux list-panes -a -F "#{pane_id}"$'\t'"#{pane_current_command}"$'\t'"#{pane_current_path}" 2>/dev/null \
|
|
438
|
+
| awk -F'\t' '{ c=$2; sub(/^.*\//,"",c); if (c=="pi") print $1 "\t" $3 }' || true
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# 兜底:本仓 pi pane 中「未被其它会话认领」且唯一者
|
|
442
|
+
# 认领来源 = 标题契约命中的 pane + 名册 tmuxPane(二者任一即视为已识别)
|
|
443
|
+
fallback_pane_for() {
|
|
444
|
+
local name="$1" repo="$2" abs="" cp j v
|
|
445
|
+
command -v tmux >/dev/null 2>&1 || return 0
|
|
446
|
+
abs="$(repo_abspath "$repo")"; [[ -n "$abs" ]] || return 0
|
|
447
|
+
local -A claimed=()
|
|
448
|
+
while IFS= read -r cp; do
|
|
449
|
+
[[ -n "$cp" ]] && claimed["$cp"]=1
|
|
450
|
+
done < <(tmux list-panes -a -F "#{pane_id}"$'\t'"#{pane_title}" 2>/dev/null \
|
|
451
|
+
| awk -F'\t' -v t="$PI_TITLE" '$2 ~ "^" t " - " { print $1 }')
|
|
452
|
+
j="$(intercom_list_json 2>/dev/null)" || j=""
|
|
453
|
+
if [[ -n "$j" ]]; then
|
|
454
|
+
cp="$(printf '%s\n' "$j" | sed -n 's/.*"tmuxPane":[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
455
|
+
while IFS= read -r v; do [[ -n "$v" ]] && claimed["$v"]=1; done <<< "$cp"
|
|
456
|
+
fi
|
|
457
|
+
local -a cand=()
|
|
458
|
+
local pid ppath
|
|
459
|
+
while IFS=$'\t' read -r pid ppath; do
|
|
460
|
+
[[ "$ppath" == "$abs" ]] || continue
|
|
461
|
+
[[ -n "${claimed[$pid]:-}" ]] && continue
|
|
462
|
+
cand+=("$pid")
|
|
463
|
+
done < <(pi_panes)
|
|
464
|
+
if (( ${#cand[@]} == 1 )); then
|
|
465
|
+
printf '%s\n' "${cand[0]}"; return 0
|
|
466
|
+
fi
|
|
467
|
+
if (( ${#cand[@]} > 1 )); then
|
|
468
|
+
warn "标题被改写且名册无 pane 记录:$abs 内有多个未认领 pi pane(${cand[*]})→ 无法唯一确定 $name"
|
|
469
|
+
fi
|
|
470
|
+
return 0
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
find_pane_for() {
|
|
474
|
+
local name="$1" repo="${2:-}" pane=""
|
|
475
|
+
# ① 终端标题契约
|
|
476
|
+
pane="$(find_pane_by_title "$name" "$repo")"
|
|
477
|
+
[[ -n "$pane" ]] && { printf '%s\n' "$pane"; return 0; }
|
|
478
|
+
# ② pi-intercom 名册 tmuxPane(权威;标题被改写仍有效)
|
|
479
|
+
pane="$(intercom_pane_for "$name" "$repo")"
|
|
480
|
+
if [[ -n "$pane" ]] && tmux_pane_live "$pane"; then printf '%s\n' "$pane"; return 0; fi
|
|
481
|
+
# ③ 本仓未认领 pi pane 唯一兜底
|
|
482
|
+
pane="$(fallback_pane_for "$name" "$repo")"
|
|
483
|
+
[[ -n "$pane" ]] && { printf '%s\n' "$pane"; return 0; }
|
|
484
|
+
return 0
|
|
365
485
|
}
|
|
366
486
|
|
|
367
487
|
# 该会话名对应的 pi 进程 PID(经 pane 句柄取 pane_pid 及其子进程中 comm=pi 者)
|
|
@@ -395,25 +515,45 @@ offline_verifiable() {
|
|
|
395
515
|
}
|
|
396
516
|
|
|
397
517
|
# 本机可见的会话名(tmux 标题 + tmux/screen 会话 + 名册),供 status 枚举;已去重
|
|
518
|
+
# ⚠️ 每条枚举 `|| true`:set -e + pipefail 下 `screen -ls`(无 socket 时 rc=1)或
|
|
519
|
+
# `tmux`(无 server 时 rc=1)会中断其余枚举,使「名册回退」永远走不到 —— 必疾。
|
|
398
520
|
visible_session_names() {
|
|
399
521
|
{ if command -v tmux >/dev/null 2>&1; then
|
|
400
|
-
tmux list-panes -a -F '#{pane_title}' 2>/dev/null \
|
|
522
|
+
{ tmux list-panes -a -F '#{pane_title}' 2>/dev/null || true; } \
|
|
401
523
|
| sed -n "s/^${PI_TITLE} - \([A-Za-z0-9_-]*\) - .*/\1/p"
|
|
402
|
-
tmux list-sessions -F '#{session_name}' 2>/dev/null
|
|
524
|
+
{ tmux list-sessions -F '#{session_name}' 2>/dev/null || true; } \
|
|
525
|
+
| sed -n 's/^nao-//p'
|
|
403
526
|
fi
|
|
404
527
|
if command -v screen >/dev/null 2>&1; then
|
|
405
|
-
screen -ls 2>/dev/null
|
|
528
|
+
{ screen -ls 2>/dev/null || true; } \
|
|
529
|
+
| sed -n 's/.*[0-9]\.nao-\([A-Za-z0-9_-]*\).*/\1/p'
|
|
406
530
|
fi
|
|
407
|
-
intercom_roster 2>/dev/null
|
|
408
|
-
} | awk '!seen[$0]++'
|
|
531
|
+
intercom_roster 2>/dev/null || true
|
|
532
|
+
} | awk -F'\t' 'NF {print $1}' | awk '!seen[$0]++'
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
# 未能归属到任何会话名的 pi pane(标题不符契约且名册无 tmuxPane 记录):
|
|
536
|
+
# 输出 pane_id<TAB>pane_title;供 status 做「非静默」提示(不假装看不到)。
|
|
537
|
+
unattributed_pi_panes() {
|
|
538
|
+
command -v tmux >/dev/null 2>&1 || return 0
|
|
539
|
+
local claimed p tt
|
|
540
|
+
claimed="$(intercom_list_json 2>/dev/null | sed -n 's/.*"tmuxPane":[[:space:]]*"\([^"]*\)".*/\1/p')" || claimed=""
|
|
541
|
+
while IFS=$'\t' read -r p tt; do
|
|
542
|
+
[[ -n "$p" ]] || continue
|
|
543
|
+
grep -qxF "$p" <<< "$claimed" && continue
|
|
544
|
+
printf '%s\t%s\n' "$p" "$tt"
|
|
545
|
+
done < <(tmux list-panes -a -F "#{pane_id}"$'\t'"#{pane_current_command}"$'\t'"#{pane_title}" 2>/dev/null \
|
|
546
|
+
| awk -F'\t' -v t="$PI_TITLE" '{ c=$2; sub(/^.*\//,"",c); if (c!="pi") next; if ($3 ~ "^" t " - ") next; print $1 "\t" $3 }' || true)
|
|
409
547
|
}
|
|
410
548
|
|
|
411
|
-
# 该 pane
|
|
549
|
+
# 该 pane 是否显示在跑 turn(启发式:扫末 12 行找状态行标记;回执/产物以 PM 核对清单为准)
|
|
550
|
+
# ⚠️ 窗口不能只用末 3 行:pi 状态行(如 "⠦ Working")上方还有分隔线,实测会落到第 4 行以外。
|
|
551
|
+
# 真伪由 roster 状态(intercom_status_for)交叉;此处仅作 pane 级补充。
|
|
412
552
|
pane_busy() {
|
|
413
|
-
local pane="$1"
|
|
414
|
-
|
|
415
|
-
[[ -n "$
|
|
416
|
-
grep -qE "$CLOSE_BUSY_PATTERN" <<< "$
|
|
553
|
+
local pane="$1" window
|
|
554
|
+
window="$(tmux capture-pane -p -t "$pane" 2>/dev/null | grep -v '^[[:space:]]*$' | tail -12)"
|
|
555
|
+
[[ -n "$window" ]] || return 1
|
|
556
|
+
grep -qE "$CLOSE_BUSY_PATTERN" <<< "$window"
|
|
417
557
|
}
|
|
418
558
|
|
|
419
559
|
# 任务在 tasks-state 的归处:
|
|
@@ -951,6 +1091,16 @@ cmd_status() {
|
|
|
951
1091
|
done < <(visible_session_names)
|
|
952
1092
|
(( found )) || echo ' (无)'
|
|
953
1093
|
[[ -f "$TASKS_STATE" ]] || echo " ($TASKS_STATE 不存在,残留判定已跳过)"
|
|
1094
|
+
# 标题被改写/无名的 pi pane:非静默——显式列出,供人工核对
|
|
1095
|
+
local -a orph=()
|
|
1096
|
+
local pn pt
|
|
1097
|
+
while IFS=$'\t' read -r pn pt; do
|
|
1098
|
+
[[ -n "$pn" ]] && orph+=("$pn($pt)")
|
|
1099
|
+
done < <(unattributed_pi_panes)
|
|
1100
|
+
if (( ${#orph[@]} > 0 )); then
|
|
1101
|
+
warn "以下 pi pane 标题不符契约且名册无 pane 记录,无法归属会话名:${orph[*]}"
|
|
1102
|
+
warn " (标题可能被外部改写;用 intercom({action:'list'}) 人工核对,或按 pane_id 处理)"
|
|
1103
|
+
fi
|
|
954
1104
|
}
|
|
955
1105
|
|
|
956
1106
|
# ---------------------------------------------------------------------------
|
|
@@ -958,7 +1108,7 @@ cmd_status() {
|
|
|
958
1108
|
cmd_close() {
|
|
959
1109
|
local force="$1" task="$2" target="$3"
|
|
960
1110
|
[[ -n "$target" ]] || die "close 需要目标:角色别名或派生会话名(如 rd-be / rd-be-T1 或 rd-infra / rd-infra-T1)"
|
|
961
|
-
local name repo cls bus pane pids r ok
|
|
1111
|
+
local name repo cls bus pane pids r ok st
|
|
962
1112
|
intercom_list_json >/dev/null 2>&1 || true # 预热名册缓存
|
|
963
1113
|
if [[ -n "$task" ]]; then
|
|
964
1114
|
resolve_role "$target"
|
|
@@ -997,18 +1147,28 @@ cmd_close() {
|
|
|
997
1147
|
return 1
|
|
998
1148
|
fi
|
|
999
1149
|
|
|
1000
|
-
# 闸门②:在跑 turn(
|
|
1150
|
+
# 闸门②:在跑 turn(pane 标记 + roster 状态双重信号)
|
|
1001
1151
|
pane="$(find_pane_for "$name" "$repo")"
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1152
|
+
bus=""
|
|
1153
|
+
# 显式双分支赋值:pane 空闲时 bus 必须落为 ""。
|
|
1154
|
+
# 曾用 `pane_busy "$pane" && bus=…`:pane_busy 为假时 && 短路,bus 在 set -u 下未绑定 → 行 1007 崩溃。
|
|
1155
|
+
if [[ -n "$pane" ]] && pane_busy "$pane"; then
|
|
1156
|
+
bus="tmux pane $pane 显示在跑 turn"
|
|
1157
|
+
fi
|
|
1158
|
+
st="$(intercom_status_for "$name" "$repo")"
|
|
1159
|
+
if [[ -z "$bus" ]]; then
|
|
1160
|
+
case "$st" in
|
|
1161
|
+
""|idle|\?) ;; # 无状态或空闲 → 不阻塞
|
|
1162
|
+
*) bus="pi-intercom 名册状态=$st(在跑 turn)" ;;
|
|
1163
|
+
esac
|
|
1164
|
+
fi
|
|
1165
|
+
# 既无 pane 也无名册状态 ⇒ 确实无法判定 → 拒绝(不静默 no-op)
|
|
1166
|
+
if [[ -z "$bus" && -z "$pane" && -z "$st" ]]; then
|
|
1167
|
+
if command -v tmux >/dev/null 2>&1; then
|
|
1168
|
+
bus="tmux 宿主但未定位到该会话的 pane(标题契约 / 名册 tmuxPane / 仓库兜底均未命中)"
|
|
1007
1169
|
else
|
|
1008
|
-
bus=""
|
|
1170
|
+
bus="非 tmux 宿主,无法确认是否在跑 turn"
|
|
1009
1171
|
fi
|
|
1010
|
-
else
|
|
1011
|
-
bus="非 tmux 宿主,无法确认是否在跑 turn"
|
|
1012
1172
|
fi
|
|
1013
1173
|
if [[ -n "$bus" && "$force" != true ]]; then
|
|
1014
1174
|
warn "拒绝回收 $name:$bus"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathan33/nao-skill",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "nao 多角色 AI 开发舰队:角色卡 / 技能 / 协作协议 / 工具链,一键接入项目(pi / Claude / 任意 Agent)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,12 +24,21 @@
|
|
|
24
24
|
],
|
|
25
25
|
"author": "Nathan Lee",
|
|
26
26
|
"license": "MIT",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"registry": "https://registry.npmjs.org/"
|
|
30
|
+
},
|
|
27
31
|
"engines": {
|
|
28
32
|
"node": ">=18"
|
|
29
33
|
},
|
|
30
34
|
"scripts": {
|
|
31
|
-
"test": "bash .agents/scripts/nao-fleet.sh check",
|
|
32
|
-
"
|
|
35
|
+
"test": "bash .agents/scripts/nao-fleet.sh check && npm run check:agents",
|
|
36
|
+
"check:agents": "tsc -p tsconfig.agents.json",
|
|
37
|
+
"prepack": "npm test"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20.19.0",
|
|
41
|
+
"typescript": "^5.4.5"
|
|
33
42
|
},
|
|
34
43
|
"repository": {
|
|
35
44
|
"type": "git",
|