@nathan33/nao-skill 0.1.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.
Files changed (32) hide show
  1. package/.agents/checklists/architecture-designer.md +42 -0
  2. package/.agents/checklists/pm.md +64 -0
  3. package/.agents/checklists/qa.md +36 -0
  4. package/.agents/checklists/rd-be.md +36 -0
  5. package/.agents/checklists/rd-fe.md +63 -0
  6. package/.agents/common/intercom-protocol.md +125 -0
  7. package/.agents/common/output-format.md +51 -0
  8. package/.agents/prompts/architecture-designer.md +102 -0
  9. package/.agents/prompts/backend-developer.md +72 -0
  10. package/.agents/prompts/frontend-developer.md +63 -0
  11. package/.agents/prompts/product-manager.md +147 -0
  12. package/.agents/prompts/test-engineer.md +79 -0
  13. package/.agents/roles.yaml +37 -0
  14. package/.agents/scripts/nao-fleet.sh +588 -0
  15. package/.agents/scripts/ui-tokens-check.sh +51 -0
  16. package/.agents/skills/arch-patterns.md +44 -0
  17. package/.agents/skills/backend-ddd-details.md +108 -0
  18. package/.agents/skills/codegraph.md +52 -0
  19. package/.agents/skills/commit.md +68 -0
  20. package/.agents/skills/frontend-ddd-details.md +95 -0
  21. package/.agents/skills/frontend-design/SKILL.md +73 -0
  22. package/.agents/skills/pm-grill.md +26 -0
  23. package/.agents/skills/pm-rice.md +36 -0
  24. package/.agents/skills/test-design.md +52 -0
  25. package/.agents/templates/AGENTS.md.example +37 -0
  26. package/.agents/templates/frontend-ui/tokens.css.example +74 -0
  27. package/.agents/templates/frontend-ui/ux-playbook.md.example +36 -0
  28. package/.agents/templates/tasks-state.md.example +34 -0
  29. package/LICENSE +21 -0
  30. package/README.md +189 -0
  31. package/bin/nao-skill.js +142 -0
  32. package/package.json +34 -0
@@ -0,0 +1,588 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # nao-fleet.sh — 按角色一键拉起 pi 会话窗口(nao 团队工具箱)
4
+ #
5
+ # 用法
6
+ # nao-fleet.sh check [--strict] 静态体检:roles.yaml/角色卡/交叉引用/白名单/布局
7
+ # nao-fleet.sh status 角色会话在线状态(权威名单见 intercom list)
8
+ # nao-fleet.sh ensure <别名>[@<repo>] [更多...] 拉起角色窗口(默认工作区=roles.yaml workspace)
9
+ # nao-fleet.sh ensure -m <model> <别名>... 显式指定模型(须命中白名单)
10
+ # nao-fleet.sh ensure --force <别名>... 忽略"已在运行"判重
11
+ #
12
+ # 角色别名 → 角色卡:见 .agents/roles.yaml(单一事实来源)
13
+ # 当前:pm / arch-designer(arch) / rd-fe / rd-be / qa
14
+ #
15
+ # 环境变量
16
+ # NAO_TERMINAL=ghostty|ptyxis|tmux|screen 强制宿主
17
+ # NAO_TMUX_LAYOUT=main-row2|grid tmux 布局(默认 main-row2)
18
+ # NAO_TMUX_MAIN_WIDTH=<10..90> main-row2 主 pane 宽度百分比(默认 50)
19
+ # NAO_SKILLS=<dir> 角色卡根目录(默认 <脚本>/../..)
20
+ # NAO_MODEL_WHITELIST=<glob,...> -m 白名单(默认空=不校验,支持 glob)
21
+ #
22
+ # tmux 宿主行为
23
+ # - 已在 tmux 内($TMUX 存在):当前窗口分屏拉起,不新建窗口。
24
+ # - 不在 tmux 内:创建 detached 会话 nao-<角色>,需 tmux attach -t nao-<角色>。
25
+ # - main-row2:第 1 个 pane 全高占左,后续每角色往右开列、每列上下 2 个:
26
+ # 1 | 2 | 4
27
+ # 1 | 3 | 5
28
+ # - grid:所有 pane 等大网格(tmux 内建 tiled)。
29
+ # =============================================================================
30
+ set -euo pipefail
31
+
32
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
33
+ SKILLS_DIR="${NAO_SKILLS:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
34
+ PROMPTS_DIR="$SKILLS_DIR/.agents/prompts"
35
+ COMMON_DIR="$SKILLS_DIR/.agents/common"
36
+ SKILLS_SUB="$SKILLS_DIR/.agents/skills"
37
+
38
+ MANIFEST="$SKILLS_DIR/.agents/roles.yaml"
39
+ CARD_MAX_LINES=200
40
+ ROLE_ORDER=()
41
+ declare -A ROLE_CARDS ROLE_WS ALIAS_ROLE
42
+ MODEL_WHITELIST="${NAO_MODEL_WHITELIST:-}"
43
+ TMUX_LAYOUT="${NAO_TMUX_LAYOUT:-main-row2}"
44
+ TMUX_MAIN_WIDTH="${NAO_TMUX_MAIN_WIDTH:-50}"
45
+
46
+ log() { printf '\033[1;32m[fleet]\033[0m %s\n' "$*"; }
47
+ warn() { printf '\033[1;33m[fleet]\033[0m %s\n' "$*" >&2; }
48
+ die() { printf '\033[1;31m[fleet]\033[0m %s\n' "$*" >&2; exit 1; }
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # roles.yaml 解析(严格子集:顶层 roles:,2 空格角色条目 / 4 空格字段,禁 Tab)
52
+ # 填充 ROLE_ORDER / ROLE_CARDS / ROLE_WS / ALIAS_ROLE;解析失败即 die
53
+ load_manifest() {
54
+ local f="$MANIFEST" out
55
+ [[ -f "$f" ]] || die "角色清单缺失: $f(请创建 .agents/roles.yaml)"
56
+ out="$(awk -F'\t' '
57
+ function die(msg) { print "ERR: " FNR ": " msg; bad=1 }
58
+ BEGIN { bad=0; in_roles=0; cur=""; n=0 }
59
+ /^[[:space:]]*#/ { next }
60
+ /^[[:space:]]*$/ { next }
61
+ {
62
+ if ($0 ~ /\t/) { die("禁止 Tab 缩进"); next }
63
+ if (!in_roles) {
64
+ if ($0 ~ /^roles:[[:space:]]*$/) { in_roles=1; next }
65
+ die("顶层仅允许 roles:,得到: " $0); next
66
+ }
67
+ if ($0 ~ /^ [^ ][^:]*:[[:space:]]*$/) {
68
+ cur=$0; sub(/^ /, "", cur); sub(/:[[:space:]]*$/, "", cur)
69
+ if (cur !~ /^[a-z0-9][a-z0-9-]*$/) { die("非法角色 id: " cur); next }
70
+ card[cur]=""; ws[cur]="."; na[cur]=0; order[++n]=cur; next
71
+ }
72
+ if (cur != "" && $0 ~ /^ [a-z_]+:[[:space:]]/) {
73
+ key=$0; sub(/^ /, "", key); sub(/:.*/, "", key)
74
+ val=$0; sub(/^ [a-z_]+:[[:space:]]*/, "", val)
75
+ if (key=="card") { card[cur]=val }
76
+ else if (key=="workspace") { ws[cur]=val }
77
+ else if (key=="aliases") {
78
+ if (val !~ /^\[[^]]*\]$/) { die(cur ".aliases 须为 [a, b] 列表: " $0); next }
79
+ gsub(/^\[|\]$/, "", val); m=split(val, arr, /,/)
80
+ for (i=1; i<=m; i++) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", arr[i]); if (arr[i]!="") alias[cur,++na[cur]]=arr[i] }
81
+ if (na[cur]==0) die(cur ".aliases 为空")
82
+ }
83
+ else die(cur " 未知字段: " key)
84
+ next
85
+ }
86
+ die("无法解析: " $0)
87
+ }
88
+ END {
89
+ if (bad) exit 1
90
+ if (!in_roles) { print "ERR: 缺少 roles: 顶层键"; exit 1 }
91
+ for (i=1; i<=n; i++) { c=order[i]; if (card[c]=="") { print "ERR: " c " 缺少 card"; bad=1 } if (na[c]==0) { print "ERR: " c " 缺少 aliases"; bad=1 } }
92
+ if (bad) exit 1
93
+ for (i=1; i<=n; i++) { c=order[i]; print "ROLE\t" c "\t" card[c] "\t" ws[c]; for (j=1; j<=na[c]; j++) print "ALIAS\t" c "\t" alias[c,j] }
94
+ }
95
+ ' "$f")" || { printf '%s\n' "$out" >&2; die "roles.yaml 解析失败: $f"; }
96
+ local k v w rest
97
+ while IFS=$'\t' read -r k v w rest; do
98
+ case "$k" in
99
+ ROLE) ROLE_ORDER+=("$v"); ROLE_CARDS["$v"]="$w"; ROLE_WS["$v"]="$rest" ;;
100
+ ALIAS) ALIAS_ROLE["$v"]="$w" ;;
101
+ esac
102
+ done <<< "$out"
103
+ }
104
+
105
+ # 读取卡片 frontmatter 字段(--- 与 --- 之间)
106
+ card_field() {
107
+ awk -v k="$2" '
108
+ /^---$/ { c++; next }
109
+ c == 1 && $0 ~ "^" k ":" { sub("^" k ": *", ""); print; exit }
110
+ ' "$1"
111
+ }
112
+
113
+ # 校验卡片/公共规范/清单中的 @.agents/... 引用均存在(含 checklists/ 子目录;跨仓库会话能解析的关键保障)
114
+ check_cross_refs() {
115
+ local rc=0 src ref
116
+ local -a srcs=()
117
+ shopt -s nullglob
118
+ srcs+=("$PROMPTS_DIR"/*.md "$COMMON_DIR"/*.md "$SKILLS_SUB"/*.md "$SKILLS_DIR"/.agents/checklists/*.md)
119
+ shopt -u nullglob
120
+ for src in "${srcs[@]}"; do
121
+ while IFS= read -r ref; do
122
+ [[ -z "$ref" ]] && continue
123
+ case "$ref" in
124
+ common/*|skills/*|prompts/*|scripts/*|templates/*|checklists/*) ;;
125
+ *) continue ;;
126
+ esac
127
+ if [[ ! -e "$SKILLS_DIR/.agents/$ref" ]]; then
128
+ printf ' ✗ %s → @.agents/%s 缺失\n' "$(basename "$src")" "$ref"
129
+ rc=1
130
+ fi
131
+ done < <(grep -hoE '@\.agents/(common|skills|prompts|scripts|templates|checklists)/[A-Za-z0-9._/-]+' "$src" | sed 's/^@\.agents\///' | sort -u)
132
+ done
133
+ [[ $rc -eq 0 ]] && echo ' ✓ 全部引用文件存在'
134
+ return $rc
135
+ }
136
+
137
+ # CodeGraph 索引健康(ensure 拉起前 / check 用;缺失或过期仅 warn,不阻塞)
138
+ check_codegraph() {
139
+ local repo="$1" name st
140
+ name="$(basename "$repo")"
141
+ command -v codegraph >/dev/null 2>&1 || { warn "$name codegraph 未安装(RD 定位将回退 grep,属预期)"; return 0; }
142
+ if [[ ! -d "$repo/.codegraph" ]]; then
143
+ warn "$name 无 CodeGraph 索引(RD 定位将回退 grep;建议 codegraph init)"
144
+ return 0
145
+ fi
146
+ st="$(cd "$repo" 2>/dev/null && codegraph status 2>&1)"
147
+ if [[ "$st" == *"Index is up to date"* ]]; then
148
+ log "$name CodeGraph 索引 ✓ up to date"
149
+ elif [[ "$st" == *"Pending Changes"* ]]; then
150
+ warn "$name CodeGraph 索引有未同步变更(建议 codegraph sync)"
151
+ elif [[ "$st" == *"Not initialized"* ]]; then
152
+ warn "$name .codegraph 存在但未初始化(建议 codegraph init)"
153
+ else
154
+ warn "$name codegraph status 输出不可解析"
155
+ fi
156
+ }
157
+
158
+ # ---------------------------------------------------------------------------
159
+ resolve_role() {
160
+ local id="${ALIAS_ROLE[$1]:-}"
161
+ [[ -n "$id" ]] || die "未知角色: $1(可用: ${ROLE_ORDER[*]})"
162
+ NAME="$id"
163
+ PROMPT="${ROLE_CARDS[$id]}"
164
+ }
165
+
166
+ detect_host() {
167
+ if [[ -n "${NAO_TERMINAL:-}" ]]; then echo "$NAO_TERMINAL"; return; fi
168
+ if [[ -n "${TMUX:-}" ]] && command -v tmux >/dev/null 2>&1; then echo tmux; return; fi
169
+ for h in ghostty ptyxis tmux; do
170
+ command -v "$h" >/dev/null 2>&1 && { echo "$h"; return; }
171
+ done
172
+ echo screen
173
+ }
174
+
175
+ running() { pgrep -f -- "pi[[:space:]].*--name $1([[:space:]]|$)" >/dev/null 2>&1; }
176
+
177
+ # 白名单命中返回 0,否则返回 1;未设白名单=放行
178
+ check_model() {
179
+ local m="$1" entry
180
+ local entries=()
181
+ [[ -z "$MODEL_WHITELIST" ]] && return 0
182
+ IFS=',' read -ra entries <<< "$MODEL_WHITELIST"
183
+ for entry in "${entries[@]}"; do
184
+ entry="${entry#"${entry%%[![:space:]]*}"}"
185
+ entry="${entry%"${entry##*[![:space:]]}"}"
186
+ [[ -z "$entry" ]] && continue
187
+ # shellcheck disable=SC2053
188
+ [[ "$m" == $entry ]] && return 0
189
+ done
190
+ return 1
191
+ }
192
+
193
+ # 16 位 checksum,与 tmux 源码 layout_checksum() 一致
194
+ _layout_checksum() {
195
+ local s="$1" c=0 b
196
+ local -a bytes=()
197
+ # shellcheck disable=SC2207
198
+ bytes=($(printf '%s' "$s" | od -An -v -tu1 | tr -s ' \n' ' '))
199
+ for b in "${bytes[@]}"; do
200
+ c=$(( ((c >> 1) + ((c & 1) << 15) + b) & 0xFFFF ))
201
+ done
202
+ printf '%04x' "$c"
203
+ }
204
+
205
+ # main-row2:主 pane 全高占左;往右每列 2 个上下堆叠
206
+ # 1 | 2 | 4
207
+ # 1 | 3 | 5
208
+ # tmux 规范:相邻子 pane 之间 1 格 gap;{} 左右排列,[] 上下排列
209
+ build_main_row2_layout() {
210
+ local win_w="$1" win_h="$2" pct="$3"
211
+ shift 3
212
+ local -a panes=("$@")
213
+ local n=${#panes[@]}
214
+ (( n >= 1 )) || return 1
215
+ if (( n == 1 )); then
216
+ printf '%sx%s,0,0,%s' "$win_w" "$win_h" "${panes[0]}"
217
+ return 0
218
+ fi
219
+
220
+ # 根:水平分割,主 pane + 右侧容器,中间 1 gap
221
+ local usable_w=$(( win_w - 1 ))
222
+ (( usable_w < 2 )) && usable_w=2
223
+ local main_w=$(( usable_w * pct / 100 ))
224
+ (( main_w < 1 )) && main_w=1
225
+ (( main_w > usable_w - 1 )) && main_w=$(( usable_w - 1 ))
226
+ local right_w=$(( usable_w - main_w ))
227
+ (( right_w < 1 )) && right_w=1
228
+ local right_x=$(( main_w + 1 ))
229
+
230
+ # 右侧 cols 列
231
+ local m=$(( n - 1 ))
232
+ local cols=$(( (m + 1) / 2 ))
233
+ (( cols < 1 )) && cols=1
234
+ local r_usable=$(( right_w - (cols - 1) ))
235
+ (( r_usable < 1 )) && r_usable=1
236
+ local cw_base=$(( r_usable / cols ))
237
+ (( cw_base < 1 )) && cw_base=1
238
+ local cw_last=$(( r_usable - cw_base * (cols - 1) ))
239
+ (( cw_last < 1 )) && cw_last=1
240
+
241
+ # 每列内部:上下 2 个,中间 1 gap
242
+ local v_usable=$(( win_h - 1 ))
243
+ (( v_usable < 2 )) && v_usable=2
244
+ local top_h=$(( v_usable / 2 ))
245
+ (( top_h < 1 )) && top_h=1
246
+ local bot_h=$(( v_usable - top_h ))
247
+ (( bot_h < 1 )) && bot_h=1
248
+ local bot_y=$(( top_h + 1 ))
249
+
250
+ local -a col_parts=()
251
+ local col col_x cw i_top i_bot
252
+ for (( col=0; col<cols; col++ )); do
253
+ col_x=$(( right_x + col * (cw_base + 1) ))
254
+ if (( col == cols - 1 )); then cw=$cw_last; else cw=$cw_base; fi
255
+ i_top=$(( col * 2 + 1 ))
256
+ i_bot=$(( col * 2 + 2 ))
257
+ if (( i_bot < n )); then
258
+ col_parts+=("${cw}x${win_h},${col_x},0[${cw}x${top_h},${col_x},0,${panes[$i_top]},${cw}x${bot_h},${col_x},${bot_y},${panes[$i_bot]}]")
259
+ elif (( i_top < n )); then
260
+ col_parts+=("${cw}x${win_h},${col_x},0,${panes[$i_top]}")
261
+ fi
262
+ done
263
+
264
+ local right_body
265
+ if (( cols == 1 )); then
266
+ right_body="${col_parts[0]}"
267
+ else
268
+ local IFS=','
269
+ right_body="${right_w}x${win_h},${right_x},0{${col_parts[*]}}"
270
+ unset IFS
271
+ fi
272
+
273
+ printf '%sx%s,0,0{%sx%s,0,0,%s,%s}' \
274
+ "$win_w" "$win_h" \
275
+ "$main_w" "$win_h" "${panes[0]}" \
276
+ "$right_body"
277
+ }
278
+
279
+ apply_tmux_layout() {
280
+ [[ -n "${TMUX:-}" ]] || return 0
281
+ if [[ "$TMUX_LAYOUT" == "grid" ]]; then
282
+ tmux select-layout tiled >/dev/null 2>&1 || true
283
+ return
284
+ fi
285
+ # main-row2
286
+ local win_w win_h
287
+ read -r win_w win_h < <(tmux display-message -p '#{window_width} #{window_height}' 2>/dev/null)
288
+ [[ -n "$win_w" && -n "$win_h" ]] || return 0
289
+
290
+ local -a pane_ids=()
291
+ local line
292
+ while IFS= read -r line; do
293
+ [[ -n "$line" ]] && pane_ids+=("$line")
294
+ done < <(tmux list-panes -F '#{pane_id}' 2>/dev/null | sed 's/^%//')
295
+ (( ${#pane_ids[@]} >= 1 )) || return 0
296
+
297
+ local pct="$TMUX_MAIN_WIDTH"
298
+ [[ "$pct" =~ ^[0-9]+$ ]] || pct=50
299
+ (( pct < 10 )) && pct=10
300
+ (( pct > 90 )) && pct=90
301
+
302
+ local full; full="$(build_main_row2_layout "$win_w" "$win_h" "$pct" "${pane_ids[@]}")"
303
+ [[ -n "$full" ]] || return 0
304
+
305
+ local ck; ck="$(_layout_checksum "$full")"
306
+ local err
307
+ if ! err=$(tmux select-layout "${ck},${full}" 2>&1); then
308
+ warn "main-row2 布局应用失败,回退 tiled"
309
+ warn " tmux: ${err:-<no message>}"
310
+ warn " layout: $full"
311
+ tmux select-layout tiled >/dev/null 2>&1 || true
312
+ fi
313
+ }
314
+
315
+ spawn_tmux() {
316
+ local name="$1" inner="$2"
317
+ case "$TMUX_LAYOUT" in
318
+ main-row2|grid) ;;
319
+ *) die "NAO_TMUX_LAYOUT 无效: $TMUX_LAYOUT(可选 main-row2|grid)" ;;
320
+ esac
321
+ local wrapped="bash -lc $(printf %q "$inner")"
322
+ if [[ -n "${TMUX:-}" ]]; then
323
+ tmux split-window -h "$wrapped" >/dev/null
324
+ apply_tmux_layout
325
+ else
326
+ tmux new-session -d -s "nao-$name" "$wrapped"
327
+ log "tmux 会话 nao-$name 已创建(附加: tmux attach -t nao-$name)"
328
+ fi
329
+ }
330
+
331
+ # 组装系统提示文件:角色卡 + 环境锚点(本机绝对路径,供跨仓库会话解析 @ 引用失败时兜底)
332
+ build_system_prompt() {
333
+ local card="$1" f
334
+ f="$(mktemp "/tmp/nao-fleet-$(basename "$card" .md)-XXXXXX.md")"
335
+ {
336
+ cat "$card"
337
+ printf '\n## 环境锚点\n- NAO_SKILLS=%s(@.agents/... 引用以会话 cwd 解析;cwd 无 .agents 时以 NAO_SKILLS 为根拼接绝对路径)\n' "$SKILLS_DIR"
338
+ } > "$f"
339
+ echo "$f"
340
+ }
341
+
342
+ spawn_one() {
343
+ local name="$1" repo="$2" model="$3" card prompt_file host inner
344
+ card="$PROMPTS_DIR/$PROMPT"
345
+ [[ -f "$card" ]] || die "角色卡不存在: $card"
346
+ [[ -d "$repo" ]] || die "工作区不存在: $repo"
347
+
348
+ prompt_file="$(build_system_prompt "$card")"
349
+ inner="cd $(printf %q "$repo") && exec pi --name $(printf %q "$name")"
350
+ [[ -n "$model" ]] && inner+=" --model $(printf %q "$model")"
351
+ inner+=" --append-system-prompt $(printf %q "$prompt_file")"
352
+ ( sleep 60; rm -f -- "$prompt_file" ) & disown 2>/dev/null || true
353
+
354
+ host="$(detect_host)"
355
+ case "$host" in
356
+ ghostty) setsid -f ghostty -e bash -lc "$inner" >/dev/null 2>&1 ;;
357
+ ptyxis) setsid -f ptyxis -- bash -lc "$inner" >/dev/null 2>&1 ;;
358
+ tmux) spawn_tmux "$name" "$inner" ;;
359
+ screen) screen -dmS "nao-$name" bash -lc "$inner" ;;
360
+ *) die "不支持的宿主: $host" ;;
361
+ esac
362
+ log "[$host] 已拉起 $name @ $repo${model:+(model=$model)}$([[ "$host" == tmux ]] && echo "(layout=$TMUX_LAYOUT)")"
363
+ }
364
+
365
+ # ---------------------------------------------------------------------------
366
+ cmd_check() {
367
+ local strict="${1:-false}"
368
+ local rc=0 f lines d
369
+ local wl_problems=0
370
+
371
+ echo "== 目录 =="
372
+ for d in "$PROMPTS_DIR" "$COMMON_DIR" "$SKILLS_SUB"; do
373
+ if [[ -d "$d" ]]; then printf ' ✓ %s\n' "$d"
374
+ else printf ' ✗ 缺失: %s\n' "$d"; rc=1; fi
375
+ done
376
+
377
+ echo "== 角色清单 roles.yaml =="
378
+ printf ' ✓ 解析成功,%d 个角色: %s\n' "${#ROLE_ORDER[@]}" "${ROLE_ORDER[*]}"
379
+
380
+ echo "== 常驻角色卡(阈值 ${CARD_MAX_LINES} 行,frontmatter 校验)=="
381
+ local a role version updated
382
+ for a in "${ROLE_ORDER[@]}"; do
383
+ f="$PROMPTS_DIR/${ROLE_CARDS[$a]}"
384
+ if [[ ! -f "$f" ]]; then
385
+ printf ' ✗ %-26s 缺失\n' "${ROLE_CARDS[$a]}"; rc=1; continue
386
+ fi
387
+ lines=$(wc -l < "$f")
388
+ if (( lines > CARD_MAX_LINES )); then
389
+ printf ' ! %-26s %3d 行(超阈值,建议拆到 skills/)\n' "${ROLE_CARDS[$a]}" "$lines"
390
+ else
391
+ printf ' ✓ %-26s %3d 行\n' "${ROLE_CARDS[$a]}" "$lines"
392
+ fi
393
+ role="$(card_field "$f" role)"
394
+ version="$(card_field "$f" version)"
395
+ updated="$(card_field "$f" updated)"
396
+ if [[ "$role" != "$a" ]]; then
397
+ printf ' ✗ %s frontmatter role=%s 与 manifest 角色 %s 不一致\n' "${ROLE_CARDS[$a]}" "${role:-<缺失>}" "$a"; rc=1
398
+ fi
399
+ if [[ ! "$version" =~ ^[0-9]+$ ]]; then
400
+ printf ' ✗ %s frontmatter version=%s 缺失或非整数\n' "${ROLE_CARDS[$a]}" "${version:-<缺失>}"; rc=1
401
+ fi
402
+ if [[ -z "$updated" ]]; then
403
+ printf ' ✗ %s frontmatter updated 缺失(应如 2026-09-21)\n' "${ROLE_CARDS[$a]}"; rc=1
404
+ fi
405
+ done
406
+
407
+ echo "== prompts/ 孤儿检查 =="
408
+ local cf found c2 orphan=0
409
+ shopt -s nullglob
410
+ for cf in "$PROMPTS_DIR"/*.md; do
411
+ found=0
412
+ for c2 in "${ROLE_ORDER[@]}"; do
413
+ [[ "$(basename "$cf")" == "${ROLE_CARDS[$c2]}" ]] && found=1
414
+ done
415
+ if (( ! found )); then printf ' ! %s 未登记于 roles.yaml\n' "$(basename "$cf")"; orphan=1; fi
416
+ done
417
+ shopt -u nullglob
418
+ (( orphan )) || echo ' ✓ 无孤儿卡'
419
+
420
+ echo "== 公共规范 =="
421
+ for f in output-format.md intercom-protocol.md; do
422
+ if [[ -f "$COMMON_DIR/$f" ]]; then printf ' ✓ %s\n' "$f"
423
+ else printf ' ✗ %s 缺失\n' "$f"; rc=1; fi
424
+ done
425
+
426
+ echo "== 按需技能 =="
427
+ if [[ -d "$SKILLS_SUB" ]]; then
428
+ shopt -s nullglob
429
+ for f in "$SKILLS_SUB"/*.md; do printf ' · %s\n' "$(basename "$f")"; done
430
+ shopt -u nullglob
431
+ fi
432
+
433
+ echo "== 交叉引用(@.agents/common|skills|prompts|scripts/... 均须存在)=="
434
+ check_cross_refs || rc=1
435
+
436
+ echo "== CodeGraph 索引(业务 repo 的索引在 ensure 拉起时检查)=="
437
+ if command -v codegraph >/dev/null 2>&1; then
438
+ if [[ -d "$PWD/.codegraph" ]]; then
439
+ local cgst
440
+ cgst="$(codegraph status 2>&1)"
441
+ if [[ "$cgst" == *"up to date"* ]]; then
442
+ echo ' ✓ 当前目录索引 up to date'
443
+ elif [[ "$cgst" == *"Pending Changes"* ]]; then
444
+ echo ' ! 当前目录索引有未同步变更(codegraph sync)'; rc=1
445
+ else
446
+ echo ' · 当前目录有索引,状态见 codegraph status'
447
+ fi
448
+ else
449
+ echo ' · 当前目录无索引(业务 repo 的索引在 ensure 拉起时检查)'
450
+ fi
451
+ else
452
+ echo ' · codegraph 未安装(回退 grep 属预期行为)'
453
+ fi
454
+
455
+ echo "== 模型白名单 =="
456
+ if [[ -z "$MODEL_WHITELIST" ]]; then
457
+ echo " (未设置 NAO_MODEL_WHITELIST,-m 不校验)"
458
+ else
459
+ local entry s dup kind tag
460
+ local entries=() seen=()
461
+ IFS=',' read -ra entries <<< "$MODEL_WHITELIST"
462
+ for entry in "${entries[@]}"; do
463
+ entry="${entry#"${entry%%[![:space:]]*}"}"
464
+ entry="${entry%"${entry##*[![:space:]]}"}"
465
+ if [[ -z "$entry" ]]; then
466
+ printf ' ! 空条目被忽略\n'
467
+ wl_problems=$((wl_problems + 1))
468
+ continue
469
+ fi
470
+ dup=0
471
+ for s in "${seen[@]}"; do [[ "$s" == "$entry" ]] && { dup=1; break; }; done
472
+ seen+=("$entry")
473
+ case "$entry" in
474
+ *'*'*|*'?'*|*'['*) kind="glob" ;;
475
+ *) kind="字面量" ;;
476
+ esac
477
+ tag=""
478
+ if (( dup )); then tag=" [重复]"; wl_problems=$((wl_problems + 1)); fi
479
+ if [[ "$entry" == "*" ]]; then tag+=" [过宽!]"; wl_problems=$((wl_problems + 1)); fi
480
+ printf ' · %-32s (%s)%s\n' "$entry" "$kind" "$tag"
481
+ done
482
+ fi
483
+
484
+ echo "== tmux 布局 =="
485
+ if [[ -n "${TMUX:-}" ]]; then
486
+ echo " 当前在 tmux 内(会话 $(tmux display-message -p '#S' 2>/dev/null || echo '?'))"
487
+ else
488
+ echo " (当前不在 tmux 内;若宿主命中 tmux 会创建 detached 会话)"
489
+ fi
490
+ case "$TMUX_LAYOUT" in
491
+ main-row2|grid)
492
+ printf ' ✓ NAO_TMUX_LAYOUT=%-10s 合法\n' "$TMUX_LAYOUT" ;;
493
+ *)
494
+ printf ' ✗ NAO_TMUX_LAYOUT=%-10s 非法(可选 main-row2|grid)\n' "$TMUX_LAYOUT"; rc=1 ;;
495
+ esac
496
+ if [[ "$TMUX_LAYOUT" == "main-row2" ]]; then
497
+ if [[ "$TMUX_MAIN_WIDTH" =~ ^[0-9]+$ ]] && (( TMUX_MAIN_WIDTH >= 10 && TMUX_MAIN_WIDTH <= 90 )); then
498
+ printf ' ✓ NAO_TMUX_MAIN_WIDTH=%-3s%% 合法\n' "$TMUX_MAIN_WIDTH"
499
+ else
500
+ printf ' ! NAO_TMUX_MAIN_WIDTH=%-3s 非法(10..90),运行时会回退 50\n' "$TMUX_MAIN_WIDTH"
501
+ fi
502
+ fi
503
+
504
+ if [[ "$strict" == "true" && $wl_problems -gt 0 ]]; then
505
+ warn "strict 模式:白名单发现 $wl_problems 项问题(空条目/重复/过宽)"
506
+ rc=1
507
+ fi
508
+
509
+ exit $rc
510
+ }
511
+
512
+ # ---------------------------------------------------------------------------
513
+ cmd_status() {
514
+ local a
515
+ echo "== 角色会话在线状态(权威名单以 intercom({action:'list'}) 为准)=="
516
+ for a in "${ROLE_ORDER[@]}"; do
517
+ if running "$a"; then
518
+ printf ' ✓ %-14s 在线(--name %s;卡片 %s)\n' "$a" "$a" "${ROLE_CARDS[$a]}"
519
+ else
520
+ printf ' · %-14s 未运行(ensure 拉起)\n' "$a"
521
+ fi
522
+ done
523
+ }
524
+
525
+ # ---------------------------------------------------------------------------
526
+ cmd_ensure() {
527
+ local force="$1" model="$2"; shift 2
528
+ local spec role repo key seen k
529
+ local -a cg_done=()
530
+ [[ $# -eq 0 ]] && die "ensure 需要至少一个角色,如: nao-fleet.sh ensure arch rd-fe"
531
+ for spec in "$@"; do
532
+ if [[ "$spec" == *"@"* ]]; then
533
+ role="${spec%%@*}"; repo="${spec#*@}"
534
+ else
535
+ role="$spec"; repo=""
536
+ fi
537
+ resolve_role "$role"
538
+ [[ -n "$repo" ]] || repo="${ROLE_WS[$NAME]:-$PWD}"
539
+ # CodeGraph 索引健康(同 repo 只查一次,不阻塞拉起)
540
+ key="$repo"; seen=0
541
+ for k in "${cg_done[@]:-}"; do [[ "$k" == "$key" ]] && seen=1; done
542
+ if (( ! seen )); then check_codegraph "$repo"; cg_done+=("$key"); fi
543
+ if [[ "$force" != "true" ]] && running "$NAME"; then
544
+ warn "$NAME 已在运行(--name 识别),跳过;确需重开请加 --force"
545
+ continue
546
+ fi
547
+ spawn_one "$NAME" "$repo" "$model"
548
+ done
549
+ }
550
+
551
+ # ---------------------------------------------------------------------------
552
+ usage() {
553
+ awk '
554
+ /^# =+$/ { c++; if (c==2) exit; next }
555
+ c==1 && /^#/ { sub(/^# ?/,""); print }
556
+ ' "$0"
557
+ exit 0
558
+ }
559
+
560
+ # ---- 入口 ----
561
+ CMD=""; FORCE=false; STRICT=false; MODEL=""; TARGETS=()
562
+ while [[ $# -gt 0 ]]; do
563
+ case "$1" in
564
+ check) CMD="check"; shift ;;
565
+ status) CMD="status"; shift ;;
566
+ ensure) CMD="ensure"; shift ;;
567
+ -m|--model)
568
+ MODEL="${2:-}"
569
+ [[ -n "$MODEL" ]] || die "-m 需要模型参数"
570
+ check_model "$MODEL" || die "模型 '$MODEL' 不在白名单内。允许: $MODEL_WHITELIST(可通过 NAO_MODEL_WHITELIST 覆盖)"
571
+ shift 2 ;;
572
+ --force) FORCE=true; shift ;;
573
+ --strict) STRICT=true; shift ;;
574
+ -h|--help) usage ;;
575
+ *) TARGETS+=("$1"); shift ;;
576
+ esac
577
+ done
578
+
579
+ case "$CMD" in
580
+ check|ensure|status) load_manifest ;;
581
+ esac
582
+
583
+ case "$CMD" in
584
+ check) cmd_check "$STRICT" ;;
585
+ status) cmd_status ;;
586
+ ensure) cmd_ensure "$FORCE" "$MODEL" "${TARGETS[@]}" ;;
587
+ *) usage ;;
588
+ esac
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # ui-tokens-check.sh — 扫描 UI 硬编码颜色值(绕过 Design Tokens)
4
+ #
5
+ # 用法
6
+ # ui-tokens-check.sh [<repo>] [-x <额外排除模式>]
7
+ # 默认 $PWD;退出码 0=通过 1=存在违规
8
+ #
9
+ # 原理
10
+ # UI 视觉应收敛到 Design Tokens(tokens.css 的 --<prefix>-* 变量)。
11
+ # 组件内裸色值(#fff / rgba() / #d4a72c)会破坏换主题/暗色模式并造成
12
+ # 风格漂移。本脚本扫出这些绕过令牌的色值,供交付前自查或接 CI/lint-staged。
13
+ #
14
+ # 排除(默认)
15
+ # node_modules / .codegraph / .d.ts
16
+ # 令牌定义文件:tokens.css / theme.css / variables.css(-x 追加自定义)
17
+ # 任意 CSS 变量定义行(--xxx: <值>)
18
+ #
19
+ # 非代码内容(测试断言里的期望色值)命中后由人/agent 判断是否豁免。
20
+ # =============================================================================
21
+ set -uo pipefail
22
+
23
+ repo="$PWD"; extra=""
24
+ while [[ $# -gt 0 ]]; do
25
+ case "$1" in
26
+ -x) extra="${2:-}"; shift 2 ;;
27
+ *) repo="$1"; shift ;;
28
+ esac
29
+ done
30
+
31
+ cd "$repo" 2>/dev/null || { echo "✗ 目录不存在: $repo" >&2; exit 2; }
32
+
33
+ file_excl="node_modules|\.codegraph|\.d\.ts|tokens\.css|theme\.css|variables\.css"
34
+ [[ -n "$extra" ]] && file_excl+="|$extra"
35
+
36
+ hits="$(grep -rnoE --include='*.vue' --include='*.ts' --include='*.tsx' \
37
+ --include='*.js' --include='*.jsx' --include='*.css' \
38
+ -E '#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(' . 2>/dev/null \
39
+ | grep -vE "$file_excl" \
40
+ | grep -viE -- 'var\(--|--[a-z0-9-]+[[:space:]]*:[[:space:]]*(#|rgba?\(|hsla?\()' \
41
+ | head -40 || true)"
42
+
43
+ if [[ -n "$hits" ]]; then
44
+ echo "✗ UI 令牌违规($(basename "$repo"))— 以下色值应改为 var(--<prefix>-*) 令牌:"
45
+ echo "$hits"
46
+ if [[ $(grep -c . <<< "$hits") -ge 40 ]]; then echo " …(更多)"; fi
47
+ exit 1
48
+ fi
49
+
50
+ echo "✓ $(basename "$repo") 无硬编码色值(Design Tokens 一致)"
51
+ exit 0
@@ -0,0 +1,44 @@
1
+ ---
2
+ description: 按需技能——架构模式速查 + ADR 模板
3
+ ---
4
+
5
+ # 架构模式速查
6
+
7
+ | 场景 | 推荐架构 | 关键技术 |
8
+ | :--- | :--- | :--- |
9
+ | 高并发交易 | 微服务 + 事件驱动 | Kubernetes, Kafka, Redis |
10
+ | 大数据处理 | Lambda 架构 | Spark, Flink, S3 |
11
+ | 多租户 SaaS | 多租户 + 模块化 | PostgreSQL schemas, API Gateway |
12
+ | 实时系统 | 事件溯源 + CQRS | Event Store, gRPC |
13
+
14
+ ## 最小示例
15
+
16
+ ```mermaid
17
+ graph TD
18
+ A[用户请求] --> B[API Gateway]
19
+ B --> C[服务注册发现]
20
+ C --> D[微服务集群]
21
+ D --> E[数据库集群]
22
+ D --> F[缓存集群]
23
+ D --> G[消息队列]
24
+ ```
25
+
26
+ ## ADR 模板
27
+
28
+ ```md
29
+ # ADR-<编号>: <标题>
30
+ - 日期:YYYY-MM-DD
31
+ - 状态:提议 / 已接受 / 已废弃
32
+ - 背景:<问题与约束>
33
+ - 决策:<选择了什么>
34
+ - 理由:<四步法分析>
35
+ - 备选方案:<列表 + 否决理由>
36
+ - 后果:<正面 / 负面 / 风险>
37
+ - 相关:<PRD / 关联 ADR>
38
+ ```
39
+
40
+ ## 误区
41
+
42
+ - 过度设计 / 追新 → 务实、成熟稳定优先
43
+ - 忽视 NFR → 性能/安全/可扩展作为核心设计目标
44
+ - 单点设计不考虑演进 → 预留扩展空间