Repository navigation
perf(commands): 상태 조회를 스크립트 한 번으로 묶어 왕복 축소 - #44
Conversation
모델이 개별 명령을 하나씩 실행하던 조회 구간을 스크립트 한 번 호출로
바꾼다. 판단(브랜치 분기·PR 재사용·리뷰 내용)은 그대로 모델이 한다.
/implement ls·find·cat package.json·git log·node -v·PRD → repo-context.sh
런당 14회 조회 → 1회
/review-pr gh pr view×2·gh pr diff → pr-context.sh
/auto-commit git fetch/status/diff×2/remote·gh pr list×2·ls → repo-state.sh
측정 (Claude Code 헤드리스, Opus 5, 복제본을 --plugin-dir 로 교체):
/implement 비용 가중 −25.2% [−32.6, −16.9] 턴 −30.6% 10런
/review-pr 비용 가중 −19.2% [−33.5, −5.0] 턴 −34.4% 10런
/auto-commit 비용 가중 −8.3% [−12.4, −3.9] 턴 −12.9% 92런
품질 지표(결함 탐지·기능 커버) 저하 없음, 완주율 전 조건 100%.
비용 가중 = 입력×1 + 캐시쓰기×1.25 + 캐시읽기×0.1 + 출력×5.
커맨드 본문에 "개별 git/gh 명령을 따로 실행하지 말 것"을 함께 넣었다.
이 문구가 없으면 스크립트를 돌리고도 확인차 git status 를 다시 부른다.
repo-state.sh 의 changed_files 가 git diff --cached 로만 만들어져
스테이징 전 변경이 누락됐다. staged/unstaged/untracked 를 모두 담도록
고쳤다. 정확성 수정이고 절감폭 영향은 확인되지 않았다 (n=5, p=0.50).
위 −8.3% 는 수정 전 측정치다.
CLAUDE.md·CONTRIBUTING.md 의 레이아웃 블록에 scripts/ 를 추가하고,
번들 스크립트 규약을 CONTRIBUTING 에 적었다.
버전 0.1.16 → 0.1.17 (patch).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWDghVZqdr4ZRFhm1aVDSN
|
변경된 파일:
판정: 승인 확인한 내용:
검증
검토한 head SHA:
|
|
|
||
| 반환 JSON에 파일 목록·디렉터리 구조·`package.json` 스크립트·**검증 명령(`verify_commands`)**·PRD/PLAN 경로·런타임 버전·git 상태가 들어 있다. | ||
|
|
||
| **검증은 `verify_commands`에 적힌 명령만 쓴다.** `npm test`가 실패한다고 `node --test` 변형을 시도하지 말 것 — 명령이 틀린 게 아니라 코드가 틀린 것이다. |
There was a problem hiding this comment.
이 문장의 node --test가 계약 검사기에 의해 implement 커맨드의 미정의 --test 파라미터 사용으로 해석됩니다. 실제 CI가 본문이 '--test' 를 쓰는데 ## Parameters 에 정의가 없음으로 실패했습니다. --test를 제거하거나 백틱 밖의 일반 텍스트로 바꿔 계약 검사를 통과시키세요.
check_contracts.py 는 인라인 코드(`...`) 안의 --flag 를 그 커맨드의 파라미터 사용으로 해석한다. `node --test` 예시가 ## Parameters 에 정의되지 않은 --test 사용으로 잡혀 CI 가 실패했다. 의미는 그대로 두고 백틱 밖 일반 텍스트로 바꿨다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWDghVZqdr4ZRFhm1aVDSN
| try: return json.loads(o) if o else {} | ||
| except Exception: return {} | ||
|
|
||
| meta = j(f'gh pr view {pr} --json title,body,author,baseRefName,headRefName,files,' |
There was a problem hiding this comment.
PR 번호가 f-string으로 셸 명령에 직접 들어가고 shell=True로 실행됩니다. 44; printf INJECTED; #를 인자로 주면 임의 명령이 실행되는 것을 재현했습니다. PR 번호를 정수로 검증하고 subprocess.run([...]) 인자 배열을 사용해 셸 해석을 제거해 주세요.
pr-context.sh 가 인자를 f-string 으로 gh 명령에 끼워 넣고 shell=True 로 실행했다. PR 번호에 셸 구문을 넣으면 임의 명령이 실행된다. `44; touch /tmp/PWNED; #` 로 파일 생성이 재현됐다. - subprocess 를 인자 배열(shell=False)로 바꿔 조립 자체를 없앴다 - PR 참조는 숫자 또는 GitHub PR URL 만 받는다. 그 외는 JSON 오류로 종료 (44 / #44 / https://.../pull/44 는 그대로 동작) repo-state.sh 도 브랜치명을 gh pr list 에 보간하고 있었다. git 이 ref 이름에 따옴표를 허용하지 않아 실제로는 뚫리지 않았지만, ref 규칙에 기대지 않도록 같은 방식(인자 배열)으로 바꿨다. 세 스크립트 모두 f-string 명령 조립이 0건이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWDghVZqdr4ZRFhm1aVDSN
|
|
||
| # 실제로 도는 검증 명령만 고른다 — 모델이 시행착오로 찾지 않도록 | ||
| verify = {} | ||
| for key in ('test', 'typecheck', 'lint', 'build'): |
There was a problem hiding this comment.
Major: 이 루프는 package.json scripts만 검사합니다. implement.md:146은 모델이 verify_commands에 적힌 명령만 실행하라고 강제하므로, pyproject.toml/go.mod/Cargo.toml/Makefile 기반 레포에서는 검증 명령이 빈 객체가 되어 기존 테스트·린트가 실행되지 않습니다. 지원 대상의 검증 명령을 함께 감지하거나 이 제한적인 목록을 강제하지 않도록 수정해 주세요.
| staged=[x for x in run('git diff --cached --name-only').split('\n') if x] | ||
| unstaged=[x for x in run('git diff --name-only').split('\n') if x] | ||
| untracked=[x for x in run('git ls-files --others --exclude-standard').split('\n') if x] | ||
| files=sorted(set(staged+unstaged+untracked))[:50] |
There was a problem hiding this comment.
Minor: 변경 파일을 50개로 자른 뒤 그 길이를 file_count로 반환합니다. auto-commit.md는 이 값을 전체 변경 파일 수로 설명하므로 51개 이상 변경 시 수가 조용히 틀립니다. 자르기 전 개수와 truncation 여부를 분리해 반환해 주세요.
verify_commands 가 package.json 스크립트만 보고 만들어졌다. implement.md 는 "이 목록의 명령만 쓴다"고 지시하므로, package.json 이 없는 Python/Go/Rust 레포에서는 목록이 비고 구현 에이전트가 검증을 아예 건너뛴다. - pyproject.toml(pytest·ruff·mypy), tox.ini/pytest.ini, go.mod(go test·go vet), Cargo.toml(cargo test·clippy), Makefile 의 실제 타깃을 감지한다 - JS 는 락파일로 패키지 매니저를 맞춘다 (npm/pnpm/yarn/bun) - verify_detected 를 함께 반환한다. implement.md 계약을 고쳐 false 일 때는 검증을 건너뛰지 말고 직접 찾도록 했다. 감지 범위도 커맨드에 적었다 repo-state.sh 는 변경 파일을 50개로 자른 뒤 그 길이를 file_count 로 냈다. auto-commit.md 는 이를 전체 변경 규모로 설명하므로 대형 변경에서 조용히 틀린다. file_count 를 자르기 전 개수로 계산하고 files_truncated 를 추가했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWDghVZqdr4ZRFhm1aVDSN
| os.chdir(root) | ||
|
|
||
| # 소스 트리 (노이즈 제외, 상한 200) | ||
| files = [f for f in run( |
There was a problem hiding this comment.
git ls-files는 추적 중인 파일만 반환하므로 아직 커밋하지 않은 소스 파일을 모두 누락합니다. /implement가 이 JSON을 프로젝트 상태 분석의 입력으로 사용하므로 새 파일이 있는 작업 트리에서 기존 구현을 빈 트리로 오인할 수 있습니다. untracked 작업 트리 파일도 포함하도록 수집해 주세요.
repo-context.sh 가 git ls-files 만 썼다. tracked 파일만 나오는데 명령이 성공하므로 뒤의 find 폴백도 타지 않는다. 새 프로젝트나 작업 중 레포에서는 file_count: 0, files: [] 이 나와 /implement 가 빈 트리로 분석하게 된다. 임시 저장소에 커밋하지 않은 package.json·src/app.ts 를 두고 재현했다. - git ls-files 와 git ls-files --others --exclude-standard 를 합친다 (.gitignore 는 계속 존중, node_modules 는 그대로 제외) - git 저장소가 아니면 find 로 떨어진다 - file_count 를 자르기 전 전체 수로 내고 files_truncated 를 추가했다. dirs 도 잘리기 전 목록에서 뽑는다 - implement.md 에 위 표기를 적었다 점검한 경우: 커밋 없는 새 레포 / 커밋+untracked 혼재 / git 아닌 디렉터리 / gitignore 존중 / 200개 초과 자르기. repo-state.sh·pr-context.sh 도 같은 부류로 다시 확인했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWDghVZqdr4ZRFhm1aVDSN
starz-woo
left a comment
There was a problem hiding this comment.
총평
방향은 타당합니다. 왕복 횟수를 줄이는 게 프롬프트 크기를 깎는 것보다 효과적이라는 가설, 비용 가중 지표, 신뢰구간·한계 명시까지 근거가 잘 갖춰져 있고 커밋 히스토리(5개 중 3개가 자체 발견 버그 수정)도 성실합니다.
다만 세 스크립트가 모든 커맨드 실행의 첫 단계에 놓이는 만큼, 실패 모드에서 조용히 틀리는 지점 두 개는 머지 전에 잡는 게 좋겠습니다.
Major 1 — repo-state.sh: 서브디렉터리에서 실행하면 untracked 파일이 누락되고 경로가 어긋남
repo-context.sh는 os.chdir(root)를 하는데 repo-state.sh는 하지 않습니다. git diff --name-only는 cwd와 무관하게 repo 전체·root 기준 경로를 주지만, git ls-files --others는 cwd 하위만, cwd 기준 상대경로로 냅니다.
임시 레포(root_untracked.txt, sub/sub_untracked.txt, staged sub2/x.txt)를 만들고 sub/에서 실행해 재현했습니다:
changed_files ['sub2/x.txt', 'sub_untracked.txt'] ← root_untracked.txt 통째로 누락
untracked ['sub_untracked.txt'] ← 실제 경로는 sub/sub_untracked.txt
/auto-commit이 서브디렉터리 cwd에서 돌면 (1) 다른 디렉터리의 새 파일을 못 보고 (2) changed_files에 root 기준과 cwd 기준 경로가 섞여 나옵니다. CLAUDE.md가 hook·auto-commit을 굳이 --show-toplevel 기준으로 맞춰둔 것도 같은 시나리오를 전제한 거라 실제로 밟히는 경로입니다.
root=run('git rev-parse --show-toplevel','.')
os.chdir(root) # ← 추가
br=run('git branch --show-current')Major 2 — pr-context.sh: gh 실패가 "빈 PR"로 성공 처리됨
run()이 non-zero에서 기본값을 반환하므로, PR이 없거나 gh 인증이 안 됐거나 다른 레포에서 돌리면 exit 0 + 전 필드 null + diff: "" 가 나옵니다.
$ bash pr-context.sh 99999
{"pr":"99999","state":null,...,"diff":""} exit=0
review-pr.md는 state를 MERGED/CLOSED만 분기하므로 null은 그냥 통과하고, 모델은 빈 diff를 받은 채 리뷰를 계속합니다. 리뷰 커맨드에서 가장 위험한 실패 모드(없는 코드에 대한 리뷰 생성)입니다. PR 번호 검증은 fail-closed로 잘 만들었는데 정작 조회 실패만 fail-open이라 일관성도 어긋납니다.
if not meta:
print(json.dumps({'pr': pr, 'error': 'gh pr view 실패 — PR 번호·인증·레포를 확인'},
ensure_ascii=False))
sys.exit(1)여기에 review-pr.md에 "error 필드가 있으면 중단하고 사용자에게 알린다" 한 줄을 같이 넣으면 됩니다.
Minor
1. bun test는 package.json 스크립트를 실행하지 않습니다 (repo-context.sh, 패키지 매니저 분기). Bun 내장 테스트 러너가 돌아서 프로젝트 테스트와 다른 결과가 납니다. bun run test여야 합니다. 같은 줄의 락파일 감지도 bun.lockb만 봐서 Bun 1.2+ 기본인 bun.lock을 놓칩니다.
2. PR URL 정규식이 호스트·레포를 버립니다 (pr-context.sh). 다른 레포의 PR URL을 넣으면 번호만 뽑아 현재 레포의 같은 번호 PR을 조용히 리뷰합니다. 숫자만 받거나, URL이면 --repo까지 넘기는 쪽이 맞습니다.
3. 신규 스크립트 3종에 테스트가 없습니다. 이 레포는 동급 컴포넌트마다 CI 테스트가 있습니다(test_gate.sh, test_danger_hook.sh, test_knowledge_wiki.py). 그리고 이 PR의 자체 수정 커밋 2개(file_count 잘림, ls-files tracked-only)와 위 Major 1이 전부 테스트 한 줄이면 잡혔을 종류입니다. test_context_scripts.sh 하나 추가할 자리로 보입니다.
4. python3가 세 커맨드의 하드 의존성이 됐습니다. 없으면 exit 127로 커맨드 자체가 죽는데, CONTRIBUTING에 새로 쓴 "never let a missing tool abort the script" 규약과 어긋납니다. 셸 앞단에서 command -v python3 확인 후 안내 JSON 정도면 충분합니다.
판정
Major 2건 수정 후 머지 권장. 둘 다 수정 자체는 각각 2~3줄이고, Minor는 후속으로 빼도 무방합니다.
🤖 Generated with Claude Code
리뷰에서 잡힌 실패 모드 두 가지. 둘 다 실패했는데 성공처럼 보이는 종류다. repo-state.sh 가 repo 루트로 이동하지 않았다. git ls-files --others 는 cwd 하위만 cwd 기준 경로로 내므로, 서브디렉터리에서 /auto-commit 을 돌리면 다른 디렉터리의 새 파일이 통째로 빠지고 changed_files 에 root 기준과 cwd 기준 경로가 섞인다. os.chdir(root) 로 수집 기준을 맞췄다. pr-context.sh 는 gh 조회가 실패해도 전 필드 null 에 diff 빈 문자열로 exit 0 했다. review-pr.md 가 state 를 MERGED/CLOSED 만 분기하므로 null 은 통과하고 모델이 빈 diff 로 리뷰를 만든다. 입력 검증과 같은 fail-closed 로 맞추고, review-pr.md 에 error 필드가 있으면 중단하도록 적었다. 그 외: - PR URL 은 owner/repo 를 살려 --repo 로 넘긴다. 번호만 뽑으면 현재 레포의 동일 번호 PR 을 조용히 리뷰한다 - bun·pnpm 의 test 서브커맨드는 package.json 스크립트를 돌리지 않으므로 항상 run 을 거친다. bun.lock(Bun 1.2+ 기본)도 감지한다 - 세 스크립트에 python3 존재 확인을 넣어 exit 127 대신 안내 JSON 을 낸다 test_context_scripts.sh 를 추가하고 CI 에 등록했다(28개). 임시 저장소를 만들어 실제 실행하고 JSON 필드만 본다. 모델 호출 없음. 이 PR 에서 나온 결함 - tracked-only 수집, 잘린 개수를 전체로 보고, 서브디렉터리 경로 어긋남, gh 실패의 빈 PR - 을 각각 되돌려 테스트가 실패하는 것까지 확인했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWDghVZqdr4ZRFhm1aVDSN
| 'error': 'gh pr view 실패 — PR 번호·인증·레포를 확인'}, | ||
| ensure_ascii=False)) | ||
| sys.exit(1) | ||
| diff = run(['gh', 'pr', 'diff', pr] + repo_args) |
There was a problem hiding this comment.
run()은 gh pr diff 실패 시 빈 문자열을 반환하지만 이 경로에서는 error를 만들지 않습니다. 그 결과 스크립트가 exit 0으로 빈 diff를 반환하고 review-pr.md의 오류 중단 조건도 우회합니다. diff 조회 실패를 명시적인 오류로 반환하고 비정상 종료하도록 처리해 주세요.
앞 커밋에서 gh pr view 실패만 fail-closed 로 바꾸고 diff 조회는 빠뜨렸다. 메타데이터는 성공하고 diff 만 실패하면(인증·네트워크·권한) 빈 diff 로 exit 0 이 나가고, review-pr.md 의 error 중단 조건도 우회한다. 코드가 없는 PR 을 리뷰하게 되는 경로가 그대로 남아 있었다. - run_ok() 로 성공 여부를 함께 받아 실패를 빈 문자열로 뭉개지 않는다 - diff 조회 실패 시 error + detail 을 내고 비정상 종료한다 test_context_scripts.sh 에 가짜 gh 를 PATH 앞에 두는 회귀 테스트를 넣었다 (메타 실패 / 메타 성공+diff 실패 / 둘 다 성공). 인증 상태와 무관하게 돌고, 이 커밋의 수정을 되돌리면 해당 2건이 실패하는 것을 확인했다. 33개. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWDghVZqdr4ZRFhm1aVDSN
Summary
모델이 개별 명령을 하나씩 실행하던 조회 구간을 스크립트 한 번 호출로 바꿉니다. 판단(브랜치 분기·PR 재사용·리뷰 내용)은 그대로 모델이 합니다.
세 커맨드에서 비용 가중 −8.3% ~ −25.2%, 잰 품질 지표에서 저하 없음.
왕복 한 번은 그때까지의 컨텍스트를 전부 다시 실어 나릅니다. 그래서 커맨드 파일 크기를 줄이는 것보다(별도 실험에서 −0.3%, p=0.92) 왕복 횟수를 줄이는 쪽이 효과가 큽니다.
Changes
신규 스크립트 3종 — 각 커맨드가 필요한 정보만 모아 JSON 하나로 반환
scripts/repo-context.shls·find·cat package.json·git log·node -v· PRD 읽기 → 런당 14회scripts/repo-state.shgit fetch/status/diff×2/remote·gh pr list×2 ·ls PLAN_*→ 8회scripts/pr-context.shgh pr view×2 ·gh pr diff→ 3회커맨드 3종 수정 — 명령 나열을 스크립트 한 줄로 교체하고, 반환 JSON 필드를 문서화
commands/implement.mdStep 3~4commands/auto-commit.mdStep 1commands/review-pr.mdStep 1원문이 아니라 답을 담는 게 핵심입니다.
package.json전문 대신verify_commands(테스트는 이 명령으로), PR 목록 대신stale_branch(이 브랜치는 죽었다) 같은 식입니다./implement에서 모델이 테스트 실행법을 찾느라 왕복 5번을 쓰던 게 여기서 사라집니다.각 커맨드에 "개별 git/gh 명령을 따로 실행하지 말 것" 한 줄을 같이 넣었습니다. 이 문구가 없으면 스크립트를 돌리고도 확인차
git status를 다시 부릅니다.문서 —
scripts/가 이미 있는데(knowledge_wiki/) 레이아웃 블록에 빠져 있었습니다.CLAUDE.md구조 블록 2곳에scripts/추가CONTRIBUTING.mdRepo layout 추가 + 번들 스크립트 규약 신설측정
Claude Code 헤드리스(
claude -p) · Opus 5 · 원본 무수정, 복제본을--plugin-dir로 교체./implement[−32.6, −16.9]/review-pr[−33.5, −5.0]/auto-commit[−12.4, −3.9]입력×1 + 캐시쓰기×1.25 + 캐시읽기×0.1 + 출력×5(상대값, 달러 아님)/auto-commit은 순열검정p=0.0003토큰 개수가 아니라 단가로 가중해 봤습니다.
/auto-commit에서 커맨드 파일을 깎은 별도 조건은 세션 총 입력이 **+1.8%**인데 비용 가중은 **−0.3%**로 갈렸습니다 — 개수만 셌으면 반대로 읽었을 값입니다.한계
/implement·/review-pr은 조건당 5런이라 표본이 작습니다. 신뢰구간이 0을 걸치지는 않았습니다/review-pr은 실제 PR 대신 같은 인터페이스의gh스텁으로 돌렸습니다. 왕복 횟수와 컨텍스트는 그대로지만 PR 데이터는 고정값입니다repo-state.sh의changed_files가git diff --cached로만 만들어져 스테이징 전 변경이 누락되던 버그를 함께 고쳤습니다. 정확성 수정이고 절감폭 영향은 확인되지 않았습니다(n=5, p=0.50). 위 −8.3%는 수정 전 측정치입니다auto-commit.md의 「브랜치 재사용 감지 방법」 참조 섹션은 여전히 개별gh pr list를 안내합니다./auto-commit의 절감폭이 유독 작은 이유일 수 있으나 이번 범위 밖입니다Type
feat— new agent / command / skill / capabilityfix— bug fixdocs— documentation onlyrefactor/choreChecklist
plugin.json에 등록했다 (해당 시) — 해당 없음, 스크립트는 등록 대상이 아니며${CLAUDE_PLUGIN_ROOT}로 접근합니다python3 .github/scripts/validate_plugin.py통과marketplace.json↔plugin.json↔ README/CLAUDE.md 일치 — 버전 변경 없음