@nathan33/nao-skill 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/checklists/architecture-designer.md +6 -2
- package/.agents/checklists/comm-templates.md +38 -0
- package/.agents/checklists/deliverable-docs.md +86 -0
- package/.agents/checklists/pm.md +40 -2
- package/.agents/checklists/qa.md +4 -1
- package/.agents/checklists/rd-be.md +4 -1
- package/.agents/checklists/rd-fe.md +4 -1
- package/.agents/common/intercom-protocol.md +30 -7
- package/.agents/common/output-format.md +10 -23
- package/.agents/prompts/architecture-designer.md +23 -10
- package/.agents/prompts/backend-developer.md +14 -5
- package/.agents/prompts/frontend-developer.md +14 -5
- package/.agents/prompts/product-manager.md +83 -44
- package/.agents/prompts/test-engineer.md +10 -4
- package/.agents/scripts/nao-fleet.sh +405 -18
- package/.agents/skills/arch-patterns.md +29 -9
- package/.agents/skills/commit.md +56 -6
- package/.agents/skills/github-flow.md +82 -0
- package/.agents/skills/research.md +70 -0
- package/.agents/skills/test-design.md +6 -0
- package/.agents/templates/AGENTS.md.example +4 -0
- package/.agents/templates/github/pull_request_template.md.example +28 -0
- package/.agents/templates/tasks-state.md.example +35 -8
- package/README.md +77 -162
- package/package.json +1 -1
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
# nao-fleet.sh — 按角色一键拉起 pi 会话窗口(nao 团队工具箱)
|
|
4
4
|
#
|
|
5
5
|
# 用法
|
|
6
|
-
# nao-fleet.sh check [--strict]
|
|
7
|
-
#
|
|
6
|
+
# nao-fleet.sh check [--strict] [-v] 静态体检:roles.yaml/缩进/EOL/角色卡/交叉引用/PR 模板/白名单/布局
|
|
7
|
+
# 默认单行摘要(含 warn 计数);-v 展开完整报告;失败始终展开
|
|
8
|
+
# nao-fleet.sh status 角色会话在线状态(按终端标题/名册判定;权威名单见 intercom list)
|
|
8
9
|
# nao-fleet.sh ensure <别名>[@<repo>] [更多...] 拉起角色窗口(默认工作区=roles.yaml workspace)
|
|
9
10
|
# nao-fleet.sh ensure -m <model> <别名>... 显式指定模型(须命中白名单)
|
|
10
11
|
# nao-fleet.sh ensure --task <编号> <别名>[@<repo>] 任务派生会话:--name <别名>-<编号>(并行隔离,避免同名冲突)
|
|
11
12
|
# nao-fleet.sh ensure --force <别名>... 忽略"已在运行"判重
|
|
13
|
+
# nao-fleet.sh close <别名|会话名> [--task <编号>] [--force]
|
|
14
|
+
# 回收已完成会话(闸门:在跑 turn / tasks-state 未推进 → 拒绝,--force 跳过)
|
|
12
15
|
#
|
|
13
16
|
# 角色别名 → 角色卡:见 .agents/roles.yaml(单一事实来源)
|
|
14
17
|
# 当前:pm / arch-designer(arch) / rd-fe / rd-be / qa
|
|
@@ -16,9 +19,18 @@
|
|
|
16
19
|
# 环境变量
|
|
17
20
|
# NAO_TERMINAL=ghostty|ptyxis|tmux|screen 强制宿主
|
|
18
21
|
# NAO_TMUX_LAYOUT=main-row2|grid tmux 布局(默认 main-row2)
|
|
19
|
-
# NAO_TMUX_MAIN_WIDTH=<10..90> main-row2 主 pane 宽度百分比(默认
|
|
22
|
+
# NAO_TMUX_MAIN_WIDTH=<10..90> main-row2 主 pane 宽度百分比(默认 35)
|
|
20
23
|
# NAO_SKILLS=<dir> 角色卡根目录(默认 <脚本>/../..)
|
|
21
24
|
# NAO_MODEL_WHITELIST=<glob,...> -m 白名单(默认空=不校验,支持 glob)
|
|
25
|
+
# NAO_CLOSE_BUSY_PATTERN=<ERE> close 的在跑 turn 判定正则(默认内置 pi 状态行标记)
|
|
26
|
+
# NAO_TASKS_STATE=<path> 任务状态文件(默认 docs/tasks-state.md,供残留检测/回收闸门)
|
|
27
|
+
#
|
|
28
|
+
# 在线判定(status / close / ensure 判重共用)
|
|
29
|
+
# pi 启动后用 OSC 0 把终端标题设为 "π - <会话名> - <cwd basename>" 并改写 argv
|
|
30
|
+
# (/proc/<pid>/cmdline 只剩 "pi")⇒ 判定一律面向「终端标题 / pi-intercom 名册 /
|
|
31
|
+
# tmux·screen 会话名」,不使用 `pgrep --name`(历史假阴性根因)。
|
|
32
|
+
# 名册经 $PI_AGENT_DIR/npm/node_modules/pi-intercom/cli.ts(一次调用、进程内缓存),
|
|
33
|
+
# 不可用时回退 tmux/screen;仍无法确认「不在线」时 close 不做静默 no-op(exit 非 0)。
|
|
22
34
|
#
|
|
23
35
|
# tmux 宿主行为
|
|
24
36
|
# - 已在 tmux 内($TMUX 存在):当前窗口分屏拉起,不新建窗口。
|
|
@@ -42,7 +54,15 @@ ROLE_ORDER=()
|
|
|
42
54
|
declare -A ROLE_CARDS ROLE_WS ALIAS_ROLE
|
|
43
55
|
MODEL_WHITELIST="${NAO_MODEL_WHITELIST:-}"
|
|
44
56
|
TMUX_LAYOUT="${NAO_TMUX_LAYOUT:-main-row2}"
|
|
45
|
-
|
|
57
|
+
# main-row2 主 pane 宽度百分比:默认值与非法值回退共用同一常量(防三处漂移)
|
|
58
|
+
TMUX_MAIN_WIDTH_DEFAULT=35
|
|
59
|
+
TMUX_MAIN_WIDTH="${NAO_TMUX_MAIN_WIDTH:-$TMUX_MAIN_WIDTH_DEFAULT}"
|
|
60
|
+
# check 输出契约:默认单行摘要(省 PM 上下文),-v 展开完整报告;失败始终展开
|
|
61
|
+
VERBOSE=false
|
|
62
|
+
# close 的在跑 turn 判定(pi 默认状态行:Working (esc to interrupt) / Thinking... / Retrying / Compacting)
|
|
63
|
+
CLOSE_BUSY_PATTERN="${NAO_CLOSE_BUSY_PATTERN:-Working \(|Thinking\.\.\.|to interrupt|to cancel|Retrying \(|Compacting|Summarizing branch}"
|
|
64
|
+
# 任务状态文件(相对当前目录;供残留检测与回收闸门)
|
|
65
|
+
TASKS_STATE="${NAO_TASKS_STATE:-docs/tasks-state.md}"
|
|
46
66
|
|
|
47
67
|
log() { printf '\033[1;32m[fleet]\033[0m %s\n' "$*"; }
|
|
48
68
|
warn() { printf '\033[1;33m[fleet]\033[0m %s\n' "$*" >&2; }
|
|
@@ -135,6 +155,43 @@ check_cross_refs() {
|
|
|
135
155
|
return $rc
|
|
136
156
|
}
|
|
137
157
|
|
|
158
|
+
# .agents/** 文本文件 EOL 契约:必须全为 LF(出现 CR 即 fail,列出文件)
|
|
159
|
+
# 排除 .nao-obsolete/(update 的旧版备份,EOL 不作为契约)
|
|
160
|
+
check_eol() {
|
|
161
|
+
local rc=0 n=0 f
|
|
162
|
+
local -a bad=()
|
|
163
|
+
while IFS= read -r -d '' f; do
|
|
164
|
+
n=$((n + 1))
|
|
165
|
+
grep -Iq $'\r' "$f" && bad+=("${f#"$SKILLS_DIR"/}")
|
|
166
|
+
done < <(find "$SKILLS_DIR/.agents" -type f ! -path '*/.nao-obsolete/*' -print0)
|
|
167
|
+
if (( ${#bad[@]} == 0 )); then
|
|
168
|
+
printf ' ✓ %d 个文本文件全 LF\n' "$n"
|
|
169
|
+
else
|
|
170
|
+
rc=1
|
|
171
|
+
printf ' ✗ %d/%d 个文件含 CR(须转 LF;CRLF 会破坏 fleet/awk 解析):\n' "${#bad[@]}" "$n"
|
|
172
|
+
printf ' %s\n' "${bad[@]}"
|
|
173
|
+
fi
|
|
174
|
+
return $rc
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# roles.yaml 缩进契约:角色 id 2 空格 / 字段 4 空格 / 禁 Tab(与 load_manifest 解析器同契约,独立报行号)
|
|
178
|
+
check_roles_indent() {
|
|
179
|
+
awk '
|
|
180
|
+
BEGIN { in_roles=0; cur=0; bad=0 }
|
|
181
|
+
/^[[:space:]]*$/ { next }
|
|
182
|
+
/^[[:space:]]*#/ { next }
|
|
183
|
+
/\t/ { printf " ✗ 行 %d: 含 Tab(禁止 Tab 缩进)\n", FNR; bad=1; next }
|
|
184
|
+
!in_roles {
|
|
185
|
+
if ($0 ~ /^roles:[[:space:]]*$/) { in_roles=1; next }
|
|
186
|
+
printf " ✗ 行 %d: 顶层仅允许 roles:(得到: %s)\n", FNR, $0; bad=1; next
|
|
187
|
+
}
|
|
188
|
+
$0 ~ /^ [^ ][^:]*:[[:space:]]*$/ { cur=1; next }
|
|
189
|
+
cur && $0 ~ /^ [a-z_]+:[[:space:]]/ { next }
|
|
190
|
+
{ printf " ✗ 行 %d: 缩进/位置非法(角色 id 须 2 空格、字段须 4 空格且挂在角色下): %s\n", FNR, $0; bad=1 }
|
|
191
|
+
END { exit (bad ? 1 : 0) }
|
|
192
|
+
' "$MANIFEST"
|
|
193
|
+
}
|
|
194
|
+
|
|
138
195
|
# CodeGraph 索引健康(ensure 拉起前 / check 用;缺失或过期仅 warn,不阻塞)
|
|
139
196
|
check_codegraph() {
|
|
140
197
|
local repo="$1" name st
|
|
@@ -173,7 +230,169 @@ detect_host() {
|
|
|
173
230
|
echo screen
|
|
174
231
|
}
|
|
175
232
|
|
|
176
|
-
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
# 会话在线判定
|
|
235
|
+
#
|
|
236
|
+
# 历史假阴性根因:pi 启动后用 OSC 0 把终端标题设为
|
|
237
|
+
# "π - <会话名> - <cwd basename>"(pi 源码 updateTerminalTitle),
|
|
238
|
+
# 并同步改写 argv —— /proc/<pid>/cmdline 只剩 "pi",`pgrep -f --name` 恒失配,
|
|
239
|
+
# 于是 status/close 长期判「不在线」(close 退化为静默空转、exit 0)。
|
|
240
|
+
# 现改为按「终端标题契约」取句柄,argv 扫描一律不用:
|
|
241
|
+
# ① pi-intercom 名册(权威在线名单;跨 tmux/ghostty/ptyxis/screen 宿主)
|
|
242
|
+
# ② tmux pane_title(pi 自设;与 pane_start_command 是否为空无关)
|
|
243
|
+
# ③ tmux detached 会话 / screen 会话名 nao-<会话名>
|
|
244
|
+
# repo(可选):给出时要求标题 basename / 名册 cwd 一致,避免跨项目同名会话互串。
|
|
245
|
+
|
|
246
|
+
PI_AGENT_DIR="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
|
247
|
+
PI_TITLE="π"
|
|
248
|
+
_INTERCOM_JSON=""; _INTERCOM_PROBED=0
|
|
249
|
+
|
|
250
|
+
repo_abspath() { ( cd "$1" >/dev/null 2>&1 && pwd ) 2>/dev/null || true; }
|
|
251
|
+
repo_basename() {
|
|
252
|
+
local abs; abs="$(repo_abspath "$1")"
|
|
253
|
+
[[ -n "$abs" ]] && printf '%s\n' "${abs##*/}"
|
|
254
|
+
return 0
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# intercom 名册 JSON(一次调用、进程内缓存);不可用返回 1(不 die)
|
|
258
|
+
intercom_list_json() {
|
|
259
|
+
if (( _INTERCOM_PROBED )); then
|
|
260
|
+
[[ -n "$_INTERCOM_JSON" ]] && printf '%s' "$_INTERCOM_JSON"
|
|
261
|
+
return
|
|
262
|
+
fi
|
|
263
|
+
_INTERCOM_PROBED=1
|
|
264
|
+
local dir="$PI_AGENT_DIR/npm/node_modules/pi-intercom"
|
|
265
|
+
local tsx="$PI_AGENT_DIR/npm/node_modules/tsx/dist/cli.mjs"
|
|
266
|
+
[[ -f "$dir/cli.ts" && -f "$tsx" ]] || return 1
|
|
267
|
+
command -v node >/dev/null 2>&1 || return 1
|
|
268
|
+
local out
|
|
269
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
270
|
+
out="$(timeout 15 node "$tsx" "$dir/cli.ts" list --json 2>/dev/null)" || return 1
|
|
271
|
+
else
|
|
272
|
+
out="$(node "$tsx" "$dir/cli.ts" list --json 2>/dev/null)" || return 1
|
|
273
|
+
fi
|
|
274
|
+
[[ -n "$out" ]] || return 1
|
|
275
|
+
_INTERCOM_JSON="$out"
|
|
276
|
+
printf '%s' "$out"
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# 名册 → "会话名<TAB>cwd"(每行一条)
|
|
280
|
+
intercom_roster() {
|
|
281
|
+
local json; json="$(intercom_list_json)" || return 1
|
|
282
|
+
awk '
|
|
283
|
+
/"name":[[:space:]]*"/ { s=$0; sub(/.*"name":[[:space:]]*"/,"",s); sub(/".*/,"",s); n=s }
|
|
284
|
+
/"cwd":[[:space:]]*"/ { s=$0; sub(/.*"cwd":[[:space:]]*"/,"",s); sub(/".*/,"",s); print n "\t" s }
|
|
285
|
+
' <<< "$json"
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# 名册中是否在线;repo 给出时须 cwd 精确匹配
|
|
289
|
+
intercom_online() {
|
|
290
|
+
local name="$1" repo="${2:-}" abs="" n c
|
|
291
|
+
intercom_list_json >/dev/null 2>&1 || return 1
|
|
292
|
+
if [[ -n "$repo" ]]; then abs="$(repo_abspath "$repo")"; fi
|
|
293
|
+
while IFS=$'\t' read -r n c; do
|
|
294
|
+
[[ "$n" == "$name" ]] || continue
|
|
295
|
+
if [[ -z "$abs" || "$c" == "$abs" ]]; then return 0; fi
|
|
296
|
+
done < <(intercom_roster)
|
|
297
|
+
return 1
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# 会话所在的 tmux pane(pi 自设标题 "π - <会话名> - <repo basename>")
|
|
301
|
+
find_pane_for() {
|
|
302
|
+
local name="$1" repo="${2:-}" base=""
|
|
303
|
+
command -v tmux >/dev/null 2>&1 || return 0
|
|
304
|
+
if [[ -n "$repo" ]]; then base="$(repo_basename "$repo")"; fi
|
|
305
|
+
tmux list-panes -a -F "#{pane_id}"$'\t'"#{pane_title}" 2>/dev/null \
|
|
306
|
+
| awk -F'\t' -v p="$PI_TITLE" -v n="$name" -v b="$base" '
|
|
307
|
+
{ t=$2
|
|
308
|
+
if (b != "") { if (t == p " - " n " - " b) { print $1; exit } }
|
|
309
|
+
else if (index(t, p " - " n " - ") == 1) { print $1; exit }
|
|
310
|
+
}'
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# 该会话名对应的 pi 进程 PID(经 pane 句柄取 pane_pid 及其子进程中 comm=pi 者)
|
|
314
|
+
pi_pids_for() {
|
|
315
|
+
local pane root pid
|
|
316
|
+
pane="$(find_pane_for "$1" "${2:-}")"
|
|
317
|
+
[[ -n "$pane" ]] || return 0
|
|
318
|
+
root="$(tmux display-message -p -t "$pane" '#{pane_pid}' 2>/dev/null)"
|
|
319
|
+
[[ -n "$root" ]] || return 0
|
|
320
|
+
{ printf '%s\n' "$root"; pgrep -P "$root" 2>/dev/null || true; } | while IFS= read -r pid; do
|
|
321
|
+
[[ "$(ps -o comm= -p "$pid" 2>/dev/null | tr -d ' ')" == "pi" ]] || continue
|
|
322
|
+
printf '%s\n' "$pid"
|
|
323
|
+
done
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
running() {
|
|
327
|
+
local name="$1" repo="${2:-}"
|
|
328
|
+
if intercom_online "$name" "$repo"; then return 0; fi
|
|
329
|
+
if [[ -n "$(find_pane_for "$name" "$repo")" ]]; then return 0; fi
|
|
330
|
+
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "nao-$name" 2>/dev/null; then return 0; fi
|
|
331
|
+
if command -v screen >/dev/null 2>&1 && screen -ls 2>/dev/null | grep -q "[0-9]\.nao-$name\b"; then return 0; fi
|
|
332
|
+
return 1
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
# 能否可信地断言「不在线」:名册可用(完整),或本机就是 tmux/screen 宿主(句柄可枚举)
|
|
336
|
+
offline_verifiable() {
|
|
337
|
+
intercom_list_json >/dev/null 2>&1 && return 0
|
|
338
|
+
[[ -n "${TMUX:-}" ]] && return 0
|
|
339
|
+
case "${NAO_TERMINAL:-}" in tmux|screen) return 0 ;; esac
|
|
340
|
+
return 1
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# 本机可见的会话名(tmux 标题 + tmux/screen 会话 + 名册),供 status 枚举;已去重
|
|
344
|
+
visible_session_names() {
|
|
345
|
+
{ if command -v tmux >/dev/null 2>&1; then
|
|
346
|
+
tmux list-panes -a -F '#{pane_title}' 2>/dev/null \
|
|
347
|
+
| sed -n "s/^${PI_TITLE} - \([A-Za-z0-9_-]*\) - .*/\1/p"
|
|
348
|
+
tmux list-sessions -F '#{session_name}' 2>/dev/null | sed -n 's/^nao-//p'
|
|
349
|
+
fi
|
|
350
|
+
if command -v screen >/dev/null 2>&1; then
|
|
351
|
+
screen -ls 2>/dev/null | sed -n 's/.*[0-9]\.nao-\([A-Za-z0-9_-]*\).*/\1/p'
|
|
352
|
+
fi
|
|
353
|
+
intercom_roster 2>/dev/null | awk -F'\t' 'NF {print $1}'
|
|
354
|
+
} | awk '!seen[$0]++'
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# 该 pane 末 3 行是否显示在跑 turn(启发式:状态行标记;回执/产物以 PM 核对清单为准)
|
|
358
|
+
pane_busy() {
|
|
359
|
+
local pane="$1" tail3
|
|
360
|
+
tail3="$(tmux capture-pane -p -t "$pane" 2>/dev/null | grep -v '^[[:space:]]*$' | tail -3)"
|
|
361
|
+
[[ -n "$tail3" ]] || return 1
|
|
362
|
+
grep -qE "$CLOSE_BUSY_PATTERN" <<< "$tail3"
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
# 任务在 tasks-state 的归处:
|
|
366
|
+
# active(在表内进行态)/ closed(在归档区表内)/ mentioned(仅散文提及)
|
|
367
|
+
# / absent(全文无记录)/ nofile
|
|
368
|
+
# 归一处:表内单元格可选带反引号/**(常见写法 `T157` / **T157**)→ 去首尾空白与标记后比较。
|
|
369
|
+
# 标题级别不限(`##` 与 `###` 子表都算);归档区 = 标题含「已验收|已归档|归档」。
|
|
370
|
+
# 边界:先按非 [A-Za-z0-9_-] 切词再比较,避免 T15 命中 T155。
|
|
371
|
+
task_state_class() {
|
|
372
|
+
local id="$1"
|
|
373
|
+
[[ -f "$TASKS_STATE" ]] || { echo nofile; return; }
|
|
374
|
+
awk -v id="$id" '
|
|
375
|
+
function norm(s) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", s); gsub(/`/, "", s); gsub(/\*\*/, "", s); return s }
|
|
376
|
+
/^#+[[:space:]]/ { sec=$0; sub(/^#+[[:space:]]*/, "", sec); next }
|
|
377
|
+
{
|
|
378
|
+
n=split($0, cells, "|")
|
|
379
|
+
for (i=1;i<=n;i++) {
|
|
380
|
+
if (norm(cells[i]) != id) continue
|
|
381
|
+
if (sec ~ /已验收|已归档|归档/) arch=1; else cell=1
|
|
382
|
+
break
|
|
383
|
+
}
|
|
384
|
+
line=$0; gsub(/[^A-Za-z0-9_-]/, " ", line)
|
|
385
|
+
m=split(line, toks, " ")
|
|
386
|
+
for (j=1;j<=m;j++) if (toks[j]==id) { seen=1; break }
|
|
387
|
+
}
|
|
388
|
+
END {
|
|
389
|
+
if (arch) { print "closed"; exit }
|
|
390
|
+
if (cell) { print "active"; exit }
|
|
391
|
+
if (seen) { print "mentioned"; exit }
|
|
392
|
+
print "absent"
|
|
393
|
+
}
|
|
394
|
+
' "$TASKS_STATE"
|
|
395
|
+
}
|
|
177
396
|
|
|
178
397
|
# 白名单命中返回 0,否则返回 1;未设白名单=放行
|
|
179
398
|
check_model() {
|
|
@@ -296,7 +515,7 @@ apply_tmux_layout() {
|
|
|
296
515
|
(( ${#pane_ids[@]} >= 1 )) || return 0
|
|
297
516
|
|
|
298
517
|
local pct="$TMUX_MAIN_WIDTH"
|
|
299
|
-
[[ "$pct" =~ ^[0-9]+$ ]] || pct=
|
|
518
|
+
[[ "$pct" =~ ^[0-9]+$ ]] || pct="$TMUX_MAIN_WIDTH_DEFAULT"
|
|
300
519
|
(( pct < 10 )) && pct=10
|
|
301
520
|
(( pct > 90 )) && pct=90
|
|
302
521
|
|
|
@@ -375,7 +594,20 @@ cmd_check() {
|
|
|
375
594
|
else printf ' ✗ 缺失: %s\n' "$d"; rc=1; fi
|
|
376
595
|
done
|
|
377
596
|
|
|
597
|
+
echo "== 文本契约(roles.yaml 缩进 2/4 · .agents/** EOL 全 LF)=="
|
|
598
|
+
if [[ -f "$MANIFEST" ]]; then
|
|
599
|
+
if check_roles_indent; then
|
|
600
|
+
printf ' ✓ roles.yaml 缩进契约(角色 id 2 空格 / 字段 4 空格 / 无 Tab)\n'
|
|
601
|
+
else
|
|
602
|
+
rc=1
|
|
603
|
+
fi
|
|
604
|
+
else
|
|
605
|
+
printf ' ✗ roles.yaml 缺失: %s\n' "$MANIFEST"; rc=1
|
|
606
|
+
fi
|
|
607
|
+
check_eol || rc=1
|
|
608
|
+
|
|
378
609
|
echo "== 角色清单 roles.yaml =="
|
|
610
|
+
load_manifest
|
|
379
611
|
printf ' ✓ 解析成功,%d 个角色: %s\n' "${#ROLE_ORDER[@]}" "${ROLE_ORDER[*]}"
|
|
380
612
|
|
|
381
613
|
echo "== 常驻角色卡(阈值 ${CARD_MAX_LINES} 行,frontmatter 校验)=="
|
|
@@ -424,6 +656,33 @@ cmd_check() {
|
|
|
424
656
|
else printf ' ✗ %s 缺失\n' "$f"; rc=1; fi
|
|
425
657
|
done
|
|
426
658
|
|
|
659
|
+
echo "== PR 模板(GitHub Flow)=="
|
|
660
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
661
|
+
echo ' · 非 git 仓库(跳过)'
|
|
662
|
+
else
|
|
663
|
+
local remote_url prtpl="" cand
|
|
664
|
+
remote_url="$(git config --get remote.origin.url 2>/dev/null || true)"
|
|
665
|
+
if [[ -z "$remote_url" ]]; then
|
|
666
|
+
echo ' · 无 remote.origin(跳过)'
|
|
667
|
+
elif [[ "$remote_url" != *github.com* ]]; then
|
|
668
|
+
echo ' · 远端非 GitHub(跳过;PR 模板不适用)'
|
|
669
|
+
else
|
|
670
|
+
shopt -s nullglob
|
|
671
|
+
for cand in .github/pull_request_template.md .github/PULL_REQUEST_TEMPLATE.md \
|
|
672
|
+
.github/PULL_REQUEST_TEMPLATE \
|
|
673
|
+
pull_request_template.md PULL_REQUEST_TEMPLATE.md \
|
|
674
|
+
docs/pull_request_template.md docs/PULL_REQUEST_TEMPLATE.md; do
|
|
675
|
+
[[ -e "$cand" ]] && { prtpl="$cand"; break; }
|
|
676
|
+
done
|
|
677
|
+
shopt -u nullglob
|
|
678
|
+
if [[ -n "$prtpl" ]]; then
|
|
679
|
+
printf ' ✓ PR 模板已就位:%s\n' "$prtpl"
|
|
680
|
+
else
|
|
681
|
+
echo ' ! 未找到 PR 模板(GitHub 远端):PM 立项时从 .agents/templates/github/pull_request_template.md.example 复制为 .github/pull_request_template.md(见 skills/github-flow.md)'
|
|
682
|
+
fi
|
|
683
|
+
fi
|
|
684
|
+
fi
|
|
685
|
+
|
|
427
686
|
echo "== 按需技能 =="
|
|
428
687
|
if [[ -d "$SKILLS_SUB" ]]; then
|
|
429
688
|
shopt -s nullglob
|
|
@@ -453,6 +712,18 @@ cmd_check() {
|
|
|
453
712
|
echo ' · codegraph 未安装(回退 grep 属预期行为)'
|
|
454
713
|
fi
|
|
455
714
|
|
|
715
|
+
echo "== pi 插件(业界调研能力)=="
|
|
716
|
+
local piset="$HOME/.pi/agent/settings.json"
|
|
717
|
+
if [[ -f "$piset" ]]; then
|
|
718
|
+
if grep -q 'pi-web-access' "$piset" 2>/dev/null; then
|
|
719
|
+
echo ' · pi-web-access ✓ 已装(PM/arch 业界调研可用)'
|
|
720
|
+
else
|
|
721
|
+
echo ' ! pi-web-access 未装(业界调研不可用;nao-skill plugins install web-access)'
|
|
722
|
+
fi
|
|
723
|
+
else
|
|
724
|
+
echo " · 未找到 $piset(跳过插件检测)"
|
|
725
|
+
fi
|
|
726
|
+
|
|
456
727
|
echo "== 模型白名单 =="
|
|
457
728
|
if [[ -z "$MODEL_WHITELIST" ]]; then
|
|
458
729
|
echo " (未设置 NAO_MODEL_WHITELIST,-m 不校验)"
|
|
@@ -498,7 +769,7 @@ cmd_check() {
|
|
|
498
769
|
if [[ "$TMUX_MAIN_WIDTH" =~ ^[0-9]+$ ]] && (( TMUX_MAIN_WIDTH >= 10 && TMUX_MAIN_WIDTH <= 90 )); then
|
|
499
770
|
printf ' ✓ NAO_TMUX_MAIN_WIDTH=%-3s%% 合法\n' "$TMUX_MAIN_WIDTH"
|
|
500
771
|
else
|
|
501
|
-
printf ' ! NAO_TMUX_MAIN_WIDTH=%-3s 非法(10..90
|
|
772
|
+
printf ' ! NAO_TMUX_MAIN_WIDTH=%-3s 非法(10..90):越界夹取到 10/90,非数字回退 %s\n' "$TMUX_MAIN_WIDTH" "$TMUX_MAIN_WIDTH_DEFAULT"
|
|
502
773
|
fi
|
|
503
774
|
fi
|
|
504
775
|
|
|
@@ -507,26 +778,114 @@ cmd_check() {
|
|
|
507
778
|
rc=1
|
|
508
779
|
fi
|
|
509
780
|
|
|
510
|
-
|
|
781
|
+
return $rc
|
|
511
782
|
}
|
|
512
783
|
|
|
513
784
|
# ---------------------------------------------------------------------------
|
|
514
785
|
cmd_status() {
|
|
515
786
|
local a
|
|
516
|
-
|
|
787
|
+
intercom_list_json >/dev/null 2>&1 || true # 预热名册缓存(后续子 shell 复用)
|
|
788
|
+
echo "== 角色会话在线状态(权威名单见 intercom({action:'list'});本表按终端标题/名册判定)=="
|
|
517
789
|
for a in "${ROLE_ORDER[@]}"; do
|
|
518
|
-
if running "$a"; then
|
|
790
|
+
if running "$a" "${ROLE_WS[$a]:-}"; then
|
|
519
791
|
printf ' ✓ %-14s 在线(--name %s;卡片 %s)\n' "$a" "$a" "${ROLE_CARDS[$a]}"
|
|
520
792
|
else
|
|
521
793
|
printf ' · %-14s 未运行(ensure 拉起)\n' "$a"
|
|
522
794
|
fi
|
|
523
795
|
done
|
|
524
796
|
echo "== 任务派生会话(--task 拉起,如 rd-be-T1)=="
|
|
525
|
-
local found=0
|
|
526
|
-
while IFS= read -r
|
|
527
|
-
[[ -n "$
|
|
528
|
-
|
|
797
|
+
local found=0 dname role id cls tag
|
|
798
|
+
while IFS= read -r dname; do
|
|
799
|
+
[[ -n "$dname" ]] || continue
|
|
800
|
+
id=""
|
|
801
|
+
for role in "${ROLE_ORDER[@]}"; do [[ "$dname" == "$role-"* ]] && { id="${dname#"$role"-}"; break; }; done
|
|
802
|
+
[[ -n "$id" ]] || continue
|
|
803
|
+
cls="$(task_state_class "$id")"
|
|
804
|
+
tag=""
|
|
805
|
+
case "$cls" in
|
|
806
|
+
closed) tag=" ! 残留($id 已归档)→ close --task $id ${dname%-*}" ;;
|
|
807
|
+
active) tag=" · $id 进行态" ;;
|
|
808
|
+
mentioned) tag=" ? $id 仅散文提及(非表内记录)→ 核对后 close --task $id ${dname%-*}" ;;
|
|
809
|
+
absent) tag=" ! 残留(tasks-state 无 $id 记录)→ 核对后 close --task $id ${dname%-*}" ;;
|
|
810
|
+
esac
|
|
811
|
+
printf ' · %s%s\n' "$dname" "$tag"
|
|
812
|
+
found=1
|
|
813
|
+
done < <(visible_session_names)
|
|
529
814
|
(( found )) || echo ' (无)'
|
|
815
|
+
[[ -f "$TASKS_STATE" ]] || echo " ($TASKS_STATE 不存在,残留判定已跳过)"
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
# ---------------------------------------------------------------------------
|
|
819
|
+
# 回收已完成会话:闸门(tasks-state 已推进 + 无在跑 turn)+ 落地(pane/会话/screen/进程)
|
|
820
|
+
cmd_close() {
|
|
821
|
+
local force="$1" task="$2" target="$3"
|
|
822
|
+
[[ -n "$target" ]] || die "close 需要目标:角色别名或派生会话名(如 rd-be / rd-be-T1)"
|
|
823
|
+
local name repo cls bus pane pids r ok
|
|
824
|
+
intercom_list_json >/dev/null 2>&1 || true # 预热名册缓存
|
|
825
|
+
if [[ -n "$task" ]]; then
|
|
826
|
+
resolve_role "$target"
|
|
827
|
+
name="${NAME}-${task}"
|
|
828
|
+
repo="${ROLE_WS[$NAME]:-$PWD}"
|
|
829
|
+
# 闸门①:tasks-state 未推进 → 拒绝(验收未过需原会话返工,回收会丢上下文)
|
|
830
|
+
cls="$(task_state_class "$task")"
|
|
831
|
+
if [[ "$cls" == "active" && "$force" != true ]]; then
|
|
832
|
+
warn "拒绝回收 $name:tasks-state 中 $task 仍在进行态(未验收/未归档)"
|
|
833
|
+
warn " 先把 $task 移入「已验收/已归档」再回收(或 --force:将丢失打回返工所需上下文)"
|
|
834
|
+
return 1
|
|
835
|
+
fi
|
|
836
|
+
[[ "$cls" == "absent" ]] && warn "tasks-state 全文无 $task 记录(仅按会话名回收)"
|
|
837
|
+
[[ "$cls" == "mentioned" ]] && log "tasks-state 仅散文提及 $task(非表内记录,不阻塞回收)"
|
|
838
|
+
[[ "$cls" == "nofile" ]] && log "未启用 $TASKS_STATE(跳过状态闸门)"
|
|
839
|
+
elif [[ -n "${ALIAS_ROLE[$target]:-}" ]]; then
|
|
840
|
+
name="${ALIAS_ROLE[$target]}"; repo="${ROLE_WS[$name]:-$PWD}"
|
|
841
|
+
warn "常驻会话 $name:任务闭环后应 ensure --force 重开,而非回收(仅在本批不再需要该角色时回收)"
|
|
842
|
+
else
|
|
843
|
+
name="$target"
|
|
844
|
+
[[ "$name" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]] || die "非法会话名: $name"
|
|
845
|
+
# 非别名的目标必须是 <已知角色>-<编号>,否则视为拼错(防静默 no-op)
|
|
846
|
+
ok=0
|
|
847
|
+
for r in "${ROLE_ORDER[@]}"; do [[ "$name" == "$r-"* ]] && ok=1; done
|
|
848
|
+
(( ok )) || die "未知角色或派生会话名: $name(可用: ${ROLE_ORDER[*]};派生名形如 rd-be-T1)"
|
|
849
|
+
repo="$PWD"
|
|
850
|
+
fi
|
|
851
|
+
|
|
852
|
+
if ! running "$name" "$repo"; then
|
|
853
|
+
if offline_verifiable; then
|
|
854
|
+
log "$name 未运行(无需回收)"; return 0
|
|
855
|
+
fi
|
|
856
|
+
warn "无法确认 $name 在线状态:宿主无 tmux/screen 句柄,且 pi-intercom 名册不可用"
|
|
857
|
+
warn " pi 启动后 argv 被改写为 \"pi\",进程扫描不可信 ⇒ 不做静默 no-op(本命令 exit 非 0)"
|
|
858
|
+
warn " 请用 intercom list 人工核对;确认已退出后忽略本提示,或用 --force 跳过闸门"
|
|
859
|
+
return 1
|
|
860
|
+
fi
|
|
861
|
+
|
|
862
|
+
# 闸门②:在跑 turn(tmux 可判;非 tmux 宿主无法判 → 需 --force)
|
|
863
|
+
pane="$(find_pane_for "$name" "$repo")"
|
|
864
|
+
if [[ -n "$pane" ]]; then
|
|
865
|
+
pane_busy "$pane" && bus="tmux pane $pane 末行显示在跑 turn"
|
|
866
|
+
else
|
|
867
|
+
bus="非 tmux 宿主,无法确认是否在跑 turn"
|
|
868
|
+
fi
|
|
869
|
+
if [[ -n "$bus" && "$force" != true ]]; then
|
|
870
|
+
warn "拒绝回收 $name:$bus"
|
|
871
|
+
warn " 等它停;回执与产物核对见 checklists/pm.md「会话回收 / 收窗核对」(或 --force 强制)"
|
|
872
|
+
return 1
|
|
873
|
+
fi
|
|
874
|
+
|
|
875
|
+
if [[ -n "$pane" ]] && tmux kill-pane -t "$pane" 2>/dev/null; then
|
|
876
|
+
log "已回收 $name(tmux pane $pane)@ $repo"; return 0
|
|
877
|
+
fi
|
|
878
|
+
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "nao-$name" 2>/dev/null && tmux kill-session -t "nao-$name" 2>/dev/null; then
|
|
879
|
+
log "已回收 $name(tmux 会话 nao-$name)@ $repo"; return 0
|
|
880
|
+
fi
|
|
881
|
+
if command -v screen >/dev/null 2>&1 && screen -ls 2>/dev/null | grep -q "nao-$name" && screen -S "nao-$name" -X quit 2>/dev/null; then
|
|
882
|
+
log "已回收 $name(screen 会话 nao-$name)@ $repo"; return 0
|
|
883
|
+
fi
|
|
884
|
+
pids="$(pi_pids_for "$name" "$repo" | tr '\n' ' ')"
|
|
885
|
+
if [[ -n "$pids" ]] && kill $pids 2>/dev/null; then
|
|
886
|
+
log "已回收 $name(结束进程: $pids)@ $repo"; return 0
|
|
887
|
+
fi
|
|
888
|
+
warn "未找到 $name 的 pane/会话/进程(可能刚好退出)"; return 1
|
|
530
889
|
}
|
|
531
890
|
|
|
532
891
|
# ---------------------------------------------------------------------------
|
|
@@ -550,8 +909,8 @@ cmd_ensure() {
|
|
|
550
909
|
key="$repo"; seen=0
|
|
551
910
|
for k in "${cg_done[@]:-}"; do [[ "$k" == "$key" ]] && seen=1; done
|
|
552
911
|
if (( ! seen )); then check_codegraph "$repo"; cg_done+=("$key"); fi
|
|
553
|
-
if [[ "$force" != "true" ]] && running "$disp"; then
|
|
554
|
-
warn "$disp
|
|
912
|
+
if [[ "$force" != "true" ]] && running "$disp" "$repo"; then
|
|
913
|
+
warn "$disp 已在运行(终端标题/名册命中),跳过;确需重开请加 --force"
|
|
555
914
|
continue
|
|
556
915
|
fi
|
|
557
916
|
spawn_one "$disp" "$repo" "$model"
|
|
@@ -573,6 +932,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
573
932
|
case "$1" in
|
|
574
933
|
check) CMD="check"; shift ;;
|
|
575
934
|
status) CMD="status"; shift ;;
|
|
935
|
+
close) CMD="close"; shift ;;
|
|
576
936
|
ensure) CMD="ensure"; shift ;;
|
|
577
937
|
-m|--model)
|
|
578
938
|
MODEL="${2:-}"
|
|
@@ -582,18 +942,45 @@ while [[ $# -gt 0 ]]; do
|
|
|
582
942
|
--force) FORCE=true; shift ;;
|
|
583
943
|
--task) TASK="${2:-}"; [[ -n "$TASK" ]] || die "--task 需要任务编号(如 T1)"; [[ "$TASK" =~ ^[A-Za-z0-9_-]+$ ]] || die "--task 非法: $TASK(仅字母/数字/-/_)"; shift 2 ;;
|
|
584
944
|
--strict) STRICT=true; shift ;;
|
|
945
|
+
-v|--verbose) VERBOSE=true; shift ;;
|
|
585
946
|
-h|--help) usage ;;
|
|
586
947
|
*) TARGETS+=("$1"); shift ;;
|
|
587
948
|
esac
|
|
588
949
|
done
|
|
589
950
|
|
|
590
951
|
case "$CMD" in
|
|
591
|
-
check
|
|
952
|
+
check) : ;; # cmd_check 自行先做文本契约体检,再 load_manifest(缩进违例时也能先出报告)
|
|
953
|
+
ensure|status|close) load_manifest ;;
|
|
592
954
|
esac
|
|
593
955
|
|
|
594
956
|
case "$CMD" in
|
|
595
|
-
check)
|
|
957
|
+
check)
|
|
958
|
+
# 默认单行摘要(派发前自检只需 exit code + 计数;完整报告 30+ 行不进 PM 上下文);-v 或失败时展开
|
|
959
|
+
# 子 shell 隔离:cmd_check 内部 die/exit 不得吞掉报告(计数从报告解析,不靠子 shell 内变量)
|
|
960
|
+
REPFILE="$(mktemp)"
|
|
961
|
+
set +e
|
|
962
|
+
( cmd_check "$STRICT" ) >"$REPFILE" 2>&1
|
|
963
|
+
CHECK_RC=$?
|
|
964
|
+
set -e
|
|
965
|
+
if (( CHECK_RC == 0 )) && ! $VERBOSE; then
|
|
966
|
+
WARNS="$(grep -c '^ !' "$REPFILE" || true)"
|
|
967
|
+
ROLES_N="$(grep -oE '解析成功,[0-9]+ 个角色' "$REPFILE" | grep -oE '[0-9]+' | head -1 || true)"
|
|
968
|
+
FILES_N="$(grep -oE '[0-9]+ 个文本文件全 LF' "$REPFILE" | grep -oE '^[0-9]+' | head -1 || true)"
|
|
969
|
+
if (( WARNS > 0 )); then
|
|
970
|
+
printf 'check: OK · roles=%s · files=%s · layout=%s · warn=%d(-v 看详情)\n' \
|
|
971
|
+
"${ROLES_N:-?}" "${FILES_N:-?}" "$TMUX_LAYOUT" "$WARNS"
|
|
972
|
+
else
|
|
973
|
+
printf 'check: OK · roles=%s · files=%s · layout=%s\n' \
|
|
974
|
+
"${ROLES_N:-?}" "${FILES_N:-?}" "$TMUX_LAYOUT"
|
|
975
|
+
fi
|
|
976
|
+
else
|
|
977
|
+
cat "$REPFILE"
|
|
978
|
+
fi
|
|
979
|
+
rm -f "$REPFILE"
|
|
980
|
+
exit $CHECK_RC
|
|
981
|
+
;;
|
|
596
982
|
status) cmd_status ;;
|
|
983
|
+
close) cmd_close "$FORCE" "$TASK" "${TARGETS[0]:-}" ;;
|
|
597
984
|
ensure) cmd_ensure "$FORCE" "$MODEL" "$TASK" "${TARGETS[@]}" ;;
|
|
598
985
|
*) usage ;;
|
|
599
986
|
esac
|
|
@@ -2,14 +2,30 @@
|
|
|
2
2
|
description: 按需技能——架构模式速查 + ADR 模板
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# 架构模式对照与 ADR
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
|
7
|
+
> 用法:先写清约束,再**对照**候选模式;禁止直接接表选型(同场景不同团队/规模,结论相反)。
|
|
8
|
+
> 检索与引用纪律见 @.agents/skills/research.md(报告落盘 `docs/research/`)。
|
|
9
|
+
|
|
10
|
+
## 模式对照表(场景信号 → 候选 → 适用条件 → 代价 → 何时别用)
|
|
11
|
+
|
|
12
|
+
| 场景信号 | 候选模式 | 适用条件 | 代价 / 风险 | 何时别用 |
|
|
13
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
14
|
+
| 单团队、领域未定、读写同源 | 模块化单体 | 团队 <10 人、交付速度优先、边界待验证 | 后期拆分成本;模块纪律靠评审守 | 多团队并行交付、需独立扩缩容 |
|
|
15
|
+
| 读远多于写、查询形态多 | CQRS | 读写比悬殊、需要多种读模型 | 双模型同步、最终一致、运维面变大 | 以强一致读为主、数据量小 |
|
|
16
|
+
| 需完整审计与回放 | 事件溯源 | 合规/审计/时间旅行需求真实存在 | 事件版本迁移、查询复杂、删除权难处理 | 普通 CRUD、无审计需求 |
|
|
17
|
+
| 跨服务/跨库的事务 | Saga / Outbox | 无法用单库事务、需要可靠消息 | 补偿逻辑、幂等设计、调试难度 | 单库事务可覆盖 |
|
|
18
|
+
| 峰值差大、多方消费 | 消息队列 / 事件驱动 | 需削峰或解耦、消费方 ≥2 | 运维成本、顺序与幂等、可观测性 | 请求-响应强耦合、无峰值 |
|
|
19
|
+
| 多租户 SaaS | schema / RLS / 库级隔离 | 按租户数 + 隔离要求分档选 | 迁移复杂、噪声邻居、连接池压力 | 少数强隔离租户(该独立部署) |
|
|
20
|
+
| 多端体验差异大 | BFF | 端形态(Web/移动/第三方)差异成主要成本 | 多一层维护与部署 | 单端产品 |
|
|
21
|
+
| 依赖不稳的三方集成 | 网关 + 熔断 + 退避 | 外部依赖有抖动历史、影响主链路 | 复杂度、配置治理 | 内部可信调用 |
|
|
22
|
+
|
|
23
|
+
## 对照四步法(写入 ADR 前必做)
|
|
24
|
+
|
|
25
|
+
1. **写清约束**:NFR 基线、团队规模与熟悉度、预算与工期、迁移成本、既有约束(`AGENTS.md`)。
|
|
26
|
+
2. **列 ≥2 个候选模式**(含"维持现状/最小改动"这个基线候选)。
|
|
27
|
+
3. **逐条打分**:适用条件是否成立 → 代价能否承受 → 团队能否维护。
|
|
28
|
+
4. **记录不采纳理由**:写进 ADR(不采纳的原因比采纳理由更有审计价值)。
|
|
13
29
|
|
|
14
30
|
## 最小示例
|
|
15
31
|
|
|
@@ -31,12 +47,16 @@ graph TD
|
|
|
31
47
|
- 状态:提议 / 已接受 / 已废弃
|
|
32
48
|
- 背景:<问题与约束>
|
|
33
49
|
- 决策:<选择了什么>
|
|
34
|
-
-
|
|
50
|
+
- 理由:<对照四步法分析>
|
|
51
|
+
- 对照过的备选模式:<模式 → 不采纳原因(含"维持现状"基线)>
|
|
52
|
+
- 参考来源:<[标题](链接) · 访问 YYYY-MM-DD;无则写"未检索到">
|
|
35
53
|
- 备选方案:<列表 + 否决理由>
|
|
36
54
|
- 后果:<正面 / 负面 / 风险>
|
|
37
|
-
- 相关:<PRD / 关联 ADR
|
|
55
|
+
- 相关:<PRD / 关联 ADR / docs/research 路径>
|
|
38
56
|
```
|
|
39
57
|
|
|
58
|
+
> 长度上限 60 行、骨架与必写/可省/禁写见 @.agents/checklists/deliverable-docs.md §四。
|
|
59
|
+
|
|
40
60
|
## 误区
|
|
41
61
|
|
|
42
62
|
- 过度设计 / 追新 → 务实、成熟稳定优先
|