Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- name: Install ruff
run: pip install --quiet ruff
# ruff에 디렉터리를 넘기면 확장자가 .py인 파일만 훑는다(실측 확인). git이
# 이름을 고정하는 훅 3개(prepare-commit-msg/commit-msg/post-commit)는 확장자가
# 이름을 고정하는 훅 2개(prepare-commit-msg/post-commit)는 확장자가
# 없어, Python으로 포팅돼도 hooks/ 하나만 지정하면 조용히 검사에서 빠진다 -
# 검사가 아무것도 안 하고 통과하는 GF-32와 같은 유형이다. 그래서 셔뱅으로
# Python 파일을 찾아 함께 넘긴다. 이 근거는 GF-135로 hooks/checks/*.py가
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
id: doc-22
title: 유사 프로젝트 조사 — AI 커밋 출처 기록 도구 비교
type: other
created_date: '2026-10-04 01:43'
updated_date: '2026-10-04 01:43'
---
2026-10-04 조사. AI 에이전트 커밋의 출처를 git에 남기는 도구·관례를 이 프로젝트와 비교한다.

## 조사 대상

| 대상 | 기록 위치 | 기록 시점 | 강제 | 메시지 형식 검증 |
| --- | --- | --- | --- | --- |
| [block/aittributor](https://github.com/block/aittributor) | `Co-authored-by` 트레일러 | `prepare-commit-msg` | 훅 | 없음 |
| [mgoodric/ai-attribution-hooks](https://github.com/mgoodric/ai-attribution-hooks) | `Assisted-by`/`Co-authored-by`/`Generated-by` | `prepare-commit-msg` + `commit-msg` | 훅 + 집계 스크립트 | 없음 |
| [git-ai](https://github.com/git-ai-project/git-ai) | git notes(`refs/notes/ai`), 줄 단위 | 에이전트가 편집할 때마다 `git ai checkpoint` | 에이전트 훅. git 훅·래퍼 없음 | 없음 |
| [Codex CLI `commit_attribution`](https://codex.danielvaughan.com/2026/03/28/codex-cli-commit-attribution/) | 트레일러 | 모델 프롬프트에 지시 주입 | 없음(모델이 따를 뿐) | 없음 |
| [Aider](https://aider.chat/docs/git.html) | `Co-authored-by` 또는 author에 `(aider)` | 도구가 커밋할 때 | 도구 안에서만 | 없음 |
| [Linux 커널 정책](https://docs.kernel.org/process/coding-assistants.html) | `Assisted-by:` 에이전트·모델 + 보조 분석 도구 | 사람이 씀 | 리뷰 | 기존 커널 규칙 |
| [crashoverride 가이드](https://crashoverride.com/resources/knowledge-base/code-ownership/attributing-ai-commits-git) | `Generated-By: <agent>/<version> (model: <id>; operator: <email>)` | — | CI "Agent Trailer Lint" | — |
| commitlint, gitlint | — | `commit-msg`(+ CI에서 커밋 범위 검사) | 훅 + CI | 있음 |

## 각 대상에서 확인한 사실

- **aittributor**: 에이전트 판정을 4단계로 한다 — 환경변수, 자기 프로세스 조상, 같은 저장소의 형제 프로세스, 에이전트 상태 파일(`~/.claude/projects/`, `~/.codex/sessions/`). 같은 이메일의 `Co-authored-by`가 이미 있으면 붙이지 않는다. lefthook 연동 또는 `.git/hooks/`에 심볼릭 링크로 설치.
- **ai-attribution-hooks**: AI 기여 비율에 따라 트레일러를 셋으로 나눈다(~33% / 35–67% / 67%+). AI 트레일러가 있으면 `commit-msg`가 `Signed-off-by`를 요구한다 — 사람이 책임진다는 이중 서명. `ai-attribution-stats.sh`가 커밋 범위에서 AI 커밋 비율과 서명 누락을 집계한다.
- **git-ai**: 커밋 메시지를 건드리지 않는다. 귀속 정보를 notes에 두고, rebase·cherry-pick·squash·reset 뒤에 최종 코드를 분석해 notes를 새 커밋으로 옮긴다(비동기, 결과적 일관성). 프롬프트는 마스킹해 git 밖에 저장. 커밋·PR별 토큰과 비용을 계산한다.
- **Codex**: 훅을 쓰지 않는다. 문서 스스로 "compliance is high but not absolute"라고 하고, 보장이 필요하면 `prepare-commit-msg` 훅을 덧대라고 권한다.
- **Linux 커널**: "AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the DCO."
- **crashoverride**: `Signed-off-by`를 운영자(사람)의 책임 인정으로 쓰라고 권한다. 과거 커밋은 다시 쓰지 말고 별도 CSV로 감사하라고 한다.

## 이 프로젝트와 비교

### 강점

- **형식·출처·이력 불변을 한 도구에서 강제한다.** 출처 도구들은 메시지 형식을 보지 않고, 형식 검사기는 출처를 남기지 않는다.
- **훅으로 강제한다.** Codex·Aider처럼 도구나 모델이 협조해야 붙는 방식이 아니어서, 에이전트가 잊어도, 사람이 커밋해도 남는다.
- **모델명과 토큰이 서버가 발급한 값이다**(Claude Code 한정). 트랜스크립트의 `message.model`과 usage를 읽는다. 다른 도구는 고정 문자열이거나 에이전트 이름까지만 남긴다.
- **`Hooks-Commit`**: 훅 자체의 버전을 커밋마다 남겨, 훅 버그의 영향 범위를 역추적할 수 있다. 다른 도구에는 없다.
- **`Task-Id` 브랜치 강제**로 커밋과 작업을 잇는다.
- **의존성이 git + python3 표준 라이브러리뿐**이고, 트레일러가 메시지에 있어 `git log`만으로 읽히며 clone·push에 그대로 따라간다. notes는 따로 push·fetch해야 한다.

### 약점

- **커밋 단위다.** git-ai는 줄 단위로 `blame`까지 된다.
- **기록이 메시지에 있어 이력을 다시 쓰면 깨진다.** git-ai는 재작성 뒤 notes를 옮겨 붙이는데, 이 프로젝트는 재작성을 금지하는 쪽(decision-24)으로 풀었다 — 사용자에게 워크플로 제약을 지운다.
- **지금은 `post-commit`의 `--amend`로 붙인다.** 해시가 바뀌고, 트레일러가 중복되고, rebase 중 실패한다. 비교한 훅 기반 도구는 전부 `prepare-commit-msg`에서 메시지 파일에 쓴다(GF-128이 같은 방향).
- **에이전트 판정이 `AI_AGENT` 하나다.** aittributor는 신호 4개를 본다(GF-130).
- **토큰 측정이 Claude Code에만 되고, 커밋 시점에 트랜스크립트를 거슬러 재구성한다**(실험 단계, doc-16). git-ai는 편집 시점에 체크포인트를 쌓는다.
- **`Signed-off-by`를 에이전트 커밋에도 자동으로 붙인다.** 커널·crashoverride·ai-attribution-hooks가 쓰는 의미(사람의 DCO 서명)와 충돌한다.
- **에디터 경로를 거부한다.** commitlint·gitlint는 `commit-msg`에서 돌아 에디터 커밋도 검증한다. 우회 차단(decision-18)과 맞바꾼 비용이다.
- **`core.hooksPath`를 점유해 다른 훅과 조합할 수 없다.** aittributor는 lefthook으로 조합된다.
- **집계 도구가 없다.** ai-attribution-hooks의 stats 스크립트, git-ai의 `stats`/`blame` 같은 읽기 도구가 없다.
- **트레일러를 손으로 위조해도 확인하는 곳이 없다.** CI 검사(DRAFT-19)는 아직 드래프트이고 서명·증명(attestation)도 없다.
- **트레일러 이름이 독자적이다.** 업계는 `Assisted-by`/`Generated-By` 쪽으로 모이는 중이다(표준은 아직 없음). GF-128의 `AI-Agent` 이름을 정할 때 고려할 것.

## 후속

- `Signed-off-by` 재검토, 체크포인트·notes 기반 측정 검토는 각각 드래프트로 남겼다.
- 이미 있는 작업과 겹치는 것: 메시지 파일 직접 쓰기·키 단위 중복 차단(GF-128), 에이전트 판정(GF-130), CI 검사(DRAFT-19).
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
id: DRAFT-22
title: Signed-off-by 자동 삽입을 재검토한다 — DCO 의미 충돌
status: Draft
assignee: []
created_date: '2026-10-04 01:43'
updated_date: '2026-10-04 01:43'
labels:
- trailers
- policy
dependencies: []
references:
- decision-25
- decision-19
documentation:
- backlog/docs/doc-22 - 유사-프로젝트-조사-—-AI-커밋-출처-기록-도구-비교.md
priority: medium
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->
post-commit은 모든 커밋에 커미터 정보로 Signed-off-by를 자동으로 붙인다(decision-25가 decision-19의 제거 조항을 대체해 유지). 그런데 Signed-off-by는 관례상 사람이 DCO를 인증한다는 서명이다.

- Linux 커널 정책: "AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the DCO."
- ai-attribution-hooks와 crashoverride 가이드는 Signed-off-by를 AI 커밋에 대한 사람의 책임 인정으로 쓰고, AI 트레일러가 있으면 이를 요구한다.

이 훅이 에이전트 커밋에도 자동으로 붙이면, DCO를 요구하는 저장소에 설치했을 때 사람이 인증하지 않은 커밋이 인증된 것처럼 보인다. 조사 근거는 doc-22.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Signed-off-by를 유지·제거·사람 커밋에만 붙임 중 하나로 정하고 decision으로 기록한다
- [ ] #2 결정에 맞게 GF-128 AC #10을 갱신한다
- [ ] #3 DCO를 요구하는 저장소에 설치할 때의 동작이 README에 적혀 있다
<!-- AC:END -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
id: DRAFT-23
title: 토큰 측정을 편집 시점 체크포인트와 git notes로 옮길지 검토한다
status: Draft
assignee: []
created_date: '2026-10-04 01:43'
updated_date: '2026-10-04 01:43'
labels:
- measurement
- spike
dependencies: []
references:
- decision-27
- decision-24
documentation:
- backlog/docs/doc-22 - 유사-프로젝트-조사-—-AI-커밋-출처-기록-도구-비교.md
- backlog/docs/doc-16 - 토큰·툴콜-측정-방법과-한계.md
priority: medium
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->
Tokens-Used/Tool-Calls는 커밋 시점에 Claude Code 트랜스크립트를 거슬러 읽어 이 커밋의 파일을 건드린 응답을 재구성한다(decision-27, doc-16). 측정 방법론은 아직 실험 단계이고 Claude Code에만 된다.

git-ai는 다르게 한다(doc-22).
- 에이전트가 파일을 고칠 때마다 체크포인트를 남기고, 커밋할 때 합친다 — 사후 재구성이 없다. Claude Code에서는 PostToolUse 훅으로 같은 일을 할 수 있다.
- 결과를 커밋 메시지가 아니라 git notes(refs/notes/ai)에 둔다. 메시지를 다시 쓰지 않아 append-only(decision-24)와 맞고, rebase·cherry-pick 뒤에도 notes를 옮겨 붙인다. 대신 git log만으로 안 보이고 notes를 따로 push·fetch해야 한다.

두 가지(체크포인트 수집, notes 저장)는 독립적으로 채택할 수 있다. 어느 쪽이 지금 방식보다 나은지 판단할 근거가 필요하다.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 같은 세션을 지금 방식과 체크포인트 방식으로 측정해 결과 차이를 비교한다
- [ ] #2 notes 저장 시 push·fetch·rebase에서 기록이 살아남는지 실측한다
- [ ] #3 채택·기각 여부를 decision으로 기록한다
<!-- AC:END -->
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
---
id: GF-127
title: 커밋 메시지 검증을 prepare-commit-msg로 이전
status: To Do
assignee: []
status: Done
assignee:
- '@claude'
created_date: '2026-09-25 19:33'
updated_date: '2026-09-26 02:35'
updated_date: '2026-10-04 01:52'
labels:
- hooks
- validation
Expand Down Expand Up @@ -33,24 +34,40 @@ type: feature

## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 제목이 [type][subsystem] <설명> 형식인지 검증한다 (subsystem은 생략 가능)
- [ ] #2 제목이 50자를 넘으면 거부한다 (바이트가 아니라 유니코드 코드포인트 기준)
- [ ] #3 본문 줄이 72자를 넘으면 거부하되, 등록된 트레일러 토큰으로 시작하는 줄은 예외로 둔다
- [ ] #4 본문이나 트레일러가 있으면 제목과의 사이에 빈 줄을 요구한다
- [ ] #5 Fixes 트레일러가 있으면 참조 해시가 저장소에 실재하는 커밋인지 검증한다
- [ ] #6 브랜치명에 <prefix>-<번호> 패턴이 없으면 거부하되 예외 브랜치와 detached HEAD는 면제한다
- [ ] #7 커밋 타입 목록을 설정 파일에서 읽으며, 목록이 비면 조용히 통과하지 않고 원인을 밝히며 중단한다
- [ ] #8 검증이 트레일러 삽입보다 먼저 실행된다
- [ ] #9 같은 커밋에서 hooks/commit-msg를 삭제한다 — 기능을 옮기고 구 훅을 남기면 검증이 두 번 실행된다
- [x] #1 제목이 [type][subsystem] <설명> 형식인지 검증한다 (subsystem은 생략 가능)
- [x] #2 제목이 50자를 넘으면 거부한다 (바이트가 아니라 유니코드 코드포인트 기준)
- [x] #3 본문 줄이 72자를 넘으면 거부하되, 등록된 트레일러 토큰으로 시작하는 줄은 예외로 둔다
- [x] #4 본문이나 트레일러가 있으면 제목과의 사이에 빈 줄을 요구한다
- [x] #5 Fixes 트레일러가 있으면 참조 해시가 저장소에 실재하는 커밋인지 검증한다
- [x] #6 브랜치명에 <prefix>-<번호> 패턴이 없으면 거부하되 예외 브랜치와 detached HEAD는 면제한다
- [x] #7 커밋 타입 목록을 설정 파일에서 읽으며, 목록이 비면 조용히 통과하지 않고 원인을 밝히며 중단한다
- [x] #8 검증이 트레일러 삽입보다 먼저 실행된다
- [x] #9 같은 커밋에서 hooks/commit-msg를 삭제한다 — 기능을 옮기고 구 훅을 남기면 검증이 두 번 실행된다
<!-- AC:END -->

## Definition of Done
<!-- DOD:BEGIN -->
- [ ] #1 python3 -m unittest 스위트 전체 통과 (이관 전이면 bats tests/ 통과)
- [ ] #2 ruff check 통과
- [ ] #3 이 저장소 자신의 커밋이 새 훅으로 정상 생성되는지 확인
- [x] #1 python3 -m unittest 스위트 전체 통과 (이관 전이면 bats tests/ 통과)
- [x] #2 ruff check 통과
- [x] #3 이 저장소 자신의 커밋이 새 훅으로 정상 생성되는지 확인
<!-- DOD:END -->

## Implementation Notes

<!-- SECTION:NOTES:BEGIN -->
구현(0d9df83, a57d038):
- commit-msg의 검증 함수(형식·길이·빈 줄·Fixes·브랜치 Task-Id·AI-Model 게이트, type 목록 비면 중단)를 동작 그대로 prepare-commit-msg로 옮기고 같은 커밋에서 hooks/commit-msg를 삭제했다(AC #9). 오류 메시지 접두어만 prepare-commit-msg:로 바뀌었다.
- 실행 순서: 재생·병합 면제 → 스테일 마커 무효화 → 에디터 경로 거부 → validate_message() → 마커 기록. 무효화를 거부 경로보다 앞에 두어 거부·예외로 끝나면 마커가 남지 않는다(GF-31의 commit-msg finally 정리를 대체).
- 메시지 파일은 주석 제거 전 원문이다. commit-msg와 같게 열 0의 '#' 줄만 빼고 commit.cleanup/core.commentChar는 보지 않는다(-m/-F 경로에서 이 훅과 구 commit-msg가 보는 내용은 같다).
- revert 제목 예외(유저 결정 b): 'Revert "..."'와 'Reapply "..."'를 fullmatch로 인정. git 2.54.0 실측으로 revert의 revert는 'Reapply "<제목>"'이 되므로 함께 넣었다. 중첩은 바깥 따옴표만 보므로 통과한다. 이 제목은 50자 제한에서도 뺀다(git이 원래 제목에 접두어를 붙여 50자 제목의 revert는 60자). 본문 72자·빈 줄 규칙은 그대로 적용.
- --amend 모호성(source=commit)은 알려진 한계로 validate_message() 주석에 남겼다.
- AC #8: 트레일러는 아직 post-commit이 커밋 뒤에 붙이므로 검증이 먼저 돈다. GF-128에서 삽입을 validate_message() 뒤에 둬야 한다고 주석에 적었다.
- 테스트: --no-verify 우회 불가(형식·길이·브랜치), clean revert --no-edit 통과, 60자 revert 제목 통과, Reapply·그 revert 통과, revert 비슷한 손글씨 제목 거부, 거부 시 스테일 마커 제거를 추가. commit-msg 전용 케이스(conf 가드, python3 부재)는 삭제하고, --no-verify로 검증을 피하던 테스트 2개와 Task-Id 없는 브랜치에서 준비 커밋을 만들던 replay 테스트를 고쳤다. install 테스트는 commit-msg 링크가 정리되는지 확인하도록 바꿨다(install.sh 변경 없음).
- 결과: python3 -m unittest discover -s tests → Ran 113 tests OK, ruff check . 통과. 이 저장소의 커밋 0d9df83이 새 훅으로 생성됐고, --no-verify로 형식 틀린 커밋은 거부됐다.

검증(2026-10-04): python3 -m unittest discover -s tests → 115 tests OK, ruff check → All checks passed. AC#7 증거 테스트가 없어 2개 추가(f5cb496), 가드를 지우면 둘 다 실패하는 것을 확인. 수동 실측: 50자 가까운 제목을 git revert --no-edit → Revert "..." 통과, 다시 revert → Reapply "..." 통과, git commit --no-verify -m 'bad subject' → 거부·마커 없음. AC#8: 검증은 커밋 생성 전 prepare-commit-msg에서, 트레일러는 커밋 생성 후 post-commit에서 붙으므로 순서가 구조적으로 보장된다. 이 저장소의 GF-127 커밋들이 새 훅으로 만들어짐(DoD#3).
<!-- SECTION:NOTES:END -->

## Comments

<!-- COMMENTS:BEGIN -->
Expand Down Expand Up @@ -84,4 +101,16 @@ prepare-commit-msg로 옮기는 순간 clean revert가 전부 거부된다. 기
함께 결정할 것: 같은 태스크에 걸린 --amend 모호성(GF-125 코멘트 #1) — source=commit이
--amend --no-edit(최종 메시지)과 --amend(뒤에 에디터 열림)를 구분하지 못한다.
---

author: @claude
created: 2026-10-04 01:39
---
2026-10-04 유저 결정: clean revert 문제는 (b)로 간다 — git이 만드는 'Revert "..."' 제목을 제목 규칙의 예외로 인정한다. 면제(a)는 cherry-pick까지 넓어지고, (c)는 git 기본 동작을 막는다. --amend 모호성(source=commit)은 이 태스크에서 해결하지 않고 알려진 한계로 남긴다.
---
<!-- COMMENTS:END -->

## Final Summary

<!-- SECTION:FINAL_SUMMARY:BEGIN -->
커밋 메시지 검증(제목 형식·50자·본문 72자·빈 줄·Fixes·브랜치 Task-Id·type 목록 가드·AI-Model 게이트)을 commit-msg에서 prepare-commit-msg로 옮기고 commit-msg를 삭제했다. 이제 --no-verify로 검증을 건너뛸 수 없다. clean git revert가 거부되는 회귀는 유저 결정 (b)대로 git이 만드는 Revert "..."/Reapply "..." 제목을 형식·50자 규칙의 예외로 인정해 막았다. 거부 시 검증 마커가 남지 않도록 무효화를 앞당겼다. 검증: unittest 115개 통과, ruff 통과, revert/reapply/--no-verify 수동 실측. 알려진 한계: --amend 에디터 경로는 이전 메시지를 검증, 손으로 쓴 Revert "..." 제목도 예외를 받음, SHA-256 저장소의 revert 본문 줄은 72자를 넘음. README·hooks/readme.md·.gitmessage의 commit-msg 언급은 GF-132/GF-134에서 정리.
<!-- SECTION:FINAL_SUMMARY:END -->
Loading
Loading