feat: link-roles.sh — симлинки ролей, агентов и памяти из workspace
Refs #2 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzkPQWjfA7RpqYWkjnqJwt
This commit is contained in:
parent
2184689eb9
commit
ceec425760
61
link-roles.sh
Executable file
61
link-roles.sh
Executable file
@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# claude-harness: симлинки из <workspace>/.claude/ в репозитории направлений (роли, агенты, память агентов).
|
||||||
|
# Запуск: bash link-roles.sh [workspace] [--map <dir>=<role> ...]
|
||||||
|
# Источник пар «каталог → роль»: таблица | Каталог | Репозиторий | Роль | в <workspace>/CLAUDE.md (роли admin|analyst|qa)
|
||||||
|
# и/или пары --map. Идемпотентно. Обычные файлы на месте ссылок не трогает (ПРОПУЩЕН).
|
||||||
|
set -euo pipefail
|
||||||
|
WS="."; MAPS=()
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--map) MAPS+=("$2"); shift 2 ;;
|
||||||
|
*) WS="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
cd "$WS"; WS="$(pwd)"
|
||||||
|
pairs=()
|
||||||
|
if [ -f CLAUDE.md ]; then
|
||||||
|
while IFS= read -r line; do pairs+=("$line"); done < <(
|
||||||
|
python3 - CLAUDE.md <<'PY'
|
||||||
|
import sys
|
||||||
|
for line in open(sys.argv[1], encoding="utf-8"):
|
||||||
|
if not line.startswith("|"): continue
|
||||||
|
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
||||||
|
if len(cells) < 3: continue
|
||||||
|
d, role = cells[0].rstrip("/"), cells[2]
|
||||||
|
if role in ("admin", "analyst", "qa") and d and not d.startswith("-"):
|
||||||
|
print(f"{d}={role}")
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
pairs+=("${MAPS[@]+"${MAPS[@]}"}")
|
||||||
|
[ ${#pairs[@]} -gt 0 ] || { echo "link-roles: нет пар каталог=роль (таблица в CLAUDE.md или --map)"; exit 0; }
|
||||||
|
mkdir -p .claude/roles .claude/agents .claude/agent-memory
|
||||||
|
|
||||||
|
link() { # $1 = путь ссылки (относительно WS), $2 = цель (относительно каталога ссылки)
|
||||||
|
local l="$1" t="$2"
|
||||||
|
if [ -L "$l" ]; then
|
||||||
|
if [ "$(readlink "$l")" = "$t" ]; then echo " есть $l"; return 0; fi
|
||||||
|
rm "$l"
|
||||||
|
elif [ -e "$l" ]; then
|
||||||
|
echo " ПРОПУЩЕН $l — обычный файл/каталог, не симлинк; убери его вручную, если нужна ссылка"; return 0
|
||||||
|
fi
|
||||||
|
ln -s "$t" "$l"; echo " создан $l → $t"
|
||||||
|
}
|
||||||
|
|
||||||
|
for p in "${pairs[@]}"; do
|
||||||
|
d=${p%%=*}; r=${p#*=}; d=${d%/}
|
||||||
|
echo "$d ($r)"
|
||||||
|
[ -d "$d" ] || { echo " WARN каталог $d не найден"; continue; }
|
||||||
|
if [ -f "$d/.claude/roles/$r.md" ]; then
|
||||||
|
link ".claude/roles/$r.md" "../../$d/.claude/roles/$r.md"
|
||||||
|
else
|
||||||
|
echo " WARN нет $d/.claude/roles/$r.md — проектный файл роли не слинкован"
|
||||||
|
fi
|
||||||
|
for a in "$d"/.claude/agents/*.md; do
|
||||||
|
[ -f "$a" ] || continue
|
||||||
|
n=$(basename "$a" .md)
|
||||||
|
link ".claude/agents/$n.md" "../../$d/.claude/agents/$n.md"
|
||||||
|
mkdir -p "$d/.claude/agent-memory/$n"
|
||||||
|
link ".claude/agent-memory/$n" "../../$d/.claude/agent-memory/$n"
|
||||||
|
done
|
||||||
|
done
|
||||||
58
tests/test-link-roles.sh
Normal file
58
tests/test-link-roles.sh
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# link-roles.sh: симлинки ролей, агентов и памяти из workspace в репозитории направлений.
|
||||||
|
L="$ROOT/link-roles.sh"
|
||||||
|
assert_file "link-roles.sh существует" "$L"
|
||||||
|
W="$TMP/ws"; mkdir -p "$W/admin/.claude/roles" "$W/admin/.claude/agents" "$W/qa/.claude/roles" "$W/docs" "$W/arm-core"
|
||||||
|
echo '# Роль: admin — проектный слой' > "$W/admin/.claude/roles/admin.md"
|
||||||
|
printf -- '---\nname: jira-admin\nmodel: sonnet\n---\n' > "$W/admin/.claude/agents/jira-admin.md"
|
||||||
|
echo '# Роль: qa — проектный слой' > "$W/qa/.claude/roles/qa.md"
|
||||||
|
cat > "$W/CLAUDE.md" <<'EOT'
|
||||||
|
# Проект
|
||||||
|
|
||||||
|
| Каталог | Репозиторий | Роль |
|
||||||
|
|---|---|---|
|
||||||
|
| arm-core/ | RSHB/arm-core | dev |
|
||||||
|
| admin/ | RSHB/arm-ka-admin | admin |
|
||||||
|
| qa | RSHB/arm-ka-qa | qa |
|
||||||
|
| docs/ | RSHB/arm-ka-docs | — |
|
||||||
|
| source/ | — | |
|
||||||
|
EOT
|
||||||
|
out=$(bash "$L" "$W" 2>&1); rc=$?
|
||||||
|
assert_eq "первый запуск: exit 0" "$rc" "0"
|
||||||
|
assert_symlink "roles/admin.md → admin/" "$W/.claude/roles/admin.md"
|
||||||
|
assert_symlink "roles/qa.md → qa/ (без завершающего /)" "$W/.claude/roles/qa.md"
|
||||||
|
assert_eq "roles/admin.md относительный" "$(readlink "$W/.claude/roles/admin.md")" "../../admin/.claude/roles/admin.md"
|
||||||
|
assert_symlink "agents/jira-admin.md" "$W/.claude/agents/jira-admin.md"
|
||||||
|
assert_symlink "agent-memory/jira-admin" "$W/.claude/agent-memory/jira-admin"
|
||||||
|
assert_file "целевой каталог памяти создан" "$W/admin/.claude/agent-memory/jira-admin"
|
||||||
|
assert_eq "dev не линкуется" "$(ls "$W/.claude/roles" | grep -c dev)" "0"
|
||||||
|
assert_eq "docs/source пропущены" "$(ls "$W/.claude/roles" | wc -l | tr -d ' ')" "2"
|
||||||
|
assert_grep "печатает 'создан'" 'создан' <(printf '%s' "$out")
|
||||||
|
out2=$(bash "$L" "$W" 2>&1); rc=$?
|
||||||
|
assert_eq "второй запуск: exit 0" "$rc" "0"
|
||||||
|
assert_eq "второй запуск: ничего не создано" "$(printf '%s' "$out2" | grep -c 'создан')" "0"
|
||||||
|
assert_grep "второй запуск: 'есть'" 'есть' <(printf '%s' "$out2")
|
||||||
|
assert_eq "второй запуск: ссылка прежняя" "$(readlink "$W/.claude/agent-memory/jira-admin")" "../../admin/.claude/agent-memory/jira-admin"
|
||||||
|
# чужой обычный файл на месте ссылки не затирается
|
||||||
|
rm "$W/.claude/roles/qa.md"; echo 'мой файл' > "$W/.claude/roles/qa.md"
|
||||||
|
out3=$(bash "$L" "$W" 2>&1)
|
||||||
|
assert_eq "обычный файл сохранён" "$(cat "$W/.claude/roles/qa.md")" "мой файл"
|
||||||
|
assert_grep "обычный файл → ПРОПУЩЕН" 'ПРОПУЩЕН' <(printf '%s' "$out3")
|
||||||
|
# обычный каталог на месте ссылки памяти тоже не затирается
|
||||||
|
rm "$W/.claude/agent-memory/jira-admin"; mkdir "$W/.claude/agent-memory/jira-admin"
|
||||||
|
out5=$(bash "$L" "$W" 2>&1); rc=$?
|
||||||
|
assert_eq "каталог на месте ссылки: exit 0" "$rc" "0"
|
||||||
|
assert_eq "каталог на месте ссылки: не симлинк" "$([ -L "$W/.claude/agent-memory/jira-admin" ] && echo link || echo dir)" "dir"
|
||||||
|
assert_grep "каталог на месте ссылки → ПРОПУЩЕН" 'ПРОПУЩЕН' <(printf '%s' "$out5")
|
||||||
|
# --map без CLAUDE.md
|
||||||
|
W2="$TMP/ws2"; mkdir -p "$W2/an/.claude/roles"; echo x > "$W2/an/.claude/roles/analyst.md"
|
||||||
|
bash "$L" "$W2" --map an=analyst >/dev/null 2>&1
|
||||||
|
assert_symlink "--map an=analyst" "$W2/.claude/roles/analyst.md"
|
||||||
|
# источник роли отсутствует → предупреждение, не ошибка
|
||||||
|
W3="$TMP/ws3"; mkdir -p "$W3/qa"
|
||||||
|
printf '| Каталог | Репозиторий | Роль |\n|---|---|---|\n| qa/ | x | qa |\n' > "$W3/CLAUDE.md"
|
||||||
|
out4=$(bash "$L" "$W3" 2>&1); rc=$?
|
||||||
|
assert_eq "нет файла роли в репозитории: exit 0" "$rc" "0"
|
||||||
|
assert_grep "нет файла роли: WARN" 'WARN' <(printf '%s' "$out4")
|
||||||
|
# CLAUDE.md без таблицы и без --map: не падает
|
||||||
|
W4="$TMP/ws4"; mkdir -p "$W4"; echo '# пусто' > "$W4/CLAUDE.md"
|
||||||
|
bash "$L" "$W4" >/dev/null 2>&1; assert_eq "нет пар: exit 0" "$?" "0"
|
||||||
Loading…
x
Reference in New Issue
Block a user