@nathan33/nao-skill 0.20.4 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/checklists/pm.md +1 -1
- package/.agents/common/intercom-protocol.md +1 -0
- package/.agents/scripts/nao-fleet.sh +421 -31
- package/README.md +17 -0
- package/package.json +1 -1
package/.agents/checklists/pm.md
CHANGED
|
@@ -77,7 +77,7 @@ description: PM 交付核对清单(按需,交付/派发前读取)
|
|
|
77
77
|
- [ ] AC 可测 + NFRs 验收方式与研发/测试/架构对齐
|
|
78
78
|
- [ ] 数据口径/埋点/指标清晰
|
|
79
79
|
- [ ] 模糊需求已 `grill-me`
|
|
80
|
-
- [ ] 环境判定已执行;开工确认卡获确认;派发前 `list`
|
|
80
|
+
- [ ] 环境判定已执行;开工确认卡获确认;派发前 `list` 验在线;**开工自检 · 本地 `main` 无领先提交**:`git rev-list --count origin/main..main` = 0(≠0 则先处置:发布收尾类由 RD 按显式 sha 推,任务期文档一律落分支。来源:arch `O1` / `docs/reports/2026-11-01-T537-arch.md` §Q5)
|
|
81
81
|
- [ ] worker 离线已检测并处理(`ensure --force` 重拉 + 恢复/重派)
|
|
82
82
|
- [ ] 降级交付已回流登记(tasks-state + 归档,标注「降级回流」)
|
|
83
83
|
- [ ] 派发前已核对目标在线且 `idle`(忙则排队/紧急抢占注明)
|
|
@@ -26,6 +26,7 @@ description: pi-intercom 多会话协作协议(常驻引用)
|
|
|
26
26
|
拉起:`bash .agents/scripts/nao-fleet.sh ensure <别名>[@<repo>]`
|
|
27
27
|
|
|
28
28
|
- **任务派生**:`ensure --task <编号> <别名>[@<repo>]` 创建 `<角色>-<编号>` 独立会话(如 `rd-be-T1`),并行隔离、互不排队/打断,避免多任务同名冲突;任务闭环即**回收**:`nao-fleet.sh close --task <编号> <别名>`(内置在跑 turn / tasks-state 闸门)。
|
|
29
|
+
- **按需角色会话(`ensure --card`,T537)不入上表**:卡来自**下游项目仓** `.agents/prompts/<role>.md`,会话名 `<role>` / `<role>-<编号>`,属**派生/一次性**语义 —— `status` 在派生节标注「一次性会话 · 下游卡」,`close` 回收时**不弹常驻警告**、且跳过 `tasks-state` active 闸门(在跑 turn 闸门不放宽)。它们不登记 `roles.yaml`,故**不在本表内**;卡即系统提示(完全可信输入),命名与目录约束见 `nao-fleet.sh --help` 与 `README.md`「按需角色会话」一节。
|
|
29
30
|
- **子代理会话不属舰队会话身份模型**:由 pi-subagents 拉起的子代理会话以 `subagent-<agent>-<id>`(无名时 `subagent-chat-<id>`)进入 intercom 名册,但**不属**舰队会话 —— `status` 只对其汇总计数(不列名、不参与在线/turn 判重),`close subagent-*` 显式拒绝(生命周期归 pi-subagents 的 stop/retention,nao 不接管)。
|
|
30
31
|
|
|
31
32
|
- **判重 / 在线判定**:脚本按 pi 自设的**终端标题**(`π - <会话名> - <仓库名>`)+ `pi-intercom` 名册判在线(**不用 `pgrep --name`**:pi 启动后 argv 被改写为 `pi`,会恒失配);已运行则跳过并 warn。确需重开加 `--force`。
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
# nao-fleet.sh ensure <别名>[@<repo>] [更多...] 拉起角色窗口(默认工作区=roles.yaml workspace)
|
|
15
15
|
# nao-fleet.sh ensure -m <model> <别名>... 显式指定模型(须命中白名单)
|
|
16
16
|
# nao-fleet.sh ensure --task <编号> <别名>[@<repo>] 任务派生会话:--name <别名>-<编号>(并行隔离,避免同名冲突)
|
|
17
|
+
# nao-fleet.sh ensure --card <卡路径> [--as <会话名>] [--task <编号>]
|
|
18
|
+
# 下游仓自定义角色会话(一次性):卡须为 <repo>/.agents/prompts/<file>.md
|
|
19
|
+
# (卡 role 与常驻 id/别名必须前缀不相交;不入 roles.yaml、不算常驻;
|
|
20
|
+
# --as 须等于卡 role 或以 <role>- 为前缀,否则拒绝 —— fleet 按前缀识别身份)
|
|
17
21
|
# nao-fleet.sh ensure --force <别名>... 忽略"已在运行"判重
|
|
18
22
|
# nao-fleet.sh close <别名|派生会话名> [--task <编号>] [--force]
|
|
19
23
|
# 回收已完成会话(派生名如 rd-be-T1;跨仓定位;定位失败/名册报在线但无句柄 ⇒ 非 0 + 诊断)
|
|
@@ -78,7 +82,12 @@ SKILLS_SUB="$SKILLS_DIR/.agents/skills"
|
|
|
78
82
|
|
|
79
83
|
MANIFEST="$SKILLS_DIR/.agents/roles.yaml"
|
|
80
84
|
CARD_MAX_LINES=200
|
|
85
|
+
# T537 · 下游仓自定义角色会话(ensure --card):卡正文 = 系统提示,故有硬边界(BR3)与命名契约(BR9)
|
|
86
|
+
CARD_MAX_BYTES=262144 # 256 KiB:超限即拒(卡正文会整段塞进 system prompt)
|
|
87
|
+
CARD_SET_MAX=30 # 卡集上限(锚根 ≤2):超出只 warn 一行,不拒
|
|
88
|
+
CARD_RESERVED_PREFIXES="subagent- nao-" # T525/T527 保留前缀(命中 ⇒ 会话拉起即失联)
|
|
81
89
|
ROLE_ORDER=()
|
|
90
|
+
NAME=""; PROMPT=""
|
|
82
91
|
declare -A ROLE_CARDS ROLE_WS ALIAS_ROLE
|
|
83
92
|
MODEL_WHITELIST="${NAO_MODEL_WHITELIST:-}"
|
|
84
93
|
TMUX_LAYOUT="${NAO_TMUX_LAYOUT:-main-row2}"
|
|
@@ -1075,8 +1084,9 @@ build_system_prompt() {
|
|
|
1075
1084
|
}
|
|
1076
1085
|
|
|
1077
1086
|
spawn_one() {
|
|
1078
|
-
local name="$1" repo="$2" model="$3" card prompt_file inner
|
|
1079
|
-
card
|
|
1087
|
+
local name="$1" repo="$2" model="$3" card_override="${4:-}" card prompt_file inner
|
|
1088
|
+
# T537:卡路径可由调用方显式给出(下游仓卡);未给出时仍走清单(PROMPTS_DIR/<card>)逐字不变
|
|
1089
|
+
if [[ -n "$card_override" ]]; then card="$card_override"; else card="$PROMPTS_DIR/${PROMPT:-}"; fi
|
|
1080
1090
|
[[ -f "$card" ]] || die "角色卡不存在: $card"
|
|
1081
1091
|
[[ -d "$repo" ]] || die "工作区不存在: $repo"
|
|
1082
1092
|
|
|
@@ -1166,6 +1176,9 @@ cmd_check() {
|
|
|
1166
1176
|
shopt -u nullglob
|
|
1167
1177
|
(( orphan )) || echo ' ✓ 无孤儿卡'
|
|
1168
1178
|
|
|
1179
|
+
echo "== 下游角色卡(若存在)=="
|
|
1180
|
+
check_downstream_cards || rc=1
|
|
1181
|
+
|
|
1169
1182
|
echo "== pi 扩展声明(T527:声明 + 文件存在,防「声明漏写 ⇒ 静默无 agent」)=="
|
|
1170
1183
|
if [[ ! -f "$SKILLS_DIR/package.json" ]]; then
|
|
1171
1184
|
# 非包根(legacy 仓内真脚本 / 夹具只有 .agents):无 manifest 可核,跳过不判 FAIL
|
|
@@ -1427,6 +1440,201 @@ session_repo_verdict() {
|
|
|
1427
1440
|
printf '%s\t%s\n' "$verdict" "$obase"
|
|
1428
1441
|
}
|
|
1429
1442
|
|
|
1443
|
+
# ---------------------------------------------------------------------------
|
|
1444
|
+
# T537 · 下游仓自定义角色会话(ensure --card)—— ADR 2026-11-01-downstream-card-sessions
|
|
1445
|
+
# 定位:卡(= 系统提示)来自**下游项目仓** <repo>/.agents/prompts/<file>.md;会话属派生/一次性
|
|
1446
|
+
# 语义(与常驻同等的 pane 托管 / status 可见 / close 可回收),但不登记 roles.yaml、不算常驻。
|
|
1447
|
+
# 三条机制不变量(BR9):① 卡 role 与常驻 id/别名**相等或前缀相交**即拒(防抢 status 枚举);
|
|
1448
|
+
# ② 卡集内部前缀反链(check 报 error,status/close 用最长匹配兜底);③ 字符集/长度契约。
|
|
1449
|
+
# 这三条是「status 能在既有 ROLE_ORDER 循环之后追加枚举而不改既有行」的前提(arch Q2)。
|
|
1450
|
+
# 定性:卡限定目录是**可审计边界,不是权限边界** —— 卡正文 = 完全可信输入(能写该仓者
|
|
1451
|
+
# 即定义角色);这里只做一致性校验(目录/常规文件/大小/命名),不做也不该做内容安全校验。
|
|
1452
|
+
|
|
1453
|
+
# 命名契约(BR9 · AC3②③ · AC6):echo 单行原因并返回 1;合法返回 0(无输出)
|
|
1454
|
+
card_name_problem() {
|
|
1455
|
+
local n="$1" r rp
|
|
1456
|
+
[[ -n "$n" ]] || { echo "名字为空"; return 1; }
|
|
1457
|
+
(( ${#n} <= 32 )) || { echo "名字过长(${#n} > 32 字符)"; return 1; }
|
|
1458
|
+
[[ "$n" =~ ^[a-z0-9][a-z0-9_-]*$ ]] \
|
|
1459
|
+
|| { echo "名字含非法字符(只允许小写字母/数字/-/_,且首字符须为字母或数字)"; return 1; }
|
|
1460
|
+
[[ "$n" != *- ]] || { echo "名字不得以 - 结尾"; return 1; }
|
|
1461
|
+
for rp in $CARD_RESERVED_PREFIXES; do
|
|
1462
|
+
[[ "$n" != "$rp"* ]] || { echo "名字不得以保留前缀 $rp 开头($rp 归 pi-subagents / nao 机制)"; return 1; }
|
|
1463
|
+
done
|
|
1464
|
+
if [[ -n "${ALIAS_ROLE[$n]:-}" ]]; then
|
|
1465
|
+
echo "名字与常驻角色/别名 $n 同名(该怎么办:改卡内 frontmatter role —— fleet 按前缀识别身份,--as 也须保留卡 role 前缀,绕不过同名冲突)"; return 1
|
|
1466
|
+
fi
|
|
1467
|
+
for r in "${ROLE_ORDER[@]}" "${!ALIAS_ROLE[@]}"; do
|
|
1468
|
+
[[ "$n" != "$r"-* ]] \
|
|
1469
|
+
|| { echo "名字以常驻前缀 $r- 开头(会抢走 status/close 对 $r 的枚举;该怎么办:改卡内 role,使其不以任何常驻 id/别名为 <x>- 前缀)"; return 1; }
|
|
1470
|
+
done
|
|
1471
|
+
return 0
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
# 卡集(status/close 的识别集 · Q2):锚根全集下 <root>/.agents/prompts/*.md 的 frontmatter role(退化文件名)。
|
|
1475
|
+
# 输出:role<TAB>卡路径<TAB>锚根。**只收合法且非常驻的 role** —— 安装副本/常驻卡不进卡集
|
|
1476
|
+
# (BR9 保证二者不相交;非法卡 ensure 必拒,其历史会话落 status 的「角色未识别」节,不静默)。
|
|
1477
|
+
# 运行时读、不落状态文件(守「不新增配置」);每 run 一次 glob + frontmatter 读,不新增 tmux/node 调用。
|
|
1478
|
+
card_role_set() {
|
|
1479
|
+
local root f role phys n=0
|
|
1480
|
+
local -a roots=() files=()
|
|
1481
|
+
mapfile -t roots < <(repo_anchor_roots)
|
|
1482
|
+
(( ${#roots[@]} )) || return 0
|
|
1483
|
+
shopt -s nullglob
|
|
1484
|
+
for root in "${roots[@]}"; do
|
|
1485
|
+
# 非常规文件(fifo/目录/设备)不入卡集:否则每次 status/close 都要打开它(fifo 会挂死)
|
|
1486
|
+
for f in "$root/.agents/prompts"/*.md; do [[ -f "$f" ]] && files+=("$f"); done
|
|
1487
|
+
done
|
|
1488
|
+
shopt -u nullglob
|
|
1489
|
+
local -A seen=()
|
|
1490
|
+
for f in "${files[@]}"; do
|
|
1491
|
+
phys="$(nao_phys_path "$f")"
|
|
1492
|
+
[[ -z "${seen[$phys]:-}" ]] || continue
|
|
1493
|
+
seen["$phys"]=1
|
|
1494
|
+
role="$(card_field "$f" role)"
|
|
1495
|
+
[[ -n "$role" ]] || role="$(basename -- "$f" .md)"
|
|
1496
|
+
[[ -z "$(card_name_problem "$role")" ]] || continue
|
|
1497
|
+
n=$((n + 1))
|
|
1498
|
+
printf '%s\t%s\t%s\n' "$role" "$f" "${f%/.agents/prompts/*}"
|
|
1499
|
+
done
|
|
1500
|
+
if (( n > CARD_SET_MAX )); then
|
|
1501
|
+
warn "下游角色卡 $n 张 > 上限 $CARD_SET_MAX:status/close 只做名字匹配,建议精简本仓 .agents/prompts/"
|
|
1502
|
+
fi
|
|
1503
|
+
return 0
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
# 卡校验(BR3 六条 · AC2 八种失败输入):通过则设 CARD_PATH / CARD_ROLE / CARD_REPO(均 realpath)。
|
|
1507
|
+
# 失败一律 die(单行「原因 + 该怎么办」),且**必在建 pane 之前**(cmd_ensure 两相执行 ⇒ BR6 零副作用)。
|
|
1508
|
+
# ① 基准 = <repo> realpath;② 显式拒 `..` 与非 .md;③ 常规文件(fifo/目录会让 cat 挂死);
|
|
1509
|
+
# ④ 256 KiB 上限;⑤ 文案含「该怎么办」;⑥ 定性见上(可审计边界)。
|
|
1510
|
+
resolve_card() { # $1=卡路径 · $2=仓(可空 ⇒ $PWD)
|
|
1511
|
+
local arg="$1" repo_arg="${2:-}" repo cand card_phys parent_phys prompts_phys role bytes why
|
|
1512
|
+
repo="${repo_arg:-$PWD}"
|
|
1513
|
+
case "/$arg/" in
|
|
1514
|
+
*/../*) die "角色卡路径不得含 '..':$arg(该怎么办:用 <repo>/.agents/prompts/<name>.md 的相对/绝对路径,不要用 .. 回退)" ;;
|
|
1515
|
+
esac
|
|
1516
|
+
case "$arg" in
|
|
1517
|
+
*.md) ;;
|
|
1518
|
+
*) die "角色卡须为 .md 文件:${arg:-<空>}(该怎么办:把卡放在 <repo>/.agents/prompts/<name>.md)" ;;
|
|
1519
|
+
esac
|
|
1520
|
+
[[ -d "$repo" ]] || die "工作区不存在:$repo(该怎么办:--card <卡>@<仓> 指定仓,或先 cd 到目标仓)"
|
|
1521
|
+
local repo_phys
|
|
1522
|
+
repo_phys="$(nao_phys_path "$repo")"
|
|
1523
|
+
# BR3②:相对路径按 realpath(repo) 解析(不是 $PWD;否则 --card <卡>@<他仓> 时两者分歧)
|
|
1524
|
+
case "$arg" in
|
|
1525
|
+
/*) cand="$arg" ;;
|
|
1526
|
+
*) cand="$repo_phys/$arg" ;;
|
|
1527
|
+
esac
|
|
1528
|
+
[[ -e "$cand" ]] || die "角色卡不存在:$arg(期望 $repo_phys/.agents/prompts/;该怎么办:在目标仓 .agents/prompts/ 下创建该卡,或用 @<仓> 指向正确仓)"
|
|
1529
|
+
# BR3③:常规文件 —— fifo/设备/目录会让 cat 永久挂死,必须**在打开之前**拒
|
|
1530
|
+
[[ -f "$cand" ]] || die "角色卡不是常规文件(目录/fifo/设备):$arg(该怎么办:换成 <repo>/.agents/prompts/<name>.md 常规文件)"
|
|
1531
|
+
card_phys="$(nao_phys_path "$cand")"
|
|
1532
|
+
prompts_phys="$(nao_phys_path "$repo_phys/.agents/prompts")"
|
|
1533
|
+
parent_phys="$(nao_phys_path "$(dirname -- "$card_phys")")"
|
|
1534
|
+
[[ "$parent_phys" == "$prompts_phys" ]] \
|
|
1535
|
+
|| die "角色卡不在 <repo>/.agents/prompts/ 内:$arg → $card_phys(符号链接逃逸也算越界;该怎么办:把卡放进 $repo_phys/.agents/prompts/)"
|
|
1536
|
+
bytes="$(wc -c < "$card_phys" | tr -d '[:space:]')"
|
|
1537
|
+
(( bytes <= CARD_MAX_BYTES )) \
|
|
1538
|
+
|| die "角色卡过大:$bytes 字节 > $CARD_MAX_BYTES(提示会整段塞进 system prompt;该怎么办:拆到 skills/ 或精简卡)"
|
|
1539
|
+
[[ -s "$card_phys" ]] || die "角色卡为空:$arg(该怎么办:至少写 frontmatter role 与一段角色说明)"
|
|
1540
|
+
role="$(card_field "$card_phys" role)"
|
|
1541
|
+
role="$(printf '%s' "$role" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^"//' -e 's/"$//')"
|
|
1542
|
+
[[ -n "$role" ]] || role="$(basename -- "$card_phys" .md)" # 退化:文件名(AC2「无 role 且无可退化文件名」由命名契约拦下)
|
|
1543
|
+
if ! why="$(card_name_problem "$role")"; then
|
|
1544
|
+
die "角色卡 role 不可用:$role —— $why(该怎么办:改卡内 frontmatter role,或在 ensure 时加 --as <合规名字>)"
|
|
1545
|
+
fi
|
|
1546
|
+
CARD_PATH="$card_phys"; CARD_ROLE="$role"; CARD_REPO="$repo_phys"
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
# check 新增节:下游角色卡(若存在)—— D7 分级
|
|
1550
|
+
# 内容类(缺 role/description、CRLF、超限、含 name: 双用)⇒ ` ! ` warn(不红 rc)
|
|
1551
|
+
# 机制类(与常驻 id/别名或前缀冲突、卡集前缀嵌套、命名契约越界)⇒ ` ✗ ` error(rc=1,运行期必拒)
|
|
1552
|
+
# overlay 兼容(AC8③):扫描目录 realpath == $PROMPTS_DIR realpath ⇒ 该根整体跳过(否则常驻卡被误报冲突)
|
|
1553
|
+
# 机制常驻卡的项目内副本(legacy 全量安装)按「同名卡文件」识别 ⇒ 跳过冲突检查(不红);
|
|
1554
|
+
# 真正的下游卡(不同文件名)才纳入校验。
|
|
1555
|
+
check_downstream_cards() {
|
|
1556
|
+
local rc=0 root f role desc bytes n=0 why base r
|
|
1557
|
+
local -a roots=() scan=() files=() roles=()
|
|
1558
|
+
local skipped=0
|
|
1559
|
+
mapfile -t roots < <(repo_anchor_roots)
|
|
1560
|
+
for root in "${roots[@]}"; do
|
|
1561
|
+
[[ -d "$root/.agents/prompts" ]] || continue
|
|
1562
|
+
if [[ "$(nao_phys_path "$root/.agents/prompts")" == "$(nao_phys_path "$PROMPTS_DIR")" ]]; then
|
|
1563
|
+
skipped=$((skipped + 1)); continue
|
|
1564
|
+
fi
|
|
1565
|
+
scan+=("$root")
|
|
1566
|
+
done
|
|
1567
|
+
if (( ${#scan[@]} == 0 )); then
|
|
1568
|
+
if (( skipped > 0 )); then
|
|
1569
|
+
echo ' · 下游卡扫描目录即机制包 prompts(overlay 生效):本节跳过'
|
|
1570
|
+
else
|
|
1571
|
+
echo ' · 未发现下游角色卡目录(<repo>/.agents/prompts/ 不存在):跳过'
|
|
1572
|
+
fi
|
|
1573
|
+
return 0
|
|
1574
|
+
fi
|
|
1575
|
+
shopt -s nullglob
|
|
1576
|
+
local -a nonreg=()
|
|
1577
|
+
for root in "${scan[@]}"; do
|
|
1578
|
+
for f in "$root/.agents/prompts"/*.md; do
|
|
1579
|
+
if [[ -f "$f" ]]; then files+=("$f"); else nonreg+=("$f"); fi
|
|
1580
|
+
done
|
|
1581
|
+
done
|
|
1582
|
+
shopt -u nullglob
|
|
1583
|
+
# 非常规文件(fifo/目录/设备):ensure --card 运行期必拒(且 cat 会挂死)⇒ 机制类 error
|
|
1584
|
+
for f in "${nonreg[@]:-}"; do
|
|
1585
|
+
[[ -n "$f" ]] || continue
|
|
1586
|
+
printf ' ✗ %s 不是常规文件(fifo/目录/设备):ensure --card 必拒,且读取会挂死\n' "$(basename -- "$f")"
|
|
1587
|
+
rc=1
|
|
1588
|
+
done
|
|
1589
|
+
if (( ${#files[@]} == 0 )); then echo ' · 下游角色卡目录为空:跳过'; return 0; fi
|
|
1590
|
+
for f in "${files[@]}"; do
|
|
1591
|
+
base="$(basename -- "$f")"
|
|
1592
|
+
role="$(card_field "$f" role)"
|
|
1593
|
+
desc="$(card_field "$f" description)"
|
|
1594
|
+
if [[ -z "$role" ]]; then
|
|
1595
|
+
printf ' ! %s 缺 frontmatter role(退化用文件名 %s)\n' "$base" "$(basename -- "$f" .md)"
|
|
1596
|
+
role="$(basename -- "$f" .md)"
|
|
1597
|
+
fi
|
|
1598
|
+
local resident=0
|
|
1599
|
+
for r in "${ROLE_ORDER[@]}"; do
|
|
1600
|
+
if [[ "$base" == "${ROLE_CARDS[$r]}" ]] && [[ "$role" == "$r" || "${ALIAS_ROLE[$role]:-}" == "$r" ]]; then
|
|
1601
|
+
resident=1; break
|
|
1602
|
+
fi
|
|
1603
|
+
done
|
|
1604
|
+
if (( resident )); then
|
|
1605
|
+
printf ' · %s 机制常驻卡副本(role=%s):跳过冲突检查\n' "$base" "$role"
|
|
1606
|
+
continue
|
|
1607
|
+
fi
|
|
1608
|
+
n=$((n + 1))
|
|
1609
|
+
roles+=("$role")
|
|
1610
|
+
printf ' · %s role=%s\n' "$base" "$role"
|
|
1611
|
+
[[ -n "$desc" ]] || printf ' ! %s 缺 frontmatter description(建议补一行用途说明)\n' "$base"
|
|
1612
|
+
if grep -q $'\r' "$f" 2>/dev/null; then
|
|
1613
|
+
printf ' ! %s 含 CRLF(须转 LF;CRLF 会破坏 frontmatter 解析)\n' "$base"
|
|
1614
|
+
fi
|
|
1615
|
+
bytes="$(wc -c < "$f" 2>/dev/null | tr -d '[:space:]')"; bytes="${bytes:-0}"
|
|
1616
|
+
(( bytes <= CARD_MAX_BYTES )) \
|
|
1617
|
+
|| printf ' ! %s %s 字节 > %s(256 KiB;建议拆到 skills/)\n' "$base" "$bytes" "$CARD_MAX_BYTES"
|
|
1618
|
+
[[ -z "$(card_field "$f" name)" ]] \
|
|
1619
|
+
|| printf ' ! %s 含 name: —— 同时被 pi-subagents 视作 agent(其路径无 ## 环境锚点,@.agents 引用不可解析)\n' "$base"
|
|
1620
|
+
if ! why="$(card_name_problem "$role")"; then
|
|
1621
|
+
printf ' ✗ %s role=%s 不可用:%s(运行期 ensure --card 必拒 ⇒ 闸门提前红)\n' "$base" "$role" "$why"
|
|
1622
|
+
rc=1
|
|
1623
|
+
fi
|
|
1624
|
+
done
|
|
1625
|
+
local i j
|
|
1626
|
+
for i in "${!roles[@]}"; do
|
|
1627
|
+
for j in "${!roles[@]}"; do
|
|
1628
|
+
(( i == j )) && continue
|
|
1629
|
+
[[ "${roles[$i]}" == "${roles[$j]}-"* ]] || continue
|
|
1630
|
+
printf ' ✗ 卡集前缀嵌套:%s 与 %s(后者会抢走前者的 status/close 枚举;须改名)\n' "${roles[$j]}" "${roles[$i]}"
|
|
1631
|
+
rc=1
|
|
1632
|
+
done
|
|
1633
|
+
done
|
|
1634
|
+
(( rc != 0 )) || printf ' ✓ 下游角色卡 %d 张,无机制冲突\n' "$n"
|
|
1635
|
+
return $rc
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1430
1638
|
# ---------------------------------------------------------------------------
|
|
1431
1639
|
cmd_status() {
|
|
1432
1640
|
local a
|
|
@@ -1446,6 +1654,9 @@ cmd_status() {
|
|
|
1446
1654
|
while IFS= read -r rl; do [[ -n "$rl" ]] && roots_blob+="$rl"$'\n'; done < <(repo_anchor_roots)
|
|
1447
1655
|
local -A pane_cwds=() roster_cwds=()
|
|
1448
1656
|
local pn pname pcwd rn rcwd prev
|
|
1657
|
+
# 保留既有键口径(按 pane_id 建键):T537 不改这里 —— AC5③ 要求不带 --card 时 status 逐字等价,
|
|
1658
|
+
# 「pane cwd 权威」路径的键口径修正会改变无名册 cwd 场景的输出 ⇒ 已登记为遗留(见 T537 RD 报告),
|
|
1659
|
+
# 本批一律走名册 cwd(T530 既有路径)。
|
|
1449
1660
|
while IFS=$'\t' read -r pn pname pcwd; do
|
|
1450
1661
|
[[ -n "$pname" ]] || continue
|
|
1451
1662
|
prev="${pane_cwds[$pname]:-}"
|
|
@@ -1457,11 +1668,69 @@ cmd_status() {
|
|
|
1457
1668
|
done < <(intercom_roster 2>/dev/null || true)
|
|
1458
1669
|
|
|
1459
1670
|
local found=0 dname role id cls tag verdict obase
|
|
1671
|
+
# T537:卡集(本仓锚下、非常驻的合法卡 role)—— 既有 ROLE_ORDER 循环**逐字不动**,卡角色在其后追加
|
|
1672
|
+
# (BR9 保证前缀不相交 ⇒ 两路不会抢同名字,零回归可证;无需 break 顺序敏感处理)
|
|
1673
|
+
local -A card_path=()
|
|
1674
|
+
local cr cp croot crowle suff
|
|
1675
|
+
while IFS=$'\t' read -r cr cp croot; do
|
|
1676
|
+
[[ -n "$cr" ]] || continue
|
|
1677
|
+
card_path["$cr"]="$cp"
|
|
1678
|
+
done < <(card_role_set)
|
|
1679
|
+
local -a unrecognized=()
|
|
1460
1680
|
while IFS= read -r dname; do
|
|
1461
1681
|
[[ -n "$dname" ]] || continue
|
|
1462
1682
|
id=""
|
|
1463
1683
|
for role in "${ROLE_ORDER[@]}"; do [[ "$dname" == "$role-"* ]] && { id="${dname#"$role"-}"; break; }; done
|
|
1464
|
-
[[ -
|
|
1684
|
+
if [[ -z "$id" ]]; then
|
|
1685
|
+
# ---- 卡会话(最长前缀匹配)或 F3「角色未识别」----
|
|
1686
|
+
crowle=""
|
|
1687
|
+
for role in "${!card_path[@]}"; do
|
|
1688
|
+
[[ "$dname" == "$role" || "$dname" == "$role-"* ]] || continue
|
|
1689
|
+
if [[ -z "$crowle" || ${#role} -gt ${#crowle} ]]; then crowle="$role"; fi
|
|
1690
|
+
done
|
|
1691
|
+
if [[ -z "$crowle" ]]; then
|
|
1692
|
+
# T525:子代理会话单列计数,不进本区
|
|
1693
|
+
[[ "$dname" == subagent-* ]] && continue
|
|
1694
|
+
# F3(D5):后缀形状像 nao 任务号 ⇒ 单列「角色未识别」,不得静默(卡被删/改名后的孤儿)
|
|
1695
|
+
if [[ "$dname" =~ (^|-)T[0-9]+$ || "$dname" =~ (^|-)M[0-9]{8}(-|$) ]]; then
|
|
1696
|
+
IFS=$'\t' read -r verdict obase <<< "$(session_repo_verdict "$roots_blob" "${pane_cwds[$dname]:-}" "${roster_cwds[$dname]:-}")"
|
|
1697
|
+
case "$verdict" in
|
|
1698
|
+
other) unrecognized+=(" ? $dname(他仓 @ ${obase:-?})角色未识别(疑似 fleet 遗留):卡被删除/改名、卡在他仓、或 overlay 改名?核对后人工处理(本行不给回收命令)") ;;
|
|
1699
|
+
unknown) unrecognized+=(" ? $dname(归属不可判定)角色未识别(疑似 fleet 遗留):卡被删除/改名、卡在他仓、或 overlay 改名?核对后人工处理(本行不给回收命令)") ;;
|
|
1700
|
+
*) unrecognized+=(" ? $dname(本仓)角色未识别(疑似 fleet 遗留):卡被删除/改名、卡在他仓、或 overlay 改名?核对后人工处理(本行不给回收命令)") ;;
|
|
1701
|
+
esac
|
|
1702
|
+
fi
|
|
1703
|
+
continue
|
|
1704
|
+
fi
|
|
1705
|
+
suff=""
|
|
1706
|
+
[[ "$dname" == "$crowle" ]] || suff="${dname#"$crowle"-}"
|
|
1707
|
+
IFS=$'\t' read -r verdict obase <<< "$(session_repo_verdict "$roots_blob" "${pane_cwds[$dname]:-}" "${roster_cwds[$dname]:-}")"
|
|
1708
|
+
case "$verdict" in
|
|
1709
|
+
# F2:他仓卡会话只标注(不读他仓 .agents/prompts)
|
|
1710
|
+
other) printf ' ~ %s(他仓 @ %s)他仓会话,勿在本仓回收\n' "$dname" "${obase:-?}"; found=1; continue ;;
|
|
1711
|
+
unknown) printf ' ? %s(归属不可判定)→ 人工核对(勿在本仓回收)\n' "$dname"; found=1; continue ;;
|
|
1712
|
+
esac
|
|
1713
|
+
if [[ -z "$suff" ]]; then
|
|
1714
|
+
printf ' · %s(一次性会话 · 下游卡 %s)→ close %s\n' "$dname" "$crowle" "$dname"
|
|
1715
|
+
found=1; continue
|
|
1716
|
+
fi
|
|
1717
|
+
# F4:后缀非任务编号形状 ⇒ 列名但不给 tag/命令(此收紧只对新(卡)前缀生效)
|
|
1718
|
+
if [[ ! "$suff" =~ ^(T[0-9]+|M[0-9]{8}(-.*)?)$ ]]; then
|
|
1719
|
+
printf ' · %s(一次性会话 · 下游卡 %s)后缀非任务编号 → 不提供回收命令(核对后人工处理)\n' "$dname" "$crowle"
|
|
1720
|
+
found=1; continue
|
|
1721
|
+
fi
|
|
1722
|
+
cls="$(task_state_class "$suff")"
|
|
1723
|
+
tag=""
|
|
1724
|
+
case "$cls" in
|
|
1725
|
+
closed) tag=" ! 残留($suff 已归档)→ close --task $suff ${dname%-"$suff"}" ;;
|
|
1726
|
+
active) tag=" · $suff 进行态" ;;
|
|
1727
|
+
mentioned) tag=" ? $suff 仅散文提及(非表内记录)→ 核对后 close --task $suff ${dname%-"$suff"}" ;;
|
|
1728
|
+
absent) tag=" ! 残留(tasks-state 无 $suff 记录)→ 核对后 close --task $suff ${dname%-"$suff"}" ;;
|
|
1729
|
+
esac
|
|
1730
|
+
printf ' · %s(一次性会话 · 下游卡 %s)%s\n' "$dname" "$crowle" "$tag"
|
|
1731
|
+
found=1
|
|
1732
|
+
continue
|
|
1733
|
+
fi
|
|
1465
1734
|
IFS=$'\t' read -r verdict obase <<< "$(session_repo_verdict "$roots_blob" "${pane_cwds[$dname]:-}" "${roster_cwds[$dname]:-}")"
|
|
1466
1735
|
case "$verdict" in
|
|
1467
1736
|
# D5:他仓会话**列出并标注**(不静默隐藏),整行不含任何可执行 close 命令(BR2)
|
|
@@ -1472,16 +1741,22 @@ cmd_status() {
|
|
|
1472
1741
|
cls="$(task_state_class "$id")"
|
|
1473
1742
|
tag=""
|
|
1474
1743
|
case "$cls" in
|
|
1475
|
-
|
|
1744
|
+
# D6(T537):任务号可含 `-`(如 M20261014-pi-ext-update)⇒ 按 $id 剥后缀(旧 ${dname%-*} 会截错角色名)
|
|
1745
|
+
closed) tag=" ! 残留($id 已归档)→ close --task $id ${dname%-"$id"}" ;;
|
|
1476
1746
|
active) tag=" · $id 进行态" ;;
|
|
1477
|
-
mentioned) tag=" ? $id 仅散文提及(非表内记录)→ 核对后 close --task $id ${dname
|
|
1478
|
-
absent) tag=" ! 残留(tasks-state 无 $id 记录)→ 核对后 close --task $id ${dname
|
|
1747
|
+
mentioned) tag=" ? $id 仅散文提及(非表内记录)→ 核对后 close --task $id ${dname%-"$id"}" ;;
|
|
1748
|
+
absent) tag=" ! 残留(tasks-state 无 $id 记录)→ 核对后 close --task $id ${dname%-"$id"}" ;;
|
|
1479
1749
|
esac
|
|
1480
1750
|
printf ' · %s%s\n' "$dname" "$tag"
|
|
1481
1751
|
found=1
|
|
1482
1752
|
done < <(visible_session_names)
|
|
1483
1753
|
(( found )) || echo ' (无)'
|
|
1484
1754
|
[[ -f "$TASKS_STATE" ]] || echo " ($TASKS_STATE 不存在,残留判定已跳过)"
|
|
1755
|
+
# D5:未识别节(F3)—— 单列,不给可执行命令(T530「非静默」对偶)
|
|
1756
|
+
if (( ${#unrecognized[@]} > 0 )); then
|
|
1757
|
+
echo '== 角色未识别(疑似 fleet 遗留)=='
|
|
1758
|
+
printf '%s\n' "${unrecognized[@]}"
|
|
1759
|
+
fi
|
|
1485
1760
|
# T525:子代理会话(pi-subagents)不属舰队会话身份模型 —— 派生节后独立一行、只计数不列名、N=0 不打印(BR3/BR6)。
|
|
1486
1761
|
local nsub
|
|
1487
1762
|
nsub="$(subagent_in_field)"
|
|
@@ -1509,11 +1784,13 @@ cmd_close() {
|
|
|
1509
1784
|
if [[ "$target" == subagent-* ]]; then
|
|
1510
1785
|
die "拒绝回收 $target:这是 pi-subagents 子代理会话(subagent-*),其生命周期不归 nao 回收(请用 pi-subagents 的 stop / retention 管理)"
|
|
1511
1786
|
fi
|
|
1512
|
-
local name repo cls bus pane pids st tgt a best=""
|
|
1787
|
+
local name repo cls bus pane pids st tgt a best="" bestc="" is_resident=1 card_role="" card_path="" cr cp croot k
|
|
1513
1788
|
intercom_list_json >/dev/null 2>&1 || true # 预热名册缓存
|
|
1514
1789
|
|
|
1515
1790
|
# ---- 目标解析:别名/canonical id | 派生会话名 <角色>-<编号> ----
|
|
1516
1791
|
# 派生名(如 qa-T508)可直接作目标;也兼容 close --task T508 qa 与 close --task T508 qa-T508。
|
|
1792
|
+
# T537:解析顺序 ① ALIAS_ROLE 精确 → ② <别名>-<编号> → ③ 卡集最长前缀 + 任务号 → ④ 卡集裸名精确 → ⑤ die。
|
|
1793
|
+
# ①② 逐字不动;③④ 为新增分支(下游卡一次性会话);类别由显式布尔 is_resident 决定(BR10,禁用 `-z task` 推断)。
|
|
1517
1794
|
tgt=""
|
|
1518
1795
|
if [[ -n "${ALIAS_ROLE[$target]:-}" ]]; then
|
|
1519
1796
|
tgt="$target"
|
|
@@ -1528,16 +1805,61 @@ cmd_close() {
|
|
|
1528
1805
|
if [[ -n "$best" ]]; then tgt="$best"; task="${target#"$best"-}"; fi
|
|
1529
1806
|
fi
|
|
1530
1807
|
if [[ -z "$tgt" ]]; then
|
|
1531
|
-
|
|
1808
|
+
# ③④ 下游卡(一次性):最长前缀匹配(纵深防御:卡集前缀嵌套由 check 报 error)
|
|
1809
|
+
local -A cset=()
|
|
1810
|
+
while IFS=$'\t' read -r cr cp croot; do
|
|
1811
|
+
[[ -n "$cr" ]] || continue
|
|
1812
|
+
cset["$cr"]="$cp"
|
|
1813
|
+
done < <(card_role_set)
|
|
1814
|
+
for k in "${!cset[@]}"; do
|
|
1815
|
+
[[ "$target" == "$k" || "$target" == "$k"-* ]] || continue
|
|
1816
|
+
if [[ -z "$bestc" || ${#k} -gt ${#bestc} ]]; then bestc="$k"; fi
|
|
1817
|
+
done
|
|
1818
|
+
if [[ -n "$bestc" ]]; then
|
|
1819
|
+
card_role="$bestc"
|
|
1820
|
+
card_path="${cset[$bestc]}"
|
|
1821
|
+
is_resident=0
|
|
1822
|
+
if [[ "$target" != "$bestc" ]]; then
|
|
1823
|
+
local tsuff="${target#"$bestc"-}"
|
|
1824
|
+
if [[ -n "$task" && "$task" != "$tsuff" ]]; then
|
|
1825
|
+
die "目标名后缀与 --task 不一致:$target(后缀 $tsuff,--task $task;该怎么办:二选一,如 close --task $tsuff $bestc)"
|
|
1826
|
+
fi
|
|
1827
|
+
task="$tsuff"
|
|
1828
|
+
fi
|
|
1829
|
+
fi
|
|
1830
|
+
fi
|
|
1831
|
+
if [[ -z "$tgt" && "$is_resident" == "1" ]]; then
|
|
1832
|
+
die "未知角色或派生会话名: $target(可用角色: ${ROLE_ORDER[*]};派生名形如 rd-be-T1 / qa-T1,亦可用 close --task <编号> <角色>;下游卡角色需在本仓锚下存在 .agents/prompts/<role>.md)"
|
|
1833
|
+
fi
|
|
1834
|
+
if [[ "$is_resident" == "1" ]]; then
|
|
1835
|
+
resolve_role "$tgt"
|
|
1836
|
+
name="$NAME"
|
|
1837
|
+
[[ -n "$task" ]] && name="${NAME}-${task}"
|
|
1838
|
+
# 目标仓:名册 cwd 优先(跨仓会话真实仓),否则回退 roles.yaml workspace / 当前目录
|
|
1839
|
+
repo="$(intercom_cwd_for "$name")"
|
|
1840
|
+
[[ -n "$repo" ]] || repo="${ROLE_WS[$NAME]:-$PWD}"
|
|
1841
|
+
else
|
|
1842
|
+
name="$card_role"
|
|
1843
|
+
[[ -n "$task" ]] && name="${card_role}-${task}"
|
|
1844
|
+
repo="$(intercom_cwd_for "$name")"
|
|
1845
|
+
[[ -n "$repo" ]] || repo="${card_path%/.agents/prompts/*}"
|
|
1846
|
+
[[ -n "$repo" ]] || repo="$PWD"
|
|
1532
1847
|
fi
|
|
1533
|
-
resolve_role "$tgt"
|
|
1534
|
-
name="$NAME"
|
|
1535
|
-
[[ -n "$task" ]] && name="${NAME}-${task}"
|
|
1536
|
-
# 目标仓:名册 cwd 优先(跨仓会话真实仓),否则回退 roles.yaml workspace / 当前目录
|
|
1537
|
-
repo="$(intercom_cwd_for "$name")"
|
|
1538
|
-
[[ -n "$repo" ]] || repo="${ROLE_WS[$NAME]:-$PWD}"
|
|
1539
1848
|
|
|
1540
|
-
if [[
|
|
1849
|
+
if [[ "$is_resident" != "1" ]]; then
|
|
1850
|
+
# BR5/BR10:一次性(下游卡)会话 —— 不弹常驻警告、不提示 ensure --force 重开;AC4③B 固定前缀在 stderr
|
|
1851
|
+
warn "一次性会话 $name(下游卡)$card_path:回收后不重开(如需重开:ensure --card <卡>)"
|
|
1852
|
+
if [[ -n "$task" ]]; then
|
|
1853
|
+
# D8:tasks-state active 闸门对卡会话**跳过**(只 log);在跑 turn 闸门**不放宽**(下面依然生效)
|
|
1854
|
+
cls="$(task_state_class "$task")"
|
|
1855
|
+
case "$cls" in
|
|
1856
|
+
active) log "跳过 tasks-state active 闸门(下游卡一次性会话):$task(产物以回执/报告为准)" ;;
|
|
1857
|
+
absent) log "tasks-state 全文无 $task 记录(下游卡一次性会话,不阻塞回收)" ;;
|
|
1858
|
+
mentioned) log "tasks-state 仅散文提及 $task(下游卡一次性会话,不阻塞回收)" ;;
|
|
1859
|
+
nofile) log "未启用 $TASKS_STATE(跳过状态闸门)" ;;
|
|
1860
|
+
esac
|
|
1861
|
+
fi
|
|
1862
|
+
elif [[ -z "$task" ]]; then
|
|
1541
1863
|
warn "常驻会话 $name:任务闭环后应 ensure --force 重开,而非回收(仅在本批不再需要该角色时回收)"
|
|
1542
1864
|
else
|
|
1543
1865
|
# 闸门①:tasks-state 未推进 → 拒绝(验收未过需原会话返工,回收会丢上下文)
|
|
@@ -1677,24 +1999,56 @@ cmd_cleanup() {
|
|
|
1677
1999
|
|
|
1678
2000
|
# ---------------------------------------------------------------------------
|
|
1679
2001
|
cmd_ensure() {
|
|
1680
|
-
local force="$1" model="$2" task="$3"
|
|
1681
|
-
local spec role repo key seen k
|
|
1682
|
-
local -a cg_done=()
|
|
1683
|
-
[[
|
|
1684
|
-
#
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
2002
|
+
local force="$1" model="$2" task="$3"
|
|
2003
|
+
local i spec role repo key seen k card_arg card_as disp
|
|
2004
|
+
local -a cg_done=() t_names=() t_repos=() t_cards=()
|
|
2005
|
+
[[ ${#TARGETS[@]} -gt 0 ]] || die "ensure 需要至少一个角色,如: nao-fleet.sh ensure arch rd-fe rd-infra"
|
|
2006
|
+
# ==== 相位 1:全量解析 + 校验(BR11 两相执行)====
|
|
2007
|
+
# 卡、role、BR9、--as 全过才进相位 2;任一项失败都在**建 pane 之前** die ⇒ BR6「零副作用」可达
|
|
2008
|
+
for i in "${!TARGETS[@]}"; do
|
|
2009
|
+
spec="${TARGETS[$i]}"; card_arg="${TARGET_CARD[$i]:-}"; card_as="${TARGET_AS[$i]:-}"
|
|
2010
|
+
role=""; repo=""
|
|
1688
2011
|
if [[ "$spec" == *"@"* ]]; then
|
|
1689
2012
|
role="${spec%%@*}"; repo="${spec#*@}"
|
|
1690
2013
|
else
|
|
1691
|
-
role="$spec"
|
|
2014
|
+
role="$spec"
|
|
1692
2015
|
fi
|
|
1693
|
-
|
|
1694
|
-
|
|
2016
|
+
if [[ -n "$card_arg" ]]; then
|
|
2017
|
+
# 下游卡目标:目标 positional 即卡路径(BR11 目标级绑定)
|
|
2018
|
+
resolve_card "$role" "$repo"
|
|
2019
|
+
if [[ -n "$card_as" ]]; then
|
|
2020
|
+
local why
|
|
2021
|
+
# 命名契约先判(字符集/保留前缀/撞常驻),再判 D9 前缀约束 —— 让「撞常驻 id」这类
|
|
2022
|
+
# 更根本的问题先点名,避免被前缀提示掩盖。
|
|
2023
|
+
if ! why="$(card_name_problem "$card_as")"; then
|
|
2024
|
+
die "--as 不可用:$card_as —— $why(该怎么办:换一个不以常驻 id/别名开头、符合 ^[a-z0-9][a-z0-9_-]*$ 且 ≤ 32 字符的名字)"
|
|
2025
|
+
fi
|
|
2026
|
+
# D9(PM 拍板):--as 必须等于卡 role 或以 `<卡 role>-` 为前缀 —— 否则会起出 status/close
|
|
2027
|
+
# 解析不到的会话(等于把「起得来但管不到」合法化);warn 不够,必须 die。
|
|
2028
|
+
if [[ "$card_as" != "$CARD_ROLE" && "$card_as" != "$CARD_ROLE"-* ]]; then
|
|
2029
|
+
die "--as 不可用:$card_as 不以卡 role($CARD_ROLE)为前缀 —— 改名不得脱离卡 role 前缀(fleet 按前缀识别身份),用 <role>-<suffix>(如 $CARD_ROLE-notes)"
|
|
2030
|
+
fi
|
|
2031
|
+
disp="$card_as"
|
|
2032
|
+
else
|
|
2033
|
+
disp="$CARD_ROLE"
|
|
2034
|
+
fi
|
|
2035
|
+
t_names+=("$disp"); t_repos+=("$CARD_REPO"); t_cards+=("$CARD_PATH")
|
|
2036
|
+
else
|
|
2037
|
+
[[ -z "$card_as" ]] || die "--as 只能用于同一目标的 --card 之后(如 ensure --card <卡> --as <会话名>)"
|
|
2038
|
+
resolve_role "$role"
|
|
2039
|
+
[[ -n "$repo" ]] || repo="${ROLE_WS[$NAME]:-$PWD}"
|
|
2040
|
+
t_names+=("$NAME"); t_repos+=("$repo"); t_cards+=("")
|
|
2041
|
+
fi
|
|
2042
|
+
done
|
|
2043
|
+
# ==== 相位 2:拉起(宿主窗口解析、判重、codegraph 状态、spawn 逐目标不变)====
|
|
2044
|
+
# T517:入口一次性解析派生宿主窗口(仅 $TMUX 分支);定位失败 ⇒ 解析处 warn 一次,各角色 pane 跳过
|
|
2045
|
+
HOST_WINDOW=""
|
|
2046
|
+
if [[ -n "${TMUX:-}" ]] && command -v tmux >/dev/null 2>&1; then resolve_host_window; fi
|
|
2047
|
+
for i in "${!t_names[@]}"; do
|
|
2048
|
+
repo="${t_repos[$i]}"
|
|
1695
2049
|
# 任务派生:会话名 = <角色>-<任务编号>,独立 intercom 身份(并行隔离,避免同名冲突)
|
|
1696
|
-
|
|
1697
|
-
[[ -n "$task" ]] && disp="${
|
|
2050
|
+
disp="${t_names[$i]}"
|
|
2051
|
+
[[ -n "$task" ]] && disp="${t_names[$i]}-${task}"
|
|
1698
2052
|
# CodeGraph 索引健康(同 repo 只查一次,不阻塞拉起)
|
|
1699
2053
|
key="$repo"; seen=0
|
|
1700
2054
|
for k in "${cg_done[@]:-}"; do [[ "$k" == "$key" ]] && seen=1; done
|
|
@@ -1703,7 +2057,11 @@ cmd_ensure() {
|
|
|
1703
2057
|
warn "$disp 已在运行(终端标题/名册命中),跳过;确需重开请加 --force"
|
|
1704
2058
|
continue
|
|
1705
2059
|
fi
|
|
1706
|
-
|
|
2060
|
+
if [[ -n "${t_cards[$i]}" ]]; then
|
|
2061
|
+
spawn_one "$disp" "$repo" "$model" "${t_cards[$i]}"
|
|
2062
|
+
else
|
|
2063
|
+
spawn_one "$disp" "$repo" "$model"
|
|
2064
|
+
fi
|
|
1707
2065
|
done
|
|
1708
2066
|
# T536/BR3/AC4:仅当本轮真拉起过 pane 才打印**恰一行**就绪提示;
|
|
1709
2067
|
# 跳过(已在运行)/失败(无宿主窗口、分屏失败)分支不打印;既有行内容与顺序不变(只追加末尾一行)。
|
|
@@ -1723,6 +2081,9 @@ usage() {
|
|
|
1723
2081
|
|
|
1724
2082
|
# ---- 入口 ----
|
|
1725
2083
|
CMD=""; FORCE=false; STRICT=false; MODEL=""; TASK=""; CLEANUP_SOCK=""; TARGETS=()
|
|
2084
|
+
# T537:目标级修饰符(--card / --as)的绑定状态 —— 只作用于**其后第一个 positional**(BR11)
|
|
2085
|
+
TARGET_CARD=(); TARGET_AS=(); PEND_CARD=false; PEND_AS=""; LAST_CARD_IDX=-1
|
|
2086
|
+
_need_ensure() { [[ "$CMD" == "ensure" ]] || die "$1 仅用于 ensure 子命令:nao-fleet.sh ensure --card <卡路径>[@<repo>] [--task <编号>] [--as <会话名>]"; }
|
|
1726
2087
|
while [[ $# -gt 0 ]]; do
|
|
1727
2088
|
case "$1" in
|
|
1728
2089
|
check) CMD="check"; shift ;;
|
|
@@ -1731,6 +2092,25 @@ while [[ $# -gt 0 ]]; do
|
|
|
1731
2092
|
cleanup) CMD="cleanup"; shift ;;
|
|
1732
2093
|
-S) CLEANUP_SOCK="${2:-}"; [[ -n "$CLEANUP_SOCK" ]] || die "-S 需要socket路径参数"; shift 2 ;;
|
|
1733
2094
|
ensure) CMD="ensure"; shift ;;
|
|
2095
|
+
# T537 · --card:目标级修饰符,**绑定其后第一个 positional**(该 positional 即卡路径)
|
|
2096
|
+
--card)
|
|
2097
|
+
_need_ensure "--card"
|
|
2098
|
+
$PEND_CARD && die "--card 不能连写:每个 --card 只绑定其后的第一个目标(卡路径)"
|
|
2099
|
+
PEND_CARD=true; PEND_AS=""; shift ;;
|
|
2100
|
+
# T537 · --as:仅在同目标的 --card 之后合法(卡路径前或后皆可;重复即用法错)
|
|
2101
|
+
--as)
|
|
2102
|
+
_need_ensure "--as"
|
|
2103
|
+
[[ -z "$PEND_AS" ]] || die "--as 重复:每个目标只能指定一个会话名"
|
|
2104
|
+
PEND_AS="${2:-}"
|
|
2105
|
+
[[ -n "$PEND_AS" ]] || die "--as 需要会话名参数"
|
|
2106
|
+
if $PEND_CARD; then
|
|
2107
|
+
: # 卡路径尚未到来:挂在同目标上
|
|
2108
|
+
elif (( LAST_CARD_IDX >= 0 )) && [[ -z "${TARGET_AS[$LAST_CARD_IDX]:-}" ]]; then
|
|
2109
|
+
TARGET_AS[$LAST_CARD_IDX]="$PEND_AS"; PEND_AS=""
|
|
2110
|
+
else
|
|
2111
|
+
die "--as 只能紧跟同一目标的 --card(如 ensure --card <卡路径> --as <会话名>)"
|
|
2112
|
+
fi
|
|
2113
|
+
shift 2 ;;
|
|
1734
2114
|
-m|--model)
|
|
1735
2115
|
MODEL="${2:-}"
|
|
1736
2116
|
[[ -n "$MODEL" ]] || die "-m 需要模型参数"
|
|
@@ -1741,9 +2121,19 @@ while [[ $# -gt 0 ]]; do
|
|
|
1741
2121
|
--strict) STRICT=true; shift ;;
|
|
1742
2122
|
-v|--verbose) VERBOSE=true; shift ;;
|
|
1743
2123
|
-h|--help) usage ;;
|
|
1744
|
-
*)
|
|
2124
|
+
*)
|
|
2125
|
+
if $PEND_CARD; then
|
|
2126
|
+
TARGETS+=("$1"); TARGET_CARD+=("$1"); TARGET_AS+=("$PEND_AS")
|
|
2127
|
+
LAST_CARD_IDX=$(( ${#TARGETS[@]} - 1 )); PEND_CARD=false; PEND_AS=""
|
|
2128
|
+
else
|
|
2129
|
+
TARGETS+=("$1"); TARGET_CARD+=(""); TARGET_AS+=(""); LAST_CARD_IDX=-1
|
|
2130
|
+
fi
|
|
2131
|
+
shift ;;
|
|
1745
2132
|
esac
|
|
1746
2133
|
done
|
|
2134
|
+
# 悬挂修饰符:命令末尾缺目标 ⇒ 用法错(非 0 + 单行,照 -S 守卫先例;AC7②)
|
|
2135
|
+
if $PEND_CARD; then die "--card 后缺少卡路径参数(该怎么办:ensure --card .agents/prompts/<name>.md)"; fi
|
|
2136
|
+
if [[ -n "$PEND_AS" ]]; then die "--as 缺少同目标的 --card 与卡路径(该怎么办:ensure --card <卡路径> --as <会话名>)"; fi
|
|
1747
2137
|
|
|
1748
2138
|
# 唯一运行前提闸门(BR1/BR3):先于 load_manifest 与任何副作用;不满足 ⇒ exit 3(命令未执行)。
|
|
1749
2139
|
# cleanup 豁免:其正常场景就是非 tmux 的隔离实验场(env -u TMUX ... -S <socket>)。
|
|
@@ -1797,6 +2187,6 @@ case "$CMD" in
|
|
|
1797
2187
|
status) cmd_status ;;
|
|
1798
2188
|
close) cmd_close "$FORCE" "$TASK" "${TARGETS[0]:-}" ;;
|
|
1799
2189
|
cleanup) cmd_cleanup ;;
|
|
1800
|
-
ensure) cmd_ensure "$FORCE" "$MODEL" "$TASK"
|
|
2190
|
+
ensure) cmd_ensure "$FORCE" "$MODEL" "$TASK" ;;
|
|
1801
2191
|
*) usage ;;
|
|
1802
2192
|
esac
|
package/README.md
CHANGED
|
@@ -91,6 +91,23 @@ PM:在 PR 核 AC 并评论 → 验收通过(授权合并)→ rd-be squash
|
|
|
91
91
|
|
|
92
92
|
不想要哪个角色,删掉对应文件即可;只留 PM + 一个研发角色也能跑。
|
|
93
93
|
|
|
94
|
+
## 按需角色会话(用你自己仓里的角色卡)
|
|
95
|
+
|
|
96
|
+
常驻的 6 个角色不用改清单就能再加一个**按需角色**(Code Review、数据核对之类):把卡放进你的项目仓 `.agents/prompts/<名字>.md`,一条命令拉起,它就和常驻角色一样被管起来——分屏托管、`status` 看得到、`close` 收得回——但**不写进 `roles.yaml`,也不算常驻**。
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
.agents/scripts/nao-fleet.sh ensure --card .agents/prompts/code-reviewer.md --task T537 # 拉起一次性 Code Review 会话
|
|
100
|
+
.agents/scripts/nao-fleet.sh status # 派生节里能看到 code-reviewer-T537
|
|
101
|
+
.agents/scripts/nao-fleet.sh close --task T537 code-reviewer # 用完直接回收
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
卡的要求很直白:必须在**当前仓**的 `.agents/prompts/` 里、是普通 `.md` 文件(≤256 KiB),frontmatter 有 `role`(缺省用文件名)和 `description`。`role` 是会话名的来源,所以不能与常驻角色撞名或抢前缀(`qa`、`qa-audit` 这类都会被拒,只能改卡内 `role`),也不能用 `subagent-` / `nao-` 开头;`--as` 只能**在同一张卡内区分会话**,名字必须等于卡 `role` 或以 `<role>-` 为前缀(`--as code-reviewer-notes` ✓,`--as audit` ✗ —— 舰队按前缀识别身份,改名脱离前缀就等于「起得来但管不到」)。写错了不用猜:失败信息都是**一行原因 + 该怎么办**,`check` 也会在「下游角色卡」节提前指出(内容类只提醒、机制类直接红)。
|
|
105
|
+
|
|
106
|
+
两点边界值得知道:
|
|
107
|
+
|
|
108
|
+
- **`--card` 只作用于紧跟它的那个目标**:`ensure qa --card x.md` 是「常驻 `qa`」+「卡 `x.md`」两个会话,不会整批串味;命令末尾悬挂 `--card`、或把它递给 `check`/`status`/`close`,都按用法错误处理。
|
|
109
|
+
- **卡正文就是系统提示**,所以它是**完全可信输入**(能写你仓库的人本来就能定义角色);fleet 只校验目录、文件类型、大小与命名的一致性,不做内容安全审查。卡同样可以被 pi-subagents 当项目 agent 用(加 `name:` 即可),但那条路径没有「环境锚点」行,卡正文别依赖 `@.agents/...` 引用才自洽。若你已经在用 overlay 清单(`NAO_SKILLS` 指向自己的清单目录),同一仓别两法并用。
|
|
110
|
+
|
|
94
111
|
## 为什么它省 token(这是本项目的核心追求)
|
|
95
112
|
|
|
96
113
|
和 AI 聊天,钱花在「它每轮要读多少字」。所以:
|