diff --git a/.clinerules b/.clinerules index 045f54f..f9b998f 100644 --- a/.clinerules +++ b/.clinerules @@ -3,6 +3,7 @@ **현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.** - 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다. +- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다. - 사용자에게 보이는 최종 답변과 설명은 기본적으로 한국어로 작성한다. - 코드 변경 전 관련 domain rule을 먼저 확인한다. - 요청 범위를 넘는 변경을 하지 않는다. diff --git a/.cursorrules b/.cursorrules index 045f54f..f9b998f 100644 --- a/.cursorrules +++ b/.cursorrules @@ -3,6 +3,7 @@ **현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.** - 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다. +- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다. - 사용자에게 보이는 최종 답변과 설명은 기본적으로 한국어로 작성한다. - 코드 변경 전 관련 domain rule을 먼저 확인한다. - 요청 범위를 넘는 변경을 하지 않는다. diff --git a/.gitignore b/.gitignore index 016e3a3..d23469d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ __pycache__/ *.egg-info/ bin/ +!agent-ops/bin/ +!agent-ops/bin/** build/ dist/ var/ diff --git a/AGENTS.md b/AGENTS.md index 045f54f..f9b998f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,7 @@ **현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.** - 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다. +- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다. - 사용자에게 보이는 최종 답변과 설명은 기본적으로 한국어로 작성한다. - 코드 변경 전 관련 domain rule을 먼저 확인한다. - 요청 범위를 넘는 변경을 하지 않는다. diff --git a/CLAUDE.md b/CLAUDE.md index 045f54f..f9b998f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,6 +3,7 @@ **현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.** - 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다. +- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다. - 사용자에게 보이는 최종 답변과 설명은 기본적으로 한국어로 작성한다. - 코드 변경 전 관련 domain rule을 먼저 확인한다. - 요청 범위를 넘는 변경을 하지 않는다. diff --git a/GEMINI.md b/GEMINI.md index 045f54f..f9b998f 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -3,6 +3,7 @@ **현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.** - 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다. +- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다. - 사용자에게 보이는 최종 답변과 설명은 기본적으로 한국어로 작성한다. - 코드 변경 전 관련 domain rule을 먼저 확인한다. - 요청 범위를 넘는 변경을 하지 않는다. diff --git a/agent-ops/.version b/agent-ops/.version index b1c7c8b..64440a3 100644 --- a/agent-ops/.version +++ b/agent-ops/.version @@ -1 +1 @@ -1.1.167 +1.1.168 diff --git a/agent-ops/bin/ai-ignore.sh b/agent-ops/bin/ai-ignore.sh new file mode 100755 index 0000000..514dccb --- /dev/null +++ b/agent-ops/bin/ai-ignore.sh @@ -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 적용" +} diff --git a/agent-ops/bin/bump-version.sh b/agent-ops/bin/bump-version.sh new file mode 100755 index 0000000..4cc50ef --- /dev/null +++ b/agent-ops/bin/bump-version.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# bump-version.sh +# patch +1, 999 초과 시 minor 올림. major는 수동 관리. +# 결과 버전을 stdout으로 출력. + +set -euo pipefail + +YELLOW='\033[1;33m'; RESET='\033[0m' + +VERSION="${1:-}" +if [[ -z "$VERSION" ]]; then + echo "사용법: $0 " >&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" diff --git a/agent-ops/bin/entry-files.sh b/agent-ops/bin/entry-files.sh new file mode 100644 index 0000000..dfb6f90 --- /dev/null +++ b/agent-ops/bin/entry-files.sh @@ -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 +} diff --git a/agent-ops/bin/init-agent-ops.sh b/agent-ops/bin/init-agent-ops.sh new file mode 100755 index 0000000..765683a --- /dev/null +++ b/agent-ops/bin/init-agent-ops.sh @@ -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 " + 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" < + +## 분류 + +- 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" <-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." diff --git a/agent-ops/bin/roadmap-dependency-checker.sh b/agent-ops/bin/roadmap-dependency-checker.sh new file mode 100755 index 0000000..71a3dfc --- /dev/null +++ b/agent-ops/bin/roadmap-dependency-checker.sh @@ -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 [locks.yaml] + roadmap-dependency-checker.sh --find-milestone [locked|rely-on|both] [locks.yaml] + +Validates locked, rely-on.target, and rely-on.status for . +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: or rely-on:, +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" diff --git a/agent-ops/bin/sync.sh b/agent-ops/bin/sync.sh new file mode 100755 index 0000000..373738d --- /dev/null +++ b/agent-ops/bin/sync.sh @@ -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}" diff --git a/agent-ops/rules/common/rules.md b/agent-ops/rules/common/rules.md index 045f54f..f9b998f 100644 --- a/agent-ops/rules/common/rules.md +++ b/agent-ops/rules/common/rules.md @@ -3,6 +3,7 @@ **현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.** - 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다. +- `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다. - 사용자에게 보이는 최종 답변과 설명은 기본적으로 한국어로 작성한다. - 코드 변경 전 관련 domain rule을 먼저 확인한다. - 요청 범위를 넘는 변경을 하지 않는다.