diff --git a/.claude/settings.json b/.claude/settings.json index 0c2baa3..c0611b0 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -2,7 +2,8 @@ "$schema": "https://json.schemastore.org/claude-code-settings.json", "permissions": { "deny": [ - "Read(./agent-task/archive/**)" + "Read(./agent-task/archive/**)", + "Read(./agent-ops/roadmap/archive/**)" ] } } diff --git a/agent-ops/bin/ai-ignore.sh b/agent-ops/bin/ai-ignore.sh index dc5b276..186deaa 100755 --- a/agent-ops/bin/ai-ignore.sh +++ b/agent-ops/bin/ai-ignore.sh @@ -17,10 +17,127 @@ agent_ops_append_unique_line() { 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 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; + +.permissions = ((.permissions // {}) | as_object) +| .permissions.deny = ( + (.permissions.deny | as_array) + | append_unique("Read(./agent-task/archive/**)") + | append_unique("Read(./agent-ops/roadmap/archive/**)") + ) +' + + agent_ops_merge_json_with_jq \ + "$file" \ + "$filter" \ + "Note: .claude/settings.json exists; add Read(./agent-task/archive/**) and Read(./agent-ops/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) +| index("Read(./agent-task/archive/**)") and index("Read(./agent-ops/roadmap/archive/**)") +' + + if command -v jq >/dev/null 2>&1; then + jq -e "$filter" "$file" >/dev/null 2>&1 + else + grep -q "agent-task/archive" "$file" && grep -q "agent-ops/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) +| .permission.read["agent-task/archive/**"] = "deny" +| .permission.read["agent-ops/roadmap/archive/**"] = "deny" +| .permission.glob = ((.permission.glob // {}) | as_object) +| .permission.glob["agent-task/archive/**"] = "deny" +| .permission.glob["agent-ops/roadmap/archive/**"] = "deny" +| .watcher = ((.watcher // {}) | as_object) +| .watcher.ignore = ( + (.watcher.ignore | as_array) + | append_unique("agent-task/archive/**") + | append_unique("agent-ops/roadmap/archive/**") + ) +' + + agent_ops_merge_json_with_jq \ + "$file" \ + "$filter" \ + "Note: opencode.json exists; add agent-task/archive/** and agent-ops/roadmap/archive/** read/glob deny and 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.read["agent-ops/roadmap/archive/**"] == "deny") +and (.permission.glob["agent-task/archive/**"] == "deny") +and (.permission.glob["agent-ops/roadmap/archive/**"] == "deny") +and ((.watcher.ignore | as_array) | index("agent-task/archive/**") and index("agent-ops/roadmap/archive/**")) +' + + if command -v jq >/dev/null 2>&1; then + jq -e "$filter" "$file" >/dev/null 2>&1 + else + grep -q "agent-task/archive" "$file" && grep -q "agent-ops/roadmap/archive" "$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 + for ignore_file in "${AGENT_OPS_AI_IGNORE_FILES[@]}"; do agent_ops_append_unique_line "$target_dir/$ignore_file" "$AGENT_OPS_TASK_ARCHIVE_IGNORE_PATTERN" agent_ops_append_unique_line "$target_dir/$ignore_file" "$AGENT_OPS_ROADMAP_ARCHIVE_IGNORE_PATTERN" @@ -39,8 +156,8 @@ ensure_agent_ops_ai_ignore_config() { } } EOF - elif ! grep -q "agent-task/archive" "$target_dir/.claude/settings.json" || ! grep -q "agent-ops/roadmap/archive" "$target_dir/.claude/settings.json"; then - echo "Note: .claude/settings.json exists; add Read(./agent-task/archive/**) and Read(./agent-ops/roadmap/archive/**) manually." + 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 @@ -65,7 +182,7 @@ EOF } } EOF - elif ! grep -q "agent-task/archive" "$target_dir/opencode.json" || ! grep -q "agent-ops/roadmap/archive" "$target_dir/opencode.json"; then - echo "Note: opencode.json exists; add agent-task/archive/** and agent-ops/roadmap/archive/** read/glob deny and watcher ignore manually." + elif ! agent_ops_opencode_config_complete "$target_dir/opencode.json"; then + agent_ops_merge_opencode_config "$target_dir/opencode.json" fi } diff --git a/agent-ops/bin/sync.sh b/agent-ops/bin/sync.sh index 2d8c349..5f45045 100755 --- a/agent-ops/bin/sync.sh +++ b/agent-ops/bin/sync.sh @@ -120,6 +120,8 @@ discover_agent_ops_targets() { } 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" @@ -131,6 +133,9 @@ agent_ops_git_paths() { 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" @@ -140,6 +145,7 @@ agent_ops_git_paths() { 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}" @@ -148,7 +154,7 @@ commit_and_push_agent_ops_scope() { ( cd "$repo" - mapfile -t paths < <(agent_ops_git_paths) + mapfile -t paths < <(agent_ops_git_paths "$include_ai_config") git add -- "${paths[@]}" if git diff --cached --quiet -- "${paths[@]}"; then @@ -308,13 +314,8 @@ if [[ "$COMMON_CHANGED" == "1" ]] && version_gt "$DST_VER" "$SRC_VER"; then exit 1 fi -ensure_agent_ops_ai_ignore_config "$PROJECT_ROOT" -ensure_agent_ops_ai_ignore_config "$TARGET" - if [[ "$COMMON_CHANGED" == "0" ]]; then - echo " 공통 파일 변경 없음: AI ignore / permission 설정만 보강" - commit_and_push_agent_ops_scope "$TARGET" "sync: ensure agent-ops AI ignore config" - commit_and_push_agent_ops_scope "$PROJECT_ROOT" "sync: ensure agent-ops AI ignore config" + echo " 공통 파일 변경 없음: .version 차이는 무시하고 버전 갱신/커밋을 건너뜀" echo -e "${GREEN}✓ 완료 (공통 변경 없음)${RESET}" exit 0 fi @@ -324,7 +325,7 @@ sync_common "$SRC_AO" "$DST_AO" echo "$NEW_VER" > "$SRC_AO/.version" echo "$NEW_VER" > "$DST_AO/.version" -commit_and_push_agent_ops_scope "$TARGET" "sync: from $(basename "$PROJECT_ROOT") v$NEW_VER" -commit_and_push_agent_ops_scope "$PROJECT_ROOT" "sync: to agentic-framework v$NEW_VER" +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/skills/common/init-agent-ops/SKILL.md b/agent-ops/skills/common/init-agent-ops/SKILL.md index 8ad4007..950428b 100644 --- a/agent-ops/skills/common/init-agent-ops/SKILL.md +++ b/agent-ops/skills/common/init-agent-ops/SKILL.md @@ -72,7 +72,7 @@ description: 프로젝트 상태를 판별하고 agent-ops 기본 스캐폴드 | `agent-ops/rules/private/` | 폴더만 생성 (내용은 개인이 작성) | | `.gitignore`에 `agent-ops/rules/private/` 추가 | git 추적 제외 | | `.geminiignore`, `.aiexclude`, `.cursorignore`, `.clineignore` | `agent-task/archive/**`, `agent-ops/roadmap/archive/**` 한 줄씩 추가 | -| `.claude/settings.json`, `opencode.json` | 파일이 없으면 `agent-task/archive/**`, `agent-ops/roadmap/archive/**` 읽기/검색 제외 설정 생성, 있으면 덮어쓰지 않고 수동 병합 안내 | +| `.claude/settings.json`, `opencode.json` | 파일이 없으면 `agent-task/archive/**`, `agent-ops/roadmap/archive/**` 읽기/검색 제외 설정 생성, 있으면 구조적으로 병합하고 병합할 수 없을 때 수동 병합 안내 | 에이전트별 파일명: @@ -178,7 +178,8 @@ common/rules.md와 내용이 중복되지 않도록 한다. 3. **AI ignore / permission 기본 설정** - `.geminiignore`, `.aiexclude`, `.cursorignore`, `.clineignore`에 `agent-task/archive/**`와 `agent-ops/roadmap/archive/**`를 추가한다 - - `.claude/settings.json`, `opencode.json`이 없으면 archive 읽기/검색 제외 설정을 생성한다 + - `.claude/settings.json`, `opencode.json`이 없으면 archive 읽기/검색 제외 설정을 생성하고, 있으면 가능한 경우 기존 설정을 보존하며 필요한 제외 설정만 병합한다 + - 대상 루트에 `.agent-ops-source`가 있으면 AI ignore / permission 파일은 보강하지 않는다 - `agent-task/archive/**`와 `agent-ops/roadmap/archive/**` 제외는 `.gitignore`에 추가하지 않는다 4. **agent-ops 폴더 구조 복사** @@ -234,7 +235,7 @@ common/rules.md와 내용이 중복되지 않도록 한다. - [ ] `rules/project/rules.md`의 도메인 매핑 테이블에 생성된 도메인이 모두 등록되어 있는가 - [ ] `.gitignore`에 `agent-ops/rules/private/` 항목이 추가되어 있는가 - [ ] `.geminiignore`, `.aiexclude`, `.cursorignore`, `.clineignore`에 `agent-task/archive/**`와 `agent-ops/roadmap/archive/**`가 포함되어 있는가 -- [ ] `.claude/settings.json`, `opencode.json`에 `agent-task/archive/**`와 `agent-ops/roadmap/archive/**` 제외 설정이 있거나, 기존 파일 수동 병합 안내를 출력했는가 +- [ ] `.claude/settings.json`, `opencode.json`에 `agent-task/archive/**`와 `agent-ops/roadmap/archive/**` 제외 설정이 있거나, 병합 불가 시 수동 병합 안내를 출력했는가 - [ ] `.gitignore`에 `agent-task/archive/**` 또는 `agent-ops/roadmap/archive/**`를 추가하지 않았는가 - 검증 실패 시: 누락된 파일/항목을 사용자에게 알리고 해당 부분만 보완한다 diff --git a/agent-ops/skills/common/sync-pull/SKILL.md b/agent-ops/skills/common/sync-pull/SKILL.md index c8ef90e..578068c 100644 --- a/agent-ops/skills/common/sync-pull/SKILL.md +++ b/agent-ops/skills/common/sync-pull/SKILL.md @@ -31,6 +31,7 @@ agent-ops/bin/sync.sh --pull - [ ] 현재 프로젝트 버전이 framework 버전과 일치하는가 - [ ] `agent-ops/bin/entry-files.sh`의 모든 진입 파일이 갱신됐고, 내용이 framework의 `agent-ops/rules/common/rules.md`와 일치하는가 - [ ] `.geminiignore`, `.aiexclude`, `.cursorignore`, `.clineignore`에 `agent-task/archive/**`와 `agent-ops/roadmap/archive/**`가 포함되어 있는가 +- [ ] agentic-framework의 AI ignore / permission 파일은 수정되거나 stage되지 않았는가 ## 금지 사항 diff --git a/agent-ops/skills/common/sync-push/SKILL.md b/agent-ops/skills/common/sync-push/SKILL.md index 55487b9..6bbe137 100644 --- a/agent-ops/skills/common/sync-push/SKILL.md +++ b/agent-ops/skills/common/sync-push/SKILL.md @@ -27,6 +27,7 @@ description: 현재 프로젝트의 agent-ops를 agentic-framework로 올리거 2. **있으면**: `agent-ops/bin/sync.sh` 실행 또는 `agent-ops/bin/sync.sh agentic-framework` 실행 3. **없으면**: 사용자에게 agentic-framework 경로 입력을 안내하고, 입력받은 경로로 실행 4. 공통 관리 파일 변경 없이 `agent-ops/.version`만 다른 경우 `sync.sh`는 버전 갱신과 commit/push를 건너뛴다 +5. 일반 프로젝트에서 agentic-framework로 올릴 때는 AI ignore / permission 파일을 보강하거나 stage하지 않는다 ### 현 프로젝트가 agentic-framework인 경우 (`.agent-ops-source` 있음) @@ -36,6 +37,7 @@ description: 현재 프로젝트의 agent-ops를 agentic-framework로 올리거 4. target이 없으면 `sync.sh`가 현재 프로젝트 기준 상위 폴더(`../`)의 하위 디렉터리 중 `agent-ops/` 폴더가 있는 프로젝트를 모두 대상으로 삼는다 5. `sync.sh`는 현재 agentic-framework의 `agent-ops/rules/common/rules.md` 내용을 대상 프로젝트 루트의 진입 파일에 덮어쓴다 6. 적용 후 각 대상 repo에서 agent-ops 공통 관리 경로(`rules/common/rules.md` 포함), 진입 파일, AI ignore / permission 파일을 stage 하여 commit/push 한다 +7. AI ignore / permission 파일은 대상 프로젝트의 기존 내용을 덮어쓰지 않고, 누락된 표준 archive 제외 설정만 보강한다 덮어쓰기 대상은 `init-agent-ops` 초기 세팅과 동일하며, 실제 목록은 `agent-ops/bin/entry-files.sh`의 `AGENT_OPS_ENTRY_FILES`를 단일 기준으로 사용한다. @@ -53,7 +55,7 @@ description: 현재 프로젝트의 agent-ops를 agentic-framework로 올리거 agent-ops/bin/sync.sh [target] ``` -푸시 대상 path는 항상 `agent-ops/.version`, `agent-ops/bin`, `agent-ops/rules/common`, `agent-ops/skills/common`, `AGENT_OPS_ENTRY_FILES`의 진입 파일, `AGENT_OPS_AI_SYNC_FILES`의 AI ignore / permission 파일로 제한한다. +푸시 대상 path는 항상 `agent-ops/.version`, `agent-ops/bin`, `agent-ops/rules/common`, `agent-ops/skills/common`, `AGENT_OPS_ENTRY_FILES`의 진입 파일로 제한한다. AI ignore / permission 파일은 framework에서 일반 프로젝트로 내려보내는 경우에만 대상 repo에서 추가 stage한다. ## 실행 결과 검증 @@ -61,8 +63,9 @@ agent-ops/bin/sync.sh [target] - [ ] 버전 충돌 경고가 없었는가 — 있었다면 사용자에게 수동 머지 필요함을 알린다 - [ ] 대상 repo의 `agent-ops/rules/common/rules.md`가 현재 agentic-framework의 `agent-ops/rules/common/rules.md`와 일치하는가 - [ ] agentic-framework에서 대상 프로젝트로 push한 경우, `agent-ops/bin/entry-files.sh`의 모든 진입 파일 내용이 현재 agentic-framework의 `agent-ops/rules/common/rules.md`와 일치하는가 -- [ ] `.geminiignore`, `.aiexclude`, `.cursorignore`, `.clineignore`에 `agent-task/archive/**`와 `agent-ops/roadmap/archive/**`가 포함되어 있는가 -- [ ] 대상 repo에 commit/push 된 path가 agent-ops 공통 관리 경로, 진입 파일, AI ignore / permission 파일로 제한되었는가 +- [ ] framework에서 대상 프로젝트로 push한 경우, `.geminiignore`, `.aiexclude`, `.cursorignore`, `.clineignore`에 `agent-task/archive/**`와 `agent-ops/roadmap/archive/**`가 포함되어 있는가 +- [ ] 일반 프로젝트에서 agentic-framework로 push한 경우, AI ignore / permission 파일이 agentic-framework에 유입되지 않았는가 +- [ ] 대상 repo에 commit/push 된 path가 방향별 허용 범위로 제한되었는가 ## 금지 사항 diff --git a/opencode.json b/opencode.json index e8b12b6..317a052 100644 --- a/opencode.json +++ b/opencode.json @@ -2,15 +2,18 @@ "$schema": "https://opencode.ai/config.json", "permission": { "read": { - "agent-task/archive/**": "deny" + "agent-task/archive/**": "deny", + "agent-ops/roadmap/archive/**": "deny" }, "glob": { - "agent-task/archive/**": "deny" + "agent-task/archive/**": "deny", + "agent-ops/roadmap/archive/**": "deny" } }, "watcher": { "ignore": [ - "agent-task/archive/**" + "agent-task/archive/**", + "agent-ops/roadmap/archive/**" ] } }