sync: agent-ops from agentic-framework v1.1.168
Some checks are pending
ci / validate (push) Waiting to run
Some checks are pending
ci / validate (push) Waiting to run
This commit is contained in:
parent
55cc42dbfd
commit
99fede0812
14 changed files with 1409 additions and 1 deletions
|
|
@ -3,6 +3,7 @@
|
|||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||
|
||||
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
||||
- 사용자에게 보이는 최종 답변과 설명은 기본적으로 한국어로 작성한다.
|
||||
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
||||
- 요청 범위를 넘는 변경을 하지 않는다.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||
|
||||
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
||||
- 사용자에게 보이는 최종 답변과 설명은 기본적으로 한국어로 작성한다.
|
||||
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
||||
- 요청 범위를 넘는 변경을 하지 않는다.
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -9,6 +9,8 @@ __pycache__/
|
|||
*.egg-info/
|
||||
|
||||
bin/
|
||||
!agent-ops/bin/
|
||||
!agent-ops/bin/**
|
||||
build/
|
||||
dist/
|
||||
var/
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||
|
||||
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
||||
- 사용자에게 보이는 최종 답변과 설명은 기본적으로 한국어로 작성한다.
|
||||
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
||||
- 요청 범위를 넘는 변경을 하지 않는다.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||
|
||||
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
||||
- 사용자에게 보이는 최종 답변과 설명은 기본적으로 한국어로 작성한다.
|
||||
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
||||
- 요청 범위를 넘는 변경을 하지 않는다.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||
|
||||
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
||||
- 사용자에게 보이는 최종 답변과 설명은 기본적으로 한국어로 작성한다.
|
||||
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
||||
- 요청 범위를 넘는 변경을 하지 않는다.
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1.1.167
|
||||
1.1.168
|
||||
|
|
|
|||
260
agent-ops/bin/ai-ignore.sh
Executable file
260
agent-ops/bin/ai-ignore.sh
Executable file
|
|
@ -0,0 +1,260 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Shared AI ignore / permission defaults for init and sync flows.
|
||||
AGENT_OPS_TASK_ARCHIVE_IGNORE_PATTERN="agent-task/archive/**"
|
||||
AGENT_ROADMAP_ARCHIVE_IGNORE_PATTERN="agent-roadmap/archive/**"
|
||||
AGENT_ROADMAP_CURRENT_LOCAL_PATTERN="agent-roadmap/current.md"
|
||||
AGENT_OPS_AI_IGNORE_BLOCK_BEGIN="# BEGIN Agent-Ops managed ignore"
|
||||
AGENT_OPS_AI_IGNORE_BLOCK_END="# END Agent-Ops managed ignore"
|
||||
AGENT_OPS_GITIGNORE_BLOCK_BEGIN="# BEGIN Agent-Ops managed gitignore"
|
||||
AGENT_OPS_GITIGNORE_BLOCK_END="# END Agent-Ops managed gitignore"
|
||||
AGENT_OPS_AI_IGNORE_FILES=(".geminiignore" ".aiexclude" ".cursorignore" ".clineignore")
|
||||
AGENT_OPS_AI_PERMISSION_FILES=(".claude/settings.json" "opencode.json")
|
||||
AGENT_OPS_AI_SYNC_FILES=(".gitignore" "${AGENT_OPS_AI_IGNORE_FILES[@]}" "${AGENT_OPS_AI_PERMISSION_FILES[@]}")
|
||||
|
||||
agent_ops_ensure_gitignore_task_artifact_block() {
|
||||
local file="$1"
|
||||
local tmp
|
||||
|
||||
touch "$file"
|
||||
if grep -qxF "$AGENT_OPS_GITIGNORE_BLOCK_BEGIN" "$file" \
|
||||
&& grep -qxF "$AGENT_OPS_GITIGNORE_BLOCK_END" "$file"; then
|
||||
tmp="$(mktemp "$file.tmp.XXXXXX")"
|
||||
awk \
|
||||
-v begin="$AGENT_OPS_GITIGNORE_BLOCK_BEGIN" \
|
||||
-v end="$AGENT_OPS_GITIGNORE_BLOCK_END" \
|
||||
-v current="$AGENT_ROADMAP_CURRENT_LOCAL_PATTERN" '
|
||||
$0 == begin {
|
||||
print begin
|
||||
print "!agent-task/"
|
||||
print "!agent-task/**/"
|
||||
print "!agent-task/**/*.md"
|
||||
print "!agent-task/**/*.log"
|
||||
print current
|
||||
print end
|
||||
in_block = 1
|
||||
next
|
||||
}
|
||||
$0 == end && in_block {
|
||||
in_block = 0
|
||||
next
|
||||
}
|
||||
!in_block { print }
|
||||
' "$file" > "$tmp"
|
||||
mv "$tmp" "$file"
|
||||
else
|
||||
if [[ -s "$file" ]]; then
|
||||
printf "\n" >> "$file"
|
||||
fi
|
||||
printf "%s\n" "$AGENT_OPS_GITIGNORE_BLOCK_BEGIN" >> "$file"
|
||||
printf "%s\n" "!agent-task/" >> "$file"
|
||||
printf "%s\n" "!agent-task/**/" >> "$file"
|
||||
printf "%s\n" "!agent-task/**/*.md" >> "$file"
|
||||
printf "%s\n" "!agent-task/**/*.log" >> "$file"
|
||||
printf "%s\n" "$AGENT_ROADMAP_CURRENT_LOCAL_PATTERN" >> "$file"
|
||||
printf "%s\n" "$AGENT_OPS_GITIGNORE_BLOCK_END" >> "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
agent_ops_ensure_ai_ignore_block() {
|
||||
local file="$1"
|
||||
local tmp
|
||||
|
||||
touch "$file"
|
||||
if grep -qxF "$AGENT_OPS_AI_IGNORE_BLOCK_BEGIN" "$file" \
|
||||
&& grep -qxF "$AGENT_OPS_AI_IGNORE_BLOCK_END" "$file"; then
|
||||
tmp="$(mktemp "$file.tmp.XXXXXX")"
|
||||
awk \
|
||||
-v begin="$AGENT_OPS_AI_IGNORE_BLOCK_BEGIN" \
|
||||
-v end="$AGENT_OPS_AI_IGNORE_BLOCK_END" \
|
||||
-v task="$AGENT_OPS_TASK_ARCHIVE_IGNORE_PATTERN" \
|
||||
-v roadmap="$AGENT_ROADMAP_ARCHIVE_IGNORE_PATTERN" '
|
||||
$0 == begin {
|
||||
print begin
|
||||
print task
|
||||
print roadmap
|
||||
print end
|
||||
in_block = 1
|
||||
next
|
||||
}
|
||||
$0 == end && in_block {
|
||||
in_block = 0
|
||||
next
|
||||
}
|
||||
!in_block { print }
|
||||
' "$file" > "$tmp"
|
||||
mv "$tmp" "$file"
|
||||
else
|
||||
if [[ -s "$file" ]]; then
|
||||
printf "\n" >> "$file"
|
||||
fi
|
||||
printf "%s\n" "$AGENT_OPS_AI_IGNORE_BLOCK_BEGIN" >> "$file"
|
||||
printf "%s\n" "$AGENT_OPS_TASK_ARCHIVE_IGNORE_PATTERN" >> "$file"
|
||||
printf "%s\n" "$AGENT_ROADMAP_ARCHIVE_IGNORE_PATTERN" >> "$file"
|
||||
printf "%s\n" "$AGENT_OPS_AI_IGNORE_BLOCK_END" >> "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
agent_ops_merge_json_with_jq() {
|
||||
local file="$1"
|
||||
local filter="$2"
|
||||
local fallback_note="$3"
|
||||
local tmp
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "$fallback_note"
|
||||
return
|
||||
fi
|
||||
|
||||
tmp="$(mktemp "$file.tmp.XXXXXX")"
|
||||
if jq "$filter" "$file" > "$tmp"; then
|
||||
mv "$tmp" "$file"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
echo "$fallback_note"
|
||||
fi
|
||||
}
|
||||
|
||||
agent_ops_merge_claude_settings() {
|
||||
local file="$1"
|
||||
local filter='
|
||||
def as_array:
|
||||
if type == "array" then . elif . == null then [] else [.] end;
|
||||
def as_object:
|
||||
if type == "object" then . else {} end;
|
||||
|
||||
.permissions = ((.permissions // {}) | as_object)
|
||||
| .permissions.deny = (
|
||||
(.permissions.deny | as_array)
|
||||
| map(select(. != "Read(./agent-roadmap/archive/**)"))
|
||||
| map(select(. != "Read(./agent-task/archive/**)"))
|
||||
)
|
||||
'
|
||||
|
||||
agent_ops_merge_json_with_jq \
|
||||
"$file" \
|
||||
"$filter" \
|
||||
"Note: .claude/settings.json exists; remove Read(./agent-task/archive/**) and Read(./agent-roadmap/archive/**) manually."
|
||||
}
|
||||
|
||||
agent_ops_claude_settings_complete() {
|
||||
local file="$1"
|
||||
local filter='
|
||||
def as_array:
|
||||
if type == "array" then . elif . == null then [] else [.] end;
|
||||
|
||||
(.permissions.deny | as_array)
|
||||
as $deny
|
||||
| (($deny | index("Read(./agent-task/archive/**)")) == null)
|
||||
and (($deny | index("Read(./agent-roadmap/archive/**)")) == null)
|
||||
'
|
||||
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
jq -e "$filter" "$file" >/dev/null 2>&1
|
||||
else
|
||||
! grep -qF "Read(./agent-task/archive/**)" "$file" \
|
||||
&& ! grep -qF "Read(./agent-roadmap/archive/**)" "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
agent_ops_merge_opencode_config() {
|
||||
local file="$1"
|
||||
local filter='
|
||||
def append_unique($item):
|
||||
if index($item) then . else . + [$item] end;
|
||||
def as_array:
|
||||
if type == "array" then . elif . == null then [] else [.] end;
|
||||
def as_object:
|
||||
if type == "object" then . else {} end;
|
||||
|
||||
.permission = ((.permission // {}) | as_object)
|
||||
| .permission.read = ((.permission.read // {}) | as_object)
|
||||
| del(.permission.read["agent-task/archive/**"])
|
||||
| del(.permission.read["agent-roadmap/archive/**"])
|
||||
| .permission.glob = ((.permission.glob // {}) | as_object)
|
||||
| del(.permission.glob["agent-task/archive/**"])
|
||||
| del(.permission.glob["agent-roadmap/archive/**"])
|
||||
| .watcher = ((.watcher // {}) | as_object)
|
||||
| .watcher.ignore = (
|
||||
(.watcher.ignore | as_array)
|
||||
| append_unique("agent-task/archive/**")
|
||||
| append_unique("agent-roadmap/archive/**")
|
||||
)
|
||||
'
|
||||
|
||||
agent_ops_merge_json_with_jq \
|
||||
"$file" \
|
||||
"$filter" \
|
||||
"Note: opencode.json exists; remove agent-task/archive/** and agent-roadmap/archive/** read/glob hard deny, and add both archive paths to watcher ignore manually."
|
||||
}
|
||||
|
||||
agent_ops_opencode_config_complete() {
|
||||
local file="$1"
|
||||
local filter='
|
||||
def as_array:
|
||||
if type == "array" then . elif . == null then [] else [.] end;
|
||||
|
||||
(.permission.read["agent-task/archive/**"] != "deny")
|
||||
and (.permission.glob["agent-task/archive/**"] != "deny")
|
||||
and (.permission.read["agent-roadmap/archive/**"] != "deny")
|
||||
and (.permission.glob["agent-roadmap/archive/**"] != "deny")
|
||||
and ((.watcher.ignore | as_array) as $ignore | (($ignore | index("agent-task/archive/**")) != null) and (($ignore | index("agent-roadmap/archive/**")) != null))
|
||||
'
|
||||
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
jq -e "$filter" "$file" >/dev/null 2>&1
|
||||
else
|
||||
grep -qF "agent-task/archive" "$file" \
|
||||
&& grep -qF "agent-roadmap/archive" "$file" \
|
||||
&& ! grep -qF "\"agent-task/archive/**\": \"deny\"" "$file" \
|
||||
&& ! grep -qF "\"agent-roadmap/archive/**\": \"deny\"" "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_agent_ops_ai_ignore_config() {
|
||||
local target_dir="$1"
|
||||
local ignore_file
|
||||
|
||||
if [[ -f "$target_dir/.agent-ops-source" ]]; then
|
||||
echo " AI ignore 보강 건너뜀: .agent-ops-source repo"
|
||||
return
|
||||
fi
|
||||
|
||||
agent_ops_ensure_gitignore_task_artifact_block "$target_dir/.gitignore"
|
||||
|
||||
for ignore_file in "${AGENT_OPS_AI_IGNORE_FILES[@]}"; do
|
||||
agent_ops_ensure_ai_ignore_block "$target_dir/$ignore_file"
|
||||
done
|
||||
|
||||
mkdir -p "$target_dir/.claude"
|
||||
if [[ ! -f "$target_dir/.claude/settings.json" ]]; then
|
||||
cat > "$target_dir/.claude/settings.json" <<'EOF'
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"permissions": {
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
EOF
|
||||
elif ! agent_ops_claude_settings_complete "$target_dir/.claude/settings.json"; then
|
||||
agent_ops_merge_claude_settings "$target_dir/.claude/settings.json"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$target_dir/opencode.json" ]]; then
|
||||
cat > "$target_dir/opencode.json" <<'EOF'
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"watcher": {
|
||||
"ignore": [
|
||||
"agent-task/archive/**",
|
||||
"agent-roadmap/archive/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
elif ! agent_ops_opencode_config_complete "$target_dir/opencode.json"; then
|
||||
agent_ops_merge_opencode_config "$target_dir/opencode.json"
|
||||
fi
|
||||
|
||||
echo " AI permission 표준 적용: archive hard deny 제거, archive ignore 적용"
|
||||
}
|
||||
28
agent-ops/bin/bump-version.sh
Executable file
28
agent-ops/bin/bump-version.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
# bump-version.sh <version>
|
||||
# patch +1, 999 초과 시 minor 올림. major는 수동 관리.
|
||||
# 결과 버전을 stdout으로 출력.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
YELLOW='\033[1;33m'; RESET='\033[0m'
|
||||
|
||||
VERSION="${1:-}"
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "사용법: $0 <major.minor.patch>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS='.' read -r major minor patch <<< "$VERSION"
|
||||
|
||||
patch=$((patch + 1))
|
||||
if [[ $patch -gt 999 ]]; then
|
||||
patch=0
|
||||
minor=$((minor + 1))
|
||||
fi
|
||||
if [[ $minor -gt 999 ]]; then
|
||||
minor=0
|
||||
echo -e "${YELLOW}⚠ minor 버전이 999를 초과했습니다. major 버전을 수동으로 올려주세요.${RESET}" >&2
|
||||
fi
|
||||
|
||||
echo "$major.$minor.$patch"
|
||||
24
agent-ops/bin/entry-files.sh
Normal file
24
agent-ops/bin/entry-files.sh
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Shared entry-point file list for init/sync scripts.
|
||||
AGENT_OPS_ENTRY_FILES=("GEMINI.md" "CLAUDE.md" "AGENTS.md" ".cursorrules" ".clinerules")
|
||||
|
||||
apply_agent_ops_entry_files() {
|
||||
local rules_md="$1"
|
||||
local target_root="$2"
|
||||
|
||||
if [[ ! -f "$rules_md" ]]; then
|
||||
if [[ -n "${YELLOW:-}" && -n "${RESET:-}" ]]; then
|
||||
echo -e "${YELLOW} rules.md 없음, 진입 파일 재적용 건너뜀${RESET}"
|
||||
else
|
||||
echo " rules.md 없음, 진입 파일 재적용 건너뜀"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
local f
|
||||
for f in "${AGENT_OPS_ENTRY_FILES[@]}"; do
|
||||
cp "$rules_md" "$target_root/$f"
|
||||
echo " 진입 파일 적용: $f"
|
||||
done
|
||||
}
|
||||
343
agent-ops/bin/init-agent-ops.sh
Executable file
343
agent-ops/bin/init-agent-ops.sh
Executable file
|
|
@ -0,0 +1,343 @@
|
|||
#!/bin/bash
|
||||
|
||||
# agent-ops/bin/init-agent-ops.sh
|
||||
# 프로젝트에 agent-ops 스캐폴드를 초기화하는 스크립트
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR=$(realpath "$(dirname "$0")")
|
||||
SOURCE_DIR=$(realpath "$SCRIPT_DIR/..")
|
||||
source "$SCRIPT_DIR/entry-files.sh"
|
||||
source "$SCRIPT_DIR/ai-ignore.sh"
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <target_directory>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_DIR=$(realpath "$1")
|
||||
|
||||
create_project_agent_ops_dirs() {
|
||||
local agent_ops_dir="$1"
|
||||
|
||||
mkdir -p "$agent_ops_dir/rules/project/domain"
|
||||
mkdir -p "$agent_ops_dir/rules/private"
|
||||
mkdir -p "$agent_ops_dir/skills/project"
|
||||
}
|
||||
|
||||
copy_common_agent_ops() {
|
||||
local source_dir="$1"
|
||||
local target_agent_ops_dir="$2"
|
||||
|
||||
mkdir -p "$target_agent_ops_dir/rules"
|
||||
mkdir -p "$target_agent_ops_dir/skills"
|
||||
|
||||
cp "$source_dir/.version" "$target_agent_ops_dir/"
|
||||
rm -rf "$target_agent_ops_dir/bin"
|
||||
rm -rf "$target_agent_ops_dir/rules/common"
|
||||
rm -rf "$target_agent_ops_dir/skills/common"
|
||||
cp -r "$source_dir/bin" "$target_agent_ops_dir/"
|
||||
cp -r "$source_dir/rules/common" "$target_agent_ops_dir/rules/"
|
||||
cp -r "$source_dir/skills/common" "$target_agent_ops_dir/skills/"
|
||||
}
|
||||
|
||||
ensure_common_rules_file() {
|
||||
local source_dir="$1"
|
||||
local target_agent_ops_dir="$2"
|
||||
local source_rules="$source_dir/rules/common/rules.md"
|
||||
local target_rules="$target_agent_ops_dir/rules/common/rules.md"
|
||||
|
||||
if [ ! -f "$source_rules" ]; then
|
||||
echo "Error: common rules file not found: $source_rules" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$target_rules")"
|
||||
cp "$source_rules" "$target_rules"
|
||||
}
|
||||
|
||||
ensure_gitignore_entry() {
|
||||
local gitignore_file="$1"
|
||||
local entry="$2"
|
||||
|
||||
if ! grep -qxF "$entry" "$gitignore_file"; then
|
||||
printf '%s\n' "$entry" >> "$gitignore_file"
|
||||
fi
|
||||
}
|
||||
|
||||
discover_agent_test_domains() {
|
||||
local target_dir="$1"
|
||||
local domain_root="$target_dir/agent-ops/rules/project/domain"
|
||||
local project_rules="$target_dir/agent-ops/rules/project/rules.md"
|
||||
|
||||
if [ -d "$domain_root" ]; then
|
||||
find "$domain_root" -mindepth 2 -maxdepth 2 -name rules.md -print \
|
||||
| while IFS= read -r rule_file; do
|
||||
basename "$(dirname "$rule_file")"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -f "$project_rules" ]; then
|
||||
grep -Eo 'agent-ops/rules/project/domain/[^/`[:space:]]+/rules\.md' "$project_rules" \
|
||||
| awk -F/ '{ print $(NF-1) }'
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_agent_test_route() {
|
||||
local rules_file="$1"
|
||||
local route_line="$2"
|
||||
local placeholder="- 현재 등록된 도메인/검증 시나리오별 테스트 문서 없음."
|
||||
local tmp
|
||||
|
||||
if grep -qxF -- "$route_line" "$rules_file"; then
|
||||
return
|
||||
fi
|
||||
|
||||
tmp="$(mktemp "$rules_file.tmp.XXXXXX")"
|
||||
if grep -qxF -- "$placeholder" "$rules_file"; then
|
||||
awk -v placeholder="$placeholder" -v route_line="$route_line" '
|
||||
$0 == placeholder { print route_line; next }
|
||||
{ print }
|
||||
' "$rules_file" > "$tmp"
|
||||
else
|
||||
awk -v route_line="$route_line" '
|
||||
BEGIN { added = 0 }
|
||||
{ print }
|
||||
!added && $0 == "## 라우팅" {
|
||||
print ""
|
||||
print route_line
|
||||
added = 1
|
||||
}
|
||||
END {
|
||||
if (!added) {
|
||||
print ""
|
||||
print "## 라우팅"
|
||||
print ""
|
||||
print route_line
|
||||
}
|
||||
}
|
||||
' "$rules_file" > "$tmp"
|
||||
fi
|
||||
mv "$tmp" "$rules_file"
|
||||
}
|
||||
|
||||
ensure_agent_test_profile() {
|
||||
local target_dir="$1"
|
||||
local env="$2"
|
||||
local profile="$3"
|
||||
local domain="$4"
|
||||
local verification_type="$5"
|
||||
local scope="$6"
|
||||
local today="$7"
|
||||
local profile_file="$target_dir/agent-test/$env/$profile.md"
|
||||
local rules_file="$target_dir/agent-test/$env/rules.md"
|
||||
local route_line
|
||||
|
||||
mkdir -p "$(dirname "$profile_file")"
|
||||
|
||||
if [ ! -f "$profile_file" ]; then
|
||||
cat > "$profile_file" <<EOF
|
||||
---
|
||||
test_env: $env
|
||||
test_profile: $profile
|
||||
domain: $domain
|
||||
verification_type: $verification_type
|
||||
last_rule_updated_at: $today
|
||||
---
|
||||
|
||||
# $profile $env 테스트
|
||||
|
||||
## 읽기 조건
|
||||
|
||||
- $domain 도메인 또는 $scope 검증 기준이 필요한 경우
|
||||
|
||||
## 적용 범위
|
||||
|
||||
- <확인 필요>
|
||||
|
||||
## 분류
|
||||
|
||||
- domain: $domain
|
||||
- verification_type: $verification_type
|
||||
- scope: $scope
|
||||
|
||||
## 환경
|
||||
|
||||
- host:
|
||||
- port:
|
||||
- runtime:
|
||||
- package manager:
|
||||
- docker:
|
||||
- external service:
|
||||
- model endpoint:
|
||||
- credential:
|
||||
|
||||
## 명령
|
||||
|
||||
- setup:
|
||||
- lint:
|
||||
- unit:
|
||||
- smoke:
|
||||
- e2e:
|
||||
- model:
|
||||
- full-cycle:
|
||||
|
||||
## 필수 검증
|
||||
|
||||
- <확인 필요>
|
||||
|
||||
## 보조 검증
|
||||
|
||||
- <확인 필요>
|
||||
|
||||
## 판정 기준
|
||||
|
||||
- <확인 필요>
|
||||
|
||||
## 기준 출력 예시
|
||||
|
||||
\`\`\`text
|
||||
<필요한 경우 기대 출력 예시>
|
||||
\`\`\`
|
||||
|
||||
## 차단 기준
|
||||
|
||||
- <확인 필요>
|
||||
|
||||
## 보고 항목
|
||||
|
||||
- 실행한 명령:
|
||||
- 성공한 검증:
|
||||
- 실패/차단된 검증:
|
||||
- 생략 사유:
|
||||
- 남은 위험:
|
||||
|
||||
## 금지 사항
|
||||
|
||||
- 확인되지 않은 host, port, token, endpoint를 추측해 쓰지 않는다.
|
||||
- secret, token, 개인 endpoint 원문은 tracked 파일에 기록하지 않는다.
|
||||
EOF
|
||||
fi
|
||||
|
||||
route_line="- $domain / $verification_type / $scope: \`agent-test/$env/$profile.md\`"
|
||||
ensure_agent_test_route "$rules_file" "$route_line"
|
||||
}
|
||||
|
||||
ensure_agent_test_local() {
|
||||
local target_dir="$1"
|
||||
local local_rules="$target_dir/agent-test/local/rules.md"
|
||||
local today
|
||||
local domain
|
||||
local created_domain_profile="0"
|
||||
|
||||
today="$(date +%F)"
|
||||
mkdir -p "$(dirname "$local_rules")"
|
||||
mkdir -p "$target_dir/agent-test/runs"
|
||||
|
||||
if [ ! -f "$local_rules" ]; then
|
||||
cat > "$local_rules" <<EOF
|
||||
---
|
||||
test_env: local
|
||||
last_rule_updated_at: $today
|
||||
---
|
||||
|
||||
# local 테스트 규칙
|
||||
|
||||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||
|
||||
## 공통 규칙
|
||||
|
||||
- local 테스트/검증은 이 파일을 기준으로 판단한다.
|
||||
- 작업 완료 검증은 변경 범위 기준으로 선택한다.
|
||||
- 필수 검증을 실행하지 못하면 차단 사유로 보고한다.
|
||||
- 최종 보고에는 실행 명령, 결과, 생략 사유, 남은 위험을 남긴다.
|
||||
- 환경값, secret, 개인 endpoint는 tracked docs/roadmap에 쓰지 않는다.
|
||||
|
||||
## 기본 환경
|
||||
|
||||
- host:
|
||||
- port:
|
||||
- runtime:
|
||||
- package manager:
|
||||
- docker:
|
||||
- external service:
|
||||
- model endpoint:
|
||||
- credential:
|
||||
|
||||
## 라우팅
|
||||
|
||||
- 현재 등록된 도메인/검증 시나리오별 테스트 문서 없음.
|
||||
|
||||
## 라우팅 규칙
|
||||
|
||||
- 여러 항목이 맞으면 모두 읽는다.
|
||||
- 도메인 매핑이나 domain rule이 있으면 각 도메인의 <domain>-smoke 문서를 기본 baseline으로 둔다.
|
||||
- 도메인이 아직 없을 때만 project-smoke를 fallback baseline으로 둔다.
|
||||
- 도메인/검증 시나리오별 문서는 다른 테스트 문서로 라우팅하지 않는다.
|
||||
EOF
|
||||
fi
|
||||
|
||||
while IFS= read -r domain; do
|
||||
[ -n "$domain" ] || continue
|
||||
created_domain_profile="1"
|
||||
ensure_agent_test_profile \
|
||||
"$target_dir" \
|
||||
"local" \
|
||||
"$domain-smoke" \
|
||||
"$domain" \
|
||||
"smoke" \
|
||||
"도메인 기본 smoke 검증" \
|
||||
"$today"
|
||||
done < <(discover_agent_test_domains "$target_dir" | sort -u)
|
||||
|
||||
if [ "$created_domain_profile" = "0" ]; then
|
||||
ensure_agent_test_profile \
|
||||
"$target_dir" \
|
||||
"local" \
|
||||
"project-smoke" \
|
||||
"project-wide" \
|
||||
"smoke" \
|
||||
"프로젝트 기본 smoke 검증" \
|
||||
"$today"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Initializing agent-ops in: $TARGET_DIR"
|
||||
echo "Source agent-ops: $SOURCE_DIR"
|
||||
|
||||
# 1. 대상 폴더 생성 (프로젝트 전용 폴더는 내용 복사 없이 폴더만 생성)
|
||||
create_project_agent_ops_dirs "$TARGET_DIR/agent-ops"
|
||||
|
||||
# 2. 공통 요소 복사 (프로젝트 전용 설정은 제외)
|
||||
copy_common_agent_ops "$SOURCE_DIR" "$TARGET_DIR/agent-ops"
|
||||
|
||||
# 3. 에이전트 진입 파일 생성 (common/rules.md 복사)
|
||||
COMMON_RULES="$SOURCE_DIR/rules/common/rules.md"
|
||||
apply_agent_ops_entry_files "$COMMON_RULES" "$TARGET_DIR"
|
||||
ensure_common_rules_file "$SOURCE_DIR" "$TARGET_DIR/agent-ops"
|
||||
|
||||
# 4. AI ignore / permission 설정
|
||||
ensure_agent_ops_ai_ignore_config "$TARGET_DIR"
|
||||
|
||||
# 5. .gitignore 설정
|
||||
TOUCH_GITIGNORE="$TARGET_DIR/.gitignore"
|
||||
touch "$TOUCH_GITIGNORE"
|
||||
agent_ops_ensure_gitignore_task_artifact_block "$TOUCH_GITIGNORE"
|
||||
if ! grep -q "agent-ops/rules/private/" "$TOUCH_GITIGNORE"; then
|
||||
echo "" >> "$TOUCH_GITIGNORE"
|
||||
echo "# Agent-Ops Private Rules" >> "$TOUCH_GITIGNORE"
|
||||
echo "agent-ops/rules/private/" >> "$TOUCH_GITIGNORE"
|
||||
fi
|
||||
if ! grep -qxF "# Agent-Test Local Environment" "$TOUCH_GITIGNORE"; then
|
||||
echo "" >> "$TOUCH_GITIGNORE"
|
||||
echo "# Agent-Test Local Environment" >> "$TOUCH_GITIGNORE"
|
||||
fi
|
||||
ensure_gitignore_entry "$TOUCH_GITIGNORE" "agent-test/local/"
|
||||
ensure_gitignore_entry "$TOUCH_GITIGNORE" "agent-test/runs/"
|
||||
|
||||
# 6. local 테스트 환경 생성
|
||||
ensure_agent_test_local "$TARGET_DIR"
|
||||
|
||||
echo "Successfully initialized agent-ops in $TARGET_DIR"
|
||||
echo "Note: agent-ops/rules/project and agent-ops/skills/project are initialized as empty."
|
||||
echo "Note: agent-test/local rules and baseline test profiles are initialized and ignored by git."
|
||||
370
agent-ops/bin/roadmap-dependency-checker.sh
Executable file
370
agent-ops/bin/roadmap-dependency-checker.sh
Executable file
|
|
@ -0,0 +1,370 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
AGENT_OPS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PROJECT_ROOT="$(cd "$AGENT_OPS_DIR/.." && pwd)"
|
||||
WORKSPACE_ROOT="$(cd "$PROJECT_ROOT/.." && pwd)"
|
||||
LOCKS_FILE="${AGENT_ROADMAP_LOCKS_FILE:-$WORKSPACE_ROOT/.agent-roadmap-sync/locks.yaml}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: roadmap-dependency-checker.sh <lock-id> [locks.yaml]
|
||||
roadmap-dependency-checker.sh --find-milestone <identity> [locked|rely-on|both] [locks.yaml]
|
||||
|
||||
Validates locked, rely-on.target, and rely-on.status for <lock-id>.
|
||||
Prints true when every rely-on.status is enable.
|
||||
Prints false when at least one rely-on.status is disable.
|
||||
With --find-milestone, prints matching lock ids as locked:<id> or rely-on:<id>,
|
||||
or none when the milestone identity is not referenced.
|
||||
|
||||
Exit codes:
|
||||
0 all dependencies are enabled
|
||||
1 at least one dependency is still disabled
|
||||
2 configuration, input, or parse error
|
||||
For --find-milestone, exit code 0 means the lookup completed; stdout is either
|
||||
matching entries or none.
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MODE="check"
|
||||
LOCK_ID=""
|
||||
FIND_IDENTITY=""
|
||||
FIND_DIRECTION="both"
|
||||
|
||||
if [[ "${1:-}" == "--find-milestone" ]]; then
|
||||
MODE="find"
|
||||
if [[ $# -lt 2 || $# -gt 4 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
FIND_IDENTITY="$2"
|
||||
if [[ -z "$FIND_IDENTITY" ]]; then
|
||||
echo "error: milestone identity is required" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ $# -ge 3 ]]; then
|
||||
case "$3" in
|
||||
locked|rely-on|both)
|
||||
FIND_DIRECTION="$3"
|
||||
if [[ $# -eq 4 ]]; then
|
||||
LOCKS_FILE="$4"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [[ $# -eq 3 ]]; then
|
||||
LOCKS_FILE="$3"
|
||||
else
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
elif [[ $# -lt 1 || $# -gt 2 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
else
|
||||
LOCK_ID="$1"
|
||||
if [[ -n "${2:-}" ]]; then
|
||||
LOCKS_FILE="$2"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "check" && -z "$LOCK_ID" ]]; then
|
||||
echo "error: lock id is required" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -r "$LOCKS_FILE" ]]; then
|
||||
echo "error: locks file not readable: $LOCKS_FILE" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "find" ]]; then
|
||||
awk -v identity="$FIND_IDENTITY" -v direction="$FIND_DIRECTION" '
|
||||
function trim(value) {
|
||||
gsub(/\r/, "", value)
|
||||
sub(/^[[:space:]]+/, "", value)
|
||||
sub(/[[:space:]]+$/, "", value)
|
||||
return value
|
||||
}
|
||||
|
||||
function unquote(value) {
|
||||
value = trim(value)
|
||||
sub(/[[:space:]]+#.*$/, "", value)
|
||||
value = trim(value)
|
||||
if (value ~ /^".*"$/ || value ~ /^'\''.*'\''$/) {
|
||||
return substr(value, 2, length(value) - 2)
|
||||
}
|
||||
return trim(value)
|
||||
}
|
||||
|
||||
function reset_entry() {
|
||||
entry_id = ""
|
||||
locked = ""
|
||||
in_rely_on = 0
|
||||
rely_count = 0
|
||||
delete rely_targets
|
||||
}
|
||||
|
||||
function finish_entry( i) {
|
||||
if (entry_id == "") {
|
||||
return
|
||||
}
|
||||
if ((direction == "both" || direction == "locked") && locked == identity) {
|
||||
print "locked:" entry_id
|
||||
found = 1
|
||||
}
|
||||
if (direction == "both" || direction == "rely-on") {
|
||||
for (i = 1; i <= rely_count; i++) {
|
||||
if (rely_targets[i] == identity) {
|
||||
print "rely-on:" entry_id
|
||||
found = 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
found = 0
|
||||
reset_entry()
|
||||
}
|
||||
|
||||
/^[[:space:]]*($|#)/ {
|
||||
next
|
||||
}
|
||||
|
||||
/^[[:space:]]*-[[:space:]]*id:[[:space:]]*/ {
|
||||
finish_entry()
|
||||
reset_entry()
|
||||
value = $0
|
||||
sub(/^[[:space:]]*-[[:space:]]*id:[[:space:]]*/, "", value)
|
||||
entry_id = unquote(value)
|
||||
next
|
||||
}
|
||||
|
||||
entry_id != "" && /^[[:space:]]*locked:[[:space:]]*/ {
|
||||
value = $0
|
||||
sub(/^[[:space:]]*locked:[[:space:]]*/, "", value)
|
||||
locked = unquote(value)
|
||||
next
|
||||
}
|
||||
|
||||
entry_id != "" && /^[[:space:]]*rely-on:[[:space:]]*/ {
|
||||
in_rely_on = 1
|
||||
next
|
||||
}
|
||||
|
||||
entry_id != "" && in_rely_on && /^[[:space:]]*-[[:space:]]*target:[[:space:]]*/ {
|
||||
value = $0
|
||||
sub(/^[[:space:]]*-[[:space:]]*target:[[:space:]]*/, "", value)
|
||||
rely_targets[++rely_count] = unquote(value)
|
||||
next
|
||||
}
|
||||
|
||||
entry_id != "" && in_rely_on && /^[[:space:]]*target:[[:space:]]*/ {
|
||||
value = $0
|
||||
sub(/^[[:space:]]*target:[[:space:]]*/, "", value)
|
||||
rely_targets[++rely_count] = unquote(value)
|
||||
next
|
||||
}
|
||||
|
||||
END {
|
||||
finish_entry()
|
||||
if (!found) {
|
||||
print "none"
|
||||
}
|
||||
}
|
||||
' "$LOCKS_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
awk -v wanted="$LOCK_ID" '
|
||||
function trim(value) {
|
||||
gsub(/\r/, "", value)
|
||||
sub(/^[[:space:]]+/, "", value)
|
||||
sub(/[[:space:]]+$/, "", value)
|
||||
return value
|
||||
}
|
||||
|
||||
function unquote(value) {
|
||||
value = trim(value)
|
||||
sub(/[[:space:]]+#.*$/, "", value)
|
||||
value = trim(value)
|
||||
if (value ~ /^".*"$/ || value ~ /^'\''.*'\''$/) {
|
||||
return substr(value, 2, length(value) - 2)
|
||||
}
|
||||
return trim(value)
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
error = message
|
||||
printf "error: %s\n", error > "/dev/stderr"
|
||||
exit 2
|
||||
}
|
||||
|
||||
function finish_dependency() {
|
||||
if (!selected || !in_dependency) {
|
||||
return
|
||||
}
|
||||
if (!dependency_has_target) {
|
||||
fail("missing rely-on.target for lock id: " wanted)
|
||||
}
|
||||
if (!dependency_has_status) {
|
||||
fail("missing rely-on.status for lock id: " wanted)
|
||||
}
|
||||
in_dependency = 0
|
||||
}
|
||||
|
||||
function finish_entry() {
|
||||
if (!selected) {
|
||||
return
|
||||
}
|
||||
finish_dependency()
|
||||
if (!entry_has_locked) {
|
||||
fail("missing locked for lock id: " wanted)
|
||||
}
|
||||
if (total == 0) {
|
||||
fail("no rely-on.status entries for lock id: " wanted)
|
||||
}
|
||||
selected = 0
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
selected = 0
|
||||
matches = 0
|
||||
total = 0
|
||||
enabled = 0
|
||||
error = ""
|
||||
entry_has_locked = 0
|
||||
in_rely_on = 0
|
||||
in_dependency = 0
|
||||
dependency_has_target = 0
|
||||
dependency_has_status = 0
|
||||
}
|
||||
|
||||
/^[[:space:]]*($|#)/ {
|
||||
next
|
||||
}
|
||||
|
||||
/^[[:space:]]*-[[:space:]]*id:[[:space:]]*/ {
|
||||
finish_entry()
|
||||
value = $0
|
||||
sub(/^[[:space:]]*-[[:space:]]*id:[[:space:]]*/, "", value)
|
||||
id = unquote(value)
|
||||
if (id == wanted) {
|
||||
if (matches > 0) {
|
||||
fail("duplicate lock id: " wanted)
|
||||
}
|
||||
selected = 1
|
||||
matches++
|
||||
total = 0
|
||||
enabled = 0
|
||||
entry_has_locked = 0
|
||||
in_rely_on = 0
|
||||
in_dependency = 0
|
||||
dependency_has_target = 0
|
||||
dependency_has_status = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
selected && /^[[:space:]]*locked:[[:space:]]*/ {
|
||||
value = $0
|
||||
sub(/^[[:space:]]*locked:[[:space:]]*/, "", value)
|
||||
locked = unquote(value)
|
||||
if (locked == "") {
|
||||
fail("empty locked for lock id: " wanted)
|
||||
}
|
||||
entry_has_locked = 1
|
||||
next
|
||||
}
|
||||
|
||||
selected && /^[[:space:]]*rely-on:[[:space:]]*/ {
|
||||
in_rely_on = 1
|
||||
next
|
||||
}
|
||||
|
||||
selected && in_rely_on && /^[[:space:]]*-[[:space:]]*target:[[:space:]]*/ {
|
||||
finish_dependency()
|
||||
in_dependency = 1
|
||||
dependency_has_target = 0
|
||||
dependency_has_status = 0
|
||||
value = $0
|
||||
sub(/^[[:space:]]*-[[:space:]]*target:[[:space:]]*/, "", value)
|
||||
target = unquote(value)
|
||||
if (target == "") {
|
||||
fail("empty rely-on.target for lock id: " wanted)
|
||||
}
|
||||
dependency_has_target = 1
|
||||
next
|
||||
}
|
||||
|
||||
selected && in_rely_on && /^[[:space:]]*target:[[:space:]]*/ {
|
||||
if (!in_dependency) {
|
||||
fail("rely-on.target outside dependency item for lock id: " wanted)
|
||||
}
|
||||
value = $0
|
||||
sub(/^[[:space:]]*target:[[:space:]]*/, "", value)
|
||||
target = unquote(value)
|
||||
if (target == "") {
|
||||
fail("empty rely-on.target for lock id: " wanted)
|
||||
}
|
||||
dependency_has_target = 1
|
||||
next
|
||||
}
|
||||
|
||||
selected && in_rely_on && /^[[:space:]]*status:[[:space:]]*/ {
|
||||
if (!in_dependency) {
|
||||
fail("rely-on.status outside dependency item for lock id: " wanted)
|
||||
}
|
||||
value = $0
|
||||
sub(/^[[:space:]]*status:[[:space:]]*/, "", value)
|
||||
status = unquote(value)
|
||||
if (status != "enable" && status != "disable") {
|
||||
fail("invalid rely-on.status for " wanted ": " status)
|
||||
}
|
||||
if (dependency_has_status) {
|
||||
fail("duplicate rely-on.status for lock id: " wanted)
|
||||
}
|
||||
dependency_has_status = 1
|
||||
total++
|
||||
if (status == "enable") {
|
||||
enabled++
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
selected && in_rely_on && /^[[:space:]]*-[[:space:]]*/ {
|
||||
finish_dependency()
|
||||
fail("missing rely-on.target for lock id: " wanted)
|
||||
}
|
||||
|
||||
selected && /^[[:space:]]*status:[[:space:]]*/ {
|
||||
fail("status outside rely-on for lock id: " wanted)
|
||||
}
|
||||
|
||||
END {
|
||||
if (error != "") {
|
||||
exit 2
|
||||
}
|
||||
if (matches == 0) {
|
||||
printf "error: lock id not found: %s\n", wanted > "/dev/stderr"
|
||||
exit 2
|
||||
}
|
||||
finish_entry()
|
||||
if (total == enabled) {
|
||||
print "true"
|
||||
exit 0
|
||||
}
|
||||
print "false"
|
||||
exit 1
|
||||
}
|
||||
' "$LOCKS_FILE"
|
||||
375
agent-ops/bin/sync.sh
Executable file
375
agent-ops/bin/sync.sh
Executable file
|
|
@ -0,0 +1,375 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ── 경로 설정 ────────────────────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
AGENT_OPS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PROJECT_ROOT="$(cd "$AGENT_OPS_DIR/.." && pwd)"
|
||||
|
||||
# ── 색상 ─────────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; RESET='\033[0m'
|
||||
|
||||
# ── 진입 파일 공통 관리 ──────────────────────────────────────────────────────
|
||||
source "$SCRIPT_DIR/entry-files.sh"
|
||||
source "$SCRIPT_DIR/ai-ignore.sh"
|
||||
|
||||
# ── 대상 경로 해석 (폴더명 / 상대경로 / 절대경로) ────────────────────────────
|
||||
resolve_target() {
|
||||
local input="$1"
|
||||
if [[ "$input" == /* ]]; then
|
||||
[[ -d "$input" ]] && echo "$input" || echo ""
|
||||
return
|
||||
fi
|
||||
if [[ "$input" == */* ]]; then
|
||||
local resolved
|
||||
resolved="$(cd "$input" 2>/dev/null && pwd)" || { echo ""; return; }
|
||||
echo "$resolved"
|
||||
return
|
||||
fi
|
||||
local sibling="$(dirname "$PROJECT_ROOT")/$input"
|
||||
[[ -d "$sibling" ]] && echo "$sibling" || echo ""
|
||||
}
|
||||
|
||||
# ── 버전 비교: v1 > v2 이면 0 반환 ───────────────────────────────────────────
|
||||
version_gt() {
|
||||
local v1="$1" v2="$2"
|
||||
[[ "$v1" == "$v2" ]] && return 1
|
||||
[[ "$(printf '%s\n%s' "$v1" "$v2" | sort -V | head -1)" == "$v2" ]]
|
||||
}
|
||||
|
||||
# ── 버전 +1 ──────────────────────────────────────────────────────────────────
|
||||
bump_version() {
|
||||
bash "$SCRIPT_DIR/bump-version.sh" "$1"
|
||||
}
|
||||
|
||||
# ── 폴더 동기화 (삭제된 파일도 반영) ────────────────────────────────────────
|
||||
sync_folder() {
|
||||
local src="$1" dst="$2" exclude="${3:-}"
|
||||
mkdir -p "$dst"
|
||||
# dst에서 src에 없는 항목 제거 (exclude 파일 보존)
|
||||
find "$dst" -mindepth 1 -maxdepth 1 | while IFS= read -r item; do
|
||||
local name
|
||||
name="$(basename "$item")"
|
||||
[[ -n "$exclude" && "$name" == "$exclude" ]] && continue
|
||||
if [[ ! -e "$src/$name" ]]; then
|
||||
rm -rf "$item"
|
||||
fi
|
||||
done
|
||||
# src에서 dst로 복사 (exclude 파일 제외)
|
||||
find "$src" -mindepth 1 -maxdepth 1 | while IFS= read -r item; do
|
||||
local name
|
||||
name="$(basename "$item")"
|
||||
[[ -n "$exclude" && "$name" == "$exclude" ]] && continue
|
||||
rm -rf "$dst/$name"
|
||||
cp -r "$item" "$dst/"
|
||||
done
|
||||
}
|
||||
|
||||
sync_common() {
|
||||
local src="$1" dst="$2"
|
||||
sync_folder "$src/rules/common" "$dst/rules/common"
|
||||
sync_folder "$src/skills/common" "$dst/skills/common"
|
||||
sync_folder "$src/bin" "$dst/bin"
|
||||
}
|
||||
|
||||
common_differs() {
|
||||
local src="$1" dst="$2"
|
||||
local path
|
||||
|
||||
for path in "rules/common" "skills/common" "bin"; do
|
||||
if [[ ! -d "$src/$path" || ! -d "$dst/$path" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if ! diff -qr "$src/$path" "$dst/$path" >/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
entry_files_differ() {
|
||||
local rules_md="$1" target_root="$2"
|
||||
local f
|
||||
|
||||
[[ -f "$rules_md" ]] || return 0
|
||||
for f in "${AGENT_OPS_ENTRY_FILES[@]}"; do
|
||||
if [[ ! -f "$target_root/$f" ]] || ! cmp -s "$rules_md" "$target_root/$f"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
create_project_agent_ops_dirs() {
|
||||
local agent_ops_dir="$1"
|
||||
mkdir -p "$agent_ops_dir/rules/project/domain"
|
||||
mkdir -p "$agent_ops_dir/rules/private"
|
||||
mkdir -p "$agent_ops_dir/skills/project"
|
||||
}
|
||||
|
||||
copy_common_scaffold() {
|
||||
local src="$1" dst="$2"
|
||||
mkdir -p "$dst/rules" "$dst/skills"
|
||||
cp "$src/.version" "$dst/"
|
||||
rm -rf "$dst/bin" "$dst/rules/common" "$dst/skills/common"
|
||||
cp -r "$src/bin" "$dst/"
|
||||
cp -r "$src/rules/common" "$dst/rules/"
|
||||
cp -r "$src/skills/common" "$dst/skills/"
|
||||
create_project_agent_ops_dirs "$dst"
|
||||
}
|
||||
|
||||
discover_agent_ops_targets() {
|
||||
local parent
|
||||
parent="$(dirname "$PROJECT_ROOT")"
|
||||
|
||||
find "$parent" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r candidate; do
|
||||
local resolved
|
||||
resolved="$(cd "$candidate" && pwd)"
|
||||
[[ "$resolved" == "$PROJECT_ROOT" ]] && continue
|
||||
[[ -d "$resolved/agent-ops" ]] || continue
|
||||
echo "$resolved"
|
||||
done
|
||||
}
|
||||
|
||||
agent_ops_git_paths() {
|
||||
local include_ai_config="${1:-1}"
|
||||
|
||||
printf '%s\n' "agent-ops/.version"
|
||||
printf '%s\n' "agent-ops/bin"
|
||||
printf '%s\n' "agent-ops/rules/common"
|
||||
printf '%s\n' "agent-ops/skills/common"
|
||||
|
||||
local f
|
||||
for f in "${AGENT_OPS_ENTRY_FILES[@]}"; do
|
||||
if [[ -e "$f" ]] || git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
|
||||
printf '%s\n' "$f"
|
||||
fi
|
||||
done
|
||||
if [[ "$include_ai_config" != "1" || -f ".agent-ops-source" ]]; then
|
||||
return
|
||||
fi
|
||||
for f in "${AGENT_OPS_AI_SYNC_FILES[@]}"; do
|
||||
if [[ -e "$f" ]] || git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
|
||||
printf '%s\n' "$f"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
commit_and_push_agent_ops_scope() {
|
||||
local repo="$1" message="$2"
|
||||
local include_ai_config="${3:-1}"
|
||||
|
||||
if ! git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: git 저장소가 아닙니다: $repo${RESET}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$repo"
|
||||
mapfile -t paths < <(agent_ops_git_paths "$include_ai_config")
|
||||
git add -- "${paths[@]}"
|
||||
if git ls-files --error-unmatch "$AGENT_ROADMAP_CURRENT_LOCAL_PATTERN" >/dev/null 2>&1 \
|
||||
&& grep -qxF "$AGENT_ROADMAP_CURRENT_LOCAL_PATTERN" ".gitignore" 2>/dev/null; then
|
||||
git rm --cached --quiet --ignore-unmatch "$AGENT_ROADMAP_CURRENT_LOCAL_PATTERN"
|
||||
paths+=("$AGENT_ROADMAP_CURRENT_LOCAL_PATTERN")
|
||||
fi
|
||||
|
||||
if git diff --cached --quiet -- "${paths[@]}"; then
|
||||
echo " agent-ops 변경 없음: commit 건너뜀"
|
||||
else
|
||||
git commit -m "$message" -- "${paths[@]}"
|
||||
fi
|
||||
|
||||
git push
|
||||
)
|
||||
}
|
||||
|
||||
sync_framework_to_target() {
|
||||
local target="$1"
|
||||
local dst_ao="$target/agent-ops"
|
||||
local src_ver
|
||||
src_ver="$(cat "$SRC_AO/.version" 2>/dev/null || echo "0.0.0")"
|
||||
|
||||
echo "▶ $(basename "$PROJECT_ROOT") → $(basename "$target")"
|
||||
|
||||
if ! git -C "$target" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: git 저장소가 아닙니다: $target${RESET}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$dst_ao" ]]; then
|
||||
echo " 최초 진행: agent-ops 공통 scaffold 복사"
|
||||
copy_common_scaffold "$SRC_AO" "$dst_ao"
|
||||
apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$target"
|
||||
echo -e "${YELLOW} init-agent-ops 스킬로 초기화를 진행하세요.${RESET}"
|
||||
else
|
||||
echo " 이후 진행: common/ 동기화"
|
||||
sync_common "$SRC_AO" "$dst_ao"
|
||||
cp "$SRC_AO/.version" "$dst_ao/.version"
|
||||
apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$target"
|
||||
fi
|
||||
ensure_agent_ops_ai_ignore_config "$target"
|
||||
|
||||
commit_and_push_agent_ops_scope "$target" "sync: agent-ops from $(basename "$PROJECT_ROOT") v$src_ver"
|
||||
echo -e "${GREEN}✓ 완료 → $(basename "$target")${RESET}"
|
||||
}
|
||||
|
||||
# ── 사용법 ────────────────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
echo "사용법: $0 [--pull] [target]"
|
||||
echo ""
|
||||
echo " (기본) push 동기화"
|
||||
echo " --pull target(공통 원본 repo, 기본 agentic-framework) → 현재 프로젝트 로 pull"
|
||||
echo ""
|
||||
echo " target: 폴더명 (동일 레벨 탐색) | 상대경로 | 절대경로"
|
||||
echo " .agent-ops-source 원본 repo에서 target 생략 시 상위 폴더의 agent-ops 적용 프로젝트 전체 동기화"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
PULL_MODE="0"
|
||||
if [[ "${1:-}" == "--pull" ]]; then
|
||||
PULL_MODE="1"
|
||||
shift
|
||||
fi
|
||||
|
||||
TARGET_INPUT="${1:-}"
|
||||
|
||||
SRC_AO="$AGENT_OPS_DIR"
|
||||
IS_FRAMEWORK="$([[ -f "$PROJECT_ROOT/.agent-ops-source" ]] && echo "1" || echo "0")"
|
||||
|
||||
# ── pull 모드: .agent-ops-source 원본 repo → 현재 프로젝트 ───────────────────
|
||||
if [[ "$PULL_MODE" == "1" ]]; then
|
||||
if [[ -z "$TARGET_INPUT" ]]; then
|
||||
usage; exit 1
|
||||
fi
|
||||
TARGET="$(resolve_target "$TARGET_INPUT")"
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
echo -e "${RED}Error: 대상을 찾을 수 없습니다: $TARGET_INPUT${RESET}"
|
||||
exit 1
|
||||
fi
|
||||
DST_AO="$TARGET/agent-ops"
|
||||
|
||||
if [[ "$IS_FRAMEWORK" == "1" ]]; then
|
||||
echo -e "${RED}Error: .agent-ops-source가 있는 공통 원본 repo에서는 --pull을 사용할 수 없습니다.${RESET}"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$TARGET/.agent-ops-source" ]]; then
|
||||
echo -e "${RED}Error: target이 공통 원본 repo가 아닙니다 (.agent-ops-source 없음)${RESET}"
|
||||
exit 1
|
||||
fi
|
||||
SRC_VER="$(cat "$SRC_AO/.version" 2>/dev/null || echo "0.0.0")"
|
||||
DST_VER="$(cat "$DST_AO/.version" 2>/dev/null || echo "0.0.0")"
|
||||
echo "▶ $(basename "$TARGET") → $(basename "$PROJECT_ROOT") (pull)"
|
||||
echo " 현재 버전: $SRC_VER | 원본 버전: $DST_VER"
|
||||
sync_common "$DST_AO" "$SRC_AO"
|
||||
cp "$DST_AO/.version" "$SRC_AO/.version"
|
||||
apply_agent_ops_entry_files "$DST_AO/rules/common/rules.md" "$PROJECT_ROOT"
|
||||
ensure_agent_ops_ai_ignore_config "$PROJECT_ROOT"
|
||||
commit_and_push_agent_ops_scope "$PROJECT_ROOT" "sync: pull from agentic-framework v$DST_VER"
|
||||
echo -e "${GREEN}✓ 완료 (pull v$DST_VER)${RESET}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── .agent-ops-source 원본 repo에서 다른 프로젝트로 ──────────────────────────
|
||||
if [[ "$IS_FRAMEWORK" == "1" ]]; then
|
||||
SRC_VER="$(cat "$SRC_AO/.version" 2>/dev/null || echo "0.0.0")"
|
||||
echo " .agent-ops-source 감지: push 동기화에서는 버전 bump 없이 v$SRC_VER 그대로 전파"
|
||||
if entry_files_differ "$SRC_AO/rules/common/rules.md" "$PROJECT_ROOT"; then
|
||||
echo " 공통 원본 repo 진입 파일 재적용"
|
||||
apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$PROJECT_ROOT"
|
||||
commit_and_push_agent_ops_scope "$PROJECT_ROOT" "sync: reapply agent-ops entry files" "0"
|
||||
fi
|
||||
if [[ -n "$TARGET_INPUT" ]]; then
|
||||
TARGET="$(resolve_target "$TARGET_INPUT")"
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
echo -e "${RED}Error: 대상을 찾을 수 없습니다: $TARGET_INPUT${RESET}"
|
||||
exit 1
|
||||
fi
|
||||
sync_framework_to_target "$TARGET"
|
||||
else
|
||||
mapfile -t TARGETS < <(discover_agent_ops_targets)
|
||||
if [[ "${#TARGETS[@]}" -eq 0 ]]; then
|
||||
echo -e "${YELLOW}agent-ops가 적용된 sibling 프로젝트가 없습니다.${RESET}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
STATUS=0
|
||||
for TARGET in "${TARGETS[@]}"; do
|
||||
if ! sync_framework_to_target "$TARGET"; then
|
||||
STATUS=1
|
||||
fi
|
||||
done
|
||||
exit "$STATUS"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── 일반 프로젝트에서 .agent-ops-source 원본 repo로 (push) ───────────────────
|
||||
if [[ -z "$TARGET_INPUT" ]]; then
|
||||
TARGET_INPUT="agentic-framework"
|
||||
fi
|
||||
|
||||
TARGET="$(resolve_target "$TARGET_INPUT")"
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
echo -e "${RED}Error: 대상을 찾을 수 없습니다: $TARGET_INPUT${RESET}"
|
||||
exit 1
|
||||
fi
|
||||
DST_AO="$TARGET/agent-ops"
|
||||
|
||||
echo "▶ $(basename "$PROJECT_ROOT") → $(basename "$TARGET")"
|
||||
|
||||
if [[ ! -f "$TARGET/.agent-ops-source" ]]; then
|
||||
echo -e "${RED}Error: target이 공통 원본 repo가 아닙니다 (.agent-ops-source 없음)${RESET}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SRC_VER="$(cat "$SRC_AO/.version" 2>/dev/null || echo "0.0.0")"
|
||||
DST_VER="$(cat "$DST_AO/.version" 2>/dev/null || echo "0.0.0")"
|
||||
echo " 현재 버전: $SRC_VER | 원본 버전: $DST_VER"
|
||||
|
||||
COMMON_CHANGED="1"
|
||||
if ! common_differs "$SRC_AO" "$DST_AO"; then
|
||||
COMMON_CHANGED="0"
|
||||
fi
|
||||
|
||||
ENTRY_FILES_CHANGED="0"
|
||||
if entry_files_differ "$SRC_AO/rules/common/rules.md" "$PROJECT_ROOT" \
|
||||
|| entry_files_differ "$DST_AO/rules/common/rules.md" "$TARGET"; then
|
||||
ENTRY_FILES_CHANGED="1"
|
||||
fi
|
||||
|
||||
if [[ "$COMMON_CHANGED" == "1" ]] && version_gt "$DST_VER" "$SRC_VER"; then
|
||||
echo -e "${RED}⚠ 버전 충돌: 원본 repo($DST_VER) > current($SRC_VER)${RESET}"
|
||||
echo -e "${YELLOW} 먼저 sync-pull로 내려받은 뒤 재시도하세요.${RESET}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$COMMON_CHANGED" == "0" && "$ENTRY_FILES_CHANGED" == "0" ]]; then
|
||||
echo " 공통 파일/진입 파일 변경 없음: .version 차이는 무시하고 버전 갱신/커밋을 건너뜀"
|
||||
echo -e "${GREEN}✓ 완료 (공통 변경 없음)${RESET}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$COMMON_CHANGED" == "0" ]]; then
|
||||
echo " 공통 파일 변경 없음: 진입 파일만 재적용"
|
||||
apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$PROJECT_ROOT"
|
||||
apply_agent_ops_entry_files "$DST_AO/rules/common/rules.md" "$TARGET"
|
||||
commit_and_push_agent_ops_scope "$TARGET" "sync: reapply agent-ops entry files" "0"
|
||||
commit_and_push_agent_ops_scope "$PROJECT_ROOT" "sync: reapply agent-ops entry files" "0"
|
||||
echo -e "${GREEN}✓ 완료 (진입 파일 재적용)${RESET}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
NEW_VER="$(bump_version "$SRC_VER")"
|
||||
sync_common "$SRC_AO" "$DST_AO"
|
||||
echo "$NEW_VER" > "$SRC_AO/.version"
|
||||
echo "$NEW_VER" > "$DST_AO/.version"
|
||||
apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$PROJECT_ROOT"
|
||||
apply_agent_ops_entry_files "$DST_AO/rules/common/rules.md" "$TARGET"
|
||||
|
||||
commit_and_push_agent_ops_scope "$TARGET" "sync: from $(basename "$PROJECT_ROOT") v$NEW_VER" "0"
|
||||
commit_and_push_agent_ops_scope "$PROJECT_ROOT" "sync: to agentic-framework v$NEW_VER" "0"
|
||||
|
||||
echo -e "${GREEN}✓ 완료 (v$SRC_VER → v$NEW_VER)${RESET}"
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
|
||||
|
||||
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||
- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
|
||||
- 사용자에게 보이는 최종 답변과 설명은 기본적으로 한국어로 작성한다.
|
||||
- 코드 변경 전 관련 domain rule을 먼저 확인한다.
|
||||
- 요청 범위를 넘는 변경을 하지 않는다.
|
||||
|
|
|
|||
Loading…
Reference in a new issue