From 9ccd930066edeb986eb553e589d021a5c8d772ac Mon Sep 17 00:00:00 2001 From: cpu-once <231006716+cpu-once@users.noreply.github.com> Date: Sat, 3 Oct 2026 12:35:46 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[docs][backlog]=20DRAFT-22=C2=B723=EC=9D=84?= =?UTF-8?q?=20GF-139=C2=B7140=EC=9C=BC=EB=A1=9C=20=EC=8A=B9=EA=B2=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task-Id: GF-139 AI-Tool: claude-code AI-Tool-Version: 2.1.285 Co-Authored-By: Claude AI-Model: claude-opus-5-5 Tokens-Used: 3086381 Tool-Calls: 6 Hooks-Commit: 0724249 Signed-off-by: cpu-once <231006716+cpu-once@users.noreply.github.com> --- ...\244\221\353\263\265-\354\240\234\352\261\260.md" | 11 ++++++++--- ...\245\274-\353\247\214\353\223\240\353\213\244.md" | 12 +++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) rename "backlog/drafts/draft-22 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" => "backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" (93%) rename "backlog/drafts/draft-23 - git-trail\353\241\234-\352\260\234\353\252\205\355\225\230\352\263\240-\352\260\234\353\205\220-\354\247\200\353\217\204-\353\254\270\354\204\234\353\245\274-\353\247\214\353\223\240\353\213\244.md" => "backlog/tasks/gf-140 - git-trail\353\241\234-\352\260\234\353\252\205\355\225\230\352\263\240-\352\260\234\353\205\220-\354\247\200\353\217\204-\353\254\270\354\204\234\353\245\274-\353\247\214\353\223\240\353\213\244.md" (90%) diff --git "a/backlog/drafts/draft-22 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" "b/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" similarity index 93% rename from "backlog/drafts/draft-22 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" rename to "backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" index 7c532a8..f10abef 100644 --- "a/backlog/drafts/draft-22 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" +++ "b/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" @@ -1,14 +1,19 @@ --- -id: DRAFT-22 +id: GF-139 title: Tokens-Used 핫픽스 — 세션별 커서와 응답 단위 중복 제거 -status: Draft +status: To Do assignee: [] created_date: '2026-10-03 03:30' -updated_date: '2026-10-03 03:31' +updated_date: '2026-10-03 03:35' labels: - hooks - bug dependencies: [] +references: + - decision-5 + - decision-19 +documentation: + - backlog/docs/doc-16 - 토큰·툴콜-측정-방법과-한계.md priority: high type: bug --- diff --git "a/backlog/drafts/draft-23 - git-trail\353\241\234-\352\260\234\353\252\205\355\225\230\352\263\240-\352\260\234\353\205\220-\354\247\200\353\217\204-\353\254\270\354\204\234\353\245\274-\353\247\214\353\223\240\353\213\244.md" "b/backlog/tasks/gf-140 - git-trail\353\241\234-\352\260\234\353\252\205\355\225\230\352\263\240-\352\260\234\353\205\220-\354\247\200\353\217\204-\353\254\270\354\204\234\353\245\274-\353\247\214\353\223\240\353\213\244.md" similarity index 90% rename from "backlog/drafts/draft-23 - git-trail\353\241\234-\352\260\234\353\252\205\355\225\230\352\263\240-\352\260\234\353\205\220-\354\247\200\353\217\204-\353\254\270\354\204\234\353\245\274-\353\247\214\353\223\240\353\213\244.md" rename to "backlog/tasks/gf-140 - git-trail\353\241\234-\352\260\234\353\252\205\355\225\230\352\263\240-\352\260\234\353\205\220-\354\247\200\353\217\204-\353\254\270\354\204\234\353\245\274-\353\247\214\353\223\240\353\213\244.md" index 6e5b703..f82c740 100644 --- "a/backlog/drafts/draft-23 - git-trail\353\241\234-\352\260\234\353\252\205\355\225\230\352\263\240-\352\260\234\353\205\220-\354\247\200\353\217\204-\353\254\270\354\204\234\353\245\274-\353\247\214\353\223\240\353\213\244.md" +++ "b/backlog/tasks/gf-140 - git-trail\353\241\234-\352\260\234\353\252\205\355\225\230\352\263\240-\352\260\234\353\205\220-\354\247\200\353\217\204-\353\254\270\354\204\234\353\245\274-\353\247\214\353\223\240\353\213\244.md" @@ -1,14 +1,20 @@ --- -id: DRAFT-23 +id: GF-140 title: git-trail로 개명하고 개념 지도 문서를 만든다 -status: Draft +status: To Do assignee: [] created_date: '2026-10-03 03:31' -updated_date: '2026-10-03 03:31' +updated_date: '2026-10-03 03:35' labels: - docs - naming dependencies: [] +references: + - decision-25 + - decision-24 +documentation: + - backlog/docs/doc-3 - AI-귀속-트레일러-레퍼런스.md + - backlog/docs/doc-20 - GitHub-원격-강제-설정-관리-—-rulesets와-저장소-설정.md priority: medium type: docs --- From a280572f5dc15b610f6c3ff36fe2ecef8c6f1b92 Mon Sep 17 00:00:00 2001 From: cpu-once <231006716+cpu-once@users.noreply.github.com> Date: Sat, 3 Oct 2026 12:36:45 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[docs][backlog]=20GF-139=20=EC=B0=A9?= =?UTF-8?q?=EC=88=98=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task-Id: GF-139 AI-Tool: claude-code AI-Tool-Version: 2.1.285 Co-Authored-By: Claude AI-Model: claude-opus-5-5 Tokens-Used: 3419214 Tool-Calls: 6 Hooks-Commit: 3d4c286 Signed-off-by: cpu-once <231006716+cpu-once@users.noreply.github.com> --- ...221\353\263\265-\354\240\234\352\261\260.md" | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git "a/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" "b/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" index f10abef..0c85eae 100644 --- "a/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" +++ "b/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" @@ -1,10 +1,11 @@ --- id: GF-139 title: Tokens-Used 핫픽스 — 세션별 커서와 응답 단위 중복 제거 -status: To Do -assignee: [] +status: In Progress +assignee: + - '@claude' created_date: '2026-10-03 03:30' -updated_date: '2026-10-03 03:35' +updated_date: '2026-10-03 03:36' labels: - hooks - bug @@ -47,3 +48,13 @@ GF-129는 GF-127→GF-128 재설계 뒤로 묶여 있어서, 값이 틀린 채 - [ ] #2 ruff check 통과 - [ ] #3 실제 커밋의 Tokens-Used가 트랜스크립트를 응답 단위로 따로 합산한 값과 일치하는지 확인 + +## Implementation Plan + + +1. 테스트 먼저(TDD): 새 세션 첫 커밋, 같은 (requestId, message.id) 줄 1회 계상, 키 없는 줄 줄 단위, 예전 숫자 커서, 커서 경계를 걸친 응답 중복 방지 +2. hooks/post-commit의 measure_claude_code_token_usage()/aggregate_usage() 수정 — 커서 형식 ' <줄 수>' +3. 기존 테스트의 커서 기대값 갱신 +4. doc-16 갱신, GF-129 코멘트 +5. 실제 커밋으로 값 대조 (구현은 서브에이전트, 검토·커밋 확인은 메인) + From a096f1d53ac7fc29d21c20ee958e9db20cf9f924 Mon Sep 17 00:00:00 2001 From: cpu-once <231006716+cpu-once@users.noreply.github.com> Date: Sat, 3 Oct 2026 12:37:33 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[test][post-commit]=20=EC=84=B8=EC=85=98?= =?UTF-8?q?=EB=B3=84=20=EC=BB=A4=EC=84=9C=C2=B7=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task-Id: GF-139 AI-Tool: claude-code AI-Tool-Version: 2.1.285 Co-Authored-By: Claude AI-Model: claude-opus-5-5 Tokens-Used: 265086 Tool-Calls: 0 Hooks-Commit: 085c5f0 Signed-off-by: cpu-once <231006716+cpu-once@users.noreply.github.com> --- tests/test_token_usage_measurement.py | 140 +++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 5 deletions(-) diff --git a/tests/test_token_usage_measurement.py b/tests/test_token_usage_measurement.py index 96050df..6e71cff 100644 --- a/tests/test_token_usage_measurement.py +++ b/tests/test_token_usage_measurement.py @@ -27,6 +27,27 @@ def assistant(input_tokens, output_tokens, tool_names=()): } +def response_line(request_id, message_id, input_tokens, output_tokens, block): + """API 응답 하나가 content 블록마다 한 줄씩 기록되는 실제 트랜스크립트 모양. + + 같은 응답의 줄들은 최상위 requestId와 message.id를 공유하고 usage도 똑같이 + 반복하며, content만 블록별로 다르다(GF-139 실측). + """ + return { + "type": "assistant", + "requestId": request_id, + "message": { + "id": message_id, + "usage": {"input_tokens": input_tokens, "output_tokens": output_tokens}, + "content": [block], + }, + } + + +TEXT_BLOCK = {"type": "text", "text": "hi"} +TOOL_BLOCK = {"type": "tool_use", "name": "bash"} + + class TokenUsageMeasurementTest(IsolatedRepoTestCase): def cursor_value(self): return self.git_dir_file(CURSOR_NAME).read_text(encoding="utf-8").strip() @@ -56,7 +77,7 @@ def test_알려진_usage_값이_정확히_합산된다(self): message = self.head_message() self.assertTrailerCount(message, "Tokens-Used: 45", 1) self.assertTrailerCount(message, "Tool-Calls: 2", 1) - self.assertEqual("3", self.cursor_value()) + self.assertEqual("fake-session 3", self.cursor_value()) def test_두_번째_커밋은_새로_추가된_줄만_집계한다(self): """[GF-96] 같은 세션의 두 번째 커밋은 첫 커밋 이후 새로 추가된 줄만 델타로 집계한다""" @@ -77,7 +98,7 @@ def test_두_번째_커밋은_새로_추가된_줄만_집계한다(self): self.assertTrailerCount(message, "Tool-Calls: 1", 1) # 누적 합(21)이 아니라 델타(6)여야 한다. self.assertTrailerCount(message, "Tokens-Used: 21", 0) - self.assertEqual("3", self.cursor_value()) + self.assertEqual("fake-session 3", self.cursor_value()) def test_트랜스크립트_파일이_없으면_사유가_명시된다(self): """[GF-97] 트랜스크립트 파일이 없으면 Tokens-Used/Tool-Calls가 unavailable (transcript-not-found)로 명시된다""" @@ -130,7 +151,7 @@ def test_실측_0은_unavailable이_아니라_0으로_명시된다(self): self.assertTrailerCount(message, "Tokens-Used: 0", 1) self.assertTrailerCount(message, "Tool-Calls: 0", 1) self.assertNotIn("unavailable", message) - self.assertEqual("1", self.cursor_value()) + self.assertEqual("fake-session 1", self.cursor_value()) def test_깨진_줄이_있으면_배치_전체가_실패하고_커서는_그대로다(self): """[GF-112] 새 구간에 깨진 줄이 하나라도 있으면 배치 전체가 unavailable (transcript-parse-failed)이고 커서는 그대로다""" @@ -143,7 +164,7 @@ def test_깨진_줄이_있으면_배치_전체가_실패하고_커서는_그대 self.commit("[feat] parse failure baseline commit", env=env) ) self.assertTrailerCount(self.head_message(), "Tokens-Used: 15", 1) - self.assertEqual("1", self.cursor_value()) + self.assertEqual("fake-session 1", self.cursor_value()) # 새 구간에 정상 줄과 깨진 줄을 함께 넣는다 — 깨진 줄만 건너뛰고 정상 줄을 # 집계하면 "일부만 집계된 값"이 정상값인 척 기록되므로, 배치 전체가 실패해야 @@ -163,7 +184,116 @@ def test_깨진_줄이_있으면_배치_전체가_실패하고_커서는_그대 ) self.assertTrailerCount(message, "Tokens-Used: 10", 0) # 커서가 그대로라야 다음 커밋이 같은 구간을 다시 시도한다. - self.assertEqual("1", self.cursor_value()) + self.assertEqual("fake-session 1", self.cursor_value()) + + def test_다른_세션의_커서가_있어도_새_세션_첫_커밋을_집계한다(self): + """[GF-139] 다른 세션이 남긴 커서가 있어도 새 세션의 첫 커밋은 트랜스크립트 0줄부터 집계한다""" + home = self.fake_home() + # 이전 세션의 긴 트랜스크립트가 커서를 3으로 남긴다. + self.write_transcript( + home, [assistant(1, 1), assistant(1, 1), assistant(1, 1)], session_id="old" + ) + self.write("a.txt", "hi\n") + self.git_ok("add", "a.txt") + self.assertAccepted( + self.commit( + "[feat] old session commit", + env=self.claude_env(home, session_id="old"), + ) + ) + self.assertEqual("old 3", self.cursor_value()) + + # 새 세션의 트랜스크립트는 커서(3)보다 짧다 - 줄 수만 비교하면 0이 된다. + self.write_transcript(home, [assistant(10, 5, ("bash",))], session_id="new") + self.write("b.txt", "bye\n") + self.git_ok("add", "b.txt") + self.assertAccepted( + self.commit( + "[feat] new session commit", + env=self.claude_env(home, session_id="new"), + ) + ) + message = self.head_message() + self.assertTrailerCount(message, "Tokens-Used: 15", 1) + self.assertTrailerCount(message, "Tool-Calls: 1", 1) + self.assertEqual("new 1", self.cursor_value()) + + def test_예전_숫자_커서는_세션_불일치로_취급한다(self): + """[GF-139] 숫자만 담긴 예전 형식 커서는 세션 불일치로 취급해 0줄부터 집계한다""" + home = self.fake_home() + self.write_transcript(home, [assistant(10, 5), assistant(1, 2)]) + self.git_dir_file(CURSOR_NAME).write_text("1", encoding="utf-8") + self.write("a.txt", "hi\n") + self.git_ok("add", "a.txt") + self.assertAccepted( + self.commit("[feat] legacy cursor commit", env=self.claude_env(home)) + ) + self.assertTrailerCount(self.head_message(), "Tokens-Used: 18", 1) + self.assertEqual("fake-session 2", self.cursor_value()) + + def test_같은_응답의_여러_줄은_usage를_한_번만_더한다(self): + """[GF-139] (requestId, message.id)가 같은 assistant 줄은 usage를 한 번만 더하고 tool_use는 줄마다 센다""" + home = self.fake_home() + self.write_transcript( + home, + [ + response_line("req-1", "msg-1", 100, 10, TEXT_BLOCK), + response_line("req-1", "msg-1", 100, 10, TOOL_BLOCK), + response_line("req-1", "msg-1", 100, 10, TOOL_BLOCK), + response_line("req-2", "msg-2", 20, 2, TOOL_BLOCK), + ], + ) + self.write("a.txt", "hi\n") + self.git_ok("add", "a.txt") + self.assertAccepted( + self.commit("[feat] dedup response lines", env=self.claude_env(home)) + ) + message = self.head_message() + self.assertTrailerCount(message, "Tokens-Used: 132", 1) + self.assertTrailerCount(message, "Tool-Calls: 3", 1) + + def test_id가_없는_줄은_줄_단위로_더한다(self): + """[GF-139] requestId나 message.id가 없는 줄은 같은 usage라도 줄마다 더한다""" + home = self.fake_home() + no_request_id = response_line("req-1", "msg-1", 7, 3, TEXT_BLOCK) + del no_request_id["requestId"] + no_message_id = response_line("req-1", "msg-1", 7, 3, TEXT_BLOCK) + del no_message_id["message"]["id"] + self.write_transcript( + home, + [no_request_id, dict(no_request_id), no_message_id, assistant(7, 3)], + ) + self.write("a.txt", "hi\n") + self.git_ok("add", "a.txt") + self.assertAccepted( + self.commit("[feat] lines without ids", env=self.claude_env(home)) + ) + self.assertTrailerCount(self.head_message(), "Tokens-Used: 40", 1) + + def test_커서_경계를_걸친_응답은_두_커밋에_중복_계상되지_않는다(self): + """[GF-139] 한 응답의 줄이 커밋 경계 앞뒤로 나뉘면 뒤 커밋은 그 usage를 다시 더하지 않는다""" + home = self.fake_home() + env = self.claude_env(home) + self.write_transcript(home, [response_line("req-1", "msg-1", 100, 10, TEXT_BLOCK)]) + self.write("a.txt", "hi\n") + self.git_ok("add", "a.txt") + self.assertAccepted(self.commit("[feat] straddle commit one", env=env)) + self.assertTrailerCount(self.head_message(), "Tokens-Used: 110", 1) + + self.write_transcript( + home, + [ + response_line("req-1", "msg-1", 100, 10, TOOL_BLOCK), + response_line("req-2", "msg-2", 5, 1, TEXT_BLOCK), + ], + ) + self.write("b.txt", "bye\n") + self.git_ok("add", "b.txt") + self.assertAccepted(self.commit("[feat] straddle commit two", env=env)) + message = self.head_message() + self.assertTrailerCount(message, "Tokens-Used: 6", 1) + self.assertTrailerCount(message, "Tool-Calls: 1", 1) + self.assertEqual("fake-session 3", self.cursor_value()) if __name__ == "__main__": From 482fd24846814d0cc4b2f72776d5af842f1fbe39 Mon Sep 17 00:00:00 2001 From: cpu-once <231006716+cpu-once@users.noreply.github.com> Date: Sat, 3 Oct 2026 12:39:04 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[fix][post-commit]=20Tokens-Used=20?= =?UTF-8?q?=EC=84=B8=EC=85=98=EB=B3=84=20=EC=BB=A4=EC=84=9C=EC=99=80=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=8B=A8=EC=9C=84=20=EA=B3=84=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task-Id: GF-139 AI-Tool: claude-code AI-Tool-Version: 2.1.285 Co-Authored-By: Claude AI-Model: claude-opus-5-5 Tokens-Used: 25915492 Tool-Calls: 135 Hooks-Commit: cb74ec2 Signed-off-by: cpu-once <231006716+cpu-once@users.noreply.github.com> --- hooks/post-commit | 59 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/hooks/post-commit b/hooks/post-commit index f406e36..91e5530 100755 --- a/hooks/post-commit +++ b/hooks/post-commit @@ -86,7 +86,10 @@ def local_get(key): MARKER = os.path.join(GIT_DIR, conf_get("gitformat.markerFile")) # Tokens-Used/Tool-Calls 델타 커서(decision-5 확장). .gitformat-verified와 같은 # "커밋 간 git-미추적 상태 파일" 관례를 따르되, 이 파일은 삭제되지 않고 -# measure_claude_code_token_usage()가 매번 최신 트랜스크립트 줄 수로 덮어쓴다. +# measure_claude_code_token_usage()가 매번 "<세션 ID> <최신 트랜스크립트 줄 수>" +# 한 줄로 덮어쓴다. 트랜스크립트는 세션마다 새 파일이라 줄 수만 저장하면 새 세션의 +# 짧은 파일을 이전 세션의 줄 수로 읽어 첫 커밋이 0이 된다 - 그래서 세션 ID를 같이 +# 저장하고, 다르면 0줄부터 다시 센다(GF-139). # 파일명은 gitformat.conf로 빼지 않는다 - 컨슈머가 오버라이드할 이유가 없는 순수 # 내부 구현 세부사항이라 다른 trailer 키 이름들과 성격이 다르다. CURSOR = os.path.join(GIT_DIR, ".gitformat-token-cursor") @@ -326,7 +329,16 @@ USAGE_FIELDS = ( # assistant 턴의 message.usage를 합산하고 tool_use 콘텐츠 블록을 센다. 줄 하나라도 # JSON 파싱에 실패하면 ValueError를 그대로 올려 배치 전체를 실패시킨다 — 불량 줄만 # 건너뛰면 "일부만 집계된 값"이 정상값인 척 기록되므로 동작 변경이다. -def aggregate_usage(lines): +# +# 트랜스크립트는 API 응답 하나를 content 블록마다 한 줄씩 기록하고, 각 줄이 같은 +# message.usage를 반복해서 들고 있다 - 줄마다 더하면 2~3배 부풀려진다(GF-139 실측: +# assistant 215줄이 응답 109개). 같은 응답의 줄은 최상위 requestId와 message.id를 +# 공유하므로 이 쌍을 응답 키로 삼아 usage는 키당 한 번만 더한다. seen_keys는 호출자가 +# 넘기는 집합으로, 여기서 본 키를 채워 넣는다 - 커서 앞 구간의 키를 미리 채워 넘기면 +# 커밋 경계를 걸친 응답이 뒤 커밋에서 다시 더해지지 않는다. 둘 중 하나라도 없는 줄은 +# 같은 응답인지 판단할 근거가 없으므로 예전처럼 줄 단위로 더한다. tool_use는 블록이 +# 줄마다 다르므로 키와 무관하게 모든 줄에서 센다. +def aggregate_usage(lines, seen_keys): tokens = 0 calls = 0 for line in lines: @@ -338,7 +350,16 @@ def aggregate_usage(lines): message = entry.get("message") if not isinstance(message, dict): continue + request_id = entry.get("requestId") + message_id = message.get("id") + response_key = None + if isinstance(request_id, str) and request_id and isinstance(message_id, str) and message_id: + response_key = (request_id, message_id) usage = message.get("usage") + if response_key is not None and response_key in seen_keys: + usage = None + elif response_key is not None: + seen_keys.add(response_key) if isinstance(usage, dict): for field in USAGE_FIELDS: value = usage.get(field) @@ -356,8 +377,9 @@ def aggregate_usage(lines): # Tokens-Used/Tool-Calls(decision-5 확장, GF-97 amendment): trailer_ai_model()과 # 같은 트랜스크립트를 message.usage(Anthropic API 응답을 그대로 기록한 값이라 # 자가신고가 아님) 합산과 tool_use 개수 집계에 쓴다. 한 세션에서 커밋이 여러 번 -# 나오므로 세션 누적치를 매번 반복하면 의미가 없어, CURSOR 파일에 "이미 처리한 -# 트랜스크립트 줄 수"를 저장해두고 그 이후 구간만 델타로 집계한다. +# 나오므로 세션 누적치를 매번 반복하면 의미가 없어, CURSOR 파일에 "세션 ID와 그 +# 세션 트랜스크립트에서 이미 처리한 줄 수"를 저장해두고 그 이후 구간만 델타로 +# 집계한다. usage는 줄이 아니라 응답 단위로 한 번씩 더한다(GF-139, aggregate_usage()). # # 성공하면 MEASUREMENT_STATUS=ok와 TOKENS_SUM/TOOL_CALL_COUNT를 채운다. 실패하면 # MEASUREMENT_STATUS=unavailable + MEASUREMENT_REASON(실패 지점을 가리키는 짧은 @@ -402,32 +424,47 @@ def measure_claude_code_token_usage(): if lines and lines[-1] == "": lines.pop() - # 커서 파일이 없거나(최초 실행) 손상됐으면 0으로 취급 - 이건 조회 자체의 - # 실패가 아니라 "아직 아무 줄도 처리한 적 없음"과 동치이므로 실패로 취급하지 않는다. + # 커서는 "<세션 ID> <줄 수>" 한 줄이다(GF-139). 파일이 없거나(최초 실행) + # 손상됐거나, 숫자만 든 예전 형식이거나, 세션 ID가 지금 세션과 다르면 0으로 + # 취급한다 - 이건 조회 자체의 실패가 아니라 "이 세션의 트랜스크립트는 아직 아무 + # 줄도 처리한 적 없음"과 동치이므로 실패로 취급하지 않는다. 세션 ID는 마지막 + # 공백 기준으로 잘라 비교한다(줄 수 쪽엔 공백이 없다). + session_id = os.environ["CLAUDE_CODE_SESSION_ID"] cursor_count = 0 try: with open(CURSOR, "rb") as f: raw_cursor = f.read().decode("utf-8", errors="replace").rstrip("\n") except OSError: raw_cursor = "" - if re.fullmatch(r"[0-9]+", raw_cursor): - cursor_count = int(raw_cursor) + cursor_parts = raw_cursor.rsplit(" ", 1) + if ( + len(cursor_parts) == 2 + and cursor_parts[0] == session_id + and re.fullmatch(r"[0-9]+", cursor_parts[1]) + ): + cursor_count = int(cursor_parts[1]) new_entries = lines[cursor_count:] if total_lines > cursor_count else [] # 입력 전체를 한 번에 슬러프해 처리하던 동작과 같은 실패 단위를 유지한다 - 한 # 줄이라도 깨지면 배치 전체가 실패해 사유 슬러그만 남고 커서는 갱신되지 않는다. + # 커서 앞 구간(같은 세션에서 이미 집계한 줄)은 합산하지 않고 응답 키만 모은다 - + # 응답 하나의 줄이 커밋 경계 앞뒤로 나뉘면 앞 커밋이 이미 그 usage를 더했다. + # 이 구간은 앞 커밋이 이미 파싱에 성공한 줄이므로 여기서 깨지면 트랜스크립트가 + # 바뀐 것이고, 새 구간과 똑같이 배치 실패로 다룬다. + seen_keys = set() try: - TOKENS_SUM, TOOL_CALL_COUNT = aggregate_usage(new_entries) + aggregate_usage(lines[:cursor_count], seen_keys) + TOKENS_SUM, TOOL_CALL_COUNT = aggregate_usage(new_entries, seen_keys) except (ValueError, TypeError): MEASUREMENT_REASON = "transcript-parse-failed" return # 여기까지 왔으면 트랜스크립트를 실제로 읽어 처리했으므로, 합산값이 0이어도 # 커서는 항상 갱신한다 - 그래야 다음 커밋이 같은 구간을 중복 집계하지 않는다. - # 개행 없이 숫자만 쓴다(읽는 쪽과 포맷을 맞춘다). + # 개행 없이 "<세션 ID> <줄 수>"만 쓴다(읽는 쪽과 포맷을 맞춘다). with open(CURSOR, "w", encoding="utf-8") as f: - f.write(str(total_lines)) + f.write(f"{session_id} {total_lines}") MEASUREMENT_STATUS = "ok" From e4fc0fd6dc28809c14170742193c0df2cc22d36e Mon Sep 17 00:00:00 2001 From: cpu-once <231006716+cpu-once@users.noreply.github.com> Date: Sat, 3 Oct 2026 12:39:47 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[docs][backlog]=20doc-16=EC=97=90=20?= =?UTF-8?q?=ED=95=AB=ED=94=BD=EC=8A=A4=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task-Id: GF-139 AI-Tool: claude-code AI-Tool-Version: 2.1.285 Co-Authored-By: Claude AI-Model: claude-opus-5-5 Tokens-Used: 0 Tool-Calls: 0 Hooks-Commit: af0e19b Signed-off-by: cpu-once <231006716+cpu-once@users.noreply.github.com> --- ...5\352\263\274-\355\225\234\352\263\204.md" | 22 ++++++++++++++++++- ...4-\354\236\254\354\236\221\354\204\261.md" | 12 +++++++++- ...1\353\263\265-\354\240\234\352\261\260.md" | 14 +++++++++++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git "a/backlog/docs/doc-16 - \355\206\240\355\201\260\302\267\355\210\264\354\275\234-\354\270\241\354\240\225-\353\260\251\353\262\225\352\263\274-\355\225\234\352\263\204.md" "b/backlog/docs/doc-16 - \355\206\240\355\201\260\302\267\355\210\264\354\275\234-\354\270\241\354\240\225-\353\260\251\353\262\225\352\263\274-\355\225\234\352\263\204.md" index d762161..57f4c69 100644 --- "a/backlog/docs/doc-16 - \355\206\240\355\201\260\302\267\355\210\264\354\275\234-\354\270\241\354\240\225-\353\260\251\353\262\225\352\263\274-\355\225\234\352\263\204.md" +++ "b/backlog/docs/doc-16 - \355\206\240\355\201\260\302\267\355\210\264\354\275\234-\354\270\241\354\240\225-\353\260\251\353\262\225\352\263\274-\355\225\234\352\263\204.md" @@ -3,7 +3,7 @@ id: doc-16 title: 토큰·툴콜 측정 방법과 한계 type: specification created_date: '2026-09-25 19:34' -updated_date: '2026-09-25 19:46' +updated_date: '2026-10-03 03:39' --- # 토큰·툴콜 측정 방법과 한계 @@ -33,6 +33,26 @@ apiBlockIndex=3 content=['tool_use'] usage합=87932 3. 그룹의 `tool_use` 블록은 **모든 줄에서 모은다**(한 응답의 도구 호출이 여러 줄에 흩어져 있다). 이 목록으로 귀속 여부를 판정한다 +## 현재 post-commit 구현 (GF-139 핫픽스) + +아래 "귀속 필터"와 "기록 형식"은 GF-129가 `prepare-commit-msg`로 옮기며 들일 목표 설계다. +지금 `post-commit`은 귀속 필터 없이 직전 커밋 이후 구간 전체를 합산해 `Tokens-Used: ` +한 값으로 남긴다. 다만 두 가지는 GF-139에서 먼저 고쳤다. + +- **응답 단위 계상.** 위 알고리즘 1·2단계를 그대로 적용한다. `(requestId, message.id)`가 + 같은 assistant 줄은 `usage`를 한 번만 더하고, 둘 중 하나라도 없는 줄은 줄 단위로 더한다. + `Tool-Calls`는 모든 줄의 `tool_use` 블록을 센다. +- **세션별 커서.** `.git/.gitformat-token-cursor`는 `<세션 ID> <줄 수>` 한 줄이다(개행 없음). + 트랜스크립트는 세션마다 새 파일이라 줄 수만 저장하면 새 세션의 짧은 파일을 이전 세션의 + 줄 수로 읽어 첫 커밋이 0이 됐다. 세션 ID가 다르거나, 숫자만 든 예전 형식이거나, 손상됐으면 + 0줄부터 센다. +- **커밋 경계를 걸친 응답.** 응답 하나의 줄이 커서 앞뒤로 나뉘면 앞 커밋이 이미 그 `usage`를 + 더했다. 그래서 같은 세션이면 커서 앞 구간의 응답 키를 먼저 모아 두고, 뒤 커밋에서는 그 + 키의 `usage`를 다시 더하지 않는다. + +형식 변경(`in`/`out`/`delta`)과 귀속 필터는 GF-129에 남아 있다. GF-129는 위 세 가지를 +`prepare-commit-msg`로 옮길 때 함께 가져가야 한다. + ## 귀속 필터 쌓인 턴 중 **이 커밋과 관계있는 턴만 골라내는 체**다. 기준은 하나 — 그 턴의 도구 호출이 diff --git "a/backlog/tasks/gf-129 - \355\206\240\355\201\260\302\267\355\210\264\354\275\234-\354\270\241\354\240\225\354\235\204-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260\354\231\200-\354\273\244\353\260\213-\352\267\200\354\206\215\354\234\274\353\241\234-\354\236\254\354\236\221\354\204\261.md" "b/backlog/tasks/gf-129 - \355\206\240\355\201\260\302\267\355\210\264\354\275\234-\354\270\241\354\240\225\354\235\204-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260\354\231\200-\354\273\244\353\260\213-\352\267\200\354\206\215\354\234\274\353\241\234-\354\236\254\354\236\221\354\204\261.md" index c041f55..e8bd9ff 100644 --- "a/backlog/tasks/gf-129 - \355\206\240\355\201\260\302\267\355\210\264\354\275\234-\354\270\241\354\240\225\354\235\204-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260\354\231\200-\354\273\244\353\260\213-\352\267\200\354\206\215\354\234\274\353\241\234-\354\236\254\354\236\221\354\204\261.md" +++ "b/backlog/tasks/gf-129 - \355\206\240\355\201\260\302\267\355\210\264\354\275\234-\354\270\241\354\240\225\354\235\204-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260\354\231\200-\354\273\244\353\260\213-\352\267\200\354\206\215\354\234\274\353\241\234-\354\236\254\354\236\221\354\204\261.md" @@ -4,7 +4,7 @@ title: 토큰·툴콜 측정을 응답 단위 중복 제거와 커밋 귀속으 status: To Do assignee: [] created_date: '2026-09-25 19:33' -updated_date: '2026-09-25 21:43' +updated_date: '2026-10-03 03:39' labels: - hooks - trailers @@ -58,3 +58,13 @@ type: bug - [ ] #2 ruff check 통과 - [ ] #3 이 저장소 자신의 커밋이 새 훅으로 정상 생성되는지 확인 + +## Comments + + +author: @claude +created: 2026-10-03 03:39 +--- +GF-139 핫픽스로 AC#1(응답 단위 계상: (requestId, message.id)가 같은 assistant 줄은 usage를 한 번만 더함)을 post-commit에 먼저 적용했다. 세션별 커서(.git/.gitformat-token-cursor = '<세션 ID> <줄 수>', 세션 불일치·예전 숫자 형식이면 0줄부터)와, 커서 경계를 걸친 응답의 중복 방지(커서 앞 구간의 응답 키를 먼저 모음)도 함께 들어갔다. 이 작업을 prepare-commit-msg로 옮길 때 세 가지를 모두 가져가야 한다. 형식 변경(in/out/delta)과 귀속 필터는 그대로 GF-129 범위다. 상세는 doc-16 '현재 post-commit 구현' 절. +--- + diff --git "a/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" "b/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" index 0c85eae..499257f 100644 --- "a/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" +++ "b/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" @@ -5,7 +5,7 @@ status: In Progress assignee: - '@claude' created_date: '2026-10-03 03:30' -updated_date: '2026-10-03 03:36' +updated_date: '2026-10-03 03:39' labels: - hooks - bug @@ -58,3 +58,15 @@ GF-129는 GF-127→GF-128 재설계 뒤로 묶여 있어서, 값이 틀린 채 4. doc-16 갱신, GF-129 코멘트 5. 실제 커밋으로 값 대조 (구현은 서브에이전트, 검토·커밋 확인은 메인) + +## Implementation Notes + + +구현(서브에이전트, TDD): +- 테스트 먼저(a096f1d): 새 세션 첫 커밋 / 예전 숫자 커서 / 같은 (requestId, message.id) 줄 1회 계상 / id 없는 줄 줄 단위 / 커서 경계를 걸친 응답 중복 방지 5건 추가, 기존 커서 기대값을 'fake-session '으로 갱신. 구현 전 실행 시 11건 중 8건 실패 확인. +- 구현(482fd24): hooks/post-commit의 aggregate_usage(lines, seen_keys)와 measure_claude_code_token_usage()만 수정. 커서 형식 '<세션 ID> <줄 수>'(개행 없음), 마지막 공백 기준 rsplit로 파싱. 세션 불일치·숫자만·손상 → 0줄부터. 같은 세션이면 커서 앞 구간을 aggregate_usage로 돌려 응답 키만 모으고(합산은 버림) 새 구간에서 그 키의 usage를 건너뜀. 앞 구간 파싱 실패도 transcript-parse-failed로 배치 실패 처리. 같은 응답의 usage는 첫 줄 값을 씀. +- 결과: python3 -m unittest discover -s tests → Ran 98 tests, OK. ruff check hooks/ tests/ + python 훅 → All checks passed. +- 실커밋 482fd24의 커서: '<세션 UUID> 1353' 형식으로 기록됨(Tokens-Used: 25915492, 이 값은 이번 세션의 첫 커밋 이후 구간 전체 합). DoD#3(응답 단위 별도 합산과 대조)은 메인에서 확인 필요. +- doc-16에 '현재 post-commit 구현 (GF-139 핫픽스)' 절 추가, GF-129에 선반영 코멘트 남김. +- 범위 밖 발견: doc-1 설치 가이드 표의 .gitformat-token-cursor 설명이 '(누적 줄 수)'로 남아 있음 — 손대지 않음. + From 304cb2da4966dc66b3ac1561d8fa5cfdafc448f1 Mon Sep 17 00:00:00 2001 From: cpu-once <231006716+cpu-once@users.noreply.github.com> Date: Sat, 3 Oct 2026 12:40:46 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[docs][backlog]=20doc-1=20=EC=BB=A4?= =?UTF-8?q?=EC=84=9C=20=ED=98=95=EC=8B=9D=20=EC=84=A4=EB=AA=85=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task-Id: GF-139 AI-Tool: claude-code AI-Tool-Version: 2.1.285 Co-Authored-By: Claude AI-Model: claude-opus-5-5 Tokens-Used: 813177 Tool-Calls: 3 Hooks-Commit: 8ffc7ee Signed-off-by: cpu-once <231006716+cpu-once@users.noreply.github.com> --- ...4\244\354\271\230-\352\260\200\354\235\264\353\223\234.md" | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git "a/backlog/docs/doc-1 - \354\204\244\354\271\230-\352\260\200\354\235\264\353\223\234.md" "b/backlog/docs/doc-1 - \354\204\244\354\271\230-\352\260\200\354\235\264\353\223\234.md" index 8014ba9..d4a6147 100644 --- "a/backlog/docs/doc-1 - \354\204\244\354\271\230-\352\260\200\354\235\264\353\223\234.md" +++ "b/backlog/docs/doc-1 - \354\204\244\354\271\230-\352\260\200\354\235\264\353\223\234.md" @@ -3,7 +3,7 @@ id: doc-1 title: 설치 가이드 type: guide created_date: '2026-09-19 05:27' -updated_date: '2026-10-03 03:16' +updated_date: '2026-10-03 03:40' --- ## 🚀 설치 @@ -45,7 +45,7 @@ cd ~/my-project | 파일/디렉터리 | 위치 | 언제 | 비고 | | ------------------------- | ---------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------- | | `.gitformat-verified` | `<대상 저장소>/.git/` | `pre-commit` 통과 시 생성, `post-commit`이 곧 삭제 | 커밋 사이에 남지 않는 임시 마커 | -| `.gitformat-token-cursor` | `<대상 저장소>/.git/` | Claude Code 실측 성공 시에만 `post-commit`이 갱신(`unavailable`일 때는 갱신 안 함) | `Tokens-Used`/`Tool-Calls` 델타 계산용 커서(누적 줄 수), 커밋 간 유지됨 | +| `.gitformat-token-cursor` | `<대상 저장소>/.git/` | Claude Code 실측 성공 시에만 `post-commit`이 갱신(`unavailable`일 때는 갱신 안 함) | `Tokens-Used`/`Tool-Calls` 델타 계산용 커서(`<세션 ID> <줄 수>`, 세션이 바뀌면 0줄부터, GF-139), 커밋 간 유지됨 | | `template/hooks/*` | 이 git-logbook 클론 자신의 `template/` 안 | `install.sh --global` 실행 시 | 클론 위치를 가리키는 심볼릭 링크, 커밋 안 됨(`.gitignore`) | **커밋 자체가 바뀌는 경우**: `post-commit`이 조건에 따라 `git commit --amend`로 From fbbff53c30f173bd4dde52e709bae94380bc1b21 Mon Sep 17 00:00:00 2001 From: cpu-once <231006716+cpu-once@users.noreply.github.com> Date: Sat, 3 Oct 2026 12:42:28 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[docs][backlog]=20GF-139=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task-Id: GF-139 AI-Tool: claude-code AI-Tool-Version: 2.1.285 Co-Authored-By: Claude AI-Model: claude-opus-5-5 Tokens-Used: 1657842 Tool-Calls: 6 Hooks-Commit: 32a923e Signed-off-by: cpu-once <231006716+cpu-once@users.noreply.github.com> --- ...1\353\263\265-\354\240\234\352\261\260.md" | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git "a/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" "b/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" index 499257f..8205782 100644 --- "a/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" +++ "b/backlog/tasks/gf-139 - Tokens-Used-\355\225\253\355\224\275\354\212\244-\342\200\224-\354\204\270\354\205\230\353\263\204-\354\273\244\354\204\234\354\231\200-\354\235\221\353\213\265-\353\213\250\354\234\204-\354\244\221\353\263\265-\354\240\234\352\261\260.md" @@ -1,11 +1,11 @@ --- id: GF-139 title: Tokens-Used 핫픽스 — 세션별 커서와 응답 단위 중복 제거 -status: In Progress +status: Done assignee: - '@claude' created_date: '2026-10-03 03:30' -updated_date: '2026-10-03 03:39' +updated_date: '2026-10-03 03:42' labels: - hooks - bug @@ -33,20 +33,20 @@ GF-129는 GF-127→GF-128 재설계 뒤로 묶여 있어서, 값이 틀린 채 ## Acceptance Criteria -- [ ] #1 커서 파일에 세션 ID와 줄 수를 함께 저장하고, 세션 ID가 다르면 트랜스크립트 0줄부터 집계한다 -- [ ] #2 예전 형식(숫자만) 커서는 세션 불일치로 취급해 0줄부터 집계한다 -- [ ] #3 (requestId, message.id)가 같은 assistant 줄은 usage를 한 번만 더한다 -- [ ] #4 requestId나 message.id가 없는 줄은 지금처럼 줄 단위로 더한다 -- [ ] #5 Tool-Calls는 지금처럼 모든 줄의 tool_use 블록을 센다 -- [ ] #6 새 세션 첫 커밋, 중복 줄, 예전 커서 형식을 다루는 테스트를 추가하고 기존 테스트의 커서 기대값을 새 형식에 맞춘다 -- [ ] #7 doc-16에 세션별 커서와 응답 단위 계상을 반영하고, GF-129 AC#1이 이 핫픽스로 선반영됐다고 코멘트를 남긴다 +- [x] #1 커서 파일에 세션 ID와 줄 수를 함께 저장하고, 세션 ID가 다르면 트랜스크립트 0줄부터 집계한다 +- [x] #2 예전 형식(숫자만) 커서는 세션 불일치로 취급해 0줄부터 집계한다 +- [x] #3 (requestId, message.id)가 같은 assistant 줄은 usage를 한 번만 더한다 +- [x] #4 requestId나 message.id가 없는 줄은 지금처럼 줄 단위로 더한다 +- [x] #5 Tool-Calls는 지금처럼 모든 줄의 tool_use 블록을 센다 +- [x] #6 새 세션 첫 커밋, 중복 줄, 예전 커서 형식을 다루는 테스트를 추가하고 기존 테스트의 커서 기대값을 새 형식에 맞춘다 +- [x] #7 doc-16에 세션별 커서와 응답 단위 계상을 반영하고, GF-129 AC#1이 이 핫픽스로 선반영됐다고 코멘트를 남긴다 ## Definition of Done -- [ ] #1 python3 -m unittest discover -s tests 전체 통과 -- [ ] #2 ruff check 통과 -- [ ] #3 실제 커밋의 Tokens-Used가 트랜스크립트를 응답 단위로 따로 합산한 값과 일치하는지 확인 +- [x] #1 python3 -m unittest discover -s tests 전체 통과 +- [x] #2 ruff check 통과 +- [x] #3 실제 커밋의 Tokens-Used가 트랜스크립트를 응답 단위로 따로 합산한 값과 일치하는지 확인 ## Implementation Plan @@ -69,4 +69,12 @@ GF-129는 GF-127→GF-128 재설계 뒤로 묶여 있어서, 값이 틀린 채 - 실커밋 482fd24의 커서: '<세션 UUID> 1353' 형식으로 기록됨(Tokens-Used: 25915492, 이 값은 이번 세션의 첫 커밋 이후 구간 전체 합). DoD#3(응답 단위 별도 합산과 대조)은 메인에서 확인 필요. - doc-16에 '현재 post-commit 구현 (GF-139 핫픽스)' 절 추가, GF-129에 선반영 코멘트 남김. - 범위 밖 발견: doc-1 설치 가이드 표의 .gitformat-token-cursor 설명이 '(누적 줄 수)'로 남아 있음 — 손대지 않음. + +메인 검증: 코드 리뷰(aggregate_usage·measure_claude_code_token_usage 두 함수만 변경, 실패 처리 유지). GF-139 테스트 5건 포함 전체 98개 통과, ruff 통과. DoD#3: 커밋 8ffc7ee의 커서 구간(1353→1391줄)을 트랜스크립트에서 응답 단위로 따로 합산 = 813,177 토큰 / 3 호출, 트레일러 값과 일치(줄 단위였다면 1,893,217). 서브에이전트 커밋 482fd24도 서브에이전트가 같은 방식으로 일치 확인(25,915,492). doc-1의 커서 설명도 새 형식으로 갱신. 알려진 한계: 서브에이전트의 토큰은 부모 트랜스크립트에 없어 서브에이전트가 만든 커밋은 0으로 남는다(doc-16에 기존 기재). + +## Final Summary + + +post-commit의 Tokens-Used 측정에서 두 버그를 고쳤다. 커서를 '<세션 ID> <줄 수>'로 저장해 새 세션 첫 커밋이 0이 되던 문제를 없앴고, (requestId, message.id) 단위로 usage를 한 번만 더해 2~3배 과대계상을 없앴다(커밋 경계를 걸친 응답 포함). 테스트 5건 추가, 전체 98개·ruff 통과, 실제 커밋 8ffc7ee 값이 독립 합산과 일치. 형식 변경과 커밋 귀속은 GF-129에 남김. +